mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-08 15:41:15 +02:00
* volume: forward fsync=true to replicas in ReplicatedWrite When a write request carries fsync=true, only the primary volume server flushed to disk: the replica fan-out URL in ReplicatedWrite only carried type/ttl/ts/cm, so replicas always wrote without fsync even when the client explicitly requested a durable write. Forward the fsync request parameter to the replica volume servers so a durable write means every replica has flushed to disk, not just the primary. Replicas without fsync are untouched (zero behavior change). * storage: flush a durable write inline while stopping The fsync flag on the write path really selects the async batch worker, and it was switched off once the store is stopping. So a fsync=true write landing during the pre-stop drain got acked without ever being flushed - and now that ReplicatedWrite forwards fsync, that covers replicas too. Flush it inline instead of queueing it. The drain keeps accepting writes, which is the whole point of preStopSeconds, and the ack still means the .dat is on disk. If the fsync fails, the append comes back off the .dat and the needle map goes back to what it pointed at before, so nothing resolves to an offset past the truncated end. * storage: make the store's stopping flag atomic SetStopping runs on the signal handler goroutine while the write and vacuum paths read the flag, so every read of it was racy. Nothing about the shutdown ordering changes; only the flag itself is now safe to read. * topology: check the errors the replication test was dropping The mock replica ignored its response write and the mock master ignored whatever Serve returned, so a broken mock would have shown up as a confusing timeout rather than a failure. Also drops the explicit listener close: grpc.Server.Stop already closes the listener it was given. --------- Co-authored-by: hzsunchao <hzsunchao@corp.netease.com> Co-authored-by: Chris Lu <chris.lu@gmail.com>
222 lines
7.4 KiB
Go
222 lines
7.4 KiB
Go
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)
|
|
}
|
|
}
|