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())