From 5b42287c22ed9ba49087c4720046fae486c3e572 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Thu, 21 May 2026 10:17:23 -0700 Subject: [PATCH] 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. --- seaweed-volume/src/storage/volume.rs | 61 ++++++++++++++++++++++++++++ weed/storage/volume_checking.go | 12 +++++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/seaweed-volume/src/storage/volume.rs b/seaweed-volume/src/storage/volume.rs index a26509460..1f29c68d0 100644 --- a/seaweed-volume/src/storage/volume.rs +++ b/seaweed-volume/src/storage/volume.rs @@ -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(); diff --git a/weed/storage/volume_checking.go b/weed/storage/volume_checking.go index 8913f683b..c32758fb6 100644 --- a/weed/storage/volume_checking.go +++ b/weed/storage/volume_checking.go @@ -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())