feat: start Phase 14 V2 core shell

Make the first V2 core owner explicit in sw-block by freezing Phase 14 docs, mode/readiness/publication semantics, and bounded command emission rules. This turns accepted Phase 13 constraints into executable core behavior without overclaiming live runtime cutover.

Made-with: Cursor
This commit is contained in:
pingqiu
2026-04-03 16:11:38 -07:00
parent 6e1b8efd68
commit fb0da91196
13 changed files with 2620 additions and 81 deletions
+352
View File
@@ -0,0 +1,352 @@
Purpose: append-only technical pack and delivery log for `Phase 14` V2 core
extraction.
---
### `14A` Technical Pack
Date: 2026-04-03
Goal: freeze the first explicit `V2 core` shell inside
`sw-block/engine/replication` so current accepted semantic constraints become
executable state/event/command/projection ownership, not only design wording
#### Layer 1: Semantic Core
##### Problem statement
`Phase 13` accepted:
1. bounded replication correctness
2. bounded assignment/publication closure
3. bounded mode normalization
But those results are still interpreted mainly as:
1. constrained-`V1` runtime behavior under `V2` rules
`14A` accepts one narrower thing:
1. the first real `V2 core` semantic shell exists as code in
`sw-block/engine/replication`
It does not accept:
1. live runtime cutover
2. adapter rebinding
3. product-surface migration
4. launch or performance claims
##### State / contract
`14A` must make these truths explicit in code:
1. one bounded `VolumeState` owns normalized mode, readiness, boundary, and
desired replica truth
2. one bounded event set expresses assignment, readiness observation, durable
boundary change, and rebuild escalation
3. one bounded command set expresses semantic decisions without runtime side
effects
4. one bounded projection expresses outward publication meaning from the same
state owner
5. the current interpretation remains:
- explicit `V2 core` shell exists
- integrated runtime authority is still `constrained_v1` until later phases
##### Must preserve
1. stable `ReplicaID` ownership
2. durable boundary truth is not inferred from diagnostic shipped progress
3. `publish_healthy` requires named readiness plus durable boundary closure
4. `degraded` and `needs_rebuild` remain distinct fail-closed modes
5. the code does not overclaim live `V2` runtime ownership
##### Reject shapes
Reject `14A` if:
1. the new core shell is only a naming wrapper with no deterministic state
update path
2. `publish_healthy` can be reached from assignment or transport convenience
without durable boundary truth
3. diagnostic sender progress is allowed to establish durable authority
4. `degraded` and `needs_rebuild` collapse into one ambiguous unhealthy bucket
5. the delivery wording implies live path cutover
#### Layer 2: Execution Core
##### Files in scope
Primary files:
1. `sw-block/engine/replication/state.go`
2. `sw-block/engine/replication/event.go`
3. `sw-block/engine/replication/command.go`
4. `sw-block/engine/replication/projection.go`
5. `sw-block/engine/replication/engine.go`
6. `sw-block/engine/replication/phase14_core_test.go`
7. `sw-block/engine/replication/doc.go`
Existing substrate kept in place:
1. `sw-block/engine/replication/registry.go`
2. `sw-block/engine/replication/sender.go`
3. `sw-block/engine/replication/session.go`
4. `sw-block/engine/replication/orchestrator.go`
5. nearby ownership/recovery tests
##### Execution order
`14A` follows the `Phase 14+` framework strictly:
1. explicit state
2. explicit events
3. explicit commands
4. explicit projection
5. deterministic engine loop
6. bounded structural tests
##### Acceptance basis
Keep the proof set small and structural:
1. identity / ownership
- stable `ReplicaID`
- endpoint change invalidates active ownership session
2. state eligibility
- only eligible primary path can reach `publish_healthy`
3. durable boundary
- barrier durability updates authority
- diagnostic shipped progress stays diagnostic
4. fail-closed modes
- `degraded` and `needs_rebuild` stay distinct and non-healthy
5. interpretation rule
- the shell begins `V2 core`
- it does not yet claim live runtime authority
##### Delivery posture
This phase uses the larger-slice execution model:
1. main developer owns semantic design and implementation
2. `sw` is used only for bounded support tasks if needed
3. `tester` validates the structural acceptance basis
4. `manager` challenges semantic adequacy and overclaim control
##### Review gate
Every `14A` code change or acceptance note should answer:
1. semantic constraint satisfied
2. overclaim avoided
3. accepted proof preserved
#### Starting point inventory
Current explicit shell already present in repo:
1. `state.go`
- `RuntimeAuthority`
- `VolumeRole`
- `ModeName`
- `ReadinessView`
- `BoundaryView`
- `ModeView`
- `VolumeState`
2. `event.go`
- assignment
- readiness observation
- barrier accepted / rejected
- checkpoint advance
- rebuild observation / commit
3. `command.go`
- `ApplyRoleCommand`
- `StartReceiverCommand`
- `ConfigureShipperCommand`
- `InvalidateSessionCommand`
- `PublishProjectionCommand`
4. `projection.go`
- `PublicationProjection`
5. `engine.go`
- deterministic `ApplyEvent()`
- recompute mode/readiness/publication
- emit bounded commands and projection
6. `phase14_core_test.go`
- structural acceptance basis for the shell
#### Immediate development target
The next development target under `14A` is not to broaden the shell.
It is to make the shell the clear semantic owner for the first complete chain:
1. `mode`
2. `readiness`
3. `publication`
and verify the package stays internally coherent before `14B` begins.
#### Verification status
Current package verification on 2026-04-03:
1. `go test ./...` in `sw-block/engine/replication`
2. result: `PASS`
3. interpretation:
- the current explicit shell is a valid starting point for `Phase 14`
- this verifies bounded internal coherence only
- this does not claim live runtime cutover
---
### `14A` Delivery Note Rev 1
Date: 2026-04-03
Scope: strengthen the first `mode -> readiness -> publication` chain inside the
explicit `V2 core` shell without adding any live adapter hook
What changed:
1. publication is now explicit core-owned state, not only an implicit boolean
threaded through readiness/projection
2. the engine now emits normalized publication-gate reasons for bootstrap and
non-primary states
3. `RF=1 / no replicas -> allocated_only` is now frozen directly in the core
shell, aligning the code with accepted `CP13-9` semantics
Files changed:
1. `sw-block/engine/replication/state.go`
- added `PublicationView`
- `VolumeState` now owns publication truth explicitly
2. `sw-block/engine/replication/projection.go`
- `PublicationProjection` now carries explicit publication state
3. `sw-block/engine/replication/engine.go`
- split publication recompute away from raw readiness bits
- added explicit gate reasons:
- `awaiting_role_apply`
- `awaiting_shipper_configured`
- `awaiting_shipper_connected`
- `awaiting_barrier_durability`
- `replica_not_primary`
- `allocated_only`
- enforced `no replicas => allocated_only`
4. `sw-block/engine/replication/phase14_core_test.go`
- strengthened the primary publication chain proof with gate-reason checks
- strengthened replica-ready proof with non-primary publication reason
- added direct `allocated_only` proof for no-replica path
Proofs added or strengthened:
1. primary publication closure proof
- assignment -> role applied -> shipper configured -> shipper connected ->
barrier durability now produces the expected gate reason at each stage
2. replica-ready is not publication proof
- `replica_ready` stays non-healthy with explicit reason
`replica_not_primary`
3. `CP13-9` allocated-only proof
- a primary assignment with no replicas remains `allocated_only`, not
`bootstrap_pending`
Validation:
1. `gofmt -w state.go projection.go engine.go phase14_core_test.go`
2. `go test ./...`
3. result: `PASS`
Constraint / overclaim / proof review:
1. semantic constraint satisfied
- `CP13-8A`: assignment/readiness/publication closure must be explicit
- `CP13-9`: `allocated_only`, `bootstrap_pending`, `replica_ready`,
`publish_healthy`, `degraded`, and `needs_rebuild` must stay bounded and
non-overlapping
2. overclaim avoided
- publication health can no longer be inferred from assignment presence,
shipper connection alone, or replica readiness
- RF=1/no-replica path no longer overclaims `bootstrap_pending`
3. proof preserved
- barrier durability remains the authority for `publish_healthy`
- diagnostic shipped progress remains non-authoritative
- constrained-`V1` runtime interpretation remains explicit
---
### `14B` Delivery Note Rev 1
Date: 2026-04-03
Scope: freeze first bounded command-emission rules so the explicit `V2 core`
decides commands from semantic gaps, not from repeated event convenience
What changed:
1. repeated assignments no longer blindly reset semantic state and re-emit the
same commands
2. command emission is now gap-driven:
- apply role only when epoch/role command state is stale
- start receiver only when replica path still needs receiver start for the
current epoch
- configure shipper only when primary path still needs current replica
configuration
- invalidate session only on a new failure transition, not every repeated
degraded event
3. assignment changes still re-emit the needed command when semantic intent
really changes
Files changed:
1. `sw-block/engine/replication/state.go`
- added private command-state tracking to `VolumeState`
2. `sw-block/engine/replication/engine.go`
- extracted assignment handling into gap-driven command logic
- preserved readiness when the assignment is repeated without semantic change
- reset only the relevant readiness edges when role/epoch/replica-set changes
- deduplicated repeated invalidation commands for the same failure reason
3. `sw-block/engine/replication/phase14_command_test.go`
- added exact command-sequence proofs
Proofs added:
1. primary repeated-assignment boundedness
- first assignment emits:
- `apply_role`
- `configure_shipper`
- `publish_projection`
- repeated identical assignment emits only:
- `publish_projection`
2. replica repeated-assignment boundedness
- first replica assignment emits:
- `apply_role`
- `start_receiver`
- `publish_projection`
- repeated identical assignment emits only:
- `publish_projection`
3. assignment-change selective reissue
- changed replica endpoint on primary path reissues only
`configure_shipper`, not the whole initial command bundle
4. repeated-failure boundedness
- first `BarrierRejected(timeout)` emits `invalidate_session`
- repeated `BarrierRejected(timeout)` does not emit duplicate invalidation
Validation:
1. `gofmt -w state.go engine.go phase14_command_test.go`
2. `go test ./...`
3. result: `PASS`
Constraint / overclaim / proof review:
1. semantic constraint satisfied
- `Phase 14B`: command emission must come from semantic state, not runtime
convenience
- `CP13-8A`: assignment/readiness/publication closure must stay explicit
- `CP13-9`: bounded mode meaning must not be destabilized by repeated command
churn
2. overclaim avoided
- repeated assignment no longer acts like proof that role apply / receiver
start / shipper configure still need to happen
- repeated failure does not create unbounded invalidation spam that looks like
fresh semantic transitions
3. proof preserved
- `14A` publication-gate proofs still hold
- barrier durability is still the only path to `publish_healthy`
- constrained-`V1` interpretation is still explicit, not broadened
+180
View File
@@ -0,0 +1,180 @@
# Phase 14
Date: 2026-04-03
Status: active
Purpose: make the `V2 core` explicit inside `sw-block/engine/replication` so
accepted semantic constraints become executable ownership, rather than staying
only as design and constrained-`V1` interpretation
## Why This Phase Exists
`Phase 13` accepted a bounded replication-correctness package on the current
chosen path, including:
1. corrected `sync_all` replication semantics
2. bounded real-workload validation
3. assignment/publication closure
4. bounded mode normalization
That package matters, but it still mostly evaluates `V1` runtime behavior under
`V2` constraints.
`Phase 14` exists to change that.
The new problem is no longer:
1. keep deepening constrained-`V1` validation as the primary path
It is:
1. make `V2 core` an explicit owner inside the repo
2. turn accepted claims into core-owned state, events, commands, and projections
3. create a bounded executable basis for later adapter rebinding
## Phase Goal
Build the first real `V2 core` inside `sw-block/engine/replication` as a
deterministic, side-effect-free semantic owner for:
1. state and transitions
2. command decisions
3. outward projection meaning
This phase does not yet claim live runtime cutover.
## Execution Rule
For all `Phase 14` work, implementation order must be:
1. define core-owned state and transitions
2. define command-emission rules
3. define projection contracts
4. only then connect adapters in later phases
Do not invert this order.
If runtime wiring comes first, `V1` mixed runtime state will silently retake
semantic authority.
## Execution Model
This phase uses the new working model:
1. primary developer
- owns `V2 core` semantic design and implementation
- decides state/transition/command/projection shape
2. `sw`
- supports bounded implementation work after semantic ownership is already
defined
- should receive only narrow, easy-to-accept tasks
3. `tester`
- validates bounded acceptance basis and checks for overclaim
4. `manager`
- performs phase challenge/review gates against semantic discipline
## Scope
### In scope
1. explicit core-owned state in `sw-block/engine/replication`
2. explicit bounded event vocabulary
3. explicit bounded command vocabulary
4. explicit normalized projection vocabulary
5. structural acceptance tests proving accepted constraints can be represented by
the new core
### Out of scope
1. no live `weed/` adapter hook yet
2. no product-surface rebinding yet
3. no broad runtime migration
4. no launch or performance claims
5. no reopening accepted `Phase 13` claim boundaries
## Phase 14 Slices
### `14A`: Mode / Readiness / Publication Core Closure
Goal:
1. make mode, readiness, and publication first-class core-owned meanings
Acceptance object:
1. `VolumeState`, event vocabulary, command vocabulary, projection vocabulary,
and deterministic engine loop exist in `sw-block/engine/replication`
2. `publish_healthy` is derived from named semantic state rather than runtime
convenience
3. fail-closed mode distinctions stay explicit:
- `allocated_only`
- `bootstrap_pending`
- `replica_ready`
- `publish_healthy`
- `degraded`
- `needs_rebuild`
4. the structural acceptance tests prove:
- stable identity ownership is preserved
- diagnostic shipped progress does not establish durable truth
- `publish_healthy` requires durable boundary truth
- `degraded` and `needs_rebuild` remain distinct fail-closed meanings
- the current integrated interpretation remains `constrained_v1`, not live
`v2_core` cutover
Status:
1. delivered
### `14B`: Assignment / Command Semantics Closure
Goal:
1. make assignment transitions and command emission rules explicit from semantic
state rather than runtime convenience
Acceptance object:
1. assignment intent, role application, receiver start, shipper configuration,
and invalidation commands are emitted as bounded semantic decisions
2. one bounded event sequence produces one bounded command sequence
3. command emission does not depend on `weed/` internals
Status:
1. active
### `14C`: Boundary / Recovery Semantic Closure
Goal:
1. make durable boundary and recovery semantics explicit in the same core owner
Acceptance object:
1. boundary truth distinguishes durable progress, checkpoint truth, and
diagnostic sender progress
2. recovery semantics preserve the accepted constraints around eligibility,
fail-closed degradation, and rebuild escalation
3. structural tests stay bounded and do not claim live path migration yet
Status:
1. planned
## Manager Review Gate
Every `Phase 14` slice must survive one challenge review that asks:
1. which semantic constraint does this slice satisfy?
2. which overclaim does this slice prevent?
3. which accepted checkpoint proof does this slice preserve?
Reject the slice if any of those questions can only be answered by vague runtime
intuition.
## Immediate Next Step
Continue with `14B`.
Use the explicit shell from `14A` as the semantic substrate, then freeze
gap-driven command emission so repeated assignments and repeated failures do not
silently turn back into runtime-convenience command spam.
+300 -72
View File
@@ -35,35 +35,53 @@ Use these rules for all later phases:
## Current Baseline
Current accepted path through `Phase 09`:
Current accepted path through `Phase 13`, with `Phase 14` now the immediate next engineering focus:
1. protocol/algo truth set is strong
2. engine recovery core is strong
3. real control delivery exists on the chosen path
4. real one-chain catch-up and rebuild closure exist on the chosen path
5. production-grade execution closure is accepted on the chosen path:
- real `TransferFullBase`
- real `TransferSnapshot`
- real `TruncateWAL`
- stronger live runtime ownership on the volume-server path
6. unified hardening validation exists on the chosen path
7. one bounded accepted path exists for:
2. engine recovery core is strong on the chosen path
3. control-plane closure is accepted on the chosen path
4. selected product-surface rebinding is accepted on the chosen path
5. bounded production hardening is accepted on the chosen path
6. one bounded accepted path exists for:
- `RF=2`
- `sync_all`
- existing master / volume-server heartbeat path
- `blockvol` as execution backend
7. `Phase 13` has now closed the bounded `WAL V1.5` contract package for the current constrained chosen path:
- real-workload package accepted
- assignment/publication closure accepted
- bounded mode normalization accepted
Phase-accounting note:
1. `Phase 08` is closed
2. `Phase 09` is also closed
3. this roadmap should now be read from the post-`Phase 09` state, not the post-`Phase 08` state
2. `Phase 09` is closed
3. `Phase 10` is closed
4. `Phase 11` is closed
5. `Phase 12` is the accepted hardening baseline for the chosen path
6. `Phase 13` is closed and should be read as one bounded constrained-runtime contract package, not as launch approval
7. `Phase 14` is now the immediate next engineering focus:
- explicit `V2 core` extraction
- not more deepening of constrained-`V1` validation by default
Important interpretation rule:
1. the accepted chosen path and claim/evidence set are real
2. the current `weed/` runtime structure is not automatically the final `V2` structure
3. until `Phase 14` establishes an explicit `V2 core`, current integrated tests should be read primarily as:
- `V1` runtime validation under `V2` constraints
- not proof that a completed `V2 runtime` already exists
4. future phases must treat:
- `v2-protocol-claim-and-evidence.md` as current claim authority
- `v2_mini_core_design.md` as engineering-structure authority
- `v2-reuse-replacement-boundary.md` as reuse vs replacement authority
This means the next phases should focus mainly on:
1. stronger control-plane closure
2. later product-surface rebinding
3. production hardening
4. bounded cleanup of low-severity residuals without reopening accepted execution semantics
1. extracting the long-term `V2 core` structure explicitly
2. rebinding `weed/` into adapter / projection / backend roles
3. closing one bounded `V2`-native runtime path before productionization
4. using `Phase 13` evidence as acceptance input rather than continuing to treat constrained-`V1` validation as the main workstream
## Phase Roadmap
@@ -263,9 +281,11 @@ Current status:
4. `P3` accepted:
- acceptance object = bounded diagnosability / blocker accounting / runbook hardening on the chosen path
- bounded operator-visible diagnosis surfaces and finite blocker accounting are accepted
5. `P4` active:
5. `P4` accepted:
- acceptance object = bounded performance floor / rollout-gate hardening on the chosen path
- not broad rollout readiness beyond the named launch envelope
6. `Phase 12` is now closed:
- one bounded hardening package is accepted for the chosen path
Current `P4` first delivery shape:
@@ -294,23 +314,211 @@ Closed-loop expectation for `P4` review:
4. remaining rollout blockers are explicit and finite
5. claims remain bounded to measured floor / named launch envelope only
After `Phase 12`:
1. move to a productionization program, not more protocol-discovery phases
2. freeze production blockers and the supported launch envelope
3. run a limited internal pilot with incident-driven hardening
4. perform controlled rollout only after explicit launch-gate review
### Post-`Phase 12`: Productionization Program
### Phase 13: V1.5 Contract Closure And Contradiction Ledger
Goal:
1. turn the accepted `Phase 12` chosen path into a bounded first-launch product envelope without reopening protocol discovery
1. close the bounded `RF=2 sync_all` contract on the current chosen path
2. freeze what `WAL V1.5` is allowed to claim
3. classify remaining live contradictions as:
- reusable-core bug
- adapter-boundary bug
- `V2`-authority bug
Delivery object:
1. one frozen `CP13-*` contract package
2. one centralized claim/evidence ledger for the active chosen path
3. one explicit contradiction/rerun queue for invalidated or narrowed evidence
Closed-loop validation:
1. protocol/unit/adversarial proofs for `CP13-1..7`
2. bounded real-workload validation, assignment/publication closure, and mode normalization are all accepted
3. explicit narrowing or restoration of claims in the centralized ledger
Non-claims:
1. not full `V2 core` extraction
2. not launch approval
3. not proof that current `weed/` structure is the final `V2` structure
Key files / ownership:
1. `sw-block/.private/phase/phase-13-*.md`
2. `sw-block/design/v2-protocol-claim-and-evidence.md`
3. `sw-block/design/v2-protocol-truths.md`
### Phase 14: V2 Core Extraction
Goal:
1. make the `V2 core` explicit as a long-term code structure
2. stop relying on implicit semantic ownership spread across runtime files
Execution rule:
1. define core-owned state and transitions first
2. freeze command-emission rules second
3. freeze projection contracts third
4. only then connect adapters
Delivery object:
1. one explicit `V2 core` package/file layout with named:
- `state`
- `event`
- `command`
- `projection`
2. one minimal real code path for:
- `ApplyEvent()`
- `Decide()`
- `EmitCommands()`
3. one bounded parity package showing accepted prototype/FSM semantics are preserved
Closed-loop validation:
1. focused engine tests proving accepted claims can be represented through the new event/command core
2. parity checks against accepted prototype/FSM semantics
3. no-overclaim review that this is structural extraction, not live-path cutover
Recommended slice order:
1. `Phase 14A`: core-owned automata
- explicit assignment / recovery / boundary / mode / publication automata
- structural tests only
2. `Phase 14B`: command semantics
- bounded command sequences derived from semantic state
- still no live `weed/` execution
3. `Phase 14C`: projection contracts
- lookup / heartbeat / debug / tester normalization from core-owned state
- surface-consistency tests
Immediate focus inside `Phase 14`:
1. start from one complete semantic chain:
- `mode`
- `readiness`
- `publication`
2. use accepted `CP13-8A` and `CP13-9` as the first hard input package
Non-claims:
1. not full live-path cutover
2. not replacement of all `weed/` logic
3. not a separate process yet
Key files / ownership:
1. `sw-block/design/v2_mini_core_design.md`
2. `sw-block/design/v2-phase14plus-semantic-framework.md`
3. `sw-block/engine/replication/`
### Phase 15: Adapter And Projection Rebinding
Goal:
1. make `weed/` a bounded adapter/projection layer instead of mixed semantic authority
2. close assignment -> readiness -> publication through named `V2` state
Delivery object:
1. one explicit adapter-boundary package on the live path
2. one explicit projection store / projection surface package for:
- readiness
- publication
- diagnostics
3. one narrowed role definition where:
- `BlockService` is closer to command executor
- registry is closer to projection store
Closed-loop validation:
1. live-path tests proving assignment delivered != receiver ready != publish healthy unless the named readiness/projection loop is closed
2. focused regression package for the `CP13-8A` bug class
3. operator-visible projection checks rather than internal-state-only proof
Recommended slice order:
1. `Phase 15A`: minimal adapter hook
- one narrow event path into the core
- one bounded command path back out
- prove no semantic split on that narrow path
2. `Phase 15B`: projection-store rebinding
- registry / lookup / tester-facing surfaces consume core-owned projection truth
- prove assignment delivered != ready != publish healthy on the real path
Non-claims:
1. not backend rewrite
2. not broader productization
3. not new transport matrix claims
Key files / ownership:
1. `sw-block/design/v2-reuse-replacement-boundary.md`
2. `sw-block/design/v2-phase14plus-semantic-framework.md`
3. `weed/server/volume_server_block.go`
4. `weed/server/block_heartbeat_loop.go`
5. `weed/server/master_block_registry.go`
6. `weed/server/master_block_failover.go`
### Phase 16: V2-Native Runtime Closure
Goal:
1. make the integrated runtime behave as a `V2`-owned recovery/control system
2. stop depending on a merely improved `WAL V1.5` path for correctness interpretation
Execution precondition:
1. do not enter `Phase 16` until `Phase 14` has frozen state / command / projection semantics
2. do not treat adapter rebinding alone as runtime closure
Delivery object:
1. one bounded product/runtime path where failover, recovery, publication, and selected surfaces are driven by the `V2 core` + adapter contract
2. one explicit runtime integration package that maps simulator/prototype failure classes to live-path behavior
Closed-loop validation:
1. end-to-end failover/recovery scenarios on the core-driven path
2. simulator-to-runtime consistency checks for the named failure classes that `V2` is supposed to survive
3. bounded real-workload checks on the core-driven path, not just on the legacy-integrated path
Non-claims:
1. not broad rollout approval
2. not physical split into an independent `V2 core process` unless the logic is already structurally independent
Key files / ownership:
1. `sw-block/engine/replication/`
2. `weed/server/`
3. selected `weed/storage/blockvol/v2bridge/*` files
### Cross-Phase Review Rule For `Phase 14+`
For any new transition, command, or projection rule in `Phase 14+`, require a
short justification in the delivery note or code review:
1. semantic constraint satisfied
- which `claim / truth / CP13-*` item it is implementing
2. overclaim avoided
- which false healthy / ready / durable / recoverable interpretation is being prevented
3. proof preserved
- which accepted test or checkpoint remains valid because of the rule
### Productionization Program After `Phase 16`
Goal:
1. turn the accepted `Phase 16` bounded path into a bounded first-launch product envelope without reopening protocol discovery or core-ownership questions
Program slices:
1. Program `P0`: launch-envelope freeze
- freeze the first supported launch envelope from accepted `P1`-`P4` evidence
- freeze the first supported launch envelope from accepted hardening + runtime-closure evidence
- lock:
- supported topology / transport matrix
- explicit exclusions
@@ -338,46 +546,51 @@ Program slices:
- widen within the same launch envelope
- block expansion
- require explicit mapping from any expansion decision back to:
- accepted `Phase 12` evidence
- accepted `Phase 16` evidence
- pilot outcomes
- incident dispositions
- reject if rollout broadens beyond the named envelope or reuses pilot success as generic production proof
Cross-cutting rules:
1. do not invent a `Phase 12 P5`; this is a separate productionization program
2. keep the accepted chosen path fixed unless incidents expose a real bug
3. treat known missing evidence as explicit constraints until cleared, especially:
1. do not invent a `Phase 12 P5`; productionization remains separate from hardening
2. do not collapse `Phase 13-16` into generic productionization; they are engineering-structure phases
3. keep the accepted chosen path fixed unless contradiction or incident evidence exposes a real bug
4. treat known missing evidence as explicit constraints until cleared, especially:
- failover-under-load performance
- hours/days soak under load
- `RF>2`
- broad transport matrix
- full gRPC-stream integration evidence
4. keep V2 post-`Phase 12` direction aligned with the learn-tree close/gate work so the roadmap does not split into contradictory stories
5. keep the roadmap aligned with:
- `v2-protocol-claim-and-evidence.md`
- `v2_mini_core_design.md`
- `v2-reuse-replacement-boundary.md`
## Module Status Map
| Module area | Current status | Current owner phase | Next target phase | Notes |
| ------------------------------------------------------------- | ---------------------------- | ------------------------ | ----------------- | ------------------------------------------------------------------------------------------ |
| `sw-block/engine/replication` core FSM/orchestrator/driver | Strong | `Phase 09` accepted | `Phase 10-12` | Main later work is control-plane/runtime integration and hardening, not new core semantics. |
| Engine executor real I/O boundary (`CatchUpIO` / `RebuildIO`) | Strong on chosen path | `Phase 09` accepted | `Phase 10/12` | Keep the boundary stable; later work is orchestration/control proof and hardening. |
| `weed/storage/blockvol/v2bridge/control.go` | Strong on chosen path | `Phase 08/09/10` accepted | `Phase 12` | Chosen-path control mapping is accepted; later work is hardening, not new mapping semantics. |
| `weed/storage/blockvol/v2bridge/reader.go` | Strong | `Phase 09` accepted | `Phase 12` | Mostly stable; later work is verification/hardening, not new semantics. |
| `weed/storage/blockvol/v2bridge/pinner.go` | Strong | `Phase 09` accepted | `Phase 12` | Retention safety proven; later work is hardening under disturbance. |
| `weed/storage/blockvol/v2bridge/executor.go` WAL scan | Strong | `Phase 09` accepted | `Phase 12` | Real execution path closed on chosen path; later work is hardening. |
| `v2bridge` `TransferFullBase` | Strong on chosen path | `Phase 09 P1` accepted | `Phase 12` | Execution closure accepted; do not reopen casually. |
| `v2bridge` `TransferSnapshot` | Strong on chosen path | `Phase 09 P2` accepted | `Phase 12` | Execution closure accepted; do not reopen casually. |
| `v2bridge` `TruncateWAL` | Strong on chosen path | `Phase 09 P3` accepted | `Phase 12` | Narrow Option A contract accepted; later work is hardening/planning improvement. |
| `weed/server/volume_server_block.go` V2 assignment intake | Strong on chosen path | `Phase 10 P4` accepted | `Phase 11/12` | VS-side ingress, convergence, idempotence, and bounded master-driven closure are accepted; later work is product-surface integration and hardening. |
| `weed/server/block_recovery.go` live runtime ownership | Strong on chosen path | `Phase 09/10` accepted | `Phase 11/12` | Serialized ownership and bounded control-plane integration are accepted; later work is product-surface integration and hardening. |
| `blockvol` WAL/flusher/checkpoint runtime | Reuse reality | Existing production code | `Phase 12` | Reuse implementation; later work is disturbance/restart hardening. |
| `blockvol` rebuild transport/server reality | Reuse with redesign boundary | Existing production code | `Phase 12` | Bounded chosen-path integration is accepted; later work is hardening under disturbance. |
| local server identity (`localServerID`) | Strong on chosen path | `Phase 10 P1` accepted | `Phase 12` | Canonical `volumeServerId` now backs chosen-path local identity; later work is hardening only. |
| Snapshot product path | Strong on chosen path | `Phase 11` accepted | `Phase 12` | Product-visible snapshot create/list/delete closure and restore workflow closure are accepted; later work is hardening, not rebinding. |
| `CSI` integration | Strong on chosen path | `Phase 11` accepted | `Phase 12` | Bounded controller/node lifecycle rebinding accepted; later work is hardening. |
| `NVMe` / `iSCSI` front-ends | Strong on chosen path | `Phase 11` accepted | `Phase 12` | Publication/address truth rebinding accepted; later work is runtime/perf hardening. |
| Testrunner / infra / metrics | Strong support layer | existing | `Phase 11-12` | Reuse to validate later product-surface and hardening phases. |
| Module area | Current status | Current owner phase | Next target phase | Notes |
| ----------- | -------------- | ------------------- | ----------------- | ----- |
| `sw-block/engine/replication` core FSM/orchestrator/driver | Strong long-term `V2 core` asset | `Phase 09` accepted, `Phase 13` active constraints | `Phase 14` | Main next work is explicit `state / event / command / projection` extraction, not reopening accepted semantics. |
| Engine executor real I/O boundary (`CatchUpIO` / `RebuildIO`) | Strong on chosen path | `Phase 09` accepted | `Phase 14/16` | Keep the boundary stable; later work is to connect it to explicit `V2 core` ownership and runtime closure. |
| `weed/storage/blockvol/v2bridge/control.go` | Strong boundary adapter on chosen path | `Phase 08/09/10` accepted | `Phase 15` | Remains a bridge between `V2` truth and runtime execution; should not accumulate new semantic authority casually. |
| `weed/storage/blockvol/v2bridge/reader.go` | Strong backend-facing adapter | `Phase 09` accepted | `Phase 15/16` | Mostly stable; later work is explicit boundary ownership and runtime proof, not new protocol semantics. |
| `weed/storage/blockvol/v2bridge/pinner.go` | Strong backend-facing adapter | `Phase 09` accepted | `Phase 15/16` | Retention safety is proven on the chosen path; later work is keeping it under explicit `V2` control boundaries. |
| `weed/storage/blockvol/v2bridge/executor.go` WAL scan | Strong backend-facing adapter | `Phase 09` accepted | `Phase 15/16` | Real execution path is closed on the chosen path; later work is core-driven runtime integration. |
| `v2bridge` `TransferFullBase` | Strong on chosen path | `Phase 09 P1` accepted | `Phase 16` | Execution closure is accepted; do not reopen casually unless core-driven runtime closure exposes a real contradiction. |
| `v2bridge` `TransferSnapshot` | Strong on chosen path | `Phase 09 P2` accepted | `Phase 16` | Execution closure is accepted; later work is bounded runtime closure rather than first-implementation discovery. |
| `v2bridge` `TruncateWAL` | Strong on chosen path | `Phase 09 P3` accepted | `Phase 16` | Narrow contract is accepted; later work is preserving that contract under explicit runtime ownership. |
| `weed/server/volume_server_block.go` V2 assignment intake | Adapter-boundary reality with accepted chosen-path closure | `Phase 10 P4` accepted, `Phase 13` contradiction pressure | `Phase 15` | This is where assignment/readiness/publication closure must become explicit adapter behavior rather than mixed service semantics. |
| `weed/server/block_recovery.go` live runtime ownership | V2-owned runtime truth on chosen path | `Phase 09/10` accepted | `Phase 15/16` | Serialized ownership is accepted; later work is making it cooperate with explicit `V2 core` and projection boundaries. |
| `weed/server/master_block_registry.go` / failover / handlers | Mixed projection/truth reality | `Phase 10-12` accepted surfaces | `Phase 15` | Should converge toward projection store + operator-visible truth surfaces rather than mixed business-logic/state storage. |
| `blockvol` WAL/flusher/checkpoint runtime | Reuse reality | Existing production code | `Phase 16` | Reuse implementation; do not let `V1` replication semantics redefine `V2` truth. |
| `blockvol` rebuild transport/server reality | Reuse with redesign boundary | Existing production code | `Phase 16` | Bounded chosen-path integration is accepted; later work is runtime closure under `V2` authority. |
| local server identity (`localServerID`) | Strong chosen-path rule with narrowed semantics | `Phase 10 P1` accepted, `Phase 13` constraints | `Phase 15` | Stable identity must remain distinct from transport address shape and explicit in later adapter/projection work. |
| Snapshot product path | Strong on chosen path | `Phase 11` accepted | `Phase 16` | Product-visible snapshot workflow is accepted on the chosen path; later work is preserving it under `V2`-native runtime closure. |
| `CSI` integration | Strong on chosen path | `Phase 11` accepted | `Phase 16` | Bounded controller/node lifecycle rebinding is accepted; later work is runtime preservation, not first rebinding. |
| `NVMe` / `iSCSI` front-ends | Strong on chosen path | `Phase 11` accepted | `Phase 16` | Publication/address truth rebinding is accepted; later work is proving the `V2`-driven runtime path beneath them. |
| Testrunner / infra / metrics | Strong support layer | existing | `Phase 13-16` | Reuse to validate contract closure, adapter contradictions, runtime closure, and later productization gates. |
## Completion-State Targets
@@ -385,23 +598,35 @@ Cross-cutting rules:
Use these rough targets to judge whether a phase is moving the product meaningfully.
| Phase | Expected completion move |
| ---------- | ----------------------------------------------------------------------------- |
| Phase | Expected completion move |
| ----- | ------------------------ |
| `Phase 09` | from validation-grade backend execution to accepted execution closure on the chosen path |
| `Phase 10` | from bounded control-entry proof to stronger end-to-end control-plane closure |
| `Phase 11` | from backend-ready path to selected product-surface readiness |
| `Phase 12` | from candidate-safe to production-safe |
| `Phase 11` | from backend-ready path to selected product-surface readiness |
| `Phase 12` | from candidate-safe to production-safe on one bounded chosen path |
| `Phase 13` | from scattered `WAL V1.5` evidence to one frozen contract/contradiction ledger |
| `Phase 14` | from implicit core semantics to explicit `V2 core` package/engine structure |
| `Phase 15` | from mixed runtime semantics to explicit adapter/projection closure |
| `Phase 16` | from improved chosen-path runtime to one bounded `V2`-native runtime path |
| Productionization | from bounded runtime closure to bounded launch-envelope / pilot / rollout review |
## Near-Term Execution Direction
If the goal is to maximize product completion efficiently, the recommended order is:
If the goal is to maximize product completion efficiently, the recommended order is now:
1. keep `Phase 09` closed and do not reopen accepted execution semantics casually
2. keep `Phase 10` closed and do not reopen accepted bounded control-plane closure casually
3. move next to `Phase 11` product surface rebinding
4. then `Phase 12` production hardening
5. then the post-`Phase 12` productionization program:
1. keep `Phase 09-12` accepted closures closed and do not reopen them casually
2. keep `Phase 13` closed as the bounded `WAL V1.5` contract package
3. move next to `Phase 14` core extraction:
- explicit `state / event / command / projection`
- minimal `ApplyEvent() -> Decide() -> EmitCommands()` path
4. then `Phase 15` adapter/projection rebinding:
- assignment
- readiness
- publication
- diagnostics
5. then `Phase 16` bounded `V2`-native runtime closure
6. only then move to the productionization program:
- freeze launch envelope
- run limited internal pilot
- harden from incidents
@@ -409,18 +634,21 @@ If the goal is to maximize product completion efficiently, the recommended order
The most important near-term engineering weight should now go to:
1. finishing `Phase 12`
2. then the bounded productionization program immediately after it
1. making `sw-block/engine/replication` the explicit long-term `V2 core`
2. keeping `weed/` changes bounded to adapter / projection / backend roles unless they are explicitly promoted into `V2`-owned authority
3. using `CP13-1..9` as acceptance input for new `V2 core` work rather than as a reason to keep extending constrained-`V1` validation
## Short Summary
The V2 line now has accepted execution closure on one bounded chosen path.
The next development plan should treat later work as bounded hardening plus productionization, not more protocol discovery.
The V2 line now has accepted execution, control, product-surface, and hardening closure on one bounded chosen path.
The main heavy engineering work still ahead is:
The next development plan should not jump directly from `Phase 12` to productionization.
It should move through four bounded engineering phases first:
1. stronger end-to-end control-plane closure
2. later product-surface rebinding
3. production hardening
4. post-`Phase 12` launch-envelope freeze, internal pilot, and controlled rollout review
1. `Phase 13`: freeze the current `WAL V1.5` constrained-runtime contract package
2. `Phase 14`: extract the explicit `V2 core`
3. `Phase 15`: rebind `weed/` as adapter/projection reality
4. `Phase 16`: close one bounded `V2`-native runtime path
Only after that should the roadmap move into bounded launch-envelope freeze, internal pilot, incident-driven hardening, and controlled rollout review.
@@ -0,0 +1,319 @@
# V2 Phase 14+ Semantic-First Framework
Date: 2026-04-03
Status: active
Purpose: define the overall `Phase 14+` implementation framework so `V2`
runtime extraction is driven by semantics first: core-owned state and
transitions, then command rules, then projection contracts, and only then
adapter rebinding
## Why This Document Exists
`Phase 13` closed one bounded constrained-runtime contract package:
1. real-workload validation
2. assignment/publication closure
3. bounded mode normalization
That package is valuable, but it is not yet a completed `V2 runtime`.
The next problem is therefore no longer:
1. keep deepening constrained-`V1` validation by default
It is:
1. how to turn the accepted semantic constraints into a real `V2 core`
2. how to sequence `Phase 14+` so `V1` mixed runtime state does not silently
regain semantic authority
## Core Rule
For `Phase 14+`, implementation order must be:
1. define core-owned state and transitions
2. define command-emission rules
3. define projection contracts
4. only then connect adapters
Do not invert this order.
If adapter/runtime wiring appears first, `V1` mixed state will silently regain
semantic authority through convenience behavior.
## Existing Inputs To Preserve
These are fixed inputs, not optional references:
1. `v2_mini_core_design.md`
2. `v2-reuse-replacement-boundary.md`
3. `v2-protocol-claim-and-evidence.md`
4. `v2-phase-development-plan.md`
5. `sw-block/engine/replication/`
## Overall Composition Model
The full `V2` runtime should be composed from smaller automata rather than one
monolithic state machine.
```mermaid
flowchart TD
assignmentState[AssignmentAutomaton]
recoveryState[RecoveryAutomaton]
boundaryState[BoundaryAutomaton]
modeState[ModeAutomaton]
publicationState[PublicationAutomaton]
coreEngine[CoreEngine]
projections[ProjectionContracts]
adapters[AdapterBoundary]
runtime[V1BackendMechanics]
assignmentState --> coreEngine
recoveryState --> coreEngine
boundaryState --> coreEngine
modeState --> coreEngine
coreEngine --> publicationState
publicationState --> projections
coreEngine --> adapters
adapters --> runtime
runtime -->|"observations/events"| adapters
adapters --> coreEngine
```
## The Five Core-Owned Automata
### 1. Assignment automaton
Owns:
1. volume intent
2. role intent
3. stable replica identity
4. epoch
5. desired replica set
Primary constraints preserved:
1. `CP13-2`
2. identity-vs-transport separation
Current seeds:
1. `sw-block/engine/replication/registry.go`
2. `sw-block/engine/replication/state.go`
### 2. Recovery automaton
Owns:
1. per-replica recovery state
2. session ownership and fencing
3. catch-up vs rebuild selection
Primary constraints preserved:
1. `CP13-4`
2. `CP13-5`
3. `CP13-6`
4. `CP13-7`
Current seeds:
1. `sw-block/engine/replication/sender.go`
2. `sw-block/engine/replication/session.go`
3. `sw-block/engine/replication/orchestrator.go`
4. `sw-block/engine/replication/outcome.go`
### 3. Boundary automaton
Owns:
1. committed truth
2. checkpoint truth
3. durable barrier truth
4. rebuild/catch-up target truth
Primary constraints preserved:
1. `T1`
2. `T9`
3. `CP13-3`
Current seeds:
1. `sw-block/engine/replication/state.go`
2. `sw-block/engine/replication/engine.go`
### 4. Mode automaton
Owns:
1. `allocated_only`
2. `bootstrap_pending`
3. `replica_ready`
4. `publish_healthy`
5. `degraded`
6. `needs_rebuild`
Primary constraints preserved:
1. `CP13-9`
2. fail-closed external meaning
Current seeds:
1. `sw-block/engine/replication/state.go`
2. `sw-block/engine/replication/engine.go`
### 5. Publication automaton
Owns:
1. readiness closure
2. publication closure
3. outward healthy vs non-healthy truth
Primary constraints preserved:
1. `CP13-8A`
2. `CP13-9`
Current seeds:
1. `sw-block/engine/replication/projection.go`
2. `sw-block/engine/replication/engine.go`
## Phase 14+ Execution Order
### Phase 14A: Core-owned automata
Goal:
1. make the five automata explicit in the core package
Deliver:
1. state definitions
2. transition tables/rules
3. event vocabulary
Validation:
1. structural acceptance tests in `sw-block/engine/replication`
Non-goal:
1. no live adapter hook
### Phase 14B: Command semantics
Goal:
1. freeze command-emission rules from semantic state, not runtime convenience
Deliver:
1. command rules for role apply, receiver start, shipper configure, invalidation,
and publication
Validation:
1. tests that one event sequence produces one bounded command sequence
Non-goal:
1. no `weed/` execution yet
### Phase 14C: Projection contracts
Goal:
1. define what external surfaces are allowed to claim and from which core state
Deliver:
1. projection structs and normalization rules for lookup/heartbeat/debug/tester
meanings
Validation:
1. mode/readiness/publication surface-consistency tests
Non-goal:
1. no live registry rewrite yet
### Phase 15A: Minimal adapter hook
Goal:
1. connect one narrow adapter ingress to the new core
Deliver:
1. one event path from `weed/` into the core
2. one command path back out
Validation:
1. prove no semantic split between adapter and core on that narrow path
### Phase 15B: Projection-store rebinding
Goal:
1. make `weed/` projection/state surfaces consume core-owned projection truth
Deliver:
1. bounded rebinding of registry / lookup / tester-facing surfaces
Validation:
1. prove assignment delivered != ready != publish healthy on the real path
### Phase 16: V2-native runtime closure
Goal:
1. make the integrated runtime behave as a `V2`-owned system rather than
constrained-`V1` semantics plus fixes
Deliver:
1. one bounded runtime path where core-owned semantics drive adapters and
projections
Validation:
1. end-to-end failover/recovery/publication scenarios on the core-driven path
## Algorithm Review Rule
For any new transition rule, command rule, or projection rule, require a short
justification in code review or delivery notes:
1. semantic constraint satisfied:
- which item from `v2-protocol-claim-and-evidence.md`,
`v2-protocol-truths.md`, or `CP13-*`
2. overclaim avoided:
- what false healthy / ready / durable / recoverable claim is being prevented
3. proof preserved:
- which accepted test or checkpoint remains valid because of this rule
This is the minimum bar for `Phase 14+`.
## Immediate Next Slice
Do not broaden `Phase 13` further.
Use the new `Phase 14` core skeleton in `sw-block/engine/replication` as the
base for one complete semantic chain:
1. `mode`
2. `readiness`
3. `publication`
This is the best next slice because it turns the newest accepted `CP13-8A` and
`CP13-9` constraints directly into core-owned state and transition logic before
adapter rebinding begins.
+495 -2
View File
@@ -319,6 +319,73 @@ projection 是给外部世界看的,不是内部原始状态 dump。
- receiver ready
- publish healthy
### 6.5 由五个小自动机构成的大自动机
`V2 core` 不应该被实现成一个“所有语义揉在一起”的大状态机。
更稳的方式是把它拆成五个 core-owned automata,再由 `CoreEngine` 组合:
1. `AssignmentAutomaton`
- volume intent
- role intent
- stable replica identity
- epoch
- desired replica set
2. `RecoveryAutomaton`
- session ownership
- catch-up vs rebuild selection
- per-replica recovery state
3. `BoundaryAutomaton`
- committed truth
- checkpoint truth
- durable barrier truth
- rebuild/catch-up target truth
4. `ModeAutomaton`
- `allocated_only`
- `bootstrap_pending`
- `replica_ready`
- `publish_healthy`
- `degraded`
- `needs_rebuild`
5. `PublicationAutomaton`
- readiness closure
- publication closure
- outward healthy vs non-healthy truth
组合关系应该是:
```mermaid
flowchart TD
assignmentState[AssignmentAutomaton]
recoveryState[RecoveryAutomaton]
boundaryState[BoundaryAutomaton]
modeState[ModeAutomaton]
publicationState[PublicationAutomaton]
coreEngine[CoreEngine]
projections[ProjectionContracts]
adapters[AdapterBoundary]
runtime[V1BackendMechanics]
assignmentState --> coreEngine
recoveryState --> coreEngine
boundaryState --> coreEngine
modeState --> coreEngine
coreEngine --> publicationState
publicationState --> projections
coreEngine --> adapters
adapters --> runtime
runtime -->|"observations/events"| adapters
adapters --> coreEngine
```
这里最关键的工程规则是:
1. 先定义 core-owned state / transitions
2. 再定义 command-emission rules
3. 再定义 projection contracts
4. 最后才接 adapter
如果顺序反过来,`V1` mixed runtime state 会重新夺回语义 authority。
---
## 7. 直接映射到仓库路径
@@ -530,6 +597,20 @@ type FrontendBackend interface {
- 为了抽接口,暂时模糊 readiness
- 为了分层,暂时把 publish truth 放宽
### 10.5 未来算法和状态转换的审查规则
`Phase 14+` 开始,任何新的 transition rule / command rule / projection
rule,都应该在 code review 或 delivery note 里回答三件事:
1. semantic constraint satisfied
- 它满足的是哪条 `claim / truth / accepted checkpoint`
2. overclaim avoided
- 它防止了哪种 false healthy / false ready / false durable / false recoverable
3. proof preserved
- 它保持了哪个已接受 proof 仍然成立
这不是文书要求,而是防止 `V2 core` 重新退化成“runtime convenience +
事后解释”的最低工程 bar。
---
## 11. V2 如何从已接受 claim 变得可靠
@@ -732,7 +813,419 @@ type FrontendBackend interface {
---
## 14. 推荐的实施顺序
## 14. `V2 core` 的 Go package 目录建议
下面不是要求一次性重排仓库,而是给出一个与当前代码现实相容的目标布局。
建议把长期 `V2` 资产继续收敛在 `sw-block/engine/replication/` 之下,并按职责细分:
```text
sw-block/engine/replication/
core/
types.go
state.go
event.go
command.go
projection.go
engine.go
session/
session.go
ownership.go
boundary/
lsn.go
retention.go
rebuild.go
adapter/
contract.go
observe.go
projection/
heartbeat.go
lookup.go
diagnostic.go
```
如果短期不想新建这么多子目录,也可以先保持现有目录平铺,但按相同职责收敛文件:
1. `types.go`: 基础 types
2. `state.go`: state/ownership structs
3. `event.go`: incoming events
4. `command.go`: emitted commands
5. `projection.go`: outward projections
6. `engine.go`: `ApplyEvent() -> Decide()`
7. `session.go` / `sender.go` / `registry.go`: 继续作为现有核心实现承载
### 14.1 与现有代码的最小映射
当前已经存在的可直接承接 `V2 core` 的文件:
- `sw-block/engine/replication/types.go`
- `sw-block/engine/replication/session.go`
- `sw-block/engine/replication/sender.go`
- `sw-block/engine/replication/registry.go`
- `sw-block/engine/replication/orchestrator.go`
- `sw-block/engine/replication/outcome.go`
- `sw-block/engine/replication/rebuild.go`
- `sw-block/engine/replication/observe.go`
这些文件已经说明:
- `V2 core` 不是概念草图
- 它已经有 sender/session/ownership/orchestrator 的真实雏形
- 后续更工程化的工作主要是把这些对象的职责再显式化,而不是重新发明
### 14.2 `weed/` 中的对应位置
`weed/` 中长期应当只保留:
- backend primitive
- adapter boundary
- projection surface
不应该让 `weed/` 成为长期语义 authority。
当前可暂时对应为:
- adapter boundary:
- `weed/storage/blockvol/v2bridge/control.go`
- `weed/server/volume_server_block.go`
- `weed/server/block_heartbeat_loop.go`
- projection surface:
- `weed/server/master_block_registry.go`
- `weed/server/master_grpc_server_block.go`
- `weed/server/master_server_handlers_block.go`
- `weed/server/volume_server_block_debug.go`
- backend primitive:
- `weed/storage/blockvol/*`
---
## 15. `struct / event / command / projection` 推荐文件落点
### 15.1 Struct
建议放置:
- `types.go`
- `Endpoint`
- `ReplicaID`
- `VolumeID`
- `SessionKind`
- `ReplicaState`
- `state.go`
- `VolumeIntent`
- `AssignmentView`
- `ReplicaSession`
- `RecoveryOwner`
- `BoundaryView`
- `ReadinessView`
规则:
- 基础标识和 enum 放 `types.go`
- 可变的 core state 放 `state.go`
### 15.2 Event
建议放 `event.go`
- `AssignmentDelivered`
- `EpochBumped`
- `RepeatedAssignmentDelivered`
- `RoleApplied`
- `ReceiverReadyObserved`
- `ShipperConfiguredObserved`
- `ShipperConnectedObserved`
- `BarrierAccepted`
- `BarrierRejected`
- `CommittedLSNAdvanced`
- `CheckpointLSNAdvanced`
- `RebuildCommitted`
- `HeartbeatCollected`
规则:
- event 只描述 observation 或 intent
- event 不直接携带“成功解释”
### 15.3 Command
建议放 `command.go`
- `ApplyRoleCommand`
- `StartReceiverCommand`
- `ConfigureShipperCommand`
- `StartCatchUpCommand`
- `StartRebuildCommand`
- `InvalidateSessionCommand`
- `PublishProjectionCommand`
规则:
- command 只表达“core 决定要做什么”
- backend 如何完成,不在 command 内定义
### 15.4 Projection
建议放 `projection.go`,再按需要拆出:
- `projection_lookup.go`
- `projection_heartbeat.go`
- `projection_diagnostic.go`
至少包括:
- `LookupProjection`
- `HeartbeatProjection`
- `DiagnosticProjection`
- `TesterProjection`
规则:
- projection 是外部可见 truth
- 不是内部 state dump
### 15.5 当前仓库里的最小过渡落点
在不大改目录的前提下,可以先这样放:
- `sw-block/engine/replication/types.go`
- 基础 type / enum
- `sw-block/engine/replication/session.go`
- session ownership
- `sw-block/engine/replication/registry.go`
- assignment reconcile
- `sw-block/engine/replication/orchestrator.go`
- engine entrypoint / event handling 骨架
- 新增:
- `sw-block/engine/replication/event.go`
- `sw-block/engine/replication/command.go`
- `sw-block/engine/replication/projection.go`
这样不会一开始就做大规模 package 重排,但可以先把抽象边界立起来。
---
## 16. 最小 `ApplyEvent() -> Decide() -> EmitCommands()` 伪代码骨架
下面给一个最小核心骨架,说明 `V2 core` 如何工作。
```go
type Core struct {
volumes map[string]*VolumeState
}
type VolumeState struct {
Intent VolumeIntent
Assignment AssignmentView
Session map[string]*ReplicaSession
Ownership map[string]*RecoveryOwner
Boundary BoundaryView
Readiness ReadinessView
}
func (c *Core) ApplyEvent(ev Event) []Command {
st := c.mustVolumeState(ev.VolumeID())
switch e := ev.(type) {
case AssignmentDelivered:
st.Intent = mergeIntent(st.Intent, e.Intent)
st.Assignment = buildAssignmentView(e)
return c.decideFromAssignment(st)
case RoleApplied:
st.Readiness.RoleApplied = true
return c.decidePublication(st)
case ReceiverReadyObserved:
st.Readiness.ReceiverReady = true
st.Readiness.ReplicaEligible = true
return c.decidePublication(st)
case ShipperConfiguredObserved:
st.Readiness.ShipperConfigured = true
return nil
case ShipperConnectedObserved:
st.Readiness.ShipperConnected = true
return c.decidePublication(st)
case BarrierAccepted:
st.Boundary.CommittedLSN = e.FlushedLSN
return c.decideDurability(st)
case CheckpointLSNAdvanced:
st.Boundary.CheckpointLSN = e.CheckpointLSN
return nil
case RebuildCommitted:
st.Boundary.AchievedLSN = e.AchievedLSN
st.Readiness.ReplicaEligible = true
return c.decidePublication(st)
default:
return nil
}
}
func (c *Core) decideFromAssignment(st *VolumeState) []Command {
var cmds []Command
cmds = append(cmds, ApplyRoleCommand{
VolumeID: st.Assignment.VolumeID,
Epoch: st.Assignment.Epoch,
Role: st.Assignment.Role,
})
if st.Assignment.Role == RoleReplica {
cmds = append(cmds, StartReceiverCommand{
VolumeID: st.Assignment.VolumeID,
DataAddr: st.Assignment.ReplicaDataAddr(),
CtrlAddr: st.Assignment.ReplicaCtrlAddr(),
})
}
if st.Assignment.Role == RolePrimary && len(st.Assignment.ReplicaEndpoints) > 0 {
cmds = append(cmds, ConfigureShipperCommand{
VolumeID: st.Assignment.VolumeID,
Replicas: st.Assignment.ReplicaEndpoints.All(),
})
}
return cmds
}
func (c *Core) decidePublication(st *VolumeState) []Command {
st.Readiness.PublishHealthy =
st.Readiness.RoleApplied &&
((st.Assignment.Role == RolePrimary) ||
(st.Readiness.ReceiverReady && st.Readiness.ReplicaEligible))
return []Command{
PublishProjectionCommand{
VolumeID: st.Assignment.VolumeID,
Readiness: st.Readiness,
},
}
}
```
这个骨架的重点不是代码细节,而是三件事:
1. `event` 先进入 core
2. core 决定 state transition
3. command 由 core 发出,adapter 去执行
这样就不会再出现:
- runtime 自己偷偷解释语义
- workload 自己顺便把系统推到 healthy
- publication 在 readiness 闭环前提前放行
---
## 17. `V2 prototype` 与 `mini core design` 的关系
这份 `mini core design` 不是脱离 prototype 重新发明的。
它和 `V2 prototype` 是继承关系,不是替代关系。
### 17.1 prototype 提供了什么
prototype / FSM / simulator 已经证明了:
1. 哪些状态对象必须存在
2. 哪些事件必须显式
3. 哪些 failure class 必须 fail-closed
4. 哪些 boundary 不能靠实现便利重新解释
也就是说,prototype 提供的是:
- 语义骨架
- 状态机对象
- failure-class closure 直觉
- ownership / session / boundary 的必要性
### 17.2 mini core design 补的是什么
`mini core design` 解决的不是“语义是否存在”,而是:
1. 这些语义对象在当前仓库里应该落在哪些 package / file
2. 谁拥有这些对象
3. 哪些通过 adapter 进入 `weed/`
4. 哪些 `V1` 行为可以复用
5. 哪些当前 `weed/` 改动只是现实验证资产
所以:
- prototype 负责回答 “应该是什么”
- mini core design 负责回答 “在当前代码库里怎么做”
### 17.3 为什么不能直接把 prototype 当 production 目录
因为 production 还必须处理:
- heartbeat / master registry
- frontend projection
- diagnostics
- current backend reuse
- bounded envelope / claim / evidence integration
所以不能简单把 prototype 原样搬进 `weed/` 或 production path。
更合理的关系是:
1. prototype 提供语义模板
2. `sw-block/engine/replication/` 承接 core semantics
3. adapter 把这些 semantics 翻译到当前 runtime
4. backend 继续执行 I/O / transport primitives
### 17.4 一个总规则
> `mini core design` 的任务不是重新设计 prototype 已经证明必要的语义对象。
> 它的任务是把这些对象放到当前代码库中可落地、可维护、可验证的位置,并明确它们与 `V1 backend` 的边界。
### 17.5 在 `V2 core` 建立前如何解释当前测试
`V2 core` 还没有以明确 package / file / event-command loop 形式落地之前,
当前大多数 integrated tests 都应该这样解释:
1. 不是在验证一个已经完成的 `V2 runtime`
2. 而是在用 `V2` 的 truth / constraint / acceptance bar 去检查当前 `V1` runtime 的表现
更准确地说:
- 现在已经存在的是:
- `V2` 的 truth
- `V2` 的 claim boundary
- `V2` 的 failure-class discipline
- `V2` 的 prototype / FSM / mini-core design
- 现在还没有完全存在的是:
- 明确拥有 runtime 语义 authority 的 `V2 core`
- 真实 live path 上的 `ApplyEvent() -> Decide() -> EmitCommands()` 闭环
- 被正式降级成 adapter / backend reality 的 `weed/` 运行结构
因此,像 `CP13-8` 这样的结果,当前最准确的解释不是:
- `V2 runtime` 已验证
而是:
- 当前 `V1` 运行现实在 `V2` 约束下,通过了一个 bounded envelope 的真实 workload 检查
这类检查点的价值依然很高,因为它证明:
1. 当前复用路径不是完全错误的
2. `V2` 约束能够识别和纠正真实 bug
3. 当前运行现实值得继续被抽象、剥离,并最终收敛成真正的 `V2 core`
所以在后续 phase 中,应当默认区分三种东西:
1. `V2` 语义证明
2. `V1-under-V2-constraints` 的运行验证
3. 后续真正的 `V2 core` 提取与 runtime closure
---
## 18. 推荐的实施顺序
### 近期
1. 继续收紧 assignment -> readiness -> publication closure
@@ -751,7 +1244,7 @@ type FrontendBackend interface {
---
## 15. 最短结论
## 19. 最短结论
你要的“更工程化”版本可以归纳为一句话:
+42
View File
@@ -0,0 +1,42 @@
package replication
// Command is one side-effect-free decision emitted by the Phase 14 core skeleton.
// Adapters execute commands later; the core only decides them.
type Command interface {
commandName() string
}
type ApplyRoleCommand struct {
VolumeID string
Epoch uint64
Role VolumeRole
}
func (ApplyRoleCommand) commandName() string { return "apply_role" }
type StartReceiverCommand struct {
VolumeID string
}
func (StartReceiverCommand) commandName() string { return "start_receiver" }
type ConfigureShipperCommand struct {
VolumeID string
Replicas []ReplicaAssignment
}
func (ConfigureShipperCommand) commandName() string { return "configure_shipper" }
type InvalidateSessionCommand struct {
VolumeID string
Reason string
}
func (InvalidateSessionCommand) commandName() string { return "invalidate_session" }
type PublishProjectionCommand struct {
VolumeID string
Projection PublicationProjection
}
func (PublishProjectionCommand) commandName() string { return "publish_projection" }
+17 -7
View File
@@ -15,13 +15,23 @@
// File layout:
//
// Slice 1 core (ownership/fencing):
// types.go — Endpoint, ReplicaState, SessionKind, SessionPhase
// sender.go — Sender: per-replica owner with execution APIs
// session.go — Session: recovery lifecycle with FSM phases
// registry.go — Registry: sender group with reconcile + assignment intent
//
// types.go Endpoint, ReplicaState, SessionKind, SessionPhase
// sender.go — Sender: per-replica owner with execution APIs
// session.go Session: recovery lifecycle with FSM phases
// registry.go — Registry: sender group with reconcile + assignment intent
//
// Phase 14 explicit core shell:
//
// state.go — VolumeState, readiness, boundary, normalized mode
// event.go — bounded event vocabulary
// command.go — side-effect-free command decisions
// projection.go — bounded outward publication projection
// engine.go — ApplyEvent() -> state update -> emit commands/projection
//
// Carried forward from prototype (accepted in Phase 4.5):
// budget.go — CatchUpBudget: bounded catch-up enforcement
// rebuild.go — RebuildState: rebuild execution FSM
// outcome.go — HandshakeResult, RecoveryOutcome classification
//
// budget.go CatchUpBudget: bounded catch-up enforcement
// rebuild.go — RebuildState: rebuild execution FSM
// outcome.go — HandshakeResult, RecoveryOutcome classification
package replication
+342
View File
@@ -0,0 +1,342 @@
package replication
// CoreEngine is the first explicit Phase 14 V2 core shell.
// It is deterministic and side-effect free: one event in, updated state and
// commands/projection out.
type CoreEngine struct {
volumes map[string]*VolumeState
}
// ApplyResult is the full result of handling one event.
type ApplyResult struct {
Commands []Command
Projection PublicationProjection
State VolumeState
}
// NewCoreEngine creates an empty core.
func NewCoreEngine() *CoreEngine {
return &CoreEngine{volumes: map[string]*VolumeState{}}
}
// ApplyEvent mutates the bounded core state and emits commands/projection.
func (e *CoreEngine) ApplyEvent(ev Event) ApplyResult {
st := e.mustState(ev.VolumeID())
var cmds []Command
switch v := ev.(type) {
case AssignmentDelivered:
cmds = append(cmds, e.applyAssignment(st, v)...)
case RoleApplied:
st.Readiness.RoleApplied = true
case ReceiverReadyObserved:
st.Readiness.ReceiverReady = true
case ShipperConfiguredObserved:
st.Readiness.ShipperConfigured = true
case ShipperConnectedObserved:
st.Readiness.ShipperConnected = true
case DiagnosticShippedAdvanced:
if v.ShippedLSN > st.Boundary.DiagnosticShippedLSN {
st.Boundary.DiagnosticShippedLSN = v.ShippedLSN
}
case BarrierAccepted:
if v.FlushedLSN > st.Boundary.DurableLSN {
st.Boundary.DurableLSN = v.FlushedLSN
}
st.Boundary.LastBarrierOK = true
st.Boundary.LastBarrierReason = ""
st.degraded = false
st.degradeReason = ""
case BarrierRejected:
st.Boundary.LastBarrierOK = false
st.Boundary.LastBarrierReason = v.Reason
st.degraded = true
st.degradeReason = v.Reason
if st.shouldInvalidate(v.Reason) {
cmds = append(cmds, InvalidateSessionCommand{
VolumeID: st.VolumeID,
Reason: v.Reason,
})
st.commands.InvalidationIssued = true
st.commands.InvalidationReason = v.Reason
}
case CheckpointAdvanced:
if v.CheckpointLSN > st.Boundary.CheckpointLSN {
st.Boundary.CheckpointLSN = v.CheckpointLSN
}
case NeedsRebuildObserved:
st.needsRebuild = true
st.rebuildReason = v.Reason
st.degraded = false
st.degradeReason = ""
if st.shouldInvalidate(v.Reason) {
cmds = append(cmds, InvalidateSessionCommand{
VolumeID: st.VolumeID,
Reason: v.Reason,
})
st.commands.InvalidationIssued = true
st.commands.InvalidationReason = v.Reason
}
case RebuildCommitted:
st.needsRebuild = false
st.rebuildReason = ""
st.degraded = false
st.degradeReason = ""
st.resetInvalidation()
if v.FlushedLSN > st.Boundary.DurableLSN {
st.Boundary.DurableLSN = v.FlushedLSN
}
if v.CheckpointLSN > st.Boundary.CheckpointLSN {
st.Boundary.CheckpointLSN = v.CheckpointLSN
}
}
e.recompute(st)
proj := e.projectionFor(st)
cmds = append(cmds, PublishProjectionCommand{
VolumeID: st.VolumeID,
Projection: proj,
})
return ApplyResult{
Commands: cmds,
Projection: proj,
State: st.Snapshot(),
}
}
// State returns a detached copy of a volume state.
func (e *CoreEngine) State(volumeID string) (VolumeState, bool) {
st, ok := e.volumes[volumeID]
if !ok {
return VolumeState{}, false
}
return st.Snapshot(), true
}
// Projection returns the current outward projection for a volume.
func (e *CoreEngine) Projection(volumeID string) (PublicationProjection, bool) {
st, ok := e.volumes[volumeID]
if !ok {
return PublicationProjection{}, false
}
return e.projectionFor(st), true
}
func (e *CoreEngine) mustState(volumeID string) *VolumeState {
if st, ok := e.volumes[volumeID]; ok {
return st
}
st := newVolumeState(volumeID)
e.volumes[volumeID] = st
return st
}
func (e *CoreEngine) recompute(st *VolumeState) {
st.Readiness.ReplicaReady = st.Role == RoleReplica &&
st.Readiness.RoleApplied &&
st.Readiness.ReceiverReady
st.Readiness.PublishHealthy = false
st.Mode.Authority = RuntimeAuthorityConstrainedV1
st.Mode.Reason = ""
st.Publication = PublicationView{}
switch {
case st.needsRebuild:
st.Publication.Reason = defaultReason(st.rebuildReason, "needs_rebuild")
st.Mode.Name = ModeNeedsRebuild
st.Mode.Reason = st.Publication.Reason
case st.degraded:
st.Publication.Reason = defaultReason(st.degradeReason, "degraded")
st.Mode.Name = ModeDegraded
st.Mode.Reason = st.Publication.Reason
case !st.hasReplicas():
st.Publication.Reason = "allocated_only"
st.Mode.Name = ModeAllocatedOnly
case st.Role == RoleReplica && st.Readiness.ReplicaReady:
st.Publication.Reason = "replica_not_primary"
st.Mode.Name = ModeReplicaReady
case st.Role == RolePrimary && st.primaryEligibleForPublish():
st.Publication.Healthy = true
st.Mode.Name = ModePublishHealthy
st.Readiness.PublishHealthy = true
case st.Readiness.Assigned || st.Readiness.RoleApplied:
st.Publication.Reason = st.bootstrapReason()
st.Mode.Name = ModeBootstrapPending
st.Mode.Reason = st.Publication.Reason
default:
st.Publication.Reason = "allocated_only"
st.Mode.Name = ModeAllocatedOnly
}
}
func (e *CoreEngine) applyAssignment(st *VolumeState, ev AssignmentDelivered) []Command {
roleChanged := st.Role != ev.Role
epochChanged := st.Epoch != ev.Epoch
replicasChanged := !sameReplicaAssignments(st.DesiredReplicas, ev.Replicas)
st.Epoch = ev.Epoch
st.Role = ev.Role
st.DesiredReplicas = append([]ReplicaAssignment(nil), ev.Replicas...)
st.Readiness.Assigned = true
st.Mode.Authority = RuntimeAuthorityConstrainedV1
if epochChanged || roleChanged {
st.Readiness.RoleApplied = false
}
switch ev.Role {
case RoleReplica:
if epochChanged || roleChanged {
st.Readiness.ReceiverReady = false
}
st.Readiness.ShipperConfigured = false
st.Readiness.ShipperConnected = false
case RolePrimary:
st.Readiness.ReceiverReady = false
if epochChanged || roleChanged || replicasChanged {
st.Readiness.ShipperConfigured = false
st.Readiness.ShipperConnected = false
}
default:
st.Readiness.ReceiverReady = false
st.Readiness.ShipperConfigured = false
st.Readiness.ShipperConnected = false
}
if epochChanged || roleChanged || replicasChanged {
st.degraded = false
st.degradeReason = ""
st.resetInvalidation()
}
var cmds []Command
if st.shouldApplyRole() {
cmds = append(cmds, ApplyRoleCommand{
VolumeID: st.VolumeID,
Epoch: st.Epoch,
Role: st.Role,
})
st.commands.RoleEpoch = st.Epoch
st.commands.Role = st.Role
}
if st.shouldStartReceiver() {
cmds = append(cmds, StartReceiverCommand{VolumeID: st.VolumeID})
st.commands.ReceiverStartEpoch = st.Epoch
}
if st.shouldConfigureShipper() {
cmds = append(cmds, ConfigureShipperCommand{
VolumeID: st.VolumeID,
Replicas: append([]ReplicaAssignment(nil), st.DesiredReplicas...),
})
st.commands.ShipperConfigEpoch = st.Epoch
st.commands.ShipperConfigReplicas = append([]ReplicaAssignment(nil), st.DesiredReplicas...)
}
return cmds
}
func (e *CoreEngine) projectionFor(st *VolumeState) PublicationProjection {
replicaIDs := make([]string, 0, len(st.DesiredReplicas))
for _, replica := range st.DesiredReplicas {
replicaIDs = append(replicaIDs, replica.ReplicaID)
}
return PublicationProjection{
VolumeID: st.VolumeID,
Epoch: st.Epoch,
Role: st.Role,
Mode: st.Mode,
Publication: st.Publication,
Readiness: st.Readiness,
Boundary: st.Boundary,
ReplicaIDs: replicaIDs,
PublishHealthy: st.Publication.Healthy,
}
}
func (st *VolumeState) primaryEligibleForPublish() bool {
return st.Role == RolePrimary &&
st.Readiness.RoleApplied &&
st.Readiness.ShipperConfigured &&
st.Readiness.ShipperConnected &&
st.Boundary.DurableLSN > 0
}
func (st *VolumeState) shouldApplyRole() bool {
return st.commands.RoleEpoch != st.Epoch || st.commands.Role != st.Role
}
func (st *VolumeState) shouldStartReceiver() bool {
return st.Role == RoleReplica &&
!st.Readiness.ReceiverReady &&
st.commands.ReceiverStartEpoch != st.Epoch
}
func (st *VolumeState) shouldConfigureShipper() bool {
return st.Role == RolePrimary &&
st.hasReplicas() &&
!st.Readiness.ShipperConfigured &&
(st.commands.ShipperConfigEpoch != st.Epoch ||
!sameReplicaAssignments(st.commands.ShipperConfigReplicas, st.DesiredReplicas))
}
func (st *VolumeState) shouldInvalidate(reason string) bool {
return !st.commands.InvalidationIssued || st.commands.InvalidationReason != reason
}
func (st *VolumeState) resetInvalidation() {
st.commands.InvalidationIssued = false
st.commands.InvalidationReason = ""
}
func (st *VolumeState) hasReplicas() bool {
return len(st.DesiredReplicas) > 0
}
func (st *VolumeState) bootstrapReason() string {
switch {
case !st.Readiness.RoleApplied:
return "awaiting_role_apply"
case st.Role == RoleReplica && !st.Readiness.ReceiverReady:
return "awaiting_receiver_ready"
case st.Role == RolePrimary && !st.Readiness.ShipperConfigured:
return "awaiting_shipper_configured"
case st.Role == RolePrimary && !st.Readiness.ShipperConnected:
return "awaiting_shipper_connected"
case st.Role == RolePrimary && st.Boundary.DurableLSN == 0:
return "awaiting_barrier_durability"
default:
return "bootstrap_pending"
}
}
func defaultReason(reason, fallback string) string {
if reason != "" {
return reason
}
return fallback
}
func sameReplicaAssignments(left, right []ReplicaAssignment) bool {
if len(left) != len(right) {
return false
}
for i := range left {
if left[i].ReplicaID != right[i].ReplicaID {
return false
}
if left[i].Endpoint != right[i].Endpoint {
return false
}
}
return true
}
+94
View File
@@ -0,0 +1,94 @@
package replication
// Event is one bounded input into the Phase 14 core skeleton.
// Events describe observation or intent; they do not perform side effects.
type Event interface {
VolumeID() string
}
// AssignmentDelivered carries the desired local role and replica set.
type AssignmentDelivered struct {
ID string
Epoch uint64
Role VolumeRole
Replicas []ReplicaAssignment
}
func (e AssignmentDelivered) VolumeID() string { return e.ID }
// RoleApplied confirms the local runtime applied the desired role.
type RoleApplied struct {
ID string
}
func (e RoleApplied) VolumeID() string { return e.ID }
// ReceiverReadyObserved confirms replica receiver readiness.
type ReceiverReadyObserved struct {
ID string
}
func (e ReceiverReadyObserved) VolumeID() string { return e.ID }
// ShipperConfiguredObserved confirms shipper wiring exists.
type ShipperConfiguredObserved struct {
ID string
}
func (e ShipperConfiguredObserved) VolumeID() string { return e.ID }
// ShipperConnectedObserved confirms the shipper is connected.
type ShipperConnectedObserved struct {
ID string
}
func (e ShipperConnectedObserved) VolumeID() string { return e.ID }
// DiagnosticShippedAdvanced updates sender-side diagnostic progress only.
type DiagnosticShippedAdvanced struct {
ID string
ShippedLSN uint64
}
func (e DiagnosticShippedAdvanced) VolumeID() string { return e.ID }
// BarrierAccepted advances authoritative durable progress.
type BarrierAccepted struct {
ID string
FlushedLSN uint64
}
func (e BarrierAccepted) VolumeID() string { return e.ID }
// BarrierRejected marks the bounded path degraded until a later success clears it.
type BarrierRejected struct {
ID string
Reason string
}
func (e BarrierRejected) VolumeID() string { return e.ID }
// CheckpointAdvanced updates the durable base-image boundary.
type CheckpointAdvanced struct {
ID string
CheckpointLSN uint64
}
func (e CheckpointAdvanced) VolumeID() string { return e.ID }
// NeedsRebuildObserved is a fail-closed rebuild escalation.
type NeedsRebuildObserved struct {
ID string
Reason string
}
func (e NeedsRebuildObserved) VolumeID() string { return e.ID }
// RebuildCommitted clears the rebuild condition with bounded durable truth.
type RebuildCommitted struct {
ID string
FlushedLSN uint64
CheckpointLSN uint64
}
func (e RebuildCommitted) VolumeID() string { return e.ID }
@@ -0,0 +1,124 @@
package replication
import (
"reflect"
"testing"
)
func TestPhase14_CommandSequence_PrimaryAssignmentIsBounded(t *testing.T) {
core := NewCoreEngine()
ev := AssignmentDelivered{
ID: "vol-cmd-primary",
Epoch: 1,
Role: RolePrimary,
Replicas: []ReplicaAssignment{
{ReplicaID: "replica-1", Endpoint: Endpoint{DataAddr: "10.0.0.10:9333", CtrlAddr: "10.0.0.10:9334", Version: 1}},
},
}
result := core.ApplyEvent(ev)
assertCommandNames(t, result.Commands, []string{
"apply_role",
"configure_shipper",
"publish_projection",
})
result = core.ApplyEvent(ev)
assertCommandNames(t, result.Commands, []string{
"publish_projection",
})
}
func TestPhase14_CommandSequence_ReplicaAssignmentIsBounded(t *testing.T) {
core := NewCoreEngine()
ev := AssignmentDelivered{
ID: "vol-cmd-replica",
Epoch: 3,
Role: RoleReplica,
Replicas: []ReplicaAssignment{
{ReplicaID: "replica-1", Endpoint: Endpoint{DataAddr: "10.0.0.11:9333", CtrlAddr: "10.0.0.11:9334", Version: 1}},
},
}
result := core.ApplyEvent(ev)
assertCommandNames(t, result.Commands, []string{
"apply_role",
"start_receiver",
"publish_projection",
})
result = core.ApplyEvent(ev)
assertCommandNames(t, result.Commands, []string{
"publish_projection",
})
}
func TestPhase14_CommandSequence_AssignmentChangeReissuesNeededCommand(t *testing.T) {
core := NewCoreEngine()
initial := AssignmentDelivered{
ID: "vol-cmd-change",
Epoch: 5,
Role: RolePrimary,
Replicas: []ReplicaAssignment{
{ReplicaID: "replica-1", Endpoint: Endpoint{DataAddr: "10.0.0.12:9333", CtrlAddr: "10.0.0.12:9334", Version: 1}},
},
}
core.ApplyEvent(initial)
core.ApplyEvent(RoleApplied{ID: "vol-cmd-change"})
core.ApplyEvent(ShipperConfiguredObserved{ID: "vol-cmd-change"})
core.ApplyEvent(ShipperConnectedObserved{ID: "vol-cmd-change"})
changed := AssignmentDelivered{
ID: "vol-cmd-change",
Epoch: 5,
Role: RolePrimary,
Replicas: []ReplicaAssignment{
{ReplicaID: "replica-1", Endpoint: Endpoint{DataAddr: "10.0.0.13:9333", CtrlAddr: "10.0.0.13:9334", Version: 2}},
},
}
result := core.ApplyEvent(changed)
assertCommandNames(t, result.Commands, []string{
"configure_shipper",
"publish_projection",
})
}
func TestPhase14_CommandSequence_InvalidateOnlyOnNewFailureTransition(t *testing.T) {
core := NewCoreEngine()
core.ApplyEvent(AssignmentDelivered{
ID: "vol-cmd-failure",
Epoch: 1,
Role: RolePrimary,
Replicas: []ReplicaAssignment{
{ReplicaID: "replica-1", Endpoint: Endpoint{DataAddr: "10.0.0.14:9333", CtrlAddr: "10.0.0.14:9334", Version: 1}},
},
})
core.ApplyEvent(RoleApplied{ID: "vol-cmd-failure"})
core.ApplyEvent(ShipperConfiguredObserved{ID: "vol-cmd-failure"})
core.ApplyEvent(ShipperConnectedObserved{ID: "vol-cmd-failure"})
core.ApplyEvent(BarrierAccepted{ID: "vol-cmd-failure", FlushedLSN: 9})
result := core.ApplyEvent(BarrierRejected{ID: "vol-cmd-failure", Reason: "timeout"})
assertCommandNames(t, result.Commands, []string{
"invalidate_session",
"publish_projection",
})
result = core.ApplyEvent(BarrierRejected{ID: "vol-cmd-failure", Reason: "timeout"})
assertCommandNames(t, result.Commands, []string{
"publish_projection",
})
}
func assertCommandNames(t *testing.T, cmds []Command, want []string) {
t.Helper()
got := make([]string, 0, len(cmds))
for _, cmd := range cmds {
got = append(got, cmd.commandName())
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("commands=%v, want %v", got, want)
}
}
@@ -0,0 +1,217 @@
package replication
import "testing"
func TestPhase14_Identity_StableReplicaIDPreservesOwnership(t *testing.T) {
r := NewRegistry()
r.ApplyAssignment(AssignmentIntent{
Replicas: []ReplicaAssignment{
{
ReplicaID: "replica-1",
Endpoint: Endpoint{
DataAddr: "10.0.0.1:9333",
CtrlAddr: "10.0.0.1:9334",
Version: 1,
},
},
},
Epoch: 1,
RecoveryTargets: map[string]SessionKind{"replica-1": SessionCatchUp},
})
sender := r.Sender("replica-1")
oldSessionID := sender.SessionID()
if err := sender.BeginConnect(oldSessionID); err != nil {
t.Fatalf("begin connect: %v", err)
}
r.Reconcile([]ReplicaAssignment{
{
ReplicaID: "replica-1",
Endpoint: Endpoint{
DataAddr: "10.0.0.2:9333",
CtrlAddr: "10.0.0.2:9334",
Version: 2,
},
},
}, 1)
if got := r.Sender("replica-1"); got != sender {
t.Fatal("stable ReplicaID should preserve sender ownership")
}
if sender.HasActiveSession() {
t.Fatal("endpoint change should invalidate active ownership session")
}
if sender.Endpoint().DataAddr != "10.0.0.2:9333" {
t.Fatalf("endpoint not updated: %s", sender.Endpoint().DataAddr)
}
}
func TestPhase14_CorePublishHealthyRequiresBarrierDurability(t *testing.T) {
core := NewCoreEngine()
result := core.ApplyEvent(AssignmentDelivered{
ID: "vol-a",
Epoch: 7,
Role: RolePrimary,
Replicas: []ReplicaAssignment{
{ReplicaID: "replica-1", Endpoint: Endpoint{DataAddr: "10.0.0.2:9333", CtrlAddr: "10.0.0.2:9334", Version: 1}},
},
})
if result.Projection.Mode.Name != ModeBootstrapPending {
t.Fatalf("mode=%s", result.Projection.Mode.Name)
}
if result.Projection.Publication.Reason != "awaiting_role_apply" {
t.Fatalf("publication_reason=%q", result.Projection.Publication.Reason)
}
if result.Projection.PublishHealthy {
t.Fatal("fresh assignment must not publish healthy")
}
result = core.ApplyEvent(RoleApplied{ID: "vol-a"})
if result.Projection.Publication.Reason != "awaiting_shipper_configured" {
t.Fatalf("publication_reason=%q", result.Projection.Publication.Reason)
}
result = core.ApplyEvent(ShipperConfiguredObserved{ID: "vol-a"})
if result.Projection.Publication.Reason != "awaiting_shipper_connected" {
t.Fatalf("publication_reason=%q", result.Projection.Publication.Reason)
}
result = core.ApplyEvent(ShipperConnectedObserved{ID: "vol-a"})
if result.Projection.PublishHealthy {
t.Fatal("connected shipper without barrier durability must stay non-healthy")
}
if result.Projection.Mode.Name != ModeBootstrapPending {
t.Fatalf("mode=%s", result.Projection.Mode.Name)
}
if result.Projection.Publication.Reason != "awaiting_barrier_durability" {
t.Fatalf("publication_reason=%q", result.Projection.Publication.Reason)
}
result = core.ApplyEvent(BarrierAccepted{ID: "vol-a", FlushedLSN: 12})
if !result.Projection.PublishHealthy {
t.Fatal("barrier durability should enable publish healthy on eligible primary path")
}
if result.Projection.Mode.Name != ModePublishHealthy {
t.Fatalf("mode=%s", result.Projection.Mode.Name)
}
if result.Projection.Boundary.DurableLSN != 12 {
t.Fatalf("durable_lsn=%d", result.Projection.Boundary.DurableLSN)
}
if result.Projection.Publication.Reason != "" {
t.Fatalf("publication_reason=%q", result.Projection.Publication.Reason)
}
}
func TestPhase14_CoreDiagnosticShippedDoesNotCreateDurableTruth(t *testing.T) {
core := NewCoreEngine()
core.ApplyEvent(AssignmentDelivered{
ID: "vol-b",
Epoch: 3,
Role: RolePrimary,
Replicas: []ReplicaAssignment{{ReplicaID: "replica-1", Endpoint: Endpoint{DataAddr: "10.0.0.3:9333", Version: 1}}},
})
core.ApplyEvent(RoleApplied{ID: "vol-b"})
core.ApplyEvent(ShipperConfiguredObserved{ID: "vol-b"})
core.ApplyEvent(ShipperConnectedObserved{ID: "vol-b"})
result := core.ApplyEvent(DiagnosticShippedAdvanced{ID: "vol-b", ShippedLSN: 99})
if result.Projection.Boundary.DurableLSN != 0 {
t.Fatalf("durable_lsn=%d, want 0", result.Projection.Boundary.DurableLSN)
}
if result.Projection.Boundary.DiagnosticShippedLSN != 99 {
t.Fatalf("diagnostic_shipped=%d", result.Projection.Boundary.DiagnosticShippedLSN)
}
if result.Projection.PublishHealthy {
t.Fatal("diagnostic shipped progress must not establish durable healthy publication")
}
if result.Projection.Mode.Name != ModeBootstrapPending {
t.Fatalf("mode=%s", result.Projection.Mode.Name)
}
}
func TestPhase14_CoreFailClosedModesStayDistinct(t *testing.T) {
core := NewCoreEngine()
core.ApplyEvent(AssignmentDelivered{
ID: "vol-c",
Epoch: 1,
Role: RolePrimary,
Replicas: []ReplicaAssignment{{ReplicaID: "replica-1", Endpoint: Endpoint{DataAddr: "10.0.0.4:9333", Version: 1}}},
})
core.ApplyEvent(RoleApplied{ID: "vol-c"})
core.ApplyEvent(ShipperConfiguredObserved{ID: "vol-c"})
core.ApplyEvent(ShipperConnectedObserved{ID: "vol-c"})
core.ApplyEvent(BarrierAccepted{ID: "vol-c", FlushedLSN: 8})
result := core.ApplyEvent(BarrierRejected{ID: "vol-c", Reason: "shipper_timeout"})
if result.Projection.Mode.Name != ModeDegraded {
t.Fatalf("mode=%s", result.Projection.Mode.Name)
}
if result.Projection.PublishHealthy {
t.Fatal("degraded mode must fail closed")
}
result = core.ApplyEvent(NeedsRebuildObserved{ID: "vol-c", Reason: "gap_too_large"})
if result.Projection.Mode.Name != ModeNeedsRebuild {
t.Fatalf("mode=%s", result.Projection.Mode.Name)
}
if result.Projection.PublishHealthy {
t.Fatal("needs_rebuild must fail closed")
}
if result.Projection.Mode.Reason != "gap_too_large" {
t.Fatalf("reason=%q", result.Projection.Mode.Reason)
}
}
func TestPhase14_CoreProjectionMarksConstrainedRuntimeAuthority(t *testing.T) {
core := NewCoreEngine()
result := core.ApplyEvent(AssignmentDelivered{
ID: "vol-d",
Epoch: 9,
Role: RoleReplica,
Replicas: []ReplicaAssignment{
{ReplicaID: "replica-1", Endpoint: Endpoint{DataAddr: "10.0.0.5:9333", Version: 1}},
},
})
if result.Projection.Mode.Authority != RuntimeAuthorityConstrainedV1 {
t.Fatalf("authority=%s", result.Projection.Mode.Authority)
}
if result.Projection.Mode.Name != ModeBootstrapPending {
t.Fatalf("mode=%s", result.Projection.Mode.Name)
}
result = core.ApplyEvent(RoleApplied{ID: "vol-d"})
result = core.ApplyEvent(ReceiverReadyObserved{ID: "vol-d"})
if result.Projection.Mode.Name != ModeReplicaReady {
t.Fatalf("mode=%s", result.Projection.Mode.Name)
}
if result.Projection.PublishHealthy {
t.Fatal("replica-ready is not the same as publish-healthy")
}
if result.Projection.Publication.Reason != "replica_not_primary" {
t.Fatalf("publication_reason=%q", result.Projection.Publication.Reason)
}
}
func TestPhase14_CoreAllocatedOnlyWithoutReplicas(t *testing.T) {
core := NewCoreEngine()
result := core.ApplyEvent(AssignmentDelivered{
ID: "vol-rf1",
Epoch: 1,
Role: RolePrimary,
})
if result.Projection.Mode.Name != ModeAllocatedOnly {
t.Fatalf("mode=%s", result.Projection.Mode.Name)
}
if result.Projection.PublishHealthy {
t.Fatal("allocated-only volume must not publish healthy")
}
if result.Projection.Publication.Reason != "allocated_only" {
t.Fatalf("publication_reason=%q", result.Projection.Publication.Reason)
}
}
+17
View File
@@ -0,0 +1,17 @@
package replication
// PublicationProjection is the bounded outward projection derived from one
// VolumeState. It is intentionally detached from runtime internals.
type PublicationProjection struct {
VolumeID string
Epoch uint64
Role VolumeRole
Mode ModeView
Publication PublicationView
Readiness ReadinessView
Boundary BoundaryView
ReplicaIDs []string
PublishHealthy bool
}
+121
View File
@@ -0,0 +1,121 @@
package replication
// RuntimeAuthority names which runtime currently owns the integrated path.
// Phase 14 starts by making the V2 core explicit, but integrated tests still
// evaluate the constrained current runtime until a live cutover exists.
type RuntimeAuthority string
const (
RuntimeAuthorityConstrainedV1 RuntimeAuthority = "constrained_v1"
RuntimeAuthorityV2Core RuntimeAuthority = "v2_core"
)
// VolumeRole tracks the local role the core is normalizing.
type VolumeRole string
const (
RoleUnknown VolumeRole = "unknown"
RolePrimary VolumeRole = "primary"
RoleReplica VolumeRole = "replica"
)
// ModeName is the normalized outward mode vocabulary for the bounded chosen path.
type ModeName string
const (
ModeAllocatedOnly ModeName = "allocated_only"
ModeBootstrapPending ModeName = "bootstrap_pending"
ModeReplicaReady ModeName = "replica_ready"
ModePublishHealthy ModeName = "publish_healthy"
ModeDegraded ModeName = "degraded"
ModeNeedsRebuild ModeName = "needs_rebuild"
)
// ReadinessView captures bounded readiness truth for the current chosen path.
type ReadinessView struct {
Assigned bool
RoleApplied bool
ReceiverReady bool
ShipperConfigured bool
ShipperConnected bool
ReplicaReady bool
PublishHealthy bool
}
// BoundaryView captures durable/publication boundary truth. DurableLSN is the
// authority for replicated durability; diagnostic shipped progress is separate.
type BoundaryView struct {
DurableLSN uint64
CheckpointLSN uint64
DiagnosticShippedLSN uint64
LastBarrierOK bool
LastBarrierReason string
}
// ModeView is the bounded external mode meaning derived from the current state.
type ModeView struct {
Name ModeName
Reason string
Authority RuntimeAuthority
}
// PublicationView is the outward publication truth derived from the same core
// state. It is kept separate from readiness so the publication automaton stays
// explicit in Phase 14A.
type PublicationView struct {
Healthy bool
Reason string
}
type commandState struct {
RoleEpoch uint64
Role VolumeRole
ReceiverStartEpoch uint64
ShipperConfigEpoch uint64
ShipperConfigReplicas []ReplicaAssignment
InvalidationIssued bool
InvalidationReason string
}
// VolumeState is the minimal V2-core-owned state for one volume on the bounded
// current path.
type VolumeState struct {
VolumeID string
Epoch uint64
Role VolumeRole
DesiredReplicas []ReplicaAssignment
Readiness ReadinessView
Boundary BoundaryView
Mode ModeView
Publication PublicationView
degraded bool
degradeReason string
needsRebuild bool
rebuildReason string
commands commandState
}
func newVolumeState(volumeID string) *VolumeState {
return &VolumeState{
VolumeID: volumeID,
Role: RoleUnknown,
Mode: ModeView{
Name: ModeAllocatedOnly,
Authority: RuntimeAuthorityConstrainedV1,
},
}
}
// Snapshot returns a detached copy of the state for external inspection/tests.
func (s *VolumeState) Snapshot() VolumeState {
out := *s
if s.DesiredReplicas != nil {
out.DesiredReplicas = append([]ReplicaAssignment(nil), s.DesiredReplicas...)
}
if s.commands.ShipperConfigReplicas != nil {
out.commands.ShipperConfigReplicas = append([]ReplicaAssignment(nil), s.commands.ShipperConfigReplicas...)
}
return out
}