diff --git a/seaweed-volume/src/storage/volume.rs b/seaweed-volume/src/storage/volume.rs index c2957e1f2..056db6491 100644 --- a/seaweed-volume/src/storage/volume.rs +++ b/seaweed-volume/src/storage/volume.rs @@ -3071,11 +3071,15 @@ impl Volume { let bytes = fake_del_needle.write_bytes(version); dst_dat.write_all(&bytes)?; + // Record the tombstone's real .dat offset, like the normal delete path, + // so a deletion left at the .dat tail stays visible to the integrity + // check on reload. Offset 0 hid the trailing tombstone and falsely + // flipped the volume read-only. let mut idx_entry_buf = [0u8; NEEDLE_MAP_ENTRY_SIZE]; crate::storage::types::idx_entry_to_bytes( &mut idx_entry_buf, key, - Offset::from_actual_offset(0), + Offset::from_actual_offset(dat_offset as i64), Size(crate::storage::types::TOMBSTONE_FILE_SIZE.into()), ); dst_idx.write_all(&idx_entry_buf)?; diff --git a/weed/storage/volume_vacuum.go b/weed/storage/volume_vacuum.go index 4814d1c7a..818bf08f9 100644 --- a/weed/storage/volume_vacuum.go +++ b/weed/storage/volume_vacuum.go @@ -389,11 +389,15 @@ func (v *Volume) makeupDiff(newDatFileName, newIdxFileName, oldDatFileName, oldI fakeDelNeedle.Id = key fakeDelNeedle.Cookie = 0x12345678 fakeDelNeedle.AppendAtNs = uint64(time.Now().UnixNano()) - _, _, _, err = fakeDelNeedle.Append(dstDatBackend, v.Version()) - if err != nil { - return fmt.Errorf("append deleted %d failed: %v", key, err) + fakeDelOffset, _, _, appendErr := fakeDelNeedle.Append(dstDatBackend, v.Version()) + if appendErr != nil { + return fmt.Errorf("append deleted %d failed: %v", key, appendErr) } - util.Uint32toBytes(idxEntryBytes[8:12], uint32(0)) + // Record the tombstone's real .dat offset, like the normal delete path, + // so a deletion left at the .dat tail stays visible to the integrity + // check on reload. Offset 0 hid the trailing tombstone and falsely + // flipped the volume read-only. + idxEntryBytes = needle_map.ToBytes(key, ToOffset(int64(fakeDelOffset)), increIdxEntry.size) } if _, err := idx.Seek(0, 2); err != nil { diff --git a/weed/storage/volume_vacuum_test.go b/weed/storage/volume_vacuum_test.go index 8689c19bc..5baec284d 100644 --- a/weed/storage/volume_vacuum_test.go +++ b/weed/storage/volume_vacuum_test.go @@ -151,6 +151,54 @@ func testCompactionByIndex(t *testing.T, needleMapKind NeedleMapKind) { } +// A deletion replayed by makeupDiff appends a tombstone to the compacted .dat. +// When that tombstone is the .dat tail, its .idx entry must carry the real +// offset, not 0, or the post-commit integrity check can't see the trailing +// tombstone and wrongly flips the volume read-only — loading a +// SortedFileNeedleMap instead of the writable LevelDb map. +func TestCommitCompactDeletionTailKeepsWritable(t *testing.T) { + dir := t.TempDir() + + v, err := NewVolume(dir, dir, "", 1, NeedleMapLevelDb, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0) + if err != nil { + t.Fatalf("volume creation: %v", err) + } + + for i := uint64(1); i <= 5; i++ { + if _, _, _, err := v.writeNeedle2(newRandomNeedle(i), true, false); err != nil { + t.Fatalf("write %d: %v", i, err) + } + } + + v.CompactByIndex(nil) + + // The sole change in the commit window is a deletion, so makeupDiff appends + // its tombstone last, putting it at the .dat tail. + if _, err := v.deleteNeedle2(newEmptyNeedle(3)); err != nil { + t.Fatalf("delete: %v", err) + } + + if err := v.CommitCompact(); err != nil { + t.Fatalf("commit compact: %v", err) + } + + if _, ok := v.nm.(*LevelDbNeedleMap); !ok { + t.Fatalf("after compaction v.nm is %T, want *LevelDbNeedleMap (volume wrongly marked read-only)", v.nm) + } + v.Close() + + // Reload from disk to confirm the integrity check passes and the volume + // stays writable. + v, err = NewVolume(dir, dir, "", 1, NeedleMapLevelDb, nil, nil, 0, needle.GetCurrentVersion(), 0, 0) + if err != nil { + t.Fatalf("reload: %v", err) + } + defer v.Close() + if v.noWriteOrDelete { + t.Fatal("volume reloaded read-only after a deletion-tail compaction") + } +} + func TestCompactVolumeFilesOffline(t *testing.T) { dir := t.TempDir() location := NewDiskLocation(dir, 10, util.MinFreeSpace{}, dir, "", nil)