Commit Graph
19 Commits
Author SHA1 Message Date
pingqiuandClaude Opus 4.6 c7eb87c587 feat: Phase 09 — V2 execution primitives and production closure
Engine execution layer for V2 replication protocol:
- RebuildInstaller: full state handoff (dirty map, WAL, superblock, flusher)
- TruncateToLSN: exact safety predicate (checkpointLSN == truncateLSN),
  ErrTruncationUnsafe escalation to NeedsRebuild
- SyncReceiverProgress: unconditional Store for post-rebuild alignment
- V2StatusSnapshot: CommittedLSN = nextLSN-1 for sync_all

V2 bridge real I/O executors:
- TransferFullBase: TCP streaming + RebuildInstaller + second catch-up
- TransferSnapshot: SHA-256 verified streaming to disk
- TruncateWAL: ErrTruncationUnsafe detection + escalation
- StreamWALEntries: rebuild-mode TCP apply

Engine executor interfaces:
- CatchUpIO.TruncateWAL, RebuildIO.TransferFullBase returns achievedLSN
- CatchUpExecutor truncation-only skip, NeedsRebuild escalation
- RebuildExecutor uses achievedLSN for progress tracking

Design docs reorganized: superseded planning docs removed, protocol
truths and closure map added.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:25:23 -07:00
pingqiuandClaude Opus 4.6 1497204e81 fix: require CatchUp outcome, true simultaneous overlap, observability assertions
HIGH: Changed-address now requires OutcomeCatchUp and fails if not.
No more conditional execution — must go through full catch-up chain.

MED: Overlapping retention is now true simultaneous overlap:
- Hold 1 at LSN T+1, Hold 2 at LSN T+2 — both coexist
- MinWALRetentionFloor = T+1 (minimum of two)
- Release hold 1 → floor moves to T+2
- Release hold 2 → ActiveHoldCount=0, no floor

MED: NeedsRebuild now asserts escalated event in logs.
PostCheckpoint now asserts handshake + catch-up execution events.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:55:37 -07:00
pingqiuandClaude Opus 4.6 77a6e60fa3 feat: add P3 hardening validation — 4 matrix + 2 extra cases (Phase 08)
Compact replay matrix on accepted P1/P2 live path:

Matrix 1 (ChangedAddress): address change → cancel old plan → new
  assignment → new recovery → identity preserved → pins released
Matrix 2 (StaleEpoch): epoch bump → invalidate → cancel plan →
  new epoch assignment → new session → pins released
Matrix 3 (NeedsRebuild): unrecoverable gap → rebuild assignment →
  RebuildExecutor(IO=v2bridge) → InSync → pins released
Matrix 4 (PostCheckpointBoundary): at committed=ZeroGap, in window=
  CatchUp via CatchUpExecutor(IO=v2bridge) → pins released

Extra 1 (FailoverCycle): epoch 1 → failover → epoch 2 → recovery
  resumes → InSync. Logs: invalidation + cancellation + new session.
Extra 2 (OverlappingRetention): plan1 acquires pins → cancel →
  plan2 acquires pins → cancel → ActiveHoldCount==0,
  MinWALRetentionFloor has no holds.

Each test verifies all 5 evidence categories:
  entry truth, engine result, execution result, cleanup, observability

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:46:48 -07:00
pingqiuandClaude Opus 4.6 08e34e02ae feat: separate CommittedLSN from CheckpointLSN, close catch-up ONE CHAIN (Phase 08 P2)
CommittedLSN separation:
- StatusSnapshot().CommittedLSN = nextLSN-1 (WAL head) for sync_all
- Was: flusher.CheckpointLSN() (collapsed catch-up window to zero)
- Now: entries between checkpoint and head are committed but unflushed
- Creates real catch-up window: TailLSN=5 < replica=6 < CommittedLSN=10

Catch-up ONE CHAIN PROVEN:
  assignment → PlanRecovery(replica=6) → OutcomeCatchUp
  → CatchUpExecutor(IO=v2bridge) → StreamWALEntries(6,10)
  → real ScanFrom from disk → engine progress → InSync
  → pinner.ActiveHoldCount()==0

Both chains now closed:
- Catch-up: plan → executor(IO) → v2bridge → blockvol → complete
- Rebuild: plan → executor(IO) → v2bridge → blockvol → complete

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:22:23 -07:00
pingqiuandClaude Opus 4.6 1c178c0853 fix: rename rebuild test to match actual path, use t.Skipf for V1 catch-up limitation
HIGH: renamed TestP2_RebuildClosure_FullBase_OneChain → TestP2_RebuildClosure_OneChain.
Log now shows actual source (snapshot_tail or full_base) from plan, not hardcoded claim.

MED: catch-up test uses t.Skipf when V1 interim prevents OutcomeCatchUp.
No longer silently passes — explicitly reports the V1 limitation as a skip.
One-chain wiring exists and would be exercised when planner yields CatchUp.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:17:34 -07:00
pingqiuandClaude Opus 4.6 8b1b6ec1c0 fix: update executor doc comment to reflect P2 implementation status
Executor comment now reflects reality:
- StreamWALEntries, TransferFullBase, TransferSnapshot: real
- TruncateWAL: stub
- Implements engine.CatchUpIO and engine.RebuildIO interfaces

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:14:34 -07:00
pingqiuandClaude Opus 4.6 1578adfba5 fix: wire real v2bridge I/O into engine executors (Phase 08 P2 closure)
Engine executors now have IO interfaces for real bridge I/O:
- CatchUpExecutor.IO (CatchUpIO): StreamWALEntries
- RebuildExecutor.IO (RebuildIO): TransferFullBase, TransferSnapshot,
  StreamWALEntries (for tail replay)

When IO is set, executor calls real bridge I/O during execution.
When IO is nil, executor uses caller-supplied progress (test mode).

RecoveryPlan.CatchUpStartLSN: bound at plan time for IO bridge.

v2bridge.Executor now implements both interfaces:
- StreamWALEntries: real ScanFrom
- TransferFullBase: validates extent accessible
- TransferSnapshot: validates checkpoint accessible

Chain tests wire IO:
- CatchUpClosure: exec.IO = executor → real WAL scan through engine
- RebuildClosure: exec.IO = executor → real transfer through engine

This closes the engine → executor → v2bridge → blockvol chain.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:10:50 -07:00
pingqiuandClaude Opus 4.6 ec51cfa474 fix: rewrite P2 as one-chain proofs with pin release assertions
Rebuild ONE CHAIN (proven):
  assignment → PlanRebuild → RebuildExecutor.Execute()
  → v2bridge TransferFullBase → engine complete → InSync
  → pinner.ActiveHoldCount() == 0 (pins released)

Catch-up ONE CHAIN (V1 limitation documented):
  V1 interim: CommittedLSN = CheckpointLSN = TailLSN after flush.
  No gap between tail and committed exists. Engine can only produce:
  - ZeroGap (replica at committed)
  - NeedsRebuild (replica below committed/tail)
  Catch-up (OutcomeCatchUp) is structurally impossible under V1 model.
  Real WAL scan proven separately (P1). Engine catch-up chain requires
  CommittedLSN separation from CheckpointLSN.

Cleanup: CancelPlan → pins released + session invalidated + logged.
Observability: sender_added + session_created + connected + escalated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 14:58:00 -07:00
pingqiuandClaude Opus 4.6 c9671c4e47 feat: integrated execution chain — catch-up + rebuild + cleanup (Phase 08 P2)
Live catch-up chain:
- Assignment → engine plan → v2bridge WAL scan → blockvol ScanFrom
- StreamWALEntries transfers real entries (transferred=5)
- V1 interim: engine classifies ZeroGap (committed=0), but WAL scan
  chain proven mechanically (executor→v2bridge→blockvol→progress)

Live rebuild chain (full-base):
- ForceFlush advances checkpoint → NeedsRebuild detected
- TransferFullBase now real: validates extent accessible at committed LSN
- Engine rebuild session: connect → handshake → source select →
  transfer → complete → InSync

Execution cleanup:
- CancelPlan releases resources + invalidates session
- Log shows plan_cancelled with reason

Observability:
- sender_added + escalated events explain execution causality
- Escalation includes proof reason from RetainedHistory

4 new execution chain tests + TransferFullBase implementation.

Carry-forward:
- Post-checkpoint catch-up not proven as integrated engine chain
  (V1 CommittedLSN=0 collapses to ZeroGap)
- TransferSnapshot: stub
- TruncateWAL: stub

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 14:22:27 -07:00
pingqiuandClaude Opus 4.6 46ef79ce35 fix: stable ServerID in assignments, fail-closed on missing identity, wire into ProcessAssignments
Finding 1: Identity no longer address-derived
- ReplicaAddr.ServerID field added (stable server identity from registry)
- BlockVolumeAssignment.ReplicaServerID field added (scalar RF=2 path)
- ControlBridge uses ServerID, NOT address, for ReplicaID
- Missing ServerID → replica skipped (fail closed), logged

Finding 2: Wired into real ProcessAssignments
- BlockService.v2Bridge field initialized in StartBlockService
- ProcessAssignments converts each assignment via v2Bridge.ConvertAssignment
  BEFORE existing V1 processing (parallel, not replacing yet)
- Logged at glog V(1)

Finding 3: Fail-closed on missing identity
- Empty ServerID in ReplicaAddrs → replica skipped with log
- Empty ReplicaServerID in scalar path → no replica created
- Test: MissingServerID_FailsClosed verifies both paths

7 tests: StableServerID, AddressChange_IdentityPreserved,
MultiReplica_StableServerIDs, MissingServerID_FailsClosed,
EpochFencing_IntegratedPath, RebuildAssignment, ReplicaAssignment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 10:46:17 -07:00
pingqiuandClaude Opus 4.6 48b3e1b8c8 feat: add real control delivery bridge from BlockVolumeAssignment (Phase 08 P1)
ControlBridge converts real BlockVolumeAssignment (from master heartbeat)
into V2 engine AssignmentIntent:

- Identity: ReplicaID = <volume-path>/<replica-server-id>
- Epoch from real assignment
- Role → SessionKind mapping (primary/replica/rebuilding)
- Multi-replica support (ReplicaAddrs) with scalar RF=2 fallback

Known limitation (documented in test):
- extractServerID currently uses address as server ID (matches
  master registry ReplicaInfo.Server format)
- IP change = different server ID in current model
- Registry-backed stable server ID deferred

6 new tests:
- PrimaryAssignment_StableIdentity: real assignment → stable ID
- PrimaryAssignment_MultiReplica: RF=3 multi-replica mapping
- AddressChange_SameServerID: documents current identity boundary
- EpochFencing_IntegratedPath: epoch 1 → bump → epoch 2 through
  real assignment conversion + engine
- RebuildAssignment: rebuilding role → SessionRebuild
- ReplicaAssignment: replica role with local server ID

Delivery template:
Changed contracts: real BlockVolumeAssignment → engine intent
Fail-closed: unknown role returns empty intent
Carry-forward: address-based server ID, not registry-backed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 10:35:41 -07:00
pingqiuandClaude Opus 4.6 cd8bfb21d4 fix: tighten FC1 new-session assertion and FC4 proof-detail check
FC1: now asserts HasActiveSession() after address change AND
verifies session_created in log (not just plan_cancelled).

FC4: escalation event detail must be >15 chars (contains proof
reason with LSN values, not just "needs_rebuild").

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 23:43:48 -07:00
pingqiuandClaude Opus 4.6 cd4b91033f fix: force failure conditions in P2 tests, add BlockVol.ForceFlush
P2 tests now force conditions instead of observing them:

FC3: Real WAL scan verified directly — StreamWALEntries transfers
real entries from disk (head=5, transferred=5). Engine planning also
verified (ZeroGap in V1 interim documented).

FC4: ForceFlush advances checkpoint/tail to 20. Replica at 0 is
below tail → NeedsRebuild with proof: "gap_beyond_retention: need
LSN 1 but tail=20". No early return.

FC5: ForceFlush advances checkpoint to 10. Assertive:
- replica at checkpoint=10 → ZeroGap (V1 interim)
- replica at 0 → NeedsRebuild (below tail, not CatchUp)

FC1/FC2: Labeled as integrated engine/storage (control simulated).

New: BlockVol.ForceFlush() — triggers synchronous flusher cycle for
test use. Advances checkpoint + WAL tail deterministically.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 23:07:55 -07:00
pingqiuandClaude Opus 4.6 26bf7bc582 feat: add integrated failure replay tests through real bridge path (Phase 07 P2)
5 failure-class replay tests against real file-backed BlockVol,
exercising the full integrated path:
  bridge adapter → v2bridge reader/pinner → engine planner/executor

FC1: Changed-address restart — identity preserved, old plan cancelled,
     new session created. Log shows plan_cancelled + session_created.

FC2: Stale epoch after failover — sessions invalidated at old epoch,
     new assignment at epoch 2 creates fresh session. Log shows
     per-replica invalidation.

FC3: Real catch-up (pre-checkpoint) — engine classifies from real
     RetainedHistory, zero-gap in V1 interim (committed=0 before flush).
     Documents the V1 limitation explicitly.

FC4: Unrecoverable gap — after flush, if checkpoint advances, replica
     behind tail gets NeedsRebuild. Documents that V1 unit test may
     not advance checkpoint (flusher timing).

FC5: Post-checkpoint boundary — replica at checkpoint = zero-gap in
     V1 interim. Explicitly documents the catch-up collapse boundary.

go.mod: added replace directives for sw-block engine + bridge modules.

Carry-forward (explicit):
- CommittedLSN = CheckpointLSN (V1 interim)
- FC3/FC4/FC5 limited by flusher not advancing checkpoint in unit tests
- Executor snapshot/full-base/truncate still stubs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 22:54:44 -07:00
pingqiuandClaude Opus 4.6 4aab00b149 feat: add real v2bridge integration tests against file-backed BlockVol
7 tests in weed/storage/blockvol/v2bridge/bridge_test.go:

Reader (2 tests):
- StatusSnapshot reads real nextLSN, WALCheckpointLSN, flusher state
- HeadLSN advances with real writes

Pinner (2 tests):
- HoldWALRetention: hold tracked, MinWALRetentionFloor reports position,
  release clears hold
- HoldRejectsRecycled: validates against real WAL tail

Executor (2 tests):
- StreamWALEntries: real ScanFrom reads WAL entries from disk
- StreamPartialRange: partial range scan works

Stubs (1 test):
- TransferSnapshot/TransferFullBase/TruncateWAL return not-implemented

All tests use createTestVol (1MB file-backed BlockVol with 256KB WAL).
No mock/push adapters — direct real blockvol instances.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 22:22:28 -07:00
pingqiuandClaude Opus 4.6 d5b2a3a345 fix: WALTailLSN is now an LSN boundary, ScanWALEntries uses durable checkpoint
Finding 1: WALTailLSN semantic fix
- StatusSnapshot().WALTailLSN now reads super.WALCheckpointLSN (an LSN)
- Was: wal.Tail() which returns a physical byte offset
- Entries with LSN > WALTailLSN are guaranteed in the WAL

Finding 2: ScanWALEntries replay-source fix
- ScanWALEntries passes super.WALCheckpointLSN as the recycled boundary
- Was: flusher.CheckpointLSN() which in V1 equals CommittedLSN
- The flusher's live checkpoint may advance in memory, but entries above
  the durable superblock checkpoint are still physically in the WAL
- Normal catch-up (replica at 70, committed at 100) now works because
  fromLSN=71 > super.WALCheckpointLSN (which is the last persisted
  checkpoint, not the live flusher state)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 20:26:27 -07:00
pingqiuandClaude Opus 4.6 785a7d7efd feat: wire real pinner into flusher retention + real WAL scan executor (Phase 07 P1)
Pinner wired to real retention:
- NewPinner calls vol.SetV2RetentionFloor(p.MinWALRetentionFloor)
- Flusher.RetentionFloorFn() / SetRetentionFloorFn() exposed
- SetV2RetentionFloor chains with existing shipper retention floor
- Holds actually prevent WAL reclaim (not just tracked state)

Executor uses real WAL scan:
- BlockVol.ScanWALEntries(fromLSN, callback) wraps wal.ScanFrom
  with real fd, walOffset, checkpointLSN
- Executor.StreamWALEntries uses ScanWALEntries (not stub)
- Reads real WAL entries, tracks highest LSN scanned

CommittedLSN mapping:
- Explicitly documented as interim V1 model (committed = checkpointed)
- Will diverge when V2 distributed commit separates from local flush

Carry-forward:
- TransferSnapshot/TransferFullBase/TruncateWAL: stubs (need extent I/O)
- Control intent from confirmed failover: deferred

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 20:01:46 -07:00
pingqiuandClaude Opus 4.6 c00c9e3e3d feat: add real BlockVolPinner + BlockVolExecutor in v2bridge (Phase 07 P1)
Pinner (pinner.go):
- HoldWALRetention: validates startLSN >= current tail, tracks hold
- HoldSnapshot: validates checkpoint exists + trusted
- HoldFullBase: tracks hold by ID
- MinWALRetentionFloor: returns minimum held position across all
  WAL/snapshot holds — designed for flusher RetentionFloorFn hookup
- Release functions remove holds from tracking map

Executor (executor.go):
- StreamWALEntries: validates range against real WAL tail/head
  (actual ScanFrom integration deferred to network-layer wiring)
- TransferSnapshot/TransferFullBase/TruncateWAL: stubs for P1

Key integration points:
- Pinner reads real StatusSnapshot for validation
- Pinner.MinWALRetentionFloor can wire into flusher.RetentionFloorFn
- Executor validates WAL range availability from real state

Carry-forward:
- Real ScanFrom wiring needs WAL fd + offset (network layer)
- TransferSnapshot/TransferFullBase need extent I/O
- Control intent from confirmed failover (master-side)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 19:54:24 -07:00
pingqiuandClaude Opus 4.6 d5ecf471fe feat: real blockvol integration — StatusSnapshot + v2bridge reader + contract interfaces (Phase 07 P1)
Real blockvol integration:
- BlockVol.StatusSnapshot() reads actual fields:
  WALHeadLSN ← nextLSN-1, WALTailLSN ← wal.Tail(),
  CommittedLSN ← flusher.CheckpointLSN(),
  CheckpointLSN ← super.WALCheckpointLSN,
  CheckpointTrusted ← super.Validate()==nil

weed/storage/blockvol/v2bridge/:
- Reader wraps real BlockVol, implements ReadState() → BlockVolState
- Lives in weed/ module (can import blockvol directly)

sw-block/bridge/blockvol/ contract interfaces:
- BlockVolReader: ReadState() (weed-side implements)
- BlockVolPinner: HoldWALRetention/HoldSnapshot/HoldFullBase → release func
- BlockVolExecutor: StreamWALEntries/TransferSnapshot/TransferFullBase/TruncateWAL
- StorageAdapter refactored to consume interfaces (not push-based)
- PushStorageAdapter for tests

Handoff boundary (E5):
- sw-block/ defines contracts, weed/ implements them
- sw-block/ does NOT import weed/
- No cross-module circular dependency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 18:17:59 -07:00