Files
seaweedfs/weed/storage/blockvol/v2bridge/control.go
T
pingqiuandClaude Opus 4.6 46ef79ce35 fix: stable ServerID in assignments, fail-closed on missing identity, wire into ProcessAssignments
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>
2026-03-31 10:46:17 -07:00

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,
},
}
}