mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-29 08:20:42 +01:00
Engine execution layer for V2 replication protocol: - RebuildInstaller: full state handoff (dirty map, WAL, superblock, flusher) - TruncateToLSN: exact safety predicate (checkpointLSN == truncateLSN), ErrTruncationUnsafe escalation to NeedsRebuild - SyncReceiverProgress: unconditional Store for post-rebuild alignment - V2StatusSnapshot: CommittedLSN = nextLSN-1 for sync_all V2 bridge real I/O executors: - TransferFullBase: TCP streaming + RebuildInstaller + second catch-up - TransferSnapshot: SHA-256 verified streaming to disk - TruncateWAL: ErrTruncationUnsafe detection + escalation - StreamWALEntries: rebuild-mode TCP apply Engine executor interfaces: - CatchUpIO.TruncateWAL, RebuildIO.TransferFullBase returns achievedLSN - CatchUpExecutor truncation-only skip, NeedsRebuild escalation - RebuildExecutor uses achievedLSN for progress tracking Design docs reorganized: superseded planning docs removed, protocol truths and closure map added. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
195 lines
5.9 KiB
Go
195 lines
5.9 KiB
Go
package v2bridge
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol"
|
|
)
|
|
|
|
// ============================================================
|
|
// Phase 09 P2: Adversarial tests for snapshot transfer
|
|
// ============================================================
|
|
|
|
// --- Adversarial 1: Snapshot on non-empty replica with higher state ---
|
|
|
|
func TestAdversarial_SnapshotOverwritesHigherState(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
// Primary: 10 entries, flush → checkpoint at 10.
|
|
primaryVol, checkpointLSN := setupSnapshotPrimary(t, dir, 10)
|
|
defer primaryVol.Close()
|
|
|
|
rebuildServer, err := blockvol.NewRebuildServer(primaryVol, "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rebuildServer.Serve()
|
|
defer rebuildServer.Stop()
|
|
|
|
// Replica: has MORE data than primary (20 entries, different pattern).
|
|
replicaVol := createTestVolNamed(t, dir, "replica.blockvol")
|
|
defer replicaVol.Close()
|
|
|
|
for i := 0; i < 20; i++ {
|
|
replicaVol.WriteLBA(uint64(i), makeBlock(byte('Z')))
|
|
}
|
|
|
|
replicaBefore := NewReader(replicaVol).ReadState()
|
|
t.Logf("replica before: head=%d (higher than primary checkpoint=%d)",
|
|
replicaBefore.WALHeadLSN, checkpointLSN)
|
|
|
|
executor := NewExecutor(replicaVol, rebuildServer.Addr())
|
|
if err := executor.TransferSnapshot(checkpointLSN); err != nil {
|
|
t.Fatalf("TransferSnapshot: %v", err)
|
|
}
|
|
|
|
// Replica must now match primary (not have its old 'Z' data).
|
|
verifyLBAMatch(t, primaryVol, replicaVol, 10)
|
|
|
|
// Runtime must converge to snapshot boundary, NOT to old higher state.
|
|
replicaAfter := NewReader(replicaVol).ReadState()
|
|
if replicaAfter.WALHeadLSN != checkpointLSN {
|
|
t.Fatalf("WALHeadLSN=%d, want %d (must converge DOWN to snapshot)",
|
|
replicaAfter.WALHeadLSN, checkpointLSN)
|
|
}
|
|
|
|
t.Logf("adversarial 1: snapshot correctly overwrote higher replica state (%d → %d)",
|
|
replicaBefore.WALHeadLSN, checkpointLSN)
|
|
}
|
|
|
|
// --- Adversarial 2: Full-base achievedLSN > target with bounded second catch-up ---
|
|
|
|
func TestAdversarial_FullBase_SecondCatchUpBounded(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
primaryVol := createTestVolNamed(t, dir, "primary.blockvol")
|
|
defer primaryVol.Close()
|
|
|
|
// Write 10, flush → checkpoint at 10.
|
|
for i := 0; i < 10; i++ {
|
|
primaryVol.WriteLBA(uint64(i), makeBlock(byte('A'+i%26)))
|
|
}
|
|
primaryVol.ForceFlush()
|
|
|
|
// Write 10 more (tail, unflushed).
|
|
for i := 10; i < 20; i++ {
|
|
primaryVol.WriteLBA(uint64(i), makeBlock(byte('T')))
|
|
}
|
|
|
|
// Capture target BEFORE rebuild server starts.
|
|
targetLSN := uint64(10) // only want up to the checkpoint
|
|
|
|
rebuildServer, err := blockvol.NewRebuildServer(primaryVol, "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rebuildServer.Serve()
|
|
defer rebuildServer.Stop()
|
|
|
|
replicaVol := createTestVolNamed(t, dir, "replica.blockvol")
|
|
defer replicaVol.Close()
|
|
|
|
executor := NewExecutor(replicaVol, rebuildServer.Addr())
|
|
|
|
// TransferFullBase with targetLSN=10 (rebuild server will flush + snapshot
|
|
// everything including the tail entries → achievedLSN will be > 10).
|
|
achievedLSN, err := executor.TransferFullBase(targetLSN)
|
|
if err != nil {
|
|
t.Fatalf("TransferFullBase: %v", err)
|
|
}
|
|
|
|
t.Logf("full-base: target=%d achieved=%d (server included tail entries)",
|
|
targetLSN, achievedLSN)
|
|
|
|
// achievedLSN must be >= target (may be higher due to server flush).
|
|
if achievedLSN < targetLSN {
|
|
t.Fatalf("achievedLSN=%d < target=%d", achievedLSN, targetLSN)
|
|
}
|
|
|
|
// The second catch-up should have been bounded to targetLSN.
|
|
// Any entries applied should not exceed target.
|
|
// (TransferFullBase's secondCatchUp bounds to targetLSN.)
|
|
|
|
// Verify at least the first 10 LBAs match.
|
|
verifyLBAMatch(t, primaryVol, replicaVol, 10)
|
|
|
|
t.Logf("adversarial 2: full-base second catch-up bounded correctly (target=%d achieved=%d)",
|
|
targetLSN, achievedLSN)
|
|
}
|
|
|
|
// --- Adversarial 3: Double snapshot rebuild on same replica ---
|
|
|
|
func TestAdversarial_DoubleSnapshotRebuild(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
primaryVol := createTestVolNamed(t, dir, "primary.blockvol")
|
|
defer primaryVol.Close()
|
|
|
|
// First era: 'A' data.
|
|
for i := 0; i < 10; i++ {
|
|
primaryVol.WriteLBA(uint64(i), makeBlock(byte('A'+i%26)))
|
|
}
|
|
primaryVol.ForceFlush()
|
|
checkpoint1 := NewReader(primaryVol).ReadState().CheckpointLSN
|
|
|
|
rebuildServer1, err := blockvol.NewRebuildServer(primaryVol, "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rebuildServer1.Serve()
|
|
|
|
replicaVol := createTestVolNamed(t, dir, "replica.blockvol")
|
|
defer replicaVol.Close()
|
|
|
|
// First snapshot rebuild.
|
|
executor1 := NewExecutor(replicaVol, rebuildServer1.Addr())
|
|
if err := executor1.TransferSnapshot(checkpoint1); err != nil {
|
|
t.Fatalf("first snapshot: %v", err)
|
|
}
|
|
rebuildServer1.Stop()
|
|
|
|
verifyLBAMatch(t, primaryVol, replicaVol, 10)
|
|
t.Logf("first rebuild: checkpoint=%d, data='A' verified", checkpoint1)
|
|
|
|
// Second era: overwrite with 'Z' data.
|
|
for i := 0; i < 10; i++ {
|
|
primaryVol.WriteLBA(uint64(i), makeBlock(byte('Z')))
|
|
}
|
|
primaryVol.ForceFlush()
|
|
checkpoint2 := NewReader(primaryVol).ReadState().CheckpointLSN
|
|
|
|
rebuildServer2, err := blockvol.NewRebuildServer(primaryVol, "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rebuildServer2.Serve()
|
|
defer rebuildServer2.Stop()
|
|
|
|
// Second snapshot rebuild on SAME replica.
|
|
executor2 := NewExecutor(replicaVol, rebuildServer2.Addr())
|
|
if err := executor2.TransferSnapshot(checkpoint2); err != nil {
|
|
t.Fatalf("second snapshot: %v", err)
|
|
}
|
|
|
|
// Replica must have 'Z' data, not stale 'A'.
|
|
blockSize := replicaVol.Info().BlockSize
|
|
for i := 0; i < 10; i++ {
|
|
data, err := replicaVol.ReadLBA(uint64(i), blockSize)
|
|
if err != nil {
|
|
t.Fatalf("ReadLBA(%d): %v", i, err)
|
|
}
|
|
if data[0] != byte('Z') {
|
|
t.Fatalf("LBA %d: got %c, want 'Z' — stale first-rebuild data leaked", i, data[0])
|
|
}
|
|
}
|
|
|
|
// Runtime converged to second checkpoint.
|
|
state := NewReader(replicaVol).ReadState()
|
|
if state.CheckpointLSN != checkpoint2 {
|
|
t.Fatalf("checkpoint=%d, want %d", state.CheckpointLSN, checkpoint2)
|
|
}
|
|
|
|
t.Logf("adversarial 3: double snapshot — second rebuild correctly replaced first (checkpoint %d → %d)",
|
|
checkpoint1, checkpoint2)
|
|
}
|