package storage import ( "os" "path/filepath" "testing" "github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb" "github.com/seaweedfs/seaweedfs/weed/storage/needle" "github.com/seaweedfs/seaweedfs/weed/storage/needle_map" "github.com/seaweedfs/seaweedfs/weed/storage/super_block" "github.com/seaweedfs/seaweedfs/weed/storage/types" "github.com/seaweedfs/seaweedfs/weed/storage/volume_info" ) // clobberIdxHead reproduces the damage a delete on a tiered read-only volume // used to do: it writes (key, offset 0, tombstone) rows over the front of .idx // instead of appending them. func clobberIdxHead(t *testing.T, idxPath string, keys []uint64) { t.Helper() f, err := os.OpenFile(idxPath, os.O_WRONLY, 0644) if err != nil { t.Fatalf("open idx: %v", err) } defer f.Close() for i, key := range keys { row := needle_map.ToBytes(types.Uint64ToNeedleId(key), types.Offset{}, types.TombstoneFileSize) if _, err := f.WriteAt(row, int64(i*types.NeedleMapEntrySize)); err != nil { t.Fatalf("clobber row %d: %v", i, err) } } } func writeTestVolume(t *testing.T, dir string, needleCount int) map[uint64]*needle.Needle { t.Helper() v, err := NewVolume(dir, dir, "", 1, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0) if err != nil { t.Fatalf("volume creation: %v", err) } written := make(map[uint64]*needle.Needle) for i := 1; i <= needleCount; i++ { n := newRandomNeedle(uint64(i)) if _, _, _, err := v.writeNeedle2(n, true, false, false); err != nil { v.Close() t.Fatalf("write needle %d: %v", i, err) } written[uint64(i)] = n } v.Close() return written } func mustReload(t *testing.T, dir string) *Volume { t.Helper() v, err := loadVolumeWithoutWorker(dir, dir, "", 1, NeedleMapInMemory, 0) if err != nil { t.Fatalf("reload volume: %v", err) } return v } func mustRead(t *testing.T, v *Volume, id uint64) []byte { t.Helper() n := newEmptyNeedle(id) if _, err := v.readNeedle(n, nil, nil); err != nil { t.Fatalf("read needle %d: %v", id, err) } return n.Data } // TestRepairIdxHeadTombstones_RestoresClobberedRows checks that a volume whose // .idx head was overwritten with offset-0 tombstones serves its first needles // again after a reload, without a full weed fix rebuild. func TestRepairIdxHeadTombstones_RestoresClobberedRows(t *testing.T) { dir := t.TempDir() const needleCount = 12 const clobbered = 4 written := writeTestVolume(t, dir, needleCount) idxPath := filepath.Join(dir, "1.idx") sizeBefore := fileSize(t, idxPath) // The rewrite replaces .idx wholesale, so it must not widen the mode. if err := os.Chmod(idxPath, 0600); err != nil { t.Fatalf("chmod idx: %v", err) } // Deletes against needles 9..12 land on the front of .idx and take the // rows indexing needles 1..4 with them. clobberIdxHead(t, idxPath, []uint64{9, 10, 11, 12}) v := mustReload(t, dir) defer v.Close() for i := uint64(1); i <= needleCount; i++ { if i >= 9 { // genuinely deleted by the tombstones that did the damage continue } if got, want := mustRead(t, v, i), written[i].Data; string(got) != string(want) { t.Fatalf("needle %d: data mismatch after recovery", i) } } if got, want := fileSize(t, idxPath), sizeBefore+int64(clobbered*types.NeedleMapEntrySize); got != want { t.Fatalf("idx size after recovery: got %d, want %d", got, want) } if st, err := os.Stat(idxPath); err != nil { t.Fatalf("stat idx: %v", err) } else if got := st.Mode().Perm(); got != 0600 { t.Fatalf("idx mode after recovery: got %o, want 600", got) } // The recovered rows go back in front, so .idx is in .dat append order // again: the fingerprint is gone and the last row is still the .dat tail. entries := readAllIdxEntries(t, idxPath) if entries[0].offset.ToActualOffset() != int64(v.SuperBlock.BlockSize()) { t.Fatalf("first row does not index the first needle in .dat: %+v", entries[0]) } for i := 1; i < clobbered; i++ { if entries[i-1].offset.ToActualOffset() >= entries[i].offset.ToActualOffset() { t.Fatalf("recovered rows are not in .dat order: %+v then %+v", entries[i-1], entries[i]) } } // The recovery is idempotent: a second load finds nothing left to restore. v.Close() v = mustReload(t, dir) if got, want := fileSize(t, idxPath), sizeBefore+int64(clobbered*types.NeedleMapEntrySize); got != want { t.Fatalf("idx grew on second load: got %d, want %d", got, want) } } // TestRepairIdxHeadTombstones_ReadOnlyVolume covers the shape the damage // actually occurs in: a read-only volume, whose reads go through the sorted // needle map. The recovery has to land before .sdx is regenerated, or the // restored rows never reach the map that answers the read. func TestRepairIdxHeadTombstones_ReadOnlyVolume(t *testing.T) { dir := t.TempDir() written := writeTestVolume(t, dir, 8) clobberIdxHead(t, filepath.Join(dir, "1.idx"), []uint64{7, 8}) if err := volume_info.SaveVolumeInfo(filepath.Join(dir, "1.vif"), &volume_server_pb.VolumeInfo{ Version: uint32(needle.GetCurrentVersion()), ReadOnly: true, }); err != nil { t.Fatalf("save vif: %v", err) } v := mustReload(t, dir) defer v.Close() if _, isSorted := v.nm.(*SortedFileNeedleMap); !isSorted { t.Fatalf("expected a sorted needle map, got %T", v.nm) } for i := uint64(1); i <= 2; i++ { if got, want := mustRead(t, v, i), written[i].Data; string(got) != string(want) { t.Fatalf("needle %d: data mismatch after recovery", i) } } } // TestRepairIdxHeadTombstones_LeavesHealthyIdxAlone guards the fingerprint: a // volume with ordinary deletes must not be rewritten. func TestRepairIdxHeadTombstones_LeavesHealthyIdxAlone(t *testing.T) { dir := t.TempDir() writeTestVolume(t, dir, 6) idxPath := filepath.Join(dir, "1.idx") v := mustReload(t, dir) if _, err := v.doDeleteRequest(newEmptyNeedle(3)); err != nil { t.Fatalf("delete needle 3: %v", err) } v.Close() sizeBefore := fileSize(t, idxPath) entriesBefore := readAllIdxEntries(t, idxPath) v = mustReload(t, dir) defer v.Close() if got := fileSize(t, idxPath); got != sizeBefore { t.Fatalf("healthy idx was rewritten: got %d, want %d", got, sizeBefore) } if got := readAllIdxEntries(t, idxPath); len(got) != len(entriesBefore) { t.Fatalf("healthy idx row count changed: got %d, want %d", len(got), len(entriesBefore)) } if _, err := v.readNeedle(newEmptyNeedle(3), nil, nil); err != ErrorDeleted { t.Fatalf("needle 3 should stay deleted, got %v", err) } } // TestRepairIdxHeadTombstones_KeepsDeletedNeedlesDeleted checks that a needle // deleted before the damage is not resurrected: its tombstone row survives at // the tail of .idx, so the key is still indexed and stays out of the recovery. // It stays unreadable either way -- as deleted if its Put row survived, as // not-found if the tombstone is all the .idx has left of it. func TestRepairIdxHeadTombstones_KeepsDeletedNeedlesDeleted(t *testing.T) { dir := t.TempDir() writeTestVolume(t, dir, 8) idxPath := filepath.Join(dir, "1.idx") v := mustReload(t, dir) if _, err := v.doDeleteRequest(newEmptyNeedle(2)); err != nil { t.Fatalf("delete needle 2: %v", err) } v.Close() clobberIdxHead(t, idxPath, []uint64{7, 8}) v = mustReload(t, dir) defer v.Close() if _, err := v.readNeedle(newEmptyNeedle(2), nil, nil); err != ErrorDeleted && err != ErrorNotFound { t.Fatalf("needle 2 was resurrected, got %v", err) } if _, err := v.readNeedle(newEmptyNeedle(1), nil, nil); err != nil { t.Fatalf("needle 1 should have been recovered: %v", err) } }