mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-21 06:36:54 +00:00
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:
co-authored by
Claude Opus 4.6
parent
4c7fbefe25
commit
6e1b8efd68
@@ -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
|
||||
@@ -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{
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user