mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
Finding 1: Identity no longer address-derived - ReplicaAddr.ServerID field added (stable server identity from registry) - BlockVolumeAssignment.ReplicaServerID field added (scalar RF=2 path) - ControlBridge uses ServerID, NOT address, for ReplicaID - Missing ServerID → replica skipped (fail closed), logged Finding 2: Wired into real ProcessAssignments - BlockService.v2Bridge field initialized in StartBlockService - ProcessAssignments converts each assignment via v2Bridge.ConvertAssignment BEFORE existing V1 processing (parallel, not replacing yet) - Logged at glog V(1) Finding 3: Fail-closed on missing identity - Empty ServerID in ReplicaAddrs → replica skipped with log - Empty ReplicaServerID in scalar path → no replica created - Test: MissingServerID_FailsClosed verifies both paths 7 tests: StableServerID, AddressChange_IdentityPreserved, MultiReplica_StableServerIDs, MissingServerID_FailsClosed, EpochFencing_IntegratedPath, RebuildAssignment, ReplicaAssignment Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
120 lines
3.9 KiB
Go
120 lines
3.9 KiB
Go
// control.go implements the real control-plane delivery bridge.
|
|
// Converts BlockVolumeAssignment (from master heartbeat) into V2 engine
|
|
// AssignmentIntent using stable server identity from the master registry.
|
|
//
|
|
// Identity rule: ReplicaID = <volume-path>/<server-id>
|
|
// ServerID comes from BlockVolumeAssignment.ReplicaServerID or
|
|
// ReplicaAddr.ServerID — NOT derived from transport addresses.
|
|
package v2bridge
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
|
|
bridge "github.com/seaweedfs/seaweedfs/sw-block/bridge/blockvol"
|
|
engine "github.com/seaweedfs/seaweedfs/sw-block/engine/replication"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol"
|
|
)
|
|
|
|
// ControlBridge converts real BlockVolumeAssignment into V2 engine intents.
|
|
type ControlBridge struct {
|
|
adapter *bridge.ControlAdapter
|
|
}
|
|
|
|
func NewControlBridge() *ControlBridge {
|
|
return &ControlBridge{adapter: bridge.NewControlAdapter()}
|
|
}
|
|
|
|
// ConvertAssignment converts a real BlockVolumeAssignment into an engine intent.
|
|
// localServerID is the identity of the local volume server (for replica/rebuild roles).
|
|
func (cb *ControlBridge) ConvertAssignment(a blockvol.BlockVolumeAssignment, localServerID string) engine.AssignmentIntent {
|
|
role := blockvol.RoleFromWire(a.Role)
|
|
volumeName := a.Path
|
|
|
|
switch role {
|
|
case blockvol.RolePrimary:
|
|
return cb.convertPrimaryAssignment(a, volumeName)
|
|
case blockvol.RoleReplica:
|
|
return cb.convertReplicaAssignment(a, volumeName, localServerID)
|
|
case blockvol.RoleRebuilding:
|
|
return cb.convertRebuildAssignment(a, volumeName, localServerID)
|
|
default:
|
|
return engine.AssignmentIntent{Epoch: a.Epoch}
|
|
}
|
|
}
|
|
|
|
func (cb *ControlBridge) convertPrimaryAssignment(a blockvol.BlockVolumeAssignment, volumeName string) engine.AssignmentIntent {
|
|
primary := bridge.MasterAssignment{
|
|
VolumeName: volumeName,
|
|
Epoch: a.Epoch,
|
|
Role: "primary",
|
|
}
|
|
|
|
var replicas []bridge.MasterAssignment
|
|
if len(a.ReplicaAddrs) > 0 {
|
|
for _, ra := range a.ReplicaAddrs {
|
|
if ra.ServerID == "" {
|
|
log.Printf("v2bridge: skipping replica with empty ServerID (data=%s)", ra.DataAddr)
|
|
continue // fail closed: skip replicas without stable identity
|
|
}
|
|
replicas = append(replicas, bridge.MasterAssignment{
|
|
VolumeName: volumeName,
|
|
Epoch: a.Epoch,
|
|
Role: "replica",
|
|
ReplicaServerID: ra.ServerID,
|
|
DataAddr: ra.DataAddr,
|
|
CtrlAddr: ra.CtrlAddr,
|
|
})
|
|
}
|
|
} else if a.ReplicaServerID != "" && a.ReplicaDataAddr != "" {
|
|
// Scalar RF=2 path with explicit ServerID.
|
|
replicas = append(replicas, bridge.MasterAssignment{
|
|
VolumeName: volumeName,
|
|
Epoch: a.Epoch,
|
|
Role: "replica",
|
|
ReplicaServerID: a.ReplicaServerID,
|
|
DataAddr: a.ReplicaDataAddr,
|
|
CtrlAddr: a.ReplicaCtrlAddr,
|
|
})
|
|
} else if a.ReplicaDataAddr != "" {
|
|
log.Printf("v2bridge: scalar replica assignment without ServerID (data=%s) — skipping", a.ReplicaDataAddr)
|
|
// Fail closed: do not create address-derived identity.
|
|
}
|
|
|
|
return cb.adapter.ToAssignmentIntent(primary, replicas)
|
|
}
|
|
|
|
func (cb *ControlBridge) convertReplicaAssignment(a blockvol.BlockVolumeAssignment, volumeName, localServerID string) engine.AssignmentIntent {
|
|
return engine.AssignmentIntent{
|
|
Epoch: a.Epoch,
|
|
Replicas: []engine.ReplicaAssignment{
|
|
{
|
|
ReplicaID: fmt.Sprintf("%s/%s", volumeName, localServerID),
|
|
Endpoint: engine.Endpoint{
|
|
DataAddr: a.ReplicaDataAddr,
|
|
CtrlAddr: a.ReplicaCtrlAddr,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (cb *ControlBridge) convertRebuildAssignment(a blockvol.BlockVolumeAssignment, volumeName, localServerID string) engine.AssignmentIntent {
|
|
replicaID := fmt.Sprintf("%s/%s", volumeName, localServerID)
|
|
return engine.AssignmentIntent{
|
|
Epoch: a.Epoch,
|
|
Replicas: []engine.ReplicaAssignment{
|
|
{
|
|
ReplicaID: replicaID,
|
|
Endpoint: engine.Endpoint{
|
|
DataAddr: a.ReplicaDataAddr,
|
|
CtrlAddr: a.ReplicaCtrlAddr,
|
|
},
|
|
},
|
|
},
|
|
RecoveryTargets: map[string]engine.SessionKind{
|
|
replicaID: engine.SessionRebuild,
|
|
},
|
|
}
|
|
}
|