refactor: preserve bounded volume mode reason heartbeat truth

Carry explicit volume_mode_reason across the heartbeat/master/API seam so outward surfaces retain the bounded core-owned explanation behind mode transitions.

Made-with: Cursor
This commit is contained in:
pingqiu
2026-04-04 14:21:31 -07:00
parent f20ec2ef79
commit 10833c8b68
17 changed files with 661 additions and 430 deletions
+75
View File
@@ -1477,3 +1477,78 @@ Conclusion:
explicit field still fall back to the previous reconstruction
3. this slice still does not claim publication reason, restart/disturbance, or
broad failover closure by itself
---
#### `16Q` Start Note Rev 1
Date: 2026-04-04
Scope: bounded explicit `volume_mode_reason` preservation on the heartbeat/master/API seam
Why this slice exists:
1. `16P` made outward `volume_mode` itself explicit across the heartbeat/master
seam
2. but the master-side outward surface still drops the reason behind that mode
and therefore cannot preserve the bounded core-owned explanation for
`bootstrap_pending`, `degraded`, or `needs_rebuild`
3. that leaves one narrow product-facing failover/publication seam even after
explicit mode truth is preserved
Chosen implementation rule:
1. widen the heartbeat wire additively with an explicit `volume_mode_reason`
field
2. emit it from the current bounded core mode/publication reason on the
core-present path
3. make master outward volume info prefer explicit heartbeat reason truth and
retain empty/default behavior as backward-compatible fallback
4. do not broaden this slice into restart/disturbance hardening or wider launch
closure
---
#### `16Q` Delivery Note Rev 1
Date: 2026-04-04
Scope: bounded explicit `volume_mode_reason` preservation on the heartbeat/master/API seam
What changed:
1. `weed/pb/master.proto`
- added additive optional `volume_mode_reason` to `BlockVolumeInfoMessage`
2. `weed/pb/master_pb/master.pb.go`
- regenerated so heartbeat wire presence is represented as `*string`
3. `weed/storage/blockvol/block_heartbeat.go`
- heartbeat wire struct now carries explicit `VolumeModeReason`
4. `weed/storage/blockvol/block_heartbeat_proto.go`
- heartbeat conversion now writes and reads `VolumeModeReason`
5. `weed/server/volume_server_block.go`
- heartbeat emission now preserves the bounded core mode/publication reason
on the core-present path
6. `weed/server/master_block_registry.go`
- primary heartbeat consume now preserves explicit heartbeat
`volume_mode_reason` truth on the registry entry
7. `weed/server/master_server_handlers_block.go` and
`weed/storage/blockvol/blockapi/types.go`
- outward volume info now surfaces `VolumeModeReason`
8. focused tests in `block_heartbeat_proto_test.go`,
`volume_server_block_test.go`, `master_block_registry_test.go`,
`master_block_observability_test.go`, and
`master_server_handlers_block_test.go`
- now prove explicit outward reason preservation and empty/default fallback
Proof / evidence:
1. `go test ./weed/storage/blockvol -count=1 -run "TestInfoMessage_(ReplicaReady|NeedsRebuild|PublishHealthy|VolumeMode|VolumeModeReason)"`
2. `go test ./weed/server -count=1 -timeout 180s -run "Test(BlockService_CollectBlockVolumeHeartbeat_(PrimaryUsesCoreReadinessGate|PrimaryNeedsRebuildUsesCoreMode|PrimaryPublishHealthyUsesCoreTruth|PrimaryDegradedUsesCoreModeTruth)|Registry_UpdateFullHeartbeat_(ConsumesExplicitVolumeModeFromPrimaryHeartbeat|VolumeModeFallsBackWhenFieldAbsent)|EntryToVolumeInfo_(ReflectsCoreInfluencedReadyConsume|ReflectsCoreInfluencedDegradedConsume)|BlockVolume(Get|List)Handler_ReflectsCoreInfluencedDegradedConsume)"`
3. result: `PASS`
Conclusion:
1. the heartbeat/master/API seam no longer preserves only outward mode names
while dropping the bounded explanation behind them
2. backward compatibility is preserved because older heartbeats without the new
field keep the previous empty/default outward reason behavior
3. this slice still does not claim restart/disturbance hardening or broad
failover closure by itself
+58 -2
View File
@@ -717,6 +717,58 @@ Evidence:
1. focused working-tree change after `16O` closeout
### `16Q`: VolumeModeReason Heartbeat Truth Rebinding
Goal:
1. close one bounded failover/publication seam by preserving explicit
`volume_mode_reason` truth across the heartbeat/master boundary instead of
dropping the reason behind outward mode transitions
2. keep the slice limited to outward `VolumeModeReason` preservation on the
current heartbeat wire and master-registry/API path, not broader restart or
launch closure
Acceptance object:
1. `BlockVolumeInfoMessage` carries an additive explicit `volume_mode_reason`
field on the heartbeat wire
2. `weed/server` heartbeat emission sets that field from the current core-owned
mode/publication reason on the core-present path
3. `master_block_registry` and outward volume-info surfaces prefer explicit
heartbeat `volume_mode_reason` truth while retaining empty/default behavior as
backward-compatible fallback when the field is absent
4. focused proofs show primary bootstrap/degraded/needs_rebuild reason survives
heartbeat/master consume as explicit outward reason truth
5. this slice still does not yet claim broad restart/disturbance or launch
closure
Current chosen path:
1. widen `master.proto` / heartbeat conversion with an additive
`volume_mode_reason` field
2. emit that field from `CollectBlockVolumeHeartbeat` using the bounded core
reason on the core-present path
3. let master outward `VolumeModeReason` prefer explicit heartbeat reason truth
and keep empty/default behavior as backward-compatible fallback
Status:
1. delivered
Delivered result:
1. heartbeat wire now carries additive explicit `volume_mode_reason` truth
2. volume-server heartbeat emission now preserves bounded core mode/publication
reason on the core-present path
3. master consume and outward volume-info/API surfaces now preserve explicit
`VolumeModeReason` truth instead of dropping the reason behind outward mode
transitions
4. older heartbeats without the new field still keep empty/default behavior
Evidence:
1. focused working-tree change after `16P` closeout
## Current Checkpoint Review Target
The current review target is the current widened bounded runtime checkpoint
@@ -805,11 +857,15 @@ boundary:
17. `16P` delivered:
- heartbeat/master consume now preserves explicit bounded `VolumeMode` truth
with backward-compatible fallback for older heartbeats
18. `16Q` delivered:
- heartbeat/master/API path now preserves explicit bounded
`VolumeModeReason` truth instead of dropping outward mode reasons at the
master boundary
After this checkpoint:
1. keep `legacy P4` only as a compatibility guard
2. continue closing broader recovery-loop and publication seams one bounded step
at a time after `PublishHealthy` rebinding
2. continue closing broader recovery-loop, publication, and disturbance seams
one bounded step at a time after outward reason preservation
3. do not yet claim full recovery-loop closure
4. do not broaden into launch claims
+1
View File
@@ -516,6 +516,7 @@ message BlockVolumeInfoMessage {
optional bool needs_rebuild = 20;
optional bool publish_healthy = 21;
optional string volume_mode = 22;
optional string volume_mode_reason = 23;
}
message BlockVolumeShortInfoMessage {
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -2,7 +2,7 @@
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc v3.21.12
// source: weed/pb/master.proto
// source: master.proto
package master_pb
@@ -1278,5 +1278,5 @@ var Seaweed_ServiceDesc = grpc.ServiceDesc{
ClientStreams: true,
},
},
Metadata: "weed/pb/master.proto",
Metadata: "master.proto",
}
@@ -416,6 +416,9 @@ func TestEntryToVolumeInfo_ReflectsCoreInfluencedReadyConsume(t *testing.T) {
if info.VolumeMode != "publish_healthy" {
t.Fatalf("expected outward VolumeMode=publish_healthy, got %q", info.VolumeMode)
}
if info.VolumeModeReason != "" {
t.Fatalf("expected empty outward VolumeModeReason for publish_healthy, got %q", info.VolumeModeReason)
}
if info.HealthState != HealthStateHealthy {
t.Fatalf("expected outward HealthState=%q, got %q", HealthStateHealthy, info.HealthState)
}
@@ -473,6 +476,9 @@ func TestEntryToVolumeInfo_ReflectsCoreInfluencedDegradedConsume(t *testing.T) {
if info.VolumeMode != "degraded" {
t.Fatalf("expected outward VolumeMode=degraded, got %q", info.VolumeMode)
}
if info.VolumeModeReason != "barrier_timeout" {
t.Fatalf("expected outward VolumeModeReason=barrier_timeout, got %q", info.VolumeModeReason)
}
if info.HealthState != HealthStateDegraded {
t.Fatalf("expected outward HealthState=%q, got %q", HealthStateDegraded, info.HealthState)
}
+25 -12
View File
@@ -71,18 +71,20 @@ type BlockVolumeEntry struct {
RebuildListenAddr string // rebuild server listen addr on primary
// CP8-2: Multi-replica support.
ReplicaFactor int // 2 or 3 (default 2)
Replicas []ReplicaInfo // one per replica (RF-1 entries)
HealthScore float64 // primary health score from heartbeat
ReplicaReady bool // all configured replicas are ready for publication
ReplicaDegraded bool // aggregate: transport degraded OR not ready
TransportDegraded bool // primary reports degraded replicas
NeedsRebuild bool // explicit primary heartbeat needs_rebuild truth when present
HasNeedsRebuild bool // whether the current primary heartbeat carried explicit needs_rebuild truth
PublishHealthy bool // explicit primary heartbeat publish_healthy truth when present
HasPublishHealthy bool // whether the current primary heartbeat carried explicit publish_healthy truth
HeartbeatVolumeMode string // explicit primary heartbeat outward volume_mode truth when present
HasHeartbeatVolumeMode bool // whether the current primary heartbeat carried explicit outward volume_mode truth
ReplicaFactor int // 2 or 3 (default 2)
Replicas []ReplicaInfo // one per replica (RF-1 entries)
HealthScore float64 // primary health score from heartbeat
ReplicaReady bool // all configured replicas are ready for publication
ReplicaDegraded bool // aggregate: transport degraded OR not ready
TransportDegraded bool // primary reports degraded replicas
NeedsRebuild bool // explicit primary heartbeat needs_rebuild truth when present
HasNeedsRebuild bool // whether the current primary heartbeat carried explicit needs_rebuild truth
PublishHealthy bool // explicit primary heartbeat publish_healthy truth when present
HasPublishHealthy bool // whether the current primary heartbeat carried explicit publish_healthy truth
HeartbeatVolumeMode string // explicit primary heartbeat outward volume_mode truth when present
HasHeartbeatVolumeMode bool // whether the current primary heartbeat carried explicit outward volume_mode truth
HeartbeatVolumeReason string // explicit primary heartbeat outward volume_mode_reason truth when present
HasHeartbeatVolumeReason bool // whether the current primary heartbeat carried explicit outward volume_mode_reason truth
// CP13-9: Normalized volume mode for external surfaces.
// Computed by recomputeReplicaState from the current entry state.
@@ -663,6 +665,7 @@ func (r *BlockVolumeRegistry) applyPrimaryHeartbeatObservation(existing *BlockVo
existing.NeedsRebuild, existing.HasNeedsRebuild = primaryNeedsRebuildObservedFromHeartbeat(info)
existing.PublishHealthy, existing.HasPublishHealthy = primaryPublishHealthyObservedFromHeartbeat(info)
existing.HeartbeatVolumeMode, existing.HasHeartbeatVolumeMode = primaryVolumeModeObservedFromHeartbeat(info)
existing.HeartbeatVolumeReason, existing.HasHeartbeatVolumeReason = primaryVolumeReasonObservedFromHeartbeat(info)
existing.WALHeadLSN = info.WalHeadLsn
// F3: only update DurabilityMode when non-empty (prevents older VS from clearing strict mode).
if info.DurabilityMode != "" {
@@ -780,6 +783,16 @@ func primaryVolumeModeObservedFromHeartbeat(info *master_pb.BlockVolumeInfoMessa
return "", false
}
func primaryVolumeReasonObservedFromHeartbeat(info *master_pb.BlockVolumeInfoMessage) (string, bool) {
if info == nil {
return "", false
}
if info.VolumeModeReason != nil {
return info.GetVolumeModeReason(), true
}
return "", false
}
func validHeartbeatVolumeMode(mode string) bool {
switch mode {
case "allocated_only", "bootstrap_pending", "publish_healthy", "degraded", "needs_rebuild":
+21 -9
View File
@@ -2357,12 +2357,14 @@ func TestRegistry_UpdateFullHeartbeat_ConsumesExplicitVolumeModeFromPrimaryHeart
tests := []struct {
name string
mode string
reason string
replicas []ReplicaInfo
wantMode string
}{
{
name: "bootstrap_pending",
mode: "bootstrap_pending",
name: "bootstrap_pending",
mode: "bootstrap_pending",
reason: "awaiting_shipper_connected",
replicas: []ReplicaInfo{{
Server: "replica-server:8080",
Path: "/blocks/vol-master-mode-bootstrap-replica.blk",
@@ -2371,8 +2373,9 @@ func TestRegistry_UpdateFullHeartbeat_ConsumesExplicitVolumeModeFromPrimaryHeart
wantMode: "bootstrap_pending",
},
{
name: "degraded",
mode: "degraded",
name: "degraded",
mode: "degraded",
reason: "barrier_timeout",
replicas: []ReplicaInfo{{
Server: "replica-server:8080",
Path: "/blocks/vol-master-mode-degraded-replica.blk",
@@ -2381,8 +2384,9 @@ func TestRegistry_UpdateFullHeartbeat_ConsumesExplicitVolumeModeFromPrimaryHeart
wantMode: "degraded",
},
{
name: "needs_rebuild",
mode: "needs_rebuild",
name: "needs_rebuild",
mode: "needs_rebuild",
reason: "gap_too_large",
replicas: []ReplicaInfo{{
Server: "replica-server:8080",
Path: "/blocks/vol-master-mode-needs-rebuild-replica.blk",
@@ -2418,16 +2422,21 @@ func TestRegistry_UpdateFullHeartbeat_ConsumesExplicitVolumeModeFromPrimaryHeart
}
mode := tt.mode
reason := tt.reason
r.UpdateFullHeartbeat("primary-server:8080", []*master_pb.BlockVolumeInfoMessage{{
Path: "/blocks/vol-master-mode-" + tt.name + "-primary.blk",
Role: blockvol.RoleToWire(blockvol.RolePrimary),
VolumeMode: &mode,
Path: "/blocks/vol-master-mode-" + tt.name + "-primary.blk",
Role: blockvol.RoleToWire(blockvol.RolePrimary),
VolumeMode: &mode,
VolumeModeReason: &reason,
}}, "")
entry, _ := r.Lookup("vol-master-mode-" + tt.name)
if !entry.HasHeartbeatVolumeMode || entry.HeartbeatVolumeMode != tt.mode {
t.Fatalf("expected explicit volume_mode truth on entry, entry=%+v", entry)
}
if tt.reason != "" && (!entry.HasHeartbeatVolumeReason || entry.HeartbeatVolumeReason != tt.reason) {
t.Fatalf("expected explicit volume_mode_reason truth on entry, entry=%+v", entry)
}
if entry.VolumeMode != tt.wantMode {
t.Fatalf("expected explicit volume_mode %q, got %q", tt.wantMode, entry.VolumeMode)
}
@@ -2463,6 +2472,9 @@ func TestRegistry_UpdateFullHeartbeat_VolumeModeFallsBackWhenFieldAbsent(t *test
if entry.HasHeartbeatVolumeMode {
t.Fatalf("did not expect explicit volume_mode when field absent, entry=%+v", entry)
}
if entry.HasHeartbeatVolumeReason {
t.Fatalf("did not expect explicit volume_mode_reason when field absent, entry=%+v", entry)
}
if entry.VolumeMode != "degraded" {
t.Fatalf("expected fallback reconstructed degraded mode, got %q", entry.VolumeMode)
}
+11 -8
View File
@@ -369,18 +369,20 @@ func (ms *MasterServer) blockVolumePromoteHandler(w http.ResponseWriter, r *http
// entryToVolumeInfo converts a BlockVolumeEntry to a blockapi.VolumeInfo.
// primaryAlive indicates whether the primary server is alive (in blockServers set).
type entryReplicaSurface struct {
ReplicaReady bool
ReplicaDegraded bool
VolumeMode string
HealthState string
ReplicaReady bool
ReplicaDegraded bool
VolumeMode string
VolumeModeReason string
HealthState string
}
func entryReplicaSurfaceInfo(e *BlockVolumeEntry, primaryAlive bool) entryReplicaSurface {
return entryReplicaSurface{
ReplicaReady: e.ReplicaReady,
ReplicaDegraded: e.ReplicaDegraded,
VolumeMode: e.VolumeMode,
HealthState: deriveHealthStateWithLiveness(e, primaryAlive),
ReplicaReady: e.ReplicaReady,
ReplicaDegraded: e.ReplicaDegraded,
VolumeMode: e.VolumeMode,
VolumeModeReason: e.HeartbeatVolumeReason,
HealthState: deriveHealthStateWithLiveness(e, primaryAlive),
}
}
@@ -423,6 +425,7 @@ func entryToVolumeInfo(e *BlockVolumeEntry, primaryAlive bool) blockapi.VolumeIn
NQN: e.NQN,
HealthState: surface.HealthState,
VolumeMode: surface.VolumeMode,
VolumeModeReason: surface.VolumeModeReason,
}
for _, ri := range e.Replicas {
info.Replicas = append(info.Replicas, blockapi.ReplicaDetail{
@@ -225,6 +225,9 @@ func TestBlockVolumeLookupHandler_ReflectsCoreInfluencedReadyConsume(t *testing.
if info.VolumeMode != "publish_healthy" {
t.Fatalf("expected outward VolumeMode=publish_healthy, got %q", info.VolumeMode)
}
if info.VolumeModeReason != "" {
t.Fatalf("expected empty outward VolumeModeReason for publish_healthy, got %q", info.VolumeModeReason)
}
}
func TestBlockVolumeDeleteHandler(t *testing.T) {
@@ -318,6 +321,9 @@ func TestBlockVolumeListHandler_ReflectsCoreInfluencedDegradedConsume(t *testing
if info.VolumeMode != "degraded" {
t.Fatalf("expected outward VolumeMode=degraded, got %q", info.VolumeMode)
}
if info.VolumeModeReason != "barrier_timeout" {
t.Fatalf("expected outward VolumeModeReason=barrier_timeout, got %q", info.VolumeModeReason)
}
}
func TestBlockAssignHandler(t *testing.T) {
+19
View File
@@ -969,6 +969,7 @@ func (bs *BlockService) CollectBlockVolumeHeartbeat() []blockvol.BlockVolumeInfo
msgs[i].NeedsRebuild = bs.heartbeatNeedsRebuild(msgs[i].Path, s)
msgs[i].PublishHealthy = bs.heartbeatPublishHealthy(msgs[i].Path, s)
msgs[i].VolumeMode = bs.heartbeatVolumeMode(msgs[i].Path, s)
msgs[i].VolumeModeReason = bs.heartbeatVolumeModeReason(msgs[i].Path, s)
}
msgs[i].ReplicaDegraded = bs.heartbeatReplicaDegraded(msgs[i].Path, msgs[i].ReplicaDegraded)
// NVMe publication: report nvme_addr and nqn if NVMe target is running.
@@ -1067,6 +1068,24 @@ func (bs *BlockService) heartbeatVolumeMode(path string, state *volReplState) st
return ""
}
// heartbeatVolumeModeReason returns the explicit outward volume mode reason that
// should be exposed on the current heartbeat surface. On the core-present path
// it prefers the mode reason and falls back to the publication reason so the
// master can preserve the bounded explanation behind outward mode transitions.
// Older paths return empty and keep backward-compatible default behavior.
func (bs *BlockService) heartbeatVolumeModeReason(path string, state *volReplState) string {
if state == nil {
return ""
}
if proj, ok := bs.CoreProjection(path); ok {
if proj.Mode.Reason != "" {
return proj.Mode.Reason
}
return proj.Publication.Reason
}
return ""
}
// heartbeatReplicaDegraded returns the bounded degraded bit for the current
// heartbeat surface. On the Phase 15 live path it prefers the core mode when
// present, then falls back to the runtime-local status bit.
+12
View File
@@ -1070,6 +1070,9 @@ func TestBlockService_CollectBlockVolumeHeartbeat_PrimaryUsesCoreReadinessGate(t
if msg.VolumeMode != "bootstrap_pending" {
t.Fatalf("expected explicit bootstrap_pending volume_mode on heartbeat, msg=%+v", msg)
}
if msg.VolumeModeReason != "awaiting_shipper_connected" {
t.Fatalf("expected explicit bootstrap reason on heartbeat, msg=%+v", msg)
}
}
func TestBlockService_CollectBlockVolumeHeartbeat_ReplicaUsesCoreReadinessGate(t *testing.T) {
@@ -1317,6 +1320,9 @@ func TestBlockService_CollectBlockVolumeHeartbeat_PrimaryNeedsRebuildUsesCoreMod
if msg.VolumeMode != "needs_rebuild" {
t.Fatalf("expected explicit needs_rebuild volume_mode on heartbeat, msg=%+v", msg)
}
if msg.VolumeModeReason != "gap_too_large" {
t.Fatalf("expected explicit needs_rebuild reason on heartbeat, msg=%+v", msg)
}
}
func TestBlockService_CollectBlockVolumeHeartbeat_PrimaryPublishHealthyUsesCoreTruth(t *testing.T) {
@@ -1351,6 +1357,9 @@ func TestBlockService_CollectBlockVolumeHeartbeat_PrimaryPublishHealthyUsesCoreT
if msg.VolumeMode != "publish_healthy" {
t.Fatalf("expected explicit publish_healthy volume_mode on heartbeat, msg=%+v", msg)
}
if msg.VolumeModeReason != "" {
t.Fatalf("expected empty publish_healthy mode reason on heartbeat, msg=%+v", msg)
}
}
func TestBlockService_CollectBlockVolumeHeartbeat_PrimaryDegradedUsesCoreModeTruth(t *testing.T) {
@@ -1381,6 +1390,9 @@ func TestBlockService_CollectBlockVolumeHeartbeat_PrimaryDegradedUsesCoreModeTru
if msg.VolumeMode != "degraded" {
t.Fatalf("expected explicit degraded volume_mode on heartbeat, msg=%+v", msg)
}
if msg.VolumeModeReason != "barrier_timeout" {
t.Fatalf("expected explicit degraded reason on heartbeat, msg=%+v", msg)
}
}
func TestBlockService_HeartbeatIncludesReplicaAddrs(t *testing.T) {
+1
View File
@@ -23,6 +23,7 @@ type BlockVolumeInfoMessage struct {
NeedsRebuild bool // explicit needs_rebuild truth on the heartbeat seam
PublishHealthy bool // explicit publish_healthy truth on the heartbeat seam
VolumeMode string // explicit outward volume_mode truth on the heartbeat seam
VolumeModeReason string // explicit outward volume_mode_reason truth on the heartbeat seam
HealthScore float64 // CP8-2: 0.0-1.0
ScrubErrors int64 // CP8-2: lifetime scrub error count
LastScrubTime int64 // CP8-2: unix seconds
+46 -44
View File
@@ -14,28 +14,29 @@ func optionalStringPtr(v string) *string {
// InfoMessageToProto converts a Go wire type to proto.
func InfoMessageToProto(m BlockVolumeInfoMessage) *master_pb.BlockVolumeInfoMessage {
return &master_pb.BlockVolumeInfoMessage{
Path: m.Path,
VolumeSize: m.VolumeSize,
BlockSize: m.BlockSize,
Epoch: m.Epoch,
Role: m.Role,
WalHeadLsn: m.WalHeadLsn,
CheckpointLsn: m.CheckpointLsn,
HasLease: m.HasLease,
DiskType: m.DiskType,
ReplicaDataAddr: m.ReplicaDataAddr,
ReplicaCtrlAddr: m.ReplicaCtrlAddr,
ReplicaReady: &m.ReplicaReady,
NeedsRebuild: &m.NeedsRebuild,
PublishHealthy: &m.PublishHealthy,
VolumeMode: optionalStringPtr(m.VolumeMode),
HealthScore: m.HealthScore,
ScrubErrors: m.ScrubErrors,
LastScrubTime: m.LastScrubTime,
ReplicaDegraded: m.ReplicaDegraded,
DurabilityMode: m.DurabilityMode,
NvmeAddr: m.NvmeAddr,
Nqn: m.NQN,
Path: m.Path,
VolumeSize: m.VolumeSize,
BlockSize: m.BlockSize,
Epoch: m.Epoch,
Role: m.Role,
WalHeadLsn: m.WalHeadLsn,
CheckpointLsn: m.CheckpointLsn,
HasLease: m.HasLease,
DiskType: m.DiskType,
ReplicaDataAddr: m.ReplicaDataAddr,
ReplicaCtrlAddr: m.ReplicaCtrlAddr,
ReplicaReady: &m.ReplicaReady,
NeedsRebuild: &m.NeedsRebuild,
PublishHealthy: &m.PublishHealthy,
VolumeMode: optionalStringPtr(m.VolumeMode),
VolumeModeReason: optionalStringPtr(m.VolumeModeReason),
HealthScore: m.HealthScore,
ScrubErrors: m.ScrubErrors,
LastScrubTime: m.LastScrubTime,
ReplicaDegraded: m.ReplicaDegraded,
DurabilityMode: m.DurabilityMode,
NvmeAddr: m.NvmeAddr,
Nqn: m.NQN,
}
}
@@ -45,28 +46,29 @@ func InfoMessageFromProto(p *master_pb.BlockVolumeInfoMessage) BlockVolumeInfoMe
return BlockVolumeInfoMessage{}
}
return BlockVolumeInfoMessage{
Path: p.Path,
VolumeSize: p.VolumeSize,
BlockSize: p.BlockSize,
Epoch: p.Epoch,
Role: p.Role,
WalHeadLsn: p.WalHeadLsn,
CheckpointLsn: p.CheckpointLsn,
HasLease: p.HasLease,
DiskType: p.DiskType,
ReplicaDataAddr: p.ReplicaDataAddr,
ReplicaCtrlAddr: p.ReplicaCtrlAddr,
ReplicaReady: p.GetReplicaReady(),
NeedsRebuild: p.GetNeedsRebuild(),
PublishHealthy: p.GetPublishHealthy(),
VolumeMode: p.GetVolumeMode(),
HealthScore: p.HealthScore,
ScrubErrors: p.ScrubErrors,
LastScrubTime: p.LastScrubTime,
ReplicaDegraded: p.ReplicaDegraded,
DurabilityMode: p.DurabilityMode,
NvmeAddr: p.NvmeAddr,
NQN: p.Nqn,
Path: p.Path,
VolumeSize: p.VolumeSize,
BlockSize: p.BlockSize,
Epoch: p.Epoch,
Role: p.Role,
WalHeadLsn: p.WalHeadLsn,
CheckpointLsn: p.CheckpointLsn,
HasLease: p.HasLease,
DiskType: p.DiskType,
ReplicaDataAddr: p.ReplicaDataAddr,
ReplicaCtrlAddr: p.ReplicaCtrlAddr,
ReplicaReady: p.GetReplicaReady(),
NeedsRebuild: p.GetNeedsRebuild(),
PublishHealthy: p.GetPublishHealthy(),
VolumeMode: p.GetVolumeMode(),
VolumeModeReason: p.GetVolumeModeReason(),
HealthScore: p.HealthScore,
ScrubErrors: p.ScrubErrors,
LastScrubTime: p.LastScrubTime,
ReplicaDegraded: p.ReplicaDegraded,
DurabilityMode: p.DurabilityMode,
NvmeAddr: p.NvmeAddr,
NQN: p.Nqn,
}
}
@@ -320,6 +320,19 @@ func TestInfoMessage_VolumeModeRoundTrip(t *testing.T) {
}
}
func TestInfoMessage_VolumeModeReasonRoundTrip(t *testing.T) {
orig := BlockVolumeInfoMessage{
Path: "/data/vol.blk",
Epoch: 1,
VolumeModeReason: "awaiting_shipper_connected",
}
pb := InfoMessageToProto(orig)
back := InfoMessageFromProto(pb)
if back.VolumeModeReason != "awaiting_shipper_connected" {
t.Fatalf("VolumeModeReason: got %q, want %q", back.VolumeModeReason, "awaiting_shipper_connected")
}
}
func TestAssignment_MultiReplicaRoundTrip(t *testing.T) {
orig := BlockVolumeAssignment{
Path: "/data/vol.blk",
+36 -35
View File
@@ -10,11 +10,11 @@ import (
type CreateVolumeRequest struct {
Name string `json:"name"`
SizeBytes uint64 `json:"size_bytes"`
ReplicaPlacement string `json:"replica_placement"` // SeaweedFS placement string: "000", "001", "010", "100"
DiskType string `json:"disk_type"` // e.g. "ssd", "hdd"
DurabilityMode string `json:"durability_mode,omitempty"` // "best_effort", "sync_all", "sync_quorum"
ReplicaFactor int `json:"replica_factor,omitempty"` // 1, 2, or 3 (default: 2)
Preset string `json:"preset,omitempty"` // "database", "general", "throughput", or ""
ReplicaPlacement string `json:"replica_placement"` // SeaweedFS placement string: "000", "001", "010", "100"
DiskType string `json:"disk_type"` // e.g. "ssd", "hdd"
DurabilityMode string `json:"durability_mode,omitempty"` // "best_effort", "sync_all", "sync_quorum"
ReplicaFactor int `json:"replica_factor,omitempty"` // 1, 2, or 3 (default: 2)
Preset string `json:"preset,omitempty"` // "database", "general", "throughput", or ""
}
// VolumeInfo describes a block volume.
@@ -39,14 +39,15 @@ type VolumeInfo struct {
ReplicaReady bool `json:"replica_ready,omitempty"`
HealthScore float64 `json:"health_score"`
ReplicaDegraded bool `json:"replica_degraded,omitempty"`
DurabilityMode string `json:"durability_mode"` // CP8-3-1
DurabilityMode string `json:"durability_mode"` // CP8-3-1
Preset string `json:"preset,omitempty"` // CP11B-1: preset used at creation
NvmeAddr string `json:"nvme_addr,omitempty"`
NQN string `json:"nqn,omitempty"`
// CP11B-4: Operator-facing health state.
HealthState string `json:"health_state"` // "healthy", "degraded", "rebuilding", "unsafe"
HealthState string `json:"health_state"` // "healthy", "degraded", "rebuilding", "unsafe"
// CP13-9: Normalized volume mode for constrained-runtime surfaces.
VolumeMode string `json:"volume_mode,omitempty"` // "allocated_only", "bootstrap_pending", "publish_healthy", "degraded", "needs_rebuild"
VolumeMode string `json:"volume_mode,omitempty"` // "allocated_only", "bootstrap_pending", "publish_healthy", "degraded", "needs_rebuild"
VolumeModeReason string `json:"volume_mode_reason,omitempty"`
}
// ResolvedPolicyResponse is the response for POST /block/volume/resolve.
@@ -115,26 +116,26 @@ type PromoteVolumeRequest struct {
type PromoteVolumeResponse struct {
NewPrimary string `json:"new_primary"`
Epoch uint64 `json:"epoch"`
Reason string `json:"reason,omitempty"` // rejection reason if failed
Rejections []PreflightRejection `json:"rejections,omitempty"` // per-replica rejection details
Reason string `json:"reason,omitempty"` // rejection reason if failed
Rejections []PreflightRejection `json:"rejections,omitempty"` // per-replica rejection details
}
// BlockStatusResponse is the response for GET /block/status.
type BlockStatusResponse struct {
VolumeCount int `json:"volume_count"`
ServerCount int `json:"server_count"`
VolumeCount int `json:"volume_count"`
ServerCount int `json:"server_count"`
PromotionLSNTolerance uint64 `json:"promotion_lsn_tolerance"`
BarrierLagLSN uint64 `json:"barrier_lag_lsn"`
PromotionsTotal int64 `json:"promotions_total"`
FailoversTotal int64 `json:"failovers_total"`
RebuildsTotal int64 `json:"rebuilds_total"`
AssignmentQueueDepth int `json:"assignment_queue_depth"`
BarrierLagLSN uint64 `json:"barrier_lag_lsn"`
PromotionsTotal int64 `json:"promotions_total"`
FailoversTotal int64 `json:"failovers_total"`
RebuildsTotal int64 `json:"rebuilds_total"`
AssignmentQueueDepth int `json:"assignment_queue_depth"`
// CP11B-4: Operator summary fields.
HealthyCount int `json:"healthy_count"`
DegradedCount int `json:"degraded_count"`
RebuildingCount int `json:"rebuilding_count"`
UnsafeCount int `json:"unsafe_count"`
NvmeCapableServers int `json:"nvme_capable_servers"`
HealthyCount int `json:"healthy_count"`
DegradedCount int `json:"degraded_count"`
RebuildingCount int `json:"rebuilding_count"`
UnsafeCount int `json:"unsafe_count"`
NvmeCapableServers int `json:"nvme_capable_servers"`
}
// PreflightRejection describes why a specific replica was rejected for promotion.
@@ -145,23 +146,23 @@ type PreflightRejection struct {
// PreflightResponse is the response for GET /block/volume/{name}/preflight.
type PreflightResponse struct {
VolumeName string `json:"volume_name"`
Promotable bool `json:"promotable"`
Reason string `json:"reason,omitempty"`
CandidateServer string `json:"candidate_server,omitempty"`
CandidateHealth float64 `json:"candidate_health,omitempty"`
CandidateWALLSN uint64 `json:"candidate_wal_lsn,omitempty"`
Rejections []PreflightRejection `json:"rejections,omitempty"`
PrimaryServer string `json:"primary_server"`
PrimaryAlive bool `json:"primary_alive"`
VolumeName string `json:"volume_name"`
Promotable bool `json:"promotable"`
Reason string `json:"reason,omitempty"`
CandidateServer string `json:"candidate_server,omitempty"`
CandidateHealth float64 `json:"candidate_health,omitempty"`
CandidateWALLSN uint64 `json:"candidate_wal_lsn,omitempty"`
Rejections []PreflightRejection `json:"rejections,omitempty"`
PrimaryServer string `json:"primary_server"`
PrimaryAlive bool `json:"primary_alive"`
}
// VolumePlanResponse is the response for POST /block/volume/plan.
type VolumePlanResponse struct {
ResolvedPolicy ResolvedPolicyView `json:"resolved_policy"`
Plan VolumePlanView `json:"plan"`
Warnings []string `json:"warnings,omitempty"`
Errors []string `json:"errors,omitempty"`
ResolvedPolicy ResolvedPolicyView `json:"resolved_policy"`
Plan VolumePlanView `json:"plan"`
Warnings []string `json:"warnings,omitempty"`
Errors []string `json:"errors,omitempty"`
}
// VolumePlanView describes the placement plan.
@@ -40,6 +40,7 @@ type VolumeInfo struct {
NvmeAddr string `json:"nvme_addr,omitempty"`
NQN string `json:"nqn,omitempty"`
VolumeMode string `json:"volume_mode,omitempty"` // CP13-9
VolumeModeReason string `json:"volume_mode_reason,omitempty"`
}
// ReplicaDetail describes one replica in the API response.
@@ -109,7 +110,7 @@ type PreflightResponse struct {
Reason string `json:"reason,omitempty"`
CandidateServer string `json:"candidate_server,omitempty"`
CandidateHealth float64 `json:"candidate_health,omitempty"`
CandidateWALLSN uint64 `json:"candidate_wal_lsn,omitempty"`
CandidateWALLSN uint64 `json:"candidate_wal_lsn,omitempty"`
Rejections []PreflightRejection `json:"rejections,omitempty"`
PrimaryServer string `json:"primary_server"`
PrimaryAlive bool `json:"primary_alive"`
@@ -137,10 +138,10 @@ type ResolvedPolicyView struct {
// VolumePlanResponse is the response for POST /block/volume/plan.
type VolumePlanResponse struct {
ResolvedPolicy ResolvedPolicyView `json:"resolved_policy"`
Plan VolumePlanView `json:"plan"`
Warnings []string `json:"warnings,omitempty"`
Errors []string `json:"errors,omitempty"`
ResolvedPolicy ResolvedPolicyView `json:"resolved_policy"`
Plan VolumePlanView `json:"plan"`
Warnings []string `json:"warnings,omitempty"`
Errors []string `json:"errors,omitempty"`
}
// VolumePlanView describes the placement plan.