Files
seaweedfs/sw-block/runtime/volumev2/failover_driver.go
T
pingqiuandClaude Opus 4.6 b82df09856 feat: Phase 18 M1 — transport-backed RF2 failover runtime
M1 milestone: failover evidence crosses transport/session seam.

Adapter seam (failover_adapter.go):
- FailoverEvidenceAdapter: query-side (promotion evidence + replica summary)
- FailoverTakeoverAdapter: execution-side (prepare + gate)
- FailoverTarget: binds NodeID + both adapters
- NewInProcessFailoverTarget: factory for in-process case

Transport seam (failover_evidence_transport.go):
- FailoverEvidenceTransport: request/response interface with nodeID routing
- FailoverEvidenceHandler: server-side registration
- InMemoryFailoverEvidenceTransport: first transport impl (in-memory)
- NewHybridInProcessFailoverTarget: transport-backed evidence + local takeover

Runtime manager (runtime_manager.go):
- InProcessRuntimeManager: participant registry + ExecuteFailover entry point
- Persisted failover snapshots/results per-volume and global-last

All failover paths (session/driver/manager) now go through adapter seam.
Old FailoverParticipant preserved as compatibility wrapper only.

Phase 18 docs: phase-18.md (M1-M5 structure), log, decisions.
Design docs updated: kernel-closure-review, claim-and-evidence ledger.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 13:57:08 -07:00

136 lines
3.9 KiB
Go

package volumev2
import (
"fmt"
"slices"
"sync"
"github.com/seaweedfs/seaweedfs/sw-block/runtime/masterv2"
)
// InProcessFailoverDriver is the first thin driver that wires one in-process
// masterv2 instance to a set of failover-capable participants. It owns no
// recovery logic; it only resolves participants and constructs sessions.
type InProcessFailoverDriver struct {
master *masterv2.Master
mu sync.RWMutex
targets map[string]FailoverTarget
}
// NewInProcessFailoverDriver creates a driver for one in-process masterv2.
func NewInProcessFailoverDriver(master *masterv2.Master) (*InProcessFailoverDriver, error) {
if master == nil {
return nil, fmt.Errorf("volumev2: master is nil")
}
return &InProcessFailoverDriver{
master: master,
targets: make(map[string]FailoverTarget),
}, nil
}
// RegisterTarget binds one stable node id to one explicit failover target.
func (d *InProcessFailoverDriver) RegisterTarget(target FailoverTarget) error {
if d == nil {
return fmt.Errorf("volumev2: failover driver is nil")
}
if target.NodeID == "" {
return fmt.Errorf("volumev2: target node id is required")
}
if target.Evidence == nil {
return fmt.Errorf("volumev2: target %q missing evidence adapter", target.NodeID)
}
d.mu.Lock()
defer d.mu.Unlock()
d.targets[target.NodeID] = target
return nil
}
// RegisterParticipant preserves the old convenience path by wrapping one
// legacy failover participant into an explicit target.
func (d *InProcessFailoverDriver) RegisterParticipant(nodeID string, participant FailoverParticipant) error {
if participant == nil {
return fmt.Errorf("volumev2: participant %q is nil", nodeID)
}
return d.RegisterTarget(FailoverTarget{
NodeID: nodeID,
Evidence: participant,
Takeover: participant,
})
}
// UnregisterParticipant removes one node from the in-process driver.
func (d *InProcessFailoverDriver) UnregisterParticipant(nodeID string) {
if d == nil || nodeID == "" {
return
}
d.mu.Lock()
defer d.mu.Unlock()
delete(d.targets, nodeID)
}
// ParticipantNodeIDs returns the currently registered node ids in stable order.
func (d *InProcessFailoverDriver) ParticipantNodeIDs() []string {
if d == nil {
return nil
}
d.mu.RLock()
defer d.mu.RUnlock()
nodeIDs := make([]string, 0, len(d.targets))
for nodeID := range d.targets {
nodeIDs = append(nodeIDs, nodeID)
}
slices.Sort(nodeIDs)
return nodeIDs
}
// NewSession constructs a FailoverSession using either the requested node ids
// or all currently registered participants when none are specified.
func (d *InProcessFailoverDriver) NewSession(volumeName string, expectedEpoch uint64, nodeIDs ...string) (*FailoverSession, error) {
if d == nil {
return nil, fmt.Errorf("volumev2: failover driver is nil")
}
targets, err := d.resolveTargets(nodeIDs)
if err != nil {
return nil, err
}
return NewFailoverSession(d.master, volumeName, expectedEpoch, targets)
}
// Execute runs one failover using the resolved participant set.
func (d *InProcessFailoverDriver) Execute(volumeName string, expectedEpoch uint64, nodeIDs ...string) (FailoverResult, error) {
session, err := d.NewSession(volumeName, expectedEpoch, nodeIDs...)
if err != nil {
return FailoverResult{}, err
}
return session.Run()
}
func (d *InProcessFailoverDriver) resolveTargets(nodeIDs []string) ([]FailoverTarget, error) {
d.mu.RLock()
defer d.mu.RUnlock()
if len(d.targets) == 0 {
return nil, fmt.Errorf("volumev2: no failover targets registered")
}
resolvedIDs := nodeIDs
if len(resolvedIDs) == 0 {
resolvedIDs = make([]string, 0, len(d.targets))
for nodeID := range d.targets {
resolvedIDs = append(resolvedIDs, nodeID)
}
slices.Sort(resolvedIDs)
}
targets := make([]FailoverTarget, 0, len(resolvedIDs))
for _, nodeID := range resolvedIDs {
target, ok := d.targets[nodeID]
if !ok {
return nil, fmt.Errorf("volumev2: unknown failover target %q", nodeID)
}
targets = append(targets, target)
}
return targets, nil
}