Files
seaweedfs/weed/storage/blockvol/v2bridge/transfer_adversarial_test.go
T
pingqiuandClaude Opus 4.6 c7eb87c587 feat: Phase 09 — V2 execution primitives and production closure
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>
2026-04-02 16:25:23 -07:00

244 lines
7.7 KiB
Go

package v2bridge
import (
"bytes"
"net"
"testing"
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol"
)
// ============================================================
// Phase 09 P1: Adversarial tests for full-base rebuild transfer
//
// These tests verify safety under failure and concurrent mutation:
// 1. Server dies mid-transfer: replica extent must not be half-installed
// 2. Concurrent writes during rebuild: achievedLSN correct, no corruption
// 3. Double rebuild (cancel + restart): second rebuild sees clean state
// ============================================================
// --- Adversarial 1: Server dies mid-transfer, replica state must be safe ---
func TestAdversarial_ServerDiesMidTransfer_ReplicaStateClean(t *testing.T) {
dir := t.TempDir()
replicaVol := createTestVolNamed(t, dir, "replica.blockvol")
defer replicaVol.Close()
// Write pre-existing data on replica (simulates stale state).
for i := 0; i < 5; i++ {
replicaVol.WriteLBA(uint64(i), makeBlock(byte('R')))
}
replicaStateBefore := NewReader(replicaVol).ReadState()
// Fake server: sends a few extent chunks, then drops connection
// before sending MsgRebuildDone. The extent should NOT be installed.
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
go func() {
conn, err := ln.Accept()
if err != nil {
return
}
// Read request frame (discard).
blockvol.ReadFrame(conn)
// Send 3 extent chunks (partial extent).
for i := 0; i < 3; i++ {
chunk := make([]byte, 4096)
for j := range chunk {
chunk[j] = byte('X') // different from replica's 'R'
}
blockvol.WriteFrame(conn, blockvol.MsgRebuildExtent, chunk)
}
// Drop connection — no MsgRebuildDone sent.
conn.Close()
}()
executor := NewExecutor(replicaVol, ln.Addr().String())
_, err = executor.TransferFullBase(100)
if err == nil {
t.Fatal("should fail when server dies mid-transfer")
}
// KEY ASSERTION: replica's pre-existing data must still be readable.
// The partial extent must NOT have been committed.
replicaStateAfter := NewReader(replicaVol).ReadState()
// WALHeadLSN should not have been reset by a partial install.
if replicaStateAfter.WALHeadLSN != replicaStateBefore.WALHeadLSN {
t.Fatalf("WALHeadLSN changed after failed transfer: before=%d after=%d",
replicaStateBefore.WALHeadLSN, replicaStateAfter.WALHeadLSN)
}
// Pre-existing data should still be readable (stale 'R' blocks).
blockSize := replicaVol.Info().BlockSize
for i := 0; i < 5; i++ {
data, err := replicaVol.ReadLBA(uint64(i), blockSize)
if err != nil {
t.Fatalf("ReadLBA(%d) after failed transfer: %v", i, err)
}
expected := makeBlock(byte('R'))
if !bytes.Equal(data, expected) {
t.Fatalf("LBA %d corrupted after failed transfer: got[0]=%d want='R'(%d)",
i, data[0], byte('R'))
}
}
t.Log("adversarial 1: server died mid-transfer — replica state is clean, pre-existing data intact")
}
// --- Adversarial 2: Concurrent writes during rebuild ---
func TestAdversarial_ConcurrentWritesDuringRebuild(t *testing.T) {
dir := t.TempDir()
primaryVol := createTestVolNamed(t, dir, "primary.blockvol")
defer primaryVol.Close()
// Write initial data + flush.
for i := 0; i < 10; i++ {
primaryVol.WriteLBA(uint64(i), makeBlock(byte('A'+i%26)))
}
primaryVol.ForceFlush()
primaryStateBefore := NewReader(primaryVol).ReadState()
t.Logf("primary before extra writes: head=%d checkpoint=%d",
primaryStateBefore.WALHeadLSN, primaryStateBefore.CheckpointLSN)
// Start rebuild server.
rebuildServer, err := blockvol.NewRebuildServer(primaryVol, "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
rebuildServer.Serve()
defer rebuildServer.Stop()
// Write MORE data on primary AFTER rebuild server started.
// These writes happen while the rebuild transfer is in progress.
for i := 10; i < 20; i++ {
primaryVol.WriteLBA(uint64(i), makeBlock(byte('Z')))
}
primaryVol.ForceFlush()
primaryStateAfter := NewReader(primaryVol).ReadState()
t.Logf("primary after extra writes: head=%d checkpoint=%d",
primaryStateAfter.WALHeadLSN, primaryStateAfter.CheckpointLSN)
// Replica: empty vol.
replicaVol := createTestVolNamed(t, dir, "replica.blockvol")
defer replicaVol.Close()
executor := NewExecutor(replicaVol, rebuildServer.Addr())
// Transfer with the ORIGINAL target (before extra writes).
achievedLSN, err := executor.TransferFullBase(primaryStateBefore.CommittedLSN)
if err != nil {
t.Fatalf("TransferFullBase: %v", err)
}
t.Logf("achievedLSN=%d (target was %d)", achievedLSN, primaryStateBefore.CommittedLSN)
// achievedLSN must be >= original target.
if achievedLSN < primaryStateBefore.CommittedLSN {
t.Fatalf("achievedLSN=%d < target=%d", achievedLSN, primaryStateBefore.CommittedLSN)
}
// The rebuild server's snapshot should include ALL flushed data
// (including the extra writes), so achievedLSN should be >= the
// extra writes' head.
if achievedLSN < primaryStateAfter.CommittedLSN {
t.Logf("note: achievedLSN=%d < post-write committed=%d (snapshot was taken between flushes)",
achievedLSN, primaryStateAfter.CommittedLSN)
}
// Verify data integrity: at minimum, the original 10 LBAs must match.
verifyLBAMatch(t, primaryVol, replicaVol, 10)
t.Logf("adversarial 2: concurrent writes during rebuild — achievedLSN=%d, data integrity verified", achievedLSN)
}
// --- Adversarial 3: Double rebuild (first cancelled, second must see clean state) ---
func TestAdversarial_DoubleRebuild_SecondSeesCleanState(t *testing.T) {
dir := t.TempDir()
primaryVol := createTestVolNamed(t, dir, "primary.blockvol")
defer primaryVol.Close()
for i := 0; i < 10; i++ {
primaryVol.WriteLBA(uint64(i), makeBlock(byte('A'+i%26)))
}
primaryVol.ForceFlush()
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()
// First rebuild: succeeds.
executor1 := NewExecutor(replicaVol, rebuildServer.Addr())
achievedLSN1, err := executor1.TransferFullBase(0)
if err != nil {
t.Fatalf("first rebuild: %v", err)
}
t.Logf("first rebuild: achievedLSN=%d", achievedLSN1)
// Verify first rebuild installed data correctly.
verifyLBAMatch(t, primaryVol, replicaVol, 10)
// Now: primary writes NEW data (different pattern).
for i := 0; i < 10; i++ {
primaryVol.WriteLBA(uint64(i), makeBlock(byte('Z')))
}
primaryVol.ForceFlush()
// Restart rebuild server (simulates new rebuild after epoch bump).
rebuildServer.Stop()
rebuildServer2, err := blockvol.NewRebuildServer(primaryVol, "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
rebuildServer2.Serve()
defer rebuildServer2.Stop()
// Second rebuild on SAME replica (simulates cancelled first + restart).
executor2 := NewExecutor(replicaVol, rebuildServer2.Addr())
achievedLSN2, err := executor2.TransferFullBase(0)
if err != nil {
t.Fatalf("second rebuild: %v", err)
}
t.Logf("second rebuild: achievedLSN=%d", achievedLSN2)
// Second rebuild's achievedLSN must be >= first rebuild's.
if achievedLSN2 < achievedLSN1 {
t.Fatalf("second rebuild achievedLSN=%d < first=%d — state regression",
achievedLSN2, achievedLSN1)
}
// Verify: replica now has the NEW data ('Z'), not the old ('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) after second rebuild: %v", i, err)
}
expected := makeBlock(byte('Z'))
if !bytes.Equal(data, expected) {
t.Fatalf("LBA %d after second rebuild: got[0]=%d want='Z'(%d) — stale first-rebuild data leaked",
i, data[0], byte('Z'))
}
}
t.Logf("adversarial 3: double rebuild — second rebuild installed new data correctly, no stale state from first")
}