mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-22 07:06:51 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+323
-313
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user