mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
V2 runtime packages: - sw-block/runtime/masterv2: identity authority (desired state, heartbeat handling, promotion arbitration via SelectPromotionCandidate) - sw-block/runtime/volumev2: per-volume micro-cluster shell (node, orchestrator, control session, iSCSI frontend, takeover gate, failover session + driver, replica summary reconstruction) - sw-block/runtime/purev2: RF1 execution shell (engine + store + dispatcher + local boundary observations) - sw-block/runtime/protocolv2: three-channel separation (heartbeat/assignment/query + replica summary) V2 binaries: - sw-block/cmd/v2singleblock: single-node RF1 block server - sw-block/cmd/purev2rf1: minimal RF1 runtime binary Milestone capabilities: - RF1 write/read/sync with engine-driven mode projection - masterv2 ↔ volumev2 heartbeat convergence + assignment reissue - Promotion query with fresh CommittedLSN/WALHeadLSN evidence - Replica summary for bounded takeover reconstruction - Primary-loss reconstruction from peer summaries (fail-closed gate) - In-process failover driver with session observability - Local boundary observations feed engine (Committed/Durable/Checkpoint) Design docs: - v2-two-loop-protocol.md: identity vs data-control separation - v2-automata-ownership-map.md: event/command ownership split - v2-loop1-surface-draft.md: heartbeat/query/assignment field spec - v2-volumev2-single-node-mvp.md: target layering - v2-kernel-closure-review.md: per-volume micro-cluster principle - v2-pure-runtime-rf1-bootstrap.md, v2-capability-map.md, v2-proof-and-retest-pyramid.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
139 lines
3.5 KiB
Go
139 lines
3.5 KiB
Go
package volumev2
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/sw-block/runtime/masterv2"
|
|
"github.com/seaweedfs/seaweedfs/sw-block/runtime/protocolv2"
|
|
)
|
|
|
|
func TestPrimaryLoss_ReconstructsBoundedTruthFromReplicaSummaries(t *testing.T) {
|
|
master := masterv2.New(masterv2.Config{})
|
|
candidate, err := master.SelectPromotionCandidate([]masterv2.PromotionQueryResponse{
|
|
{
|
|
VolumeName: "vol-a",
|
|
NodeID: "node-b",
|
|
CommittedLSN: 15,
|
|
WALHeadLSN: 18,
|
|
Eligible: true,
|
|
},
|
|
{
|
|
VolumeName: "vol-a",
|
|
NodeID: "node-c",
|
|
CommittedLSN: 12,
|
|
WALHeadLSN: 13,
|
|
Eligible: true,
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("select candidate: %v", err)
|
|
}
|
|
|
|
truth, err := ReconstructPrimaryTruth(candidate.NodeID, []protocolv2.ReplicaSummaryResponse{
|
|
{
|
|
VolumeName: "vol-a",
|
|
NodeID: "node-b",
|
|
Epoch: 4,
|
|
Role: "replica",
|
|
Mode: "replica_ready",
|
|
CommittedLSN: 15,
|
|
DurableLSN: 15,
|
|
CheckpointLSN: 10,
|
|
TargetLSN: 40,
|
|
AchievedLSN: 30,
|
|
RecoveryPhase: "catching_up",
|
|
LastBarrierOK: true,
|
|
Eligible: true,
|
|
},
|
|
{
|
|
VolumeName: "vol-a",
|
|
NodeID: "node-c",
|
|
Epoch: 4,
|
|
Role: "replica",
|
|
Mode: "replica_ready",
|
|
CommittedLSN: 12,
|
|
DurableLSN: 12,
|
|
CheckpointLSN: 8,
|
|
TargetLSN: 40,
|
|
AchievedLSN: 20,
|
|
RecoveryPhase: "catching_up",
|
|
LastBarrierOK: true,
|
|
Eligible: true,
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("reconstruct truth: %v", err)
|
|
}
|
|
|
|
if truth.PrimaryNodeID != "node-b" {
|
|
t.Fatalf("primary_node=%q, want node-b", truth.PrimaryNodeID)
|
|
}
|
|
if truth.CommittedLSN != 15 {
|
|
t.Fatalf("committed_lsn=%d, want 15", truth.CommittedLSN)
|
|
}
|
|
if truth.DurableLSN != 15 {
|
|
t.Fatalf("durable_lsn=%d, want 15", truth.DurableLSN)
|
|
}
|
|
if truth.CheckpointLSN != 10 {
|
|
t.Fatalf("checkpoint_lsn=%d, want 10", truth.CheckpointLSN)
|
|
}
|
|
if truth.TargetLSN != 40 {
|
|
t.Fatalf("target_lsn=%d, want 40", truth.TargetLSN)
|
|
}
|
|
if truth.AchievedLSN != 20 {
|
|
t.Fatalf("achieved_lsn=%d, want 20", truth.AchievedLSN)
|
|
}
|
|
if truth.RecoveryPhase != "catching_up" {
|
|
t.Fatalf("recovery_phase=%q, want catching_up", truth.RecoveryPhase)
|
|
}
|
|
if truth.Degraded {
|
|
t.Fatalf("unexpected degraded truth: %+v", truth)
|
|
}
|
|
}
|
|
|
|
func TestPrimaryLoss_ReconstructionFailsClosedOnMismatchAndNeedsRebuild(t *testing.T) {
|
|
truth, err := ReconstructPrimaryTruth("node-b", []protocolv2.ReplicaSummaryResponse{
|
|
{
|
|
VolumeName: "vol-a",
|
|
NodeID: "node-b",
|
|
Epoch: 5,
|
|
Role: "replica",
|
|
Mode: "replica_ready",
|
|
CommittedLSN: 15,
|
|
DurableLSN: 15,
|
|
CheckpointLSN: 10,
|
|
RecoveryPhase: "idle",
|
|
LastBarrierOK: true,
|
|
Eligible: true,
|
|
LastBarrierReason: "",
|
|
},
|
|
{
|
|
VolumeName: "vol-a",
|
|
NodeID: "node-c",
|
|
Epoch: 4,
|
|
Role: "replica",
|
|
Mode: "needs_rebuild",
|
|
CommittedLSN: 12,
|
|
DurableLSN: 10,
|
|
CheckpointLSN: 8,
|
|
RecoveryPhase: "needs_rebuild",
|
|
LastBarrierOK: false,
|
|
LastBarrierReason: "timeout",
|
|
Eligible: false,
|
|
Reason: "needs_rebuild",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("reconstruct truth: %v", err)
|
|
}
|
|
if !truth.Degraded {
|
|
t.Fatalf("expected degraded truth: %+v", truth)
|
|
}
|
|
if !truth.NeedsRebuild {
|
|
t.Fatalf("expected needs_rebuild truth: %+v", truth)
|
|
}
|
|
if truth.RecoveryPhase != "needs_rebuild" {
|
|
t.Fatalf("recovery_phase=%q, want needs_rebuild", truth.RecoveryPhase)
|
|
}
|
|
}
|