You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
160 lines
5.3 KiB
160 lines
5.3 KiB
package replication
|
|
|
|
import "fmt"
|
|
|
|
// === Phase 06: Execution Driver ===
|
|
//
|
|
// Convenience flow classification (Phase 06 P0):
|
|
//
|
|
// ProcessAssignment → stepwise engine task (real entry point)
|
|
// ExecuteRecovery → planner (connect + classify outcome)
|
|
// CompleteCatchUp → TEST-ONLY convenience (bundles plan+execute+complete)
|
|
// CompleteRebuild → TEST-ONLY convenience (bundles plan+execute+complete)
|
|
// UpdateSenderEpoch → stepwise engine task
|
|
// InvalidateEpoch → stepwise engine task
|
|
//
|
|
// The real engine flow splits catch-up and rebuild into:
|
|
// 1. Plan: acquire resources (pin WAL or snapshot)
|
|
// 2. Execute: stream entries stepwise (not one-shot)
|
|
// 3. Complete: release resources, transition to InSync
|
|
//
|
|
// RecoveryDriver is the Phase 06 replacement for the synchronous
|
|
// convenience helpers. It plans, acquires resources, and provides
|
|
// a stepwise execution interface.
|
|
|
|
// RecoveryPlan represents a planned recovery operation with acquired resources.
|
|
type RecoveryPlan struct {
|
|
ReplicaID string
|
|
SessionID uint64
|
|
Outcome RecoveryOutcome
|
|
Proof *RecoverabilityProof
|
|
|
|
// Resource pins (non-nil when resources are acquired).
|
|
RetentionPin *RetentionPin // for catch-up
|
|
SnapshotPin *SnapshotPin // for rebuild
|
|
|
|
// Targets.
|
|
CatchUpTarget uint64 // for catch-up: target LSN
|
|
TruncateLSN uint64 // non-zero if truncation required
|
|
RebuildSource RebuildSource
|
|
}
|
|
|
|
// RecoveryDriver plans and executes recovery operations using real
|
|
// storage adapter inputs. It replaces the synchronous convenience
|
|
// helpers (CompleteCatchUp, CompleteRebuild) with a resource-aware,
|
|
// stepwise execution model.
|
|
type RecoveryDriver struct {
|
|
Orchestrator *RecoveryOrchestrator
|
|
Storage StorageAdapter
|
|
}
|
|
|
|
// NewRecoveryDriver creates a driver with a fresh orchestrator.
|
|
func NewRecoveryDriver(storage StorageAdapter) *RecoveryDriver {
|
|
return &RecoveryDriver{
|
|
Orchestrator: NewRecoveryOrchestrator(),
|
|
Storage: storage,
|
|
}
|
|
}
|
|
|
|
// PlanRecovery connects, handshakes from real storage state, classifies
|
|
// the outcome, and acquires the necessary resources (WAL pin or snapshot pin).
|
|
// Returns a RecoveryPlan that the caller can execute stepwise.
|
|
func (d *RecoveryDriver) PlanRecovery(replicaID string, replicaFlushedLSN uint64) (*RecoveryPlan, error) {
|
|
history := d.Storage.GetRetainedHistory()
|
|
|
|
result := d.Orchestrator.ExecuteRecovery(replicaID, replicaFlushedLSN, &history)
|
|
if result.Error != nil {
|
|
return nil, result.Error
|
|
}
|
|
|
|
plan := &RecoveryPlan{
|
|
ReplicaID: replicaID,
|
|
SessionID: d.Orchestrator.Registry.Sender(replicaID).SessionID(),
|
|
Outcome: result.Outcome,
|
|
Proof: result.Proof,
|
|
}
|
|
|
|
switch result.Outcome {
|
|
case OutcomeZeroGap:
|
|
// Already completed by ExecuteRecovery.
|
|
return plan, nil
|
|
|
|
case OutcomeCatchUp:
|
|
// Acquire WAL retention pin.
|
|
pin, err := d.Storage.PinWALRetention(replicaFlushedLSN)
|
|
if err != nil {
|
|
d.Orchestrator.Log.Record(replicaID, plan.SessionID, "wal_pin_failed", err.Error())
|
|
return nil, fmt.Errorf("WAL retention pin failed: %w", err)
|
|
}
|
|
plan.RetentionPin = &pin
|
|
plan.CatchUpTarget = history.CommittedLSN
|
|
|
|
// Check if truncation is needed (replica ahead).
|
|
proof := history.ProveRecoverability(replicaFlushedLSN)
|
|
if proof.Reason == "replica_ahead_needs_truncation" {
|
|
plan.TruncateLSN = history.CommittedLSN
|
|
}
|
|
|
|
d.Orchestrator.Log.Record(replicaID, plan.SessionID, "plan_catchup",
|
|
fmt.Sprintf("target=%d pin=%d truncate=%d", plan.CatchUpTarget, pin.PinID, plan.TruncateLSN))
|
|
return plan, nil
|
|
|
|
case OutcomeNeedsRebuild:
|
|
// No resource acquisition — needs rebuild assignment first.
|
|
return plan, nil
|
|
}
|
|
|
|
return plan, nil
|
|
}
|
|
|
|
// PlanRebuild acquires rebuild resources (snapshot pin + optional WAL pin)
|
|
// from real storage state. Called after a rebuild assignment.
|
|
func (d *RecoveryDriver) PlanRebuild(replicaID string) (*RecoveryPlan, error) {
|
|
history := d.Storage.GetRetainedHistory()
|
|
source, snapLSN := history.RebuildSourceDecision()
|
|
|
|
plan := &RecoveryPlan{
|
|
ReplicaID: replicaID,
|
|
SessionID: d.Orchestrator.Registry.Sender(replicaID).SessionID(),
|
|
Outcome: OutcomeNeedsRebuild,
|
|
RebuildSource: source,
|
|
}
|
|
|
|
if source == RebuildSnapshotTail {
|
|
// Pin snapshot.
|
|
snapPin, err := d.Storage.PinSnapshot(snapLSN)
|
|
if err != nil {
|
|
d.Orchestrator.Log.Record(replicaID, plan.SessionID, "snapshot_pin_failed", err.Error())
|
|
return nil, fmt.Errorf("snapshot pin failed: %w", err)
|
|
}
|
|
plan.SnapshotPin = &snapPin
|
|
|
|
// Pin WAL retention for tail replay.
|
|
retPin, err := d.Storage.PinWALRetention(snapLSN)
|
|
if err != nil {
|
|
d.Storage.ReleaseSnapshot(snapPin)
|
|
d.Orchestrator.Log.Record(replicaID, plan.SessionID, "wal_pin_failed", err.Error())
|
|
return nil, fmt.Errorf("WAL retention pin failed: %w", err)
|
|
}
|
|
plan.RetentionPin = &retPin
|
|
|
|
d.Orchestrator.Log.Record(replicaID, plan.SessionID, "plan_rebuild_snapshot_tail",
|
|
fmt.Sprintf("snapshot=%d", snapLSN))
|
|
} else {
|
|
d.Orchestrator.Log.Record(replicaID, plan.SessionID, "plan_rebuild_full_base", "")
|
|
}
|
|
|
|
return plan, nil
|
|
}
|
|
|
|
// ReleasePlan releases any resources acquired by a plan.
|
|
func (d *RecoveryDriver) ReleasePlan(plan *RecoveryPlan) {
|
|
if plan.RetentionPin != nil {
|
|
d.Storage.ReleaseWALRetention(*plan.RetentionPin)
|
|
plan.RetentionPin = nil
|
|
}
|
|
if plan.SnapshotPin != nil {
|
|
d.Storage.ReleaseSnapshot(*plan.SnapshotPin)
|
|
plan.SnapshotPin = nil
|
|
}
|
|
}
|