mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-22 07:06:51 +00:00
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:
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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. 最短结论
|
||||
|
||||
你要的“更工程化”版本可以归纳为一句话:
|
||||
|
||||
|
||||
@@ -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" }
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user