Files
seaweedfs/weed/storage/blockvol/block_heartbeat.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

128 lines
4.7 KiB
Go

package blockvol
import (
"math"
"time"
)
// BlockVolumeInfoMessage is the heartbeat status for one block volume.
// Mirrors the proto message that will be generated from master.proto.
type BlockVolumeInfoMessage struct {
Path string // volume file path (unique ID on this server)
VolumeSize uint64 // logical size in bytes
BlockSize uint32 // block size in bytes
Epoch uint64 // current fencing epoch
Role uint32 // blockvol.Role as uint32 for wire compat
WalHeadLsn uint64 // WAL head LSN
CheckpointLsn uint64 // last flushed LSN
HasLease bool // whether volume holds a valid lease
DiskType string // e.g., "ssd", "hdd"
ReplicaDataAddr string // receiver data listen addr (VS reports in heartbeat)
ReplicaCtrlAddr string // receiver ctrl listen addr
HealthScore float64 // CP8-2: 0.0-1.0
ScrubErrors int64 // CP8-2: lifetime scrub error count
LastScrubTime int64 // CP8-2: unix seconds
ReplicaDegraded bool // CP8-2: true if any replica shipper degraded
DurabilityMode string // CP8-3-1: "best_effort", "sync_all", "sync_quorum"
NvmeAddr string // NVMe/TCP target address (ip:port), empty if NVMe disabled
NQN string // NVMe subsystem NQN, empty if NVMe disabled
ReplicaShipperStates []ReplicaShipperStatus // CP13-7: per-replica state from primary's shipper group
}
// BlockVolumeShortInfoMessage is used for delta heartbeats
// (new/deleted block volumes).
type BlockVolumeShortInfoMessage struct {
Path string
VolumeSize uint64
BlockSize uint32
DiskType string
}
// BlockVolumeAssignment carries a role/epoch/lease assignment
// from master to volume server for one block volume.
type BlockVolumeAssignment struct {
Path string // which block volume
Epoch uint64 // new epoch
Role uint32 // target role (blockvol.Role as uint32)
LeaseTtlMs uint32 // lease TTL in milliseconds (0 = no lease)
ReplicaDataAddr string // where primary ships WAL data (scalar, RF=2 compat)
ReplicaCtrlAddr string // where primary sends barriers (scalar, RF=2 compat)
ReplicaServerID string // V2: stable server identity for scalar replica (from registry)
RebuildAddr string // where rebuild server listens
ReplicaAddrs []ReplicaAddr // CP8-2: multi-replica addrs (precedence over scalar)
}
// ToBlockVolumeInfoMessage converts a BlockVol's current state
// to a heartbeat info message. diskType is caller-supplied metadata
// (e.g. "ssd", "hdd") since the volume itself does not track disk type.
func ToBlockVolumeInfoMessage(path, diskType string, vol *BlockVol) BlockVolumeInfoMessage {
info := vol.Info()
status := vol.Status()
hs := vol.HealthStats()
return BlockVolumeInfoMessage{
Path: path,
VolumeSize: info.VolumeSize,
BlockSize: info.BlockSize,
Epoch: status.Epoch,
Role: RoleToWire(status.Role),
WalHeadLsn: status.WALHeadLSN,
CheckpointLsn: status.CheckpointLSN,
HasLease: status.HasLease,
DiskType: diskType,
HealthScore: status.HealthScore,
ScrubErrors: hs.ScrubErrors,
LastScrubTime: hs.LastScrubTime,
ReplicaDegraded: status.ReplicaDegraded,
DurabilityMode: vol.DurabilityMode().String(),
ReplicaShipperStates: vol.ReplicaShipperStates(),
}
}
// ToBlockVolumeShortInfoMessage returns a short info message
// for delta heartbeats. diskType is caller-supplied metadata.
func ToBlockVolumeShortInfoMessage(path, diskType string, vol *BlockVol) BlockVolumeShortInfoMessage {
info := vol.Info()
return BlockVolumeShortInfoMessage{
Path: path,
VolumeSize: info.VolumeSize,
BlockSize: info.BlockSize,
DiskType: diskType,
}
}
// maxValidRole is the highest defined Role value.
const maxValidRole = uint32(RoleDraining)
// RoleFromWire converts a uint32 wire role to blockvol.Role.
// Unknown values are mapped to RoleNone to prevent invalid roles
// from propagating through the system.
func RoleFromWire(r uint32) Role {
if r > maxValidRole {
return RoleNone
}
return Role(r)
}
// RoleToWire converts a blockvol.Role to uint32 for wire.
func RoleToWire(r Role) uint32 {
return uint32(r)
}
// LeaseTTLFromWire converts milliseconds to time.Duration.
func LeaseTTLFromWire(ms uint32) time.Duration {
return time.Duration(ms) * time.Millisecond
}
// LeaseTTLToWire converts time.Duration to milliseconds.
// Durations exceeding ~49.7 days are clamped to math.MaxUint32.
func LeaseTTLToWire(d time.Duration) uint32 {
ms := d.Milliseconds()
if ms > math.MaxUint32 {
return math.MaxUint32
}
if ms < 0 {
return 0
}
return uint32(ms)
}