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>
151 lines
4.1 KiB
Go
151 lines
4.1 KiB
Go
package volumev2
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/seaweedfs/seaweedfs/sw-block/runtime/protocolv2"
|
|
)
|
|
|
|
// ReconstructedPrimaryTruth is the bounded truth a newly selected primary can
|
|
// derive from replica summaries before resuming data-control ownership.
|
|
type ReconstructedPrimaryTruth struct {
|
|
VolumeName string
|
|
PrimaryNodeID string
|
|
Epoch uint64
|
|
CommittedLSN uint64
|
|
DurableLSN uint64
|
|
CheckpointLSN uint64
|
|
TargetLSN uint64
|
|
AchievedLSN uint64
|
|
RecoveryPhase string
|
|
ReplicaCount int
|
|
Degraded bool
|
|
NeedsRebuild bool
|
|
Reason string
|
|
}
|
|
|
|
// ReconstructPrimaryTruth derives a bounded recovery view for a newly chosen
|
|
// primary from the latest replica summaries. It intentionally stays smaller
|
|
// than the full internal engine/session graph and fail-closes on ambiguous
|
|
// epoch or recovery signals.
|
|
func ReconstructPrimaryTruth(primaryNodeID string, summaries []protocolv2.ReplicaSummaryResponse) (ReconstructedPrimaryTruth, error) {
|
|
if primaryNodeID == "" {
|
|
return ReconstructedPrimaryTruth{}, fmt.Errorf("volumev2: primary node id is required")
|
|
}
|
|
if len(summaries) == 0 {
|
|
return ReconstructedPrimaryTruth{}, fmt.Errorf("volumev2: replica summaries are required")
|
|
}
|
|
|
|
var (
|
|
selected protocolv2.ReplicaSummaryResponse
|
|
foundSelected bool
|
|
recoveryObserved bool
|
|
aggregateTarget uint64
|
|
aggregateAchieved uint64
|
|
)
|
|
for _, summary := range summaries {
|
|
if summary.NodeID == primaryNodeID {
|
|
selected = summary
|
|
foundSelected = true
|
|
break
|
|
}
|
|
}
|
|
if !foundSelected {
|
|
return ReconstructedPrimaryTruth{}, fmt.Errorf("volumev2: selected primary %q missing from summaries", primaryNodeID)
|
|
}
|
|
if !selected.Eligible {
|
|
return ReconstructedPrimaryTruth{}, fmt.Errorf("volumev2: selected primary %q is not eligible: %s", primaryNodeID, selected.Reason)
|
|
}
|
|
|
|
result := ReconstructedPrimaryTruth{
|
|
VolumeName: selected.VolumeName,
|
|
PrimaryNodeID: selected.NodeID,
|
|
Epoch: selected.Epoch,
|
|
CommittedLSN: selected.CommittedLSN,
|
|
DurableLSN: selected.DurableLSN,
|
|
CheckpointLSN: selected.CheckpointLSN,
|
|
TargetLSN: selected.TargetLSN,
|
|
AchievedLSN: selected.AchievedLSN,
|
|
RecoveryPhase: selected.RecoveryPhase,
|
|
}
|
|
|
|
for _, summary := range summaries {
|
|
if summary.VolumeName != selected.VolumeName {
|
|
return ReconstructedPrimaryTruth{}, fmt.Errorf("volumev2: mixed volume summaries %q and %q", selected.VolumeName, summary.VolumeName)
|
|
}
|
|
if summary.Mode == "needs_rebuild" || summary.Reason == "needs_rebuild" {
|
|
result.NeedsRebuild = true
|
|
}
|
|
if !summary.LastBarrierOK && summary.LastBarrierReason != "" {
|
|
result.Degraded = true
|
|
if result.Reason == "" {
|
|
result.Reason = summary.LastBarrierReason
|
|
}
|
|
}
|
|
if summary.Epoch != selected.Epoch {
|
|
result.Degraded = true
|
|
result.Reason = "peer_epoch_mismatch"
|
|
continue
|
|
}
|
|
result.ReplicaCount++
|
|
|
|
if summary.CommittedLSN > result.CommittedLSN {
|
|
result.Degraded = true
|
|
result.Reason = "selected_not_most_recent"
|
|
}
|
|
if isRecoveryPhase(summary.RecoveryPhase) {
|
|
if !recoveryObserved {
|
|
aggregateTarget = summary.TargetLSN
|
|
aggregateAchieved = summary.AchievedLSN
|
|
recoveryObserved = true
|
|
} else {
|
|
if summary.TargetLSN > aggregateTarget {
|
|
aggregateTarget = summary.TargetLSN
|
|
}
|
|
if summary.AchievedLSN < aggregateAchieved {
|
|
aggregateAchieved = summary.AchievedLSN
|
|
}
|
|
}
|
|
result.RecoveryPhase = mergeRecoveryPhase(result.RecoveryPhase, summary.RecoveryPhase)
|
|
}
|
|
}
|
|
|
|
if recoveryObserved {
|
|
result.TargetLSN = aggregateTarget
|
|
result.AchievedLSN = aggregateAchieved
|
|
}
|
|
if result.NeedsRebuild {
|
|
result.RecoveryPhase = "needs_rebuild"
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func isRecoveryPhase(phase string) bool {
|
|
switch phase {
|
|
case "catching_up", "rebuilding", "needs_rebuild":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func mergeRecoveryPhase(current, next string) string {
|
|
if recoveryRank(next) > recoveryRank(current) {
|
|
return next
|
|
}
|
|
return current
|
|
}
|
|
|
|
func recoveryRank(phase string) int {
|
|
switch phase {
|
|
case "needs_rebuild":
|
|
return 3
|
|
case "rebuilding":
|
|
return 2
|
|
case "catching_up":
|
|
return 1
|
|
default:
|
|
return 0
|
|
}
|
|
}
|