mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-11 14:28:33 +01:00
fix: derive WAL pin from actual replay need, PlanRebuild fails closed
WAL pin tied to actual recovery contract: - Truncation-only (replica ahead): no WAL pin acquired - Real catch-up: pins from replicaFlushedLSN (actual replay start) - Logs distinguish plan_truncate_only from plan_catchup PlanRebuild precondition checks: - Error on missing sender - Error on no active session - Error on non-rebuild session kind - All fail closed with clear error messages 4 new tests: - ReplicaAhead_NoWALPin: truncation-only, no WAL resources - PlanRebuild_MissingSender: returns error - PlanRebuild_NoSession: returns error - PlanRebuild_NonRebuildSession: returns error Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
d4f7697dd8
commit
45bf111ce8
2 files changed
+111
-11
No files matched your search
@@ -316,6 +316,86 @@ func TestDriver_PlanRecovery_ReplicaAhead_Truncation(t *testing.T) {
|
||||
driver.ReleasePlan(plan)
|
||||
}
|
||||
|
||||
// --- Truncation-only: no WAL pin needed ---
|
||||
|
||||
func TestDriver_PlanRecovery_ReplicaAhead_NoWALPin(t *testing.T) {
|
||||
storage := newMockStorage(RetainedHistory{
|
||||
HeadLSN: 100, TailLSN: 0, CommittedLSN: 100,
|
||||
})
|
||||
driver := NewRecoveryDriver(storage)
|
||||
|
||||
driver.Orchestrator.ProcessAssignment(AssignmentIntent{
|
||||
Replicas: []ReplicaAssignment{
|
||||
{ReplicaID: "r1", Endpoint: Endpoint{DataAddr: "r1:9333", Version: 1}},
|
||||
},
|
||||
Epoch: 1,
|
||||
RecoveryTargets: map[string]SessionKind{"r1": SessionCatchUp},
|
||||
})
|
||||
|
||||
// Replica ahead — truncation only, no WAL replay.
|
||||
plan, err := driver.PlanRecovery("r1", 105)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if plan.RetentionPin != nil {
|
||||
t.Fatal("truncation-only should NOT pin WAL")
|
||||
}
|
||||
if plan.TruncateLSN != 100 {
|
||||
t.Fatalf("truncate=%d, want 100", plan.TruncateLSN)
|
||||
}
|
||||
if len(storage.pinnedWAL) != 0 {
|
||||
t.Fatal("no WAL pins should exist for truncation-only")
|
||||
}
|
||||
}
|
||||
|
||||
// --- PlanRebuild precondition checks ---
|
||||
|
||||
func TestDriver_PlanRebuild_MissingSender(t *testing.T) {
|
||||
storage := newMockStorage(RetainedHistory{CommittedLSN: 100})
|
||||
driver := NewRecoveryDriver(storage)
|
||||
|
||||
_, err := driver.PlanRebuild("nonexistent")
|
||||
if err == nil {
|
||||
t.Fatal("should fail for missing sender")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriver_PlanRebuild_NoSession(t *testing.T) {
|
||||
storage := newMockStorage(RetainedHistory{CommittedLSN: 100})
|
||||
driver := NewRecoveryDriver(storage)
|
||||
|
||||
driver.Orchestrator.ProcessAssignment(AssignmentIntent{
|
||||
Replicas: []ReplicaAssignment{
|
||||
{ReplicaID: "r1", Endpoint: Endpoint{DataAddr: "r1:9333", Version: 1}},
|
||||
},
|
||||
Epoch: 1,
|
||||
// No recovery target → no session.
|
||||
})
|
||||
|
||||
_, err := driver.PlanRebuild("r1")
|
||||
if err == nil {
|
||||
t.Fatal("should fail when no session exists")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriver_PlanRebuild_NonRebuildSession(t *testing.T) {
|
||||
storage := newMockStorage(RetainedHistory{CommittedLSN: 100})
|
||||
driver := NewRecoveryDriver(storage)
|
||||
|
||||
driver.Orchestrator.ProcessAssignment(AssignmentIntent{
|
||||
Replicas: []ReplicaAssignment{
|
||||
{ReplicaID: "r1", Endpoint: Endpoint{DataAddr: "r1:9333", Version: 1}},
|
||||
},
|
||||
Epoch: 1,
|
||||
RecoveryTargets: map[string]SessionKind{"r1": SessionCatchUp}, // NOT rebuild
|
||||
})
|
||||
|
||||
_, err := driver.PlanRebuild("r1")
|
||||
if err == nil {
|
||||
t.Fatal("should fail when session is not rebuild")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Full-base rebuild pin ---
|
||||
|
||||
func TestDriver_PlanRebuild_FullBase_PinsBaseImage(t *testing.T) {
|
||||
|
||||
Reference in new issue
Block a user