All 43 actions pass on m01/m02 hardware. Auto-failover PASS. dd_write: 30s → 123ms. Post-failover write: 33,621 IOPS. 1. WAL retention: remove keepup retention floor (MinShippedLSN). WAL cannot be pinned during sustained async writes — any pin strategy either fills WAL (blocking writes) or over-recycles (breaking catch-up). Flusher recycles freely. Future LBA map will provide catch-up without WAL retention. MinShippedLSN on ShipperGroup retained as diagnostic surface. 2. Registry stale-cleanup race: add RegisteredAt grace period. Race: master registers volume → next VS heartbeat arrives before VS discovers the volume → stale cleanup deletes the entry → failover finds 0 entries. Fix: skip stale cleanup for entries registered within 30s (> 2 heartbeat intervals). 2 new tests: grace protects new entry, old entry still cleaned. 3. Shutdown heartbeat: VS disconnect heartbeat no longer claims block inventory authority. Previously, the shutdown beat's empty inventory triggered stale cleanup, deleting the entry before failover could use it. Scenario fix: recovery-baseline-failover.yaml now kills the correct node (discovered primary, not hardcoded), connects to the correct new primary for post-failover verification. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
19 KiB
V2 Validation Matrix
Date: 2026-04-08 Status: active
Purpose
This document defines the concrete validation matrix for the V2 protocol and runtime.
It answers four practical questions:
- what must be green before a stage can be called ready
- which existing V1 tests should be reused
- which new V2-only tests are required because truth ownership changed
- what the final validation signal must be for each scenario
This document is the stage-oriented companion to:
sw-block/design/v2-proof-and-retest-pyramid.mdsw-block/design/v2-reuse-replacement-boundary.mdsw-block/design/v2-sync-recovery-protocol.mdsw-block/design/v2-rebuild-mvp-session-protocol.md
Validation Stages
V2 validation is gated in three layers:
Rebuild Ready: primary-driven rebuild is correct, convergent, and fail-closedRestore Ready: exact snapshot/export/import and snapshot-tail recovery are correctV2 Ready: assignment, sync facts, keep/catch/rebuild, failover/rejoin, and publication semantics close end-to-end
The intent is to avoid claiming overall V2 readiness from rebuild-only proof.
Reuse Policy From V1
Reuse unchanged
Reuse existing V1 tests unchanged when the observable contract is still the same in V2:
- WAL append/replay correctness
- flusher and checkpoint correctness
- dirty-map and extent correctness
- receiver data-plane correctness
- barrier/fsync data integrity when semantic meaning did not change
- snapshot export/import integrity tests
- fail-closed mid-transfer and corruption detection tests
Reuse with adapter
Reuse the intent, but update the trigger path when V2 moved the decision point to primary-owned sync facts:
- rebuild tests that formerly used direct local install helpers
- catch-up tests that formerly bypassed sync-driven entry
- reconnect/rejoin tests that now must enter via
syncAck -> primary decision - failover tests that now must prove assignment/session/projection layering
Retire or replace
Do not reuse tests that encode V1.5 semantics that V2 intentionally removed:
- shipper or replica self-escalation to rebuild
CP13-6style max-bytes retention-triggered rebuild decisions- direct local shortcut paths used as if they were protocol truth
Stage Gate Summary
| Stage | Closure meaning | Must-have scope |
|---|---|---|
Rebuild Ready |
V2 rebuild is safe and correct on the real session-controlled path | rebuild kernel, runtime, trigger scenarios, data identity |
Restore Ready |
exact base restore and snapshot-tail recovery are safe and exact | snapshot boundary, integrity, partial-failure safety, tail convergence |
V2 Ready |
primary-owned assignment/session/projection semantics close on real flows | bootstrap, keepup, catchup, rebuild, failover, rejoin, publish gating |
Matrix Linkage
Read this matrix together with:
v2-capability-map.mdfor ownership of the capability tierv2-integration-matrix.mdfor real scenario coverage
This matrix answers "is the capability closed at all?" It does not by itself guarantee that enough real topology/workload/failure scenarios have been exercised. That second question belongs to the integration matrix.
| Validation rows | Capability tier | Primary protocol refs | Main integration refs |
|---|---|---|---|
R1-R12 |
Tier 3: RF=2 Recovery And Failover | v2-sync-recovery-protocol.md, v2-rebuild-mvp-session-protocol.md |
I-R1-I-R8 |
S1-S10 |
Tier 5: Lifecycle Capability | v2-rebuild-mvp-session-protocol.md and snapshot/restore execution rules |
I-S1-I-S4 |
V1-V2 |
Tier 2: RF=2 Replication Base | v2-sync-recovery-protocol.md |
I-V1, I-V2 |
V3-V8 |
Tier 3: RF=2 Recovery And Failover | v2-sync-recovery-protocol.md, v2-rebuild-mvp-session-protocol.md |
I-V3, I-V4, I-V5 |
V9-V10 |
Tier 4: Multi-Replica Runtime (RF>=3) |
v2-sync-recovery-protocol.md |
future RF>=3 integrated rows; currently bounded engine/component proof only |
V11 |
Tier 3 / Tier 8 boundary | recovery protocol plus launch-envelope disturbance claims | I-V5 |
V12-V13 |
Tier 6: Control Plane And Operations | ownership / observability docs and control-plane protocol surfaces | I-V4, I-V6 |
V14 |
Tier 0: Semantic Foundation | v2-protocol-truths.md, v2-sync-recovery-protocol.md |
exercised across I-V* and negative component packs |
Matrix A: Rebuild Ready
| ID | Priority | Scenario | Trigger / entry | Reuse | Main proof | Final validation | Coverage | File | Evidence |
|---|---|---|---|---|---|---|---|---|---|
R1 |
P0 | Fresh replica join rebuild | replica reports applied_lsn=0, primary decides rebuild |
New | canonical primary-decided rebuild entry exists | session completes and replica returns to steady state | Covered | weed/storage/blockvol/test/component/rebuild_matrix_gaps_test.go |
TestRebuild_R1_SyncAckDrivenDecision — (1) protocol engine decides rebuild from syncAck(applied=0, wal_tail=N), (2) SendSessionControl over real TCP to receiver ctrl port, (3) accepted ack read from TCP, (4) base lane over real TCP via RebuildTransportServer/Client, (5) data verified block-by-block |
R2 |
P0 | Primary-initiated 1GB rebuild with live writes | explicit primary rebuild decision | New | two-line rebuild under realistic size, 4KB blocks, live WAL, flusher active | stop writes, flush both, full extent SHA-256 match | Covered | weed/storage/blockvol/test/component/rebuild_primary_initiated_test.go |
TestRebuild_PrimaryInitiated_1GB_WithLiveWrites |
R3 |
P0 | Stale replica restart beyond WAL window | reconnect with applied_lsn < wal_tail |
New | stale restart naturally enters rebuild | final extent digest match | Covered | weed/storage/blockvol/test/component/rebuild_matrix_gaps_test.go |
TestRebuild_R3_StaleReplicaRestartBeyondWAL |
R4 |
P0 | Rebuild completion dual gate | base finishes before WAL or WAL before base | Reuse with adapter | no premature completion | complete only after base_complete && wal_applied_lsn >= target_lsn |
Covered | weed/storage/blockvol/test/component/rebuild_crash_test.go |
TestRebuild_CompletionRequiresBothLanes |
R5 |
P0 | Mid-transfer failure is fail-closed | connection drop or server death mid-base | Reuse | partial rebuild does not commit mixed state | replica remains logically unchanged after failure | Covered | weed/storage/blockvol/test/component/rebuild_matrix_gaps_test.go |
TestRebuild_R5_ConnectionDropMidBase |
R6 |
P0 | Wrong session / epoch rejected | stale control/data/ack frames | Reuse with adapter | stale traffic cannot mutate active rebuild | explicit reject or ignore; active session remains valid | Covered | weed/storage/blockvol/test/component/rebuild_crash_test.go |
TestRebuild_EpochMismatch_WALEntryRejected, TestRebuild_ControlSurface_StartSupersedeAndComplete |
R7 |
P0 | Overlap correctness | same LBA hit by base and WAL in different orders | Reuse | bitmap and WAL-wins semantics are correct | block-by-block compare on overlap set | Covered | weed/storage/blockvol/test/component/rebuild_mvp_test.go |
TestRebuild_WALApplied_NeverOverwrittenByBase, TestRebuild_BitmapSetOnApplied_NotReceived, TestRebuild_BasePlusWAL_ConvergesToTarget |
R8 |
P1 | Rebuild timeout fails closed | no progress ack | New | watchdog and cancel path are real | failed session, no silent success, pin cleared | Covered | weed/server/volume_server_block_test.go |
TestBlockService_WireLocalReplicaRebuildSessionAcks_TimeoutFailsClosedAndClearsPin |
R9 |
P1 | Progress pin tracks rebuild truth | rebuild emits progress | New | retention floor follows wal_applied_lsn, not barrier-only closure |
observed floor moves and later clears | Covered | weed/storage/blockvol/test/component/rebuild_retention_pin_test.go, weed/server/volume_server_block_test.go |
TestRebuild_RetentionPin_FlusherRespectsRebuildPin, TestBlockService_WireLocalReplicaRebuildSessionAcks_ProgressUpdatesCoreAndPin |
R10 |
P1 | Failover-rejoin rebuild | old primary comes back as replica | New | rebuild survives real topology change | final extent digest match | Covered | weed/storage/blockvol/test/component/rebuild_failover_rejoin_test.go |
TestRebuild_R10_FailoverRejoinRebuild — forced WAL recycling past nodeA position, engine strictly asserts rebuild (not catchup/keepup), CRC validated |
R11 |
P1 | Non-empty stale replica full overwrite | replica has old dirty/WAL state | New | full-base rebuild discards stale local runtime correctly | final extent digest match | Covered | weed/storage/blockvol/test/component/rebuild_r11_r12_test.go |
TestRebuild_R11_DivergentReplicaFullOverwrite |
R12 |
P1 | Restart rebuild, not resume rebuild | crash mid-session | New | current MVP restart semantics are explicit and safe | fresh rebuild converges without durable base-progress | Covered | weed/storage/blockvol/test/component/rebuild_r11_r12_test.go |
TestRebuild_R12_CrashMidRebuild_FreshSessionConverges |
Rebuild Ready minimum gate
Rebuild Ready requires all of:
R1R2R3R4R5R6R7
Matrix B: Restore Ready
| ID | Priority | Scenario | Trigger / entry | Reuse | Main proof | Final validation | Coverage | File | Evidence |
|---|---|---|---|---|---|---|---|---|---|
S1 |
P0 | Exact snapshot export at requested boundary | explicit snapshot request at BaseLSN |
Reuse | snapshot base is exact, not approximate | manifest boundary and export boundary match exactly | Covered (V1) | weed/storage/blockvol/v2bridge/transfer_test.go |
V1 snapshot export tests |
S2 |
P0 | Snapshot checksum mismatch fails | corrupt payload or wrong digest | Reuse + New | no silent bad restore | restore rejected before commit | Covered | weed/storage/blockvol/v2bridge/transfer_test.go, weed/storage/blockvol/test/component/restore_s2_corruption_test.go |
Epoch mismatch: TestP1_TransferFullBase_EpochMismatch; Payload corruption: TestRestore_S2_CorruptWALEntryRejected (truncated + empty payloads rejected), TestRestore_S2_CorruptBaseBlockDetected (short block documented as transport-layer responsibility) |
S3 |
P0 | Partial snapshot transfer does not commit | disconnect mid-stream | Reuse | no half-installed snapshot state | original state preserved | Covered (V1) | weed/storage/blockvol/v2bridge/transfer_test.go |
V1 partial-transfer fail-closed tests |
S4 |
P0 | Snapshot import exactness | import known snapshot image | Reuse | imported extent equals exported snapshot image | full extent digest match | Covered (V1) | weed/storage/blockvol/v2bridge/transfer_test.go |
V1 import exactness tests |
S5 |
P0 | Snapshot-tail rebuild | exact snapshot install plus WAL tail replay | New | restore and live convergence close together | final extent digest match | Covered | weed/storage/blockvol/test/component/restore_ready_test.go |
TestRestore_S5_SnapshotTailRebuild |
S6 |
P0 | Boundary mismatch rejected | server returns wrong base boundary | Reuse | exact restore contract is enforced | explicit failure, no commit | Covered (V1) | weed/storage/blockvol/v2bridge/transfer_test.go |
V1 boundary mismatch tests |
S7 |
P1 | Restart after snapshot install before tail replay | crash between base and tail phases | New | snapshot state is durable and fresh decision can continue recovery | final extent digest match | Covered | weed/storage/blockvol/test/component/restore_ready_test.go |
TestRestore_S7_CrashBetweenBaseAndTail |
S8 |
P1 | Snapshot under concurrent writes | writes continue after snapshot boundary | Reuse with adapter | exact base is preserved and later writes arrive through WAL tail | final extent digest match | Covered | weed/storage/blockvol/test/component/restore_ready_test.go |
TestRestore_S8_SnapshotUnderConcurrentWrites |
S9 |
P1 | Stale snapshot request rejected | requested historical boundary no longer satisfiable | New | protocol refuses unverifiable restore request | explicit failure | Covered (unit) | sw-block/engine/replication/restore_ready_test.go |
TestRestore_S9_StaleSnapshotRequestRejected + TestRestore_S9_BoundaryJustOutsideRetention — gap_beyond_retention when replica LSN < WAL tail, edge case at tail boundary |
S10 |
P1 | Snapshot-tail chosen from trusted checkpoint | primary selects exact restore path | New | planner/runtime choose snapshot-tail only when allowed | reaches steady state without semantic drift | Covered (unit) | sw-block/engine/replication/restore_ready_test.go |
TestRestore_S10_SnapshotTailChosenFromTrustedCheckpoint — 5 cases: trusted+covered→snapshot-tail, untrusted→full-base, WAL gap→full-base, no checkpoint→full-base, checkpoint>committed edge |
Restore Ready minimum gate
Restore Ready requires all of:
S1S2S3S4S5S6
Matrix C: V2 Ready
| ID | Priority | Scenario | Trigger / entry | Reuse | Main proof | Final validation | Coverage | File | Evidence |
|---|---|---|---|---|---|---|---|---|---|
V1 |
P0 | Bootstrap to healthy primary | fresh assignment/bootstrap | Reuse | stage-0 bootstrap closure still holds | healthy publish with correct projection | Covered | weed/storage/blockvol/test/component/publish_healthy_test.go |
TestPublishHealthy_WholeChain_FreshRF2; Stage 0 hardware PASS (phase20-t6-stage0) |
V2 |
P0 | Sustained write plus barrier closure | fio/dd/fsync style load | Reuse | healthy data-plane remains correct under workload | data checksum and barrier success | Covered | weed/storage/blockvol/test/component/bootstrap_shipping_test.go |
TestBootstrap_SyncCacheIsDurabilityFence_NotWriteLBA; Stage 1 hardware 32/33 |
V3 |
P0 | Sync timeout to rebuild | timeout fact enters primary decision | New | fact-driven recovery entry is real | rebuild completes, projection returns to steady state | Covered (server) | weed/server/block_recovery_test.go |
TestP16B_FactTriggeredRebuildCycle_AutoInstallsRebuildAndReachesInSync |
V4 |
P0 | Sync facts choose keepup vs catchup vs rebuild | different replica reported positions | New | primary owns recovery classification | expected path is chosen from facts | Covered (engine) | sw-block/protocol/engine_test.go |
TestSyncAck_ReplicaCaughtUp_KeepUp, TestSyncAck_ReplicaBehindWithinWAL_CatchUp, TestSyncAck_ReplicaBeyondWAL_Rebuild |
V5 |
P0 | Rebuild complete requires durability proof for publish | rebuild finishes but publish requires DurableLSN > 0 | New | DurableLSN=0 after rebuild blocks publish; DurableLSN>0 (from rebuild completion or barrier) enables publish | no publish without durability evidence | Covered (engine) | sw-block/protocol/v2_ready_test.go |
TestV2Ready_V5_RebuildCompleteNotPublishReady — 3 cases: (1) rebuild+DurableLSN>0 → publish (engine design choice: rebuild completion is durability proof), (2) DurableLSN=0 → hard-asserts no publish, (3) barrier after DurableLSN=0 rebuild → hard-asserts publish |
V6 |
P0 | Only one live session per replica | repeated triggers or supersede | New | no dual-contract ambiguity | at most one active session per replica | Covered | weed/storage/blockvol/test/component/rebuild_mvp_test.go |
TestRebuild_ControlSurface_StartSupersedeAndComplete |
V7 |
P0 | Session failure re-enters facts path | catchup/rebuild fail or timeout | New | no local self-escalation survives in V2 | new action always comes from fresh primary decision | Covered (server) | weed/server/block_recovery_test.go |
TestP16B_OnCatchUpFailed_ReentersFactDecisionForRebuild |
V8 |
P0 | Primary failover and old-primary rejoin | failover then rejoin | New | assignment/session/projection layering closes end-to-end | rejoined node converges and health surfaces are correct | Partial | weed/storage/blockvol/test/component/rebuild_failover_rejoin_test.go |
TestRebuild_R10_FailoverRejoinRebuild — proves role swap + engine rebuild decision + CRC convergence, but does NOT verify weed/server projection or master health surface |
V9 |
P1 | Mixed health aggregate projection | one in-sync, one rebuilding, one stale | Reuse with adapter | volume health is derived from aggregate replica state | projection matches expected degraded or healthy mode | Covered (engine) | sw-block/protocol/v2_ready_test.go |
TestV2Ready_V9_MixedHealthAggregateProjection — hard-asserts: one rebuilding → needs_rebuild; all converged + barrier → publish_healthy |
V10 |
P1 | Retention floor under active rebuild | rebuild with live WAL pressure | New | active recovery truth is reflected into WAL retention | no premature WAL loss while rebuild is active | Covered | weed/storage/blockvol/test/component/rebuild_retention_pin_test.go |
TestRebuild_RetentionPin_FlusherRespectsRebuildPin |
V11 |
P1 | Long-haul write through recovery | workload continues during fault and recovery | New | no hidden divergence across long runtime | final extent digest match | Partial | weed/storage/blockvol/test/component/rebuild_primary_initiated_test.go |
TestRebuild_PrimaryInitiated_1GB_WithLiveWrites — rebuild-only, not full fault+recovery cycle |
V12 |
P1 | Operator-triggered rebuild hint | admin or explicit rebuild assignment | New | alternate entry still converges into same session protocol | rebuild completes on same execution path | Missing | — | — |
V13 |
P1 | Observability coherence | running V2 recovery | Reuse with adapter | logs, projections, diagnostics, and state snapshots agree | diagnostic surfaces remain aligned | Missing | — | — |
V14 |
P1 | Negative fail-closed matrix | wrong epoch, wrong session, stale ack, wrong kind | Reuse with adapter | ambiguity always biases toward reject or degrade | explicit failure or ignore path only | Covered (engine) | sw-block/protocol/v2_ready_test.go, weed/storage/blockvol/test/component/rebuild_crash_test.go |
TestV2Ready_V14_NegativeFailClosedMatrix, TestRebuild_EpochMismatch_WALEntryRejected |
V2 Ready minimum gate
V2 Ready requires:
- all
Rebuild Readyminimum rows - all
Restore Readyminimum rows V1V2V3V4V5V6V7V8
Current Anchor Tests
The following tests are already strong anchors for the matrix and should be treated as seed evidence instead of being replaced:
| Matrix row | Current anchor |
|---|---|
R2 |
weed/storage/blockvol/test/component/rebuild_primary_initiated_test.go |
R4, R5, R6, R7 |
rebuild session, transport, and executor tests under weed/storage/blockvol/ and weed/storage/blockvol/v2bridge/ |
V3, V6, V7 |
focused recovery/runtime tests under weed/server/ |
V1, V2 |
existing bootstrap/workload acceptance and component packs reused from current runner and weed/storage/blockvol/test/component/ |
S1-S4 |
existing snapshot export/import tests under weed/storage/blockvol/v2bridge/ |
Recommended Next Pass
When turning this matrix into execution work, use this order:
- mark existing tests against
R*,S*, andV* - classify each row as
covered,partial, ormissing - fill
Rebuild Readygaps first - fill
Restore Readynext - keep
V2 Readysmall and stage-gated, not as one giant acceptance bucket
The working rule is:
- reuse V1 execution-muscle tests whenever the contract is unchanged
- add new V2 tests only where primary-owned truth changed the entry path or the closure meaning