mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-26 18:04:33 +00:00
* fix 11400 * persist failed-recovery quarantine and harden rollback - record the unavailable state in a .unavailable marker, fsync it, and re-arm it on load so a restart cannot serve an unverified pair - quarantine the volume so heartbeats stop advertising it - block MarkVolumeWritable while unavailable, rechecked under noWriteLock - fail every request of a failed batch, not only the succeeded ones - restore the needle map and truncate .dat on inline fsync rollback failure - add truncateIndex for the sorted-file needle map - mirror the fail-closed semantics in the Rust volume server * volume: erase rolled-back mappings instead of leaving tombstones A rolled-back batch or failed inline write used Delete() to undo a needle that did not exist beforehand, leaving a tombstoned map entry whose stale offset makes the next write to that needle fail reading a header that no longer exists. Add removeMapping/restoreMapping to the mappers so recovery erases entries that were absent before the batch and reinstates the exact prior offset/size for ones that were, including tombstones. The index row still goes through Delete so a replay forgets the needle. * volume: gate bulk readers on unavailable and fsync the marker's dir - fsync_dir(&self.dir) synced the volume dir's parent, not the dir holding .unavailable; pass the marker path so the create survives a host crash - export UnavailableError and check it in ReadAllNeedles, VolumeTailSender, VolumeIncrementalCopy, and IncrementalBackup so replica-sync paths cannot stream or append data from an unverified .dat/.idx pair; mirror on the Rust side via read_dat_slice, read_all_needles, dat_scan_plan, and the incremental-copy handler * volume: drop issue references from comments near touched code * volume: stop active scans when the volume becomes unavailable The stream entry-point checks ran once per RPC, so a volume quarantined by a failed recovery mid-scan kept serving data. Recheck availability per needle/chunk on the detached read paths: tail scan and heartbeat, read-all, incremental copy, incremental backup writes, and the Rust StreamingBody chunk reads. Rust incremental copy also rejects a quarantined volume before sync_to_disk touches the backend. --------- Co-authored-by: Chris Lu <chris.lu@gmail.com>
545 lines
19 KiB
Go
545 lines
19 KiB
Go
package storage
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/backend"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/types"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// countingBackend counts Sync calls and can be made to fail them.
|
|
type countingBackend struct {
|
|
backend.BackendStorageFile
|
|
syncCount int
|
|
syncErr error
|
|
syncErrOnce bool
|
|
truncateErr error
|
|
truncateCount int
|
|
}
|
|
|
|
func (b *countingBackend) Sync() error {
|
|
b.syncCount++
|
|
if b.syncErr != nil {
|
|
err := b.syncErr
|
|
if b.syncErrOnce {
|
|
b.syncErr = nil
|
|
b.syncErrOnce = false
|
|
}
|
|
return err
|
|
}
|
|
return b.BackendStorageFile.Sync()
|
|
}
|
|
|
|
func (b *countingBackend) Truncate(off int64) error {
|
|
b.truncateCount++
|
|
if b.truncateErr != nil {
|
|
return b.truncateErr
|
|
}
|
|
return b.BackendStorageFile.Truncate(off)
|
|
}
|
|
|
|
func newCountingVolume(t *testing.T) (*Volume, *countingBackend) {
|
|
return newCountingVolumeWithKind(t, NeedleMapInMemory)
|
|
}
|
|
|
|
func newCountingVolumeWithKind(t *testing.T, needleMapKind NeedleMapKind) (*Volume, *countingBackend) {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
v, err := NewVolume(dir, dir, "", 1, needleMapKind, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
|
|
require.NoError(t, err)
|
|
t.Cleanup(v.Close)
|
|
counting := &countingBackend{BackendStorageFile: v.DataBackend}
|
|
v.DataBackend = counting
|
|
return v, counting
|
|
}
|
|
|
|
func reopenCountingVolume(t *testing.T, v *Volume) *Volume {
|
|
t.Helper()
|
|
dir, dirIdx, id, needleMapKind := v.dir, v.dirIdx, v.Id, v.needleMapKind
|
|
v.Close()
|
|
reloaded, err := NewVolume(dir, dirIdx, "", id, needleMapKind,
|
|
&super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
|
|
require.NoError(t, err)
|
|
t.Cleanup(reloaded.Close)
|
|
return reloaded
|
|
}
|
|
|
|
// A durable write reaching a stopping server used to silently drop its fsync,
|
|
// so the ack promised durability the .dat did not have. It now flushes inline
|
|
// instead of queueing on the batch worker that is winding down.
|
|
func TestWriteNeedle2FsyncsInlineWhileStopping(t *testing.T) {
|
|
v, counting := newCountingVolume(t)
|
|
|
|
_, _, _, err := v.writeNeedle2(newRandomNeedle(1), true, true, true)
|
|
require.NoError(t, err, "a durable write must still be accepted while stopping")
|
|
require.Equal(t, 1, counting.syncCount, "the write should have been flushed inline")
|
|
|
|
// A non-durable write keeps the drain cheap: still accepted, still no fsync.
|
|
_, _, _, err = v.writeNeedle2(newRandomNeedle(2), true, false, true)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, counting.syncCount, "a write that did not ask for fsync must not pay for one")
|
|
}
|
|
|
|
func fixedNeedle(id uint64, data string) *needle.Needle {
|
|
n := new(needle.Needle)
|
|
n.Data = []byte(data)
|
|
n.Checksum = needle.NewCRC(n.Data)
|
|
n.Id = types.Uint64ToNeedleId(id)
|
|
return n
|
|
}
|
|
|
|
// An append we could not flush is not data to vouch for: the inline path takes
|
|
// it back off the .dat and fails the write. The needle map has to come back
|
|
// with it, or it would resolve to an offset past the truncated end.
|
|
func TestWriteNeedle2TruncatesWhenInlineFsyncFails(t *testing.T) {
|
|
v, counting := newCountingVolume(t)
|
|
|
|
kept := fixedNeedle(1, "first-copy")
|
|
_, _, _, err := v.writeNeedle2(kept, true, true, true)
|
|
require.NoError(t, err)
|
|
keptEntry, found := v.nm.Get(kept.Id)
|
|
require.True(t, found)
|
|
keptOffset, keptSize := keptEntry.Offset, keptEntry.Size
|
|
before, _, err := v.DataBackend.GetStat()
|
|
require.NoError(t, err)
|
|
|
|
counting.syncErr = errors.New("disk went away")
|
|
_, _, _, err = v.writeNeedle2(fixedNeedle(1, "second-copy"), true, true, true)
|
|
require.Error(t, err, "a write whose fsync failed must not be acked")
|
|
|
|
after, _, err := v.DataBackend.GetStat()
|
|
require.NoError(t, err)
|
|
require.Equal(t, before, after, "the unflushed append should have been truncated away")
|
|
|
|
now, found := v.nm.Get(kept.Id)
|
|
require.True(t, found, "the mapping the failed write replaced should be back")
|
|
require.Equal(t, keptOffset, now.Offset, "the index must not point past the truncated end")
|
|
require.Equal(t, keptSize, now.Size)
|
|
|
|
counting.syncErr = nil
|
|
readBack := new(needle.Needle)
|
|
readBack.Id = kept.Id
|
|
_, err = v.readNeedle(readBack, nil, nil)
|
|
require.NoError(t, err, "the surviving needle must still be readable")
|
|
require.Equal(t, []byte("first-copy"), readBack.Data)
|
|
}
|
|
|
|
// Same rollback for a needle the failed write introduced: with nothing to go
|
|
// back to, the mapping goes away rather than pointing past the end.
|
|
func TestWriteNeedle2DropsIndexOfUnflushedNewNeedle(t *testing.T) {
|
|
v, counting := newCountingVolume(t)
|
|
|
|
counting.syncErr = errors.New("disk went away")
|
|
fresh := fixedNeedle(7, "never-landed")
|
|
_, _, _, err := v.writeNeedle2(fresh, true, true, true)
|
|
require.Error(t, err)
|
|
|
|
if entry, found := v.nm.Get(fresh.Id); found {
|
|
require.True(t, entry.Size.IsDeleted(), "a needle that never reached the disk must not resolve")
|
|
}
|
|
|
|
counting.syncErr = nil
|
|
readBack := new(needle.Needle)
|
|
readBack.Id = fresh.Id
|
|
_, err = v.readNeedle(readBack, nil, nil)
|
|
require.Error(t, err, "reading the rolled-back needle should fail cleanly, not read past the end")
|
|
}
|
|
|
|
// The async durable path must roll back the mapper as well as the data tail
|
|
// when its batch fsync fails. The current worker only truncates the data file,
|
|
// so this test exposes the dangling mapping.
|
|
func TestWriteNeedle2RollsBackBatchFsyncFailure(t *testing.T) {
|
|
v, counting := newCountingVolume(t)
|
|
initialFileCount := v.nm.FileCount()
|
|
initialDeletedCount := v.nm.DeletedCount()
|
|
initialContentSize := v.nm.ContentSize()
|
|
initialDeletedSize := v.nm.DeletedSize()
|
|
initialMaxFileKey := v.nm.MaxFileKey()
|
|
|
|
before, _, err := v.DataBackend.GetStat()
|
|
require.NoError(t, err)
|
|
|
|
counting.syncErr = errors.New("batch fsync failed")
|
|
counting.syncErrOnce = true
|
|
fresh := fixedNeedle(8, "batch-never-landed")
|
|
_, _, _, err = v.writeNeedle2(fresh, true, true, false)
|
|
require.Error(t, err, "a batch whose fsync failed must not be acknowledged")
|
|
|
|
after, _, err := v.DataBackend.GetStat()
|
|
require.NoError(t, err)
|
|
require.Equal(t, before, after, "the failed batch append should be truncated")
|
|
|
|
if entry, found := v.nm.Get(fresh.Id); found {
|
|
require.True(t, entry.Size.IsDeleted(), "the failed batch must not leave a live mapping")
|
|
}
|
|
require.Equal(t, initialFileCount, v.nm.FileCount())
|
|
require.Equal(t, initialDeletedCount, v.nm.DeletedCount())
|
|
require.Equal(t, initialContentSize, v.nm.ContentSize())
|
|
require.Equal(t, initialDeletedSize, v.nm.DeletedSize())
|
|
require.Equal(t, initialMaxFileKey, v.nm.MaxFileKey())
|
|
}
|
|
|
|
func TestWriteNeedle2RollsBackBatchFsyncFailureAndReloadsNewNeedle(t *testing.T) {
|
|
v, counting := newCountingVolume(t)
|
|
counting.syncErr = errors.New("batch fsync failed")
|
|
counting.syncErrOnce = true
|
|
|
|
fresh := fixedNeedle(9, "batch-never-landed")
|
|
_, _, _, err := v.writeNeedle2(fresh, true, true, false)
|
|
require.Error(t, err)
|
|
|
|
reloaded := reopenCountingVolume(t, v)
|
|
if entry, found := reloaded.nm.Get(fresh.Id); found {
|
|
require.True(t, entry.Size.IsDeleted(), "the failed batch must not reload a live mapping")
|
|
}
|
|
|
|
readBack := new(needle.Needle)
|
|
readBack.Id = fresh.Id
|
|
_, err = reloaded.readNeedle(readBack, nil, nil)
|
|
require.Error(t, err, "the failed batch must not be readable after reload")
|
|
require.False(t, reloaded.IsReadOnly(), "a completed rollback should keep the volume healthy")
|
|
}
|
|
|
|
func TestWriteNeedle2RollsBackBatchFsyncFailureAndReloadsOverwrite(t *testing.T) {
|
|
v, counting := newCountingVolume(t)
|
|
|
|
kept := fixedNeedle(10, "first-copy")
|
|
_, _, _, err := v.writeNeedle2(kept, true, true, true)
|
|
require.NoError(t, err)
|
|
keptEntry, found := v.nm.Get(kept.Id)
|
|
require.True(t, found)
|
|
|
|
counting.syncErr = errors.New("batch fsync failed")
|
|
counting.syncErrOnce = true
|
|
_, _, _, err = v.writeNeedle2(fixedNeedle(10, "second-copy"), true, true, false)
|
|
require.Error(t, err)
|
|
|
|
now, found := v.nm.Get(kept.Id)
|
|
require.True(t, found)
|
|
require.Equal(t, keptEntry.Offset, now.Offset)
|
|
require.Equal(t, keptEntry.Size, now.Size)
|
|
|
|
reloaded := reopenCountingVolume(t, v)
|
|
readBack := new(needle.Needle)
|
|
readBack.Id = kept.Id
|
|
_, err = reloaded.readNeedle(readBack, nil, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte("first-copy"), readBack.Data)
|
|
}
|
|
|
|
func TestBatchDeleteRollsBackOnFsyncFailure(t *testing.T) {
|
|
v, counting := newCountingVolume(t)
|
|
|
|
kept := fixedNeedle(11, "delete-me")
|
|
_, _, _, err := v.writeNeedle2(kept, true, true, true)
|
|
require.NoError(t, err)
|
|
|
|
counting.syncErr = errors.New("batch fsync failed")
|
|
counting.syncErrOnce = true
|
|
deleteRequest := needle.NewAsyncRequest(&needle.Needle{Id: kept.Id}, false)
|
|
deleteRequest.ActualSize = needle.GetActualSize(0, v.Version())
|
|
require.True(t, v.asyncRequestAppend(deleteRequest))
|
|
_, _, _, err = deleteRequest.WaitComplete()
|
|
require.Error(t, err)
|
|
|
|
readBack := new(needle.Needle)
|
|
readBack.Id = kept.Id
|
|
_, err = v.readNeedle(readBack, nil, nil)
|
|
require.NoError(t, err, "a failed batch delete must restore the live mapping")
|
|
require.Equal(t, []byte("delete-me"), readBack.Data)
|
|
|
|
reloaded := reopenCountingVolume(t, v)
|
|
readBack = new(needle.Needle)
|
|
readBack.Id = kept.Id
|
|
_, err = reloaded.readNeedle(readBack, nil, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte("delete-me"), readBack.Data)
|
|
}
|
|
|
|
func TestBatchSyncFailureEntersFailClosedWhenTruncateFails(t *testing.T) {
|
|
v, counting := newCountingVolume(t)
|
|
counting.syncErr = errors.New("batch fsync failed")
|
|
counting.syncErrOnce = true
|
|
counting.truncateErr = errors.New("truncate failed")
|
|
|
|
fresh := fixedNeedle(12, "cannot-be-recovered")
|
|
_, _, _, err := v.writeNeedle2(fresh, true, true, false)
|
|
require.Error(t, err)
|
|
require.True(t, v.IsReadOnly(), "a failed recovery must make the volume unavailable")
|
|
require.NotNil(t, v.UnavailableError())
|
|
|
|
readBack := new(needle.Needle)
|
|
readBack.Id = fresh.Id
|
|
_, err = v.readNeedle(readBack, nil, nil)
|
|
require.Error(t, err, "an unavailable volume must reject reads")
|
|
|
|
_, _, _, err = v.writeNeedle2(fixedNeedle(13, "after-failure"), true, true, false)
|
|
require.Error(t, err, "an unavailable volume must reject later writes")
|
|
require.Equal(t, 1, counting.truncateCount)
|
|
}
|
|
|
|
func TestUnavailableVolumeCannotBeMarkedWritable(t *testing.T) {
|
|
dir := t.TempDir()
|
|
store := newIdxSplitStore(t, dir, dir)
|
|
const vid = needle.VolumeId(19)
|
|
require.NoError(t, store.AddVolume(vid, "", NeedleMapInMemory, "000", "", 0,
|
|
needle.GetCurrentVersion(), 0, types.HardDriveType, 0))
|
|
|
|
v := store.findVolume(vid)
|
|
require.NotNil(t, v)
|
|
v.markIoUnavailable(errors.New("batch recovery failed"))
|
|
|
|
require.Error(t, store.MarkVolumeWritable(vid), "manual writable transition must not bypass quarantine")
|
|
require.NotNil(t, v.UnavailableError())
|
|
}
|
|
|
|
func TestMixedBatchSyncFailureRollsBackAsOneUnit(t *testing.T) {
|
|
v, counting := newCountingVolume(t)
|
|
|
|
overwritten := fixedNeedle(14, "keep-overwrite")
|
|
deleted := fixedNeedle(15, "keep-delete")
|
|
_, _, _, err := v.writeNeedle2(overwritten, true, true, true)
|
|
require.NoError(t, err)
|
|
_, _, _, err = v.writeNeedle2(deleted, true, true, true)
|
|
require.NoError(t, err)
|
|
|
|
counting.syncErr = errors.New("batch fsync failed")
|
|
counting.syncErrOnce = true
|
|
replacement := fixedNeedle(14, "failed-overwrite")
|
|
fresh := fixedNeedle(16, "failed-new")
|
|
deleteRequest := needle.NewAsyncRequest(&needle.Needle{Id: deleted.Id}, false)
|
|
requests := []*needle.AsyncRequest{
|
|
needle.NewAsyncRequest(replacement, true),
|
|
deleteRequest,
|
|
needle.NewAsyncRequest(fresh, true),
|
|
}
|
|
v.processBatch(requests)
|
|
|
|
for _, request := range requests {
|
|
_, _, _, err = request.WaitComplete()
|
|
require.Error(t, err, "every request in a failed batch must fail")
|
|
}
|
|
|
|
readBack := new(needle.Needle)
|
|
readBack.Id = overwritten.Id
|
|
_, err = v.readNeedle(readBack, nil, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte("keep-overwrite"), readBack.Data)
|
|
|
|
readBack = new(needle.Needle)
|
|
readBack.Id = deleted.Id
|
|
_, err = v.readNeedle(readBack, nil, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte("keep-delete"), readBack.Data)
|
|
|
|
readBack = new(needle.Needle)
|
|
readBack.Id = fresh.Id
|
|
_, err = v.readNeedle(readBack, nil, nil)
|
|
require.Error(t, err)
|
|
|
|
reloaded := reopenCountingVolume(t, v)
|
|
readBack = new(needle.Needle)
|
|
readBack.Id = overwritten.Id
|
|
_, err = reloaded.readNeedle(readBack, nil, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte("keep-overwrite"), readBack.Data)
|
|
|
|
readBack = new(needle.Needle)
|
|
readBack.Id = deleted.Id
|
|
_, err = reloaded.readNeedle(readBack, nil, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte("keep-delete"), readBack.Data)
|
|
|
|
readBack = new(needle.Needle)
|
|
readBack.Id = fresh.Id
|
|
_, err = reloaded.readNeedle(readBack, nil, nil)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestWriteNeedle2EntersFailClosedWhenInlineRollbackFails(t *testing.T) {
|
|
v, counting := newCountingVolume(t)
|
|
counting.syncErr = errors.New("inline fsync failed")
|
|
counting.syncErrOnce = true
|
|
counting.truncateErr = errors.New("truncate failed")
|
|
|
|
_, _, _, err := v.writeNeedle2(fixedNeedle(20, "cannot-be-recovered"), true, true, true)
|
|
require.Error(t, err)
|
|
require.NotNil(t, v.UnavailableError())
|
|
|
|
readBack := new(needle.Needle)
|
|
readBack.Id = 20
|
|
_, err = v.readNeedle(readBack, nil, nil)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestUnavailableVolumeStaysUnavailableAfterReload(t *testing.T) {
|
|
v, _ := newCountingVolume(t)
|
|
v.markIoUnavailable(errors.New("batch recovery failed"))
|
|
|
|
reloaded := reopenCountingVolume(t, v)
|
|
require.NotNil(t, reloaded.UnavailableError(), "the unavailable state must survive a reload")
|
|
require.True(t, reloaded.IsReadOnly())
|
|
|
|
_, _, quarantined := reloaded.getIoErrorState()
|
|
require.True(t, quarantined, "a reloaded unavailable volume must stay out of heartbeats")
|
|
|
|
readBack := new(needle.Needle)
|
|
readBack.Id = 1
|
|
_, err := reloaded.readNeedle(readBack, nil, nil)
|
|
require.Error(t, err)
|
|
_, _, _, err = reloaded.writeNeedle2(fixedNeedle(1, "after-reload"), true, true, false)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestBatchFsyncRollbackWithLevelDbMapper(t *testing.T) {
|
|
v, counting := newCountingVolumeWithKind(t, NeedleMapLevelDb)
|
|
|
|
kept := fixedNeedle(17, "leveldb-old")
|
|
_, _, _, err := v.writeNeedle2(kept, true, true, true)
|
|
require.NoError(t, err)
|
|
|
|
counting.syncErr = errors.New("batch fsync failed")
|
|
counting.syncErrOnce = true
|
|
_, _, _, err = v.writeNeedle2(fixedNeedle(17, "leveldb-new"), true, true, false)
|
|
require.Error(t, err)
|
|
|
|
readBack := new(needle.Needle)
|
|
readBack.Id = kept.Id
|
|
_, err = v.readNeedle(readBack, nil, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte("leveldb-old"), readBack.Data)
|
|
|
|
reloaded := reopenCountingVolume(t, v)
|
|
readBack = new(needle.Needle)
|
|
readBack.Id = kept.Id
|
|
_, err = reloaded.readNeedle(readBack, nil, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte("leveldb-old"), readBack.Data)
|
|
}
|
|
|
|
func TestBatchFsyncRollbackRestoresOriginalValueAfterRepeatedNeedleId(t *testing.T) {
|
|
v, counting := newCountingVolume(t)
|
|
|
|
kept := fixedNeedle(18, "original-value")
|
|
_, _, _, err := v.writeNeedle2(kept, true, true, true)
|
|
require.NoError(t, err)
|
|
|
|
counting.syncErr = errors.New("batch fsync failed")
|
|
counting.syncErrOnce = true
|
|
firstReplacement := fixedNeedle(18, "first-failed-value")
|
|
secondReplacement := fixedNeedle(18, "second-failed-value")
|
|
requests := []*needle.AsyncRequest{
|
|
needle.NewAsyncRequest(firstReplacement, true),
|
|
needle.NewAsyncRequest(secondReplacement, true),
|
|
}
|
|
v.processBatch(requests)
|
|
|
|
for _, request := range requests {
|
|
_, _, _, err = request.WaitComplete()
|
|
require.Error(t, err)
|
|
}
|
|
|
|
readBack := new(needle.Needle)
|
|
readBack.Id = kept.Id
|
|
_, err = v.readNeedle(readBack, nil, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte("original-value"), readBack.Data)
|
|
|
|
reloaded := reopenCountingVolume(t, v)
|
|
readBack = new(needle.Needle)
|
|
readBack.Id = kept.Id
|
|
_, err = reloaded.readNeedle(readBack, nil, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte("original-value"), readBack.Data)
|
|
}
|
|
|
|
// Rolling back a first-time write must not leave a tombstoned map entry: the
|
|
// stale offset would make the next write to that needle fail reading a header
|
|
// that no longer exists.
|
|
func TestBatchRollbackLeavesNoPhantomMapping(t *testing.T) {
|
|
v, counting := newCountingVolume(t)
|
|
|
|
counting.syncErr = errors.New("batch fsync failed")
|
|
counting.syncErrOnce = true
|
|
fresh := fixedNeedle(24, "batch-never-landed")
|
|
_, _, _, err := v.writeNeedle2(fresh, true, true, false)
|
|
require.Error(t, err)
|
|
|
|
_, _, _, err = v.writeNeedle2(fresh, true, true, false)
|
|
require.NoError(t, err, "rewriting a rolled-back needle must succeed")
|
|
|
|
readBack := new(needle.Needle)
|
|
readBack.Id = fresh.Id
|
|
_, err = v.readNeedle(readBack, nil, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte("batch-never-landed"), readBack.Data)
|
|
}
|
|
|
|
// A request that already failed on its own must still surface the batch
|
|
// failure: keeping its earlier error would hide that nothing was persisted,
|
|
// or that recovery itself failed.
|
|
func TestFailedBatchMarksEveryRequestFailed(t *testing.T) {
|
|
v, counting := newCountingVolume(t)
|
|
|
|
kept := fixedNeedle(22, "original")
|
|
_, _, _, err := v.writeNeedle2(kept, true, true, true)
|
|
require.NoError(t, err)
|
|
|
|
counting.syncErr = errors.New("batch fsync failed")
|
|
counting.syncErrOnce = true
|
|
|
|
badCookie := fixedNeedle(22, "wrong-cookie")
|
|
badCookie.Cookie = kept.Cookie + 1
|
|
requests := []*needle.AsyncRequest{
|
|
needle.NewAsyncRequest(badCookie, true),
|
|
needle.NewAsyncRequest(fixedNeedle(23, "fresh"), true),
|
|
}
|
|
v.processBatch(requests)
|
|
|
|
for _, request := range requests {
|
|
_, _, _, err = request.WaitComplete()
|
|
require.ErrorContains(t, err, "batch fsync failed")
|
|
}
|
|
}
|
|
|
|
// The quarantine is what CollectHeartbeat keys on: an unavailable volume must
|
|
// not be announced to the master at all.
|
|
func TestUnavailableVolumeIsSkippedInHeartbeat(t *testing.T) {
|
|
store := newTestStore(t, 1)
|
|
v := mountTestVolume(t, store.Locations[0], 1, "pics")
|
|
fillTestVolume(t, v)
|
|
v.markIoUnavailable(errors.New("batch recovery failed"))
|
|
|
|
heartbeat := store.CollectHeartbeat()
|
|
for _, m := range heartbeat.Volumes {
|
|
require.NotEqual(t, uint32(1), m.Id, "a failed-recovery volume must not be announced")
|
|
}
|
|
}
|
|
|
|
// The pre-stop drain exists so writes already assigned to this server land.
|
|
// Refusing them once stopping would turn every rolling restart into client
|
|
// write failures for the length of the drain.
|
|
func TestStoreWriteVolumeNeedleStaysDurableWhileStopping(t *testing.T) {
|
|
dir := t.TempDir()
|
|
store := newIdxSplitStore(t, dir, dir)
|
|
const vid = needle.VolumeId(1)
|
|
require.NoError(t, store.AddVolume(vid, "", NeedleMapInMemory, "000", "", 0, needle.GetCurrentVersion(), 0, types.HardDriveType, 0))
|
|
|
|
v := store.findVolume(vid)
|
|
require.NotNil(t, v)
|
|
counting := &countingBackend{BackendStorageFile: v.DataBackend}
|
|
v.DataBackend = counting
|
|
|
|
store.SetStopping()
|
|
counting.syncCount = 0
|
|
|
|
isUnchanged, err := store.WriteVolumeNeedle(vid, newRandomNeedle(1), true, true)
|
|
require.NoError(t, err, "the drain must keep accepting durable writes")
|
|
require.False(t, isUnchanged)
|
|
require.Equal(t, 1, counting.syncCount, "the accepted write must actually be on disk")
|
|
}
|