fix(storage): surface stat error on zero-size idx scrub, mirror to rust (#9612)

fix(storage): harden zero-size idx scrub and mirror to rust

When a zero-size .idx is found, openIndex stats the backing .dat through
v.DataBackend: wrap that GetStat failure with %w, fix the indices typo, and
guard both openIndex and scrubVolumeData against a nil DataBackend (closed or
remote-only volumes) instead of panicking.

Add rust scrub tests for empty (superblock-only .dat, zero-size .idx) and
healthy volumes, keeping the volume server in parity with the go zero-size
scrub handling.
This commit is contained in:
Chris Lu
2026-05-21 10:17:23 -07:00
committed by GitHub
parent 3392493f0a
commit 5b42287c22
2 changed files with 71 additions and 2 deletions
+61
View File
@@ -3643,6 +3643,67 @@ mod tests {
);
}
#[test]
fn test_scrub_empty_volume() {
// Mirror of Go's TestScrubVolumeData "zero-size volume without index"
// case (weed/storage/volume_checking_test.go): a freshly created /
// pre-allocated volume has a superblock-only .dat and a zero-size .idx,
// and must scrub clean instead of being flagged as corrupt.
let tmp = TempDir::new().unwrap();
let dir = tmp.path().to_str().unwrap();
let v = make_test_volume(dir);
// .dat holds only the superblock; .idx is empty.
assert_eq!(v.dat_file_size().unwrap(), SUPER_BLOCK_SIZE as u64);
let (files_checked, broken) = v.scrub().unwrap();
assert_eq!(files_checked, 0);
assert!(
broken.is_empty(),
"empty volume should scrub clean, got {:?}",
broken
);
// The index-only mode must agree.
let (idx_checked, idx_broken) = v.scrub_index().unwrap();
assert_eq!(idx_checked, 0);
assert!(
idx_broken.is_empty(),
"empty volume should scrub_index clean, got {:?}",
idx_broken
);
}
#[test]
fn test_scrub_healthy_volume() {
// Mirror of Go's TestScrubVolumeData "healthy volume" case: a volume
// with live needles scrubs clean and the .dat size accounting matches.
let tmp = TempDir::new().unwrap();
let dir = tmp.path().to_str().unwrap();
let mut v = make_test_volume(dir);
for i in 1..=5 {
let data = format!("needle data {}", i);
let mut n = Needle {
id: NeedleId(i),
cookie: Cookie(i as u32),
data: data.as_bytes().to_vec(),
data_size: data.len() as u32,
..Needle::default()
};
v.write_needle(&mut n, true).unwrap();
}
v.sync_to_disk().unwrap();
let (files_checked, broken) = v.scrub().unwrap();
assert_eq!(files_checked, 5);
assert!(
broken.is_empty(),
"healthy volume should scrub clean, got {:?}",
broken
);
}
#[test]
fn test_volume_multiple_needles() {
let tmp = TempDir::new().unwrap();
+10 -2
View File
@@ -29,14 +29,18 @@ func (v *Volume) openIndex() (*os.File, int64, error) {
}
if idxStat.Size() == 0 {
if v.DataBackend == nil {
idxFile.Close()
return nil, 0, fmt.Errorf("volume %v has no data backend", v.Id)
}
volumeFileSize, _, err := v.DataBackend.GetStat()
if err != nil {
idxFile.Close()
return nil, 0, fmt.Errorf("failed to stat storage for zero-size IDX volume %v", v.Id)
return nil, 0, fmt.Errorf("failed to stat storage for zero-size IDX volume %v: %w", v.Id, err)
}
// account for pre-allocated volumes (f.ex. after running "volume.grow") without data, as these
// are allowed to have zero-size indeces.
// are allowed to have zero-size indices.
if volumeFileSize > int64(super_block.SuperBlockSize) {
idxFile.Close()
return nil, 0, fmt.Errorf("zero-size IDX file for volume %v with store size %d", v.Id, volumeFileSize)
@@ -62,6 +66,10 @@ func (v *Volume) ScrubIndex() (int64, []error) {
// scrubVolumeData checks a volume content + index for issues.
func (v *Volume) scrubVolumeData(idxFile *os.File, idxFileSize int64) (int64, []error) {
if v.DataBackend == nil {
return 0, []error{fmt.Errorf("volume %d has no data backend", v.Id)}
}
// full scrubbing means also scrubbing the index
var count int64
_, errs := idx.CheckIndexFile(idxFile, idxFileSize, v.Version())