feat: CP13-9 — mode normalization for constrained V1 runtime

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) <noreply@anthropic.com>
This commit is contained in:
pingqiu
2026-04-03 15:02:50 -07:00
co-authored by Claude Opus 4.6
parent 4c7fbefe25
commit 6e1b8efd68
6 changed files with 295 additions and 2 deletions
@@ -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
+53 -2
View File
@@ -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.
@@ -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{
+133
View File
@@ -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")
}
+2
View File
@@ -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.
@@ -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.