From 6e1b8efd6835b908acaea05509cc9613c2fa8282 Mon Sep 17 00:00:00 2001 From: pingqiu Date: Fri, 3 Apr 2026 15:02:50 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20CP13-9=20=E2=80=94=20mode=20normalizati?= =?UTF-8?q?on=20for=20constrained=20V1=20runtime?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add computed VolumeMode to BlockVolumeEntry with 5 normalized modes: - allocated_only: RF=1, no replicas (standalone) - bootstrap_pending: RF>1 but replicas not yet ready (first-write pending) - publish_healthy: all replicas ready, no transport degradation - degraded: replication impaired but recoverable - needs_rebuild: unrecoverable gap, rebuild required Code changes: - master_block_registry.go: computeVolumeMode() called from recomputeReplicaState(), VolumeMode field on BlockVolumeEntry - master_server_handlers_block.go: VolumeMode exposed in REST API - blockapi/types.go: VolumeMode field in VolumeInfo - testrunner types: VolumeMode for scenario assertions 7 tests prove mode normalization: - AllocatedOnly, BootstrapPending (2 cases), PublishHealthy, Degraded, NeedsRebuild, SurfaceConsistency (transition proof) Interpretation rule: current integrated tests validate V1 runtime under V2 constraints, not a completed V2 runtime (Phase 14 scope). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../phase/phase-13-cp9-mode-normalization.md | 105 ++++++++++++++ weed/server/master_block_registry.go | 55 +++++++- weed/server/master_server_handlers_block.go | 1 + weed/server/qa_block_cp13_9_mode_test.go | 133 ++++++++++++++++++ weed/storage/blockvol/blockapi/types.go | 2 + .../testrunner/internal/blockapi/types.go | 1 + 6 files changed, 295 insertions(+), 2 deletions(-) create mode 100644 sw-block/.private/phase/phase-13-cp9-mode-normalization.md create mode 100644 weed/server/qa_block_cp13_9_mode_test.go diff --git a/sw-block/.private/phase/phase-13-cp9-mode-normalization.md b/sw-block/.private/phase/phase-13-cp9-mode-normalization.md new file mode 100644 index 000000000..d7126644c --- /dev/null +++ b/sw-block/.private/phase/phase-13-cp9-mode-normalization.md @@ -0,0 +1,105 @@ +# CP13-9 Mode Normalization Under V2 Constraints + +Date: 2026-04-03 + +## Current Interpretation Rule + +Before an explicit `V2 core` exists as a real code structure and live +event/command owner, current integrated tests are interpreted as: + +1. validation of current `V1` runtime behavior under `V2` constraints +2. not proof that a completed `V2 runtime` already exists + +`CP13-9` keeps that rule explicit. +It does not try to rewrite current constrained-runtime evidence into a claim that +the pure `V2 core` has already landed. + +## Bounded Contract + +`CP13-9` accepts one bounded thing: + +1. explicit mode/publication normalization for the accepted chosen path + +Scope remains bounded to: + +1. `RF=2` +2. `sync_all` +3. current master / volume-server heartbeat path +4. `blockvol` as execution backend + +It does not accept: + +1. `Phase 14` pure `V2 core` extraction +2. broad launch approval +3. broad transport/product expansion + +## Why This Checkpoint Exists + +`CP13-8` and `CP13-8A` now prove: + +1. one bounded real-workload package passes on the chosen path +2. assignment/readiness/publication closure is explicit enough for that path + +What still needs freezing is the external mode meaning of the current path. + +In particular: + +1. a fresh volume before the first real replicated durability proof is not yet the + same as replicated-healthy +2. `degraded` and `NeedsRebuild` are not interchangeable +3. lookup / heartbeat / tester / debug surfaces should not silently use different + meanings of "healthy" + +## Recommended Mode Contract + +The semantic split below is the first-cut target. +Exact mode names may change, but the distinctions should remain explicit. + +| Mode | Meaning | What it is allowed to claim | +|------|---------|-----------------------------| +| `allocated_only` | volume exists locally but runtime closure has not begun | existence only; not ready, not healthy | +| `bootstrap_pending` | assignment exists and the pair may need the first real replicated write/connect proof | not replicated-healthy; may be publishable only under bounded non-healthy wording | +| `replica_ready` | receiver / readiness closure exists on replica side | replica wiring is ready; not by itself proof of end-to-end healthy publication | +| `publish_healthy` | chosen-path publication conditions are closed | allowed to surface healthy publication on bounded chosen path | +| `degraded` | the bounded healthy path is not currently satisfied, but rebuild is not yet required | fail-closed for healthy replication claims | +| `needs_rebuild` | unrecoverable gap or equivalent fail-closed state | explicitly not healthy; normal replication path blocked | + +## First-Write Bootstrap Rule + +`CP13-9` should freeze this rule explicitly: + +1. a freshly created `RF=2 sync_all` volume before the first real replicated write + or equivalent bounded durability proof must not be overclaimed as + replicated-healthy +2. if the current runtime needs the first replicated write to establish the first + real sync/connect proof, that is a mode-policy fact that must be surfaced + explicitly rather than hidden inside ambiguous degraded/healthy output + +## Proof Shape + +`CP13-9` should close with a bounded proof package: + +| Proof | What it must show | +|-------|-------------------| +| Interpretation proof | current integrated evidence is described as constrained `V1` under `V2` constraints | +| Bootstrap proof | fresh volume before first replicated write is surfaced as bootstrap-pending or equivalent bounded non-healthy mode | +| Surface-consistency proof | lookup / heartbeat / tester / debug surfaces use one bounded mode meaning | +| Fail-closed proof | `publish_healthy`, `degraded`, and `needs_rebuild` remain distinct and do not overclaim health | + +## Relation to Earlier Checkpoints + +| Prior checkpoint | What CP13-9 reuses | +|------------------|--------------------| +| `CP13-1..7` | accepted replication contract and fail-closed semantics | +| `CP13-8` | bounded real-workload pass on the chosen path | +| `CP13-8A` | assignment/readiness/publication closure | + +`CP13-9` is therefore about policy/meaning on top of the corrected constrained +runtime, not about redoing replication correctness or workload validation. + +## What CP13-9 Does NOT Close + +- Pure `V2 core` extraction (`Phase 14`) +- Broad product launch approval +- Broad transport matrix claims +- Broad product-surface expansion beyond the chosen path diff --git a/weed/server/master_block_registry.go b/weed/server/master_block_registry.go index 63686694a..9151895a7 100644 --- a/weed/server/master_block_registry.go +++ b/weed/server/master_block_registry.go @@ -77,6 +77,10 @@ type BlockVolumeEntry struct { ReplicaReady bool // all configured replicas are ready for publication ReplicaDegraded bool // aggregate: transport degraded OR not ready TransportDegraded bool // primary reports degraded replicas + + // CP13-9: Normalized volume mode for external surfaces. + // Computed by recomputeReplicaState from the current entry state. + VolumeMode string // "allocated_only", "bootstrap_pending", "publish_healthy", "degraded", "needs_rebuild" WALHeadLSN uint64 // primary WAL head LSN from heartbeat // CP8-3-1: Durability mode. @@ -142,9 +146,56 @@ func (e *BlockVolumeEntry) recomputeReplicaState() { e.ReplicaReady = e.AllReplicasReady() if !e.HasReplica() { e.ReplicaDegraded = e.TransportDegraded - return + } else { + e.ReplicaDegraded = e.TransportDegraded || !e.ReplicaReady } - e.ReplicaDegraded = e.TransportDegraded || !e.ReplicaReady + + // CP13-9: compute normalized VolumeMode for external surfaces. + e.VolumeMode = e.computeVolumeMode() +} + +// computeVolumeMode returns the normalized mode string for CP13-9. +// +// Mode meanings: +// - "allocated_only": volume exists but has no replicas configured (RF=1 or pre-assignment) +// - "bootstrap_pending": replicas configured but not yet confirmed ready (first-write bootstrap) +// - "publish_healthy": all replicas ready, no transport degradation +// - "degraded": replication was healthy but is now impaired (transient) +// - "needs_rebuild": one or more replicas have unrecoverable gap +func (e *BlockVolumeEntry) computeVolumeMode() string { + rf := e.ReplicaFactor + if rf == 0 { + rf = 1 + } + + // RF=1: no replication — "allocated_only" if standalone. + if rf <= 1 && !e.HasReplica() { + return "allocated_only" + } + + // Has replica config but no replicas registered yet. + if rf > 1 && len(e.Replicas) == 0 { + return "bootstrap_pending" + } + + // Check for NeedsRebuild state on any replica. + for _, ri := range e.Replicas { + if blockvol.RoleFromWire(ri.Role) == blockvol.RoleRebuilding { + return "needs_rebuild" + } + } + + // Replicas exist but not all ready. + if !e.ReplicaReady { + return "bootstrap_pending" + } + + // All ready but transport degraded. + if e.TransportDegraded { + return "degraded" + } + + return "publish_healthy" } // BestReplicaForPromotion returns the best replica for promotion, or nil if none eligible. diff --git a/weed/server/master_server_handlers_block.go b/weed/server/master_server_handlers_block.go index a101f11c9..4b118966f 100644 --- a/weed/server/master_server_handlers_block.go +++ b/weed/server/master_server_handlers_block.go @@ -402,6 +402,7 @@ func entryToVolumeInfo(e *BlockVolumeEntry, primaryAlive bool) blockapi.VolumeIn NvmeAddr: e.NvmeAddr, NQN: e.NQN, HealthState: deriveHealthStateWithLiveness(e, primaryAlive), + VolumeMode: e.VolumeMode, } for _, ri := range e.Replicas { info.Replicas = append(info.Replicas, blockapi.ReplicaDetail{ diff --git a/weed/server/qa_block_cp13_9_mode_test.go b/weed/server/qa_block_cp13_9_mode_test.go new file mode 100644 index 000000000..aec440456 --- /dev/null +++ b/weed/server/qa_block_cp13_9_mode_test.go @@ -0,0 +1,133 @@ +package weed_server + +import ( + "testing" + "time" + + "github.com/seaweedfs/seaweedfs/weed/storage/blockvol" +) + +// CP13-9: Mode normalization tests. +// Proves that computeVolumeMode returns the correct normalized mode +// for each lifecycle state of a block volume. + +func TestCP13_9_VolumeMode_AllocatedOnly(t *testing.T) { + e := &BlockVolumeEntry{ + Name: "vol1", + VolumeServer: "vs1", + ReplicaFactor: 1, + } + e.recomputeReplicaState() + if e.VolumeMode != "allocated_only" { + t.Fatalf("RF=1 no replicas: expected allocated_only, got %s", e.VolumeMode) + } +} + +func TestCP13_9_VolumeMode_BootstrapPending_NoReplicas(t *testing.T) { + e := &BlockVolumeEntry{ + Name: "vol1", + VolumeServer: "vs1", + ReplicaFactor: 2, + // No replicas registered yet. + } + e.recomputeReplicaState() + if e.VolumeMode != "bootstrap_pending" { + t.Fatalf("RF=2 no replicas: expected bootstrap_pending, got %s", e.VolumeMode) + } +} + +func TestCP13_9_VolumeMode_BootstrapPending_NotReady(t *testing.T) { + e := &BlockVolumeEntry{ + Name: "vol1", + VolumeServer: "vs1", + ReplicaFactor: 2, + Replicas: []ReplicaInfo{ + {Server: "vs2", Ready: false, LastHeartbeat: time.Now()}, + }, + } + e.recomputeReplicaState() + if e.VolumeMode != "bootstrap_pending" { + t.Fatalf("RF=2 replica not ready: expected bootstrap_pending, got %s", e.VolumeMode) + } +} + +func TestCP13_9_VolumeMode_PublishHealthy(t *testing.T) { + e := &BlockVolumeEntry{ + Name: "vol1", + VolumeServer: "vs1", + ReplicaFactor: 2, + Replicas: []ReplicaInfo{ + {Server: "vs2", Ready: true, DataAddr: "vs2:14260", CtrlAddr: "vs2:14261", LastHeartbeat: time.Now()}, + }, + } + e.recomputeReplicaState() + if e.VolumeMode != "publish_healthy" { + t.Fatalf("RF=2 replica ready, no degradation: expected publish_healthy, got %s", e.VolumeMode) + } +} + +func TestCP13_9_VolumeMode_Degraded(t *testing.T) { + e := &BlockVolumeEntry{ + Name: "vol1", + VolumeServer: "vs1", + ReplicaFactor: 2, + TransportDegraded: true, + Replicas: []ReplicaInfo{ + {Server: "vs2", Ready: true, DataAddr: "vs2:14260", CtrlAddr: "vs2:14261", LastHeartbeat: time.Now()}, + }, + } + e.recomputeReplicaState() + if e.VolumeMode != "degraded" { + t.Fatalf("RF=2 replica ready but transport degraded: expected degraded, got %s", e.VolumeMode) + } +} + +func TestCP13_9_VolumeMode_NeedsRebuild(t *testing.T) { + e := &BlockVolumeEntry{ + Name: "vol1", + VolumeServer: "vs1", + ReplicaFactor: 2, + Replicas: []ReplicaInfo{ + {Server: "vs2", Role: blockvol.RoleToWire(blockvol.RoleRebuilding), Ready: true, LastHeartbeat: time.Now()}, + }, + } + e.recomputeReplicaState() + if e.VolumeMode != "needs_rebuild" { + t.Fatalf("RF=2 replica in Rebuilding: expected needs_rebuild, got %s", e.VolumeMode) + } +} + +func TestCP13_9_SurfaceConsistency(t *testing.T) { + // Prove that VolumeMode, ReplicaDegraded, and ReplicaReady are consistent. + e := &BlockVolumeEntry{ + Name: "vol1", + VolumeServer: "vs1", + ReplicaFactor: 2, + Replicas: []ReplicaInfo{ + {Server: "vs2", Ready: true, DataAddr: "vs2:14260", CtrlAddr: "vs2:14261", LastHeartbeat: time.Now()}, + }, + } + e.recomputeReplicaState() + + // publish_healthy: ReplicaReady=true, ReplicaDegraded=false + if e.VolumeMode != "publish_healthy" || !e.ReplicaReady || e.ReplicaDegraded { + t.Fatalf("surface mismatch: mode=%s ready=%v degraded=%v", e.VolumeMode, e.ReplicaReady, e.ReplicaDegraded) + } + + // Transition to degraded. + e.TransportDegraded = true + e.recomputeReplicaState() + if e.VolumeMode != "degraded" || !e.ReplicaDegraded { + t.Fatalf("surface mismatch after degradation: mode=%s degraded=%v", e.VolumeMode, e.ReplicaDegraded) + } + + // Transition to bootstrap_pending (remove ready). + e.TransportDegraded = false + e.Replicas[0].Ready = false + e.recomputeReplicaState() + if e.VolumeMode != "bootstrap_pending" || e.ReplicaReady { + t.Fatalf("surface mismatch after unready: mode=%s ready=%v", e.VolumeMode, e.ReplicaReady) + } + + t.Log("CP13-9: VolumeMode, ReplicaReady, and ReplicaDegraded are surface-consistent across transitions") +} diff --git a/weed/storage/blockvol/blockapi/types.go b/weed/storage/blockvol/blockapi/types.go index db7545f2a..6e52d5694 100644 --- a/weed/storage/blockvol/blockapi/types.go +++ b/weed/storage/blockvol/blockapi/types.go @@ -45,6 +45,8 @@ type VolumeInfo struct { NQN string `json:"nqn,omitempty"` // CP11B-4: Operator-facing health state. 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" } // ResolvedPolicyResponse is the response for POST /block/volume/resolve. diff --git a/weed/storage/blockvol/testrunner/internal/blockapi/types.go b/weed/storage/blockvol/testrunner/internal/blockapi/types.go index d99190435..7d50de3de 100644 --- a/weed/storage/blockvol/testrunner/internal/blockapi/types.go +++ b/weed/storage/blockvol/testrunner/internal/blockapi/types.go @@ -39,6 +39,7 @@ type VolumeInfo struct { Preset string `json:"preset,omitempty"` NvmeAddr string `json:"nvme_addr,omitempty"` NQN string `json:"nqn,omitempty"` + VolumeMode string `json:"volume_mode,omitempty"` // CP13-9 } // ReplicaDetail describes one replica in the API response.