/kiro-validate-gap gap-focused sweep. Per-spec gap analysis written to each spec's research.md (3 new, 2 appended), grounded in file:line evidence: - config-upgrade-snapshot (new): real open gap — transport + (term,lsn) marker + COW substrate exist, but the entity-bearing snapshot PAYLOAD does not (16-byte LSN sentinel). HARD-blocked on entity-bearing-snapshots. Closes bug-857. Effort L (XL incl. blocker) / Risk High. - raft-commit-safety (new): CLOSED — G4 Figure-8 commit (raft.c:733) + G6 vote freshness (raft.c:157) done, full sim coverage; the "(0,0) stopgap / broken build" concern is stale (build green, real vote tuple at cluster.c:435). - raft-sim-conformance (new): mostly open — raft.tla is reference-only; only 1 of 4 invariants (Log Matching) checked in code. Missing PRNG/replay, message-fault bag, scheduler, 3 invariant checkers, fault switches, scenarios. Effort L / Risk Medium. - ha-repl-hardening (append): RESCOPE/superseded — 31/33 EARS closed; Req 2.3 + Req 6 obsolete (CTRL_PRIMARY_COMMITTING removed). Residual: docs only. - ha-convergence-recovery (append): done bar Task 8 (live E2E); bug-1028 post-failover availability resolved 2026-06-23. Skipped ha-elect-ctx-lifecycle (no requirements.md). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MSxwb2Xt5F84ATgiAZnH8V
46 KiB
Gap Analysis: HA Replication Hardening
Analysis date: 2026-06-04. Phase:
requirements-generated. Purpose: identify what exists, what's missing, and viable implementation approaches.
Analysis Summary
- Scope: 7 requirements — heartbeat-driven state convergence, WAL catch-up wiring, per-standby tracking, snapshot transfer, batch WAL transfer, COMMITTING gate, observability
- Key finding: The primary already sends heartbeats (
OPCode_nop) and receives replies; theCtrlHelloReplyalready carriescommitted_lsnandconfig_tag;persist_checkpointalready exists. The gaps are in wiring, not new infrastructure. - Largest gaps: (1) heartbeat reply doesn't carry standby's
applied_lsn/config_tag; (2) WAL catch-up handler is a stub; (3) snapshot transfer path is a stub; (4) no per-standby convergence tracking in the primary. - Recommendation: Extend existing heartbeat reply with state fields; wire the WAL catch-up stub to
apply_wal_entry(); implement snapshot transfer using existingpersist_checkpoint(); add per-standby convergence tracking toha_state_tor extendstandby_conn_t.
1. Current State Investigation
1.1 Heartbeat Protocol
Current state: Asymmetric, unidirectional state push.
- Primary sends
OPCode_nopviaprimary_heartbeat_fn(ha.c:2880) everyheartbeat_interval_ms(3s default) - Standby receives nop in
myreq_handler(cluster.c:225), sends emptyRequestReply, callsha_on_heartbeat_received(ha.c:2655) to reset liveness watchdog CtrlHelloRequest/CtrlHelloReply(controller.capnp:835-846) carryappliedLsn,configTag,committedLsn— but only exchanged once on connect, not periodically
Gap: Heartbeat reply is empty. Standby's current (applied_lsn, config_tag) is not reported back to the primary on each heartbeat cycle.
1.2 Per-Standby State Tracking
Current state: standby_conn_t (repl.h:22-33) tracks per-standby acked_lsn and acked_config_lsn (for WAL and config quorum), plus in_catch_up and pending_ack_count. ha_state_t has standby_endpoints (zarray of endpoints) and n_peers, but no per-standby convergence tracking.
Gap: No field for the standby's self-reported applied_lsn or config_tag from heartbeat replies. The primary has no way to know whether a standby has converged without receiving explicit WAL/config ACKs.
1.3 WAL Catch-Up Handler
Current state: Stub at ha.c dispatch gate (OPCode_walCatchupEntry returns 0). The primary-side streaming loop in config_repl.c:1489-1520 reads WAL segments and sends OPCode_walCatchupEntry messages, but the standby does nothing with them.
Gap: Standby never applies WAL catch-up entries to local state. apply_wal_entry() (persist.c:685) exists and is used during startup WAL replay — can be reused.
1.4 Snapshot Transfer
Current state: handle_ctrlhello (config_repl.c:1359) checks repl_needs_snapshot() (repl.c:491) and if true, logs WARN at line 1418 ("snapshot transfer not yet implemented") and skips. persist_checkpoint() (persist.c:610) exists and creates a consistent checkpoint.
Gap: The snapshot creation and transfer path is entirely unimplemented. Without it, any standby whose LSN predates the WAL retention window is permanently dead.
1.5 WAL Entry Batching
Current state: The WAL streaming loop in config_repl.c:1481-1521 sends one xce_request per WAL entry. xce_request enqueues to the endpoint's send buffer (non-blocking), but the loop processes entries sequentially with pread() per entry + malloc/free per payload.
Gap: No batching. For a standby catching up on millions of entries, this is CPU-intensive and slow. The wire protocol needs a batch message format.
1.6 COMMITTING Gate
Current state: ha_repl_ack_received() (ha.c:2197) exists with correct quorum math, but is only called from repl_ack_decode_and_dispatch() (ha.c:1859) which processes OPCode_replCatchUpDone. No standby sends this opcode. The committing timeout (10s) is the only path to PRIMARY.
Gap: The majority-convergence check is unreachable. The committing timeout degrades the cluster on every startup.
2. Requirement-to-Asset Map
| Req | Key Gap | Key Reusable Asset | Gap Tag |
|---|---|---|---|
| 1 — Heartbeat carries standby state | Reply body is empty; no state fields in nop reply | OPCode_nop send/receive loop, CtrlHelloReply fields in capnp schema, xce_reply pattern in cluster.c:225-228 |
Missing |
| 2 — Per-standby convergence tracking | No applied_lsn/config_tag reported per standby |
standby_conn_t with acked_lsn, ha->standby_endpoints, repl_wait_majority_ack quorum math |
Missing |
| 3 — WAL catch-up handler | Stub returns 0 (ha.c:882) | apply_wal_entry() (persist.c:685), primary WAL streaming loop (config_repl.c:1489) |
Stub |
| 4 — Snapshot transfer | Stub logs WARN, skips (config_repl.c:1417-1425) | persist_checkpoint() (persist.c:610), repl_needs_snapshot() (repl.c:491), CtrlHelloReply.needsSnapshot field |
Missing |
| 5 — Batch WAL transfer | One entry per xce_request |
Existing WAL streaming loop, capnp message format for walCatchupEntry | Missing |
| 6 — COMMITTING gate | ha_repl_ack_received unreachable; timeout-only path |
ha_repl_ack_received() (ha.c:2197) with correct quorum math, ha_promote_to_primary() |
Missing |
| 7 — Observability | Logs exist but need new convergence/snapshot events | INFO/WARN macros in xcelog.h |
Missing (alongside) |
3. Implementation Approach Options
Option A: Extend heartbeat reply with state fields (Recommended)
Extend the existing OPCode_nop reply or create a lightweight heartbeat-ack to carry (applied_lsn, config_tag):
- Standby side: In the nop handler (cluster.c:225), populate reply with current
applied_lsnandconfig_tagbeforexce_reply - Primary side: In the heartbeat send path or a reply handler, read the standby's reported state and update per-standby tracking
- Why: Minimal new code — reuses existing heartbeat loop; no new opcodes needed if we extend the reply body; follows Raft AppendEntries pattern
Trade-offs:
- ✅ Reuses existing infrastructure; minimal new opcodes
- ✅ Heartbeat already fires every 3s — convergence detected promptly
- ❌ Need capnp schema change if RequestReply needs new fields
- ❌ Primary must process heartbeat replies (currently fire-and-forget)
Option B: Use CtrlHelloReply periodically
Send CtrlHelloRequest periodically (not just on connect) and use the existing CtrlHelloReply fields:
- Already carries
committedLsn,configTagin reply - Already carries
appliedLsn,configTagin request - Why: No capnp schema changes; ctrlhello already carries all needed fields
Trade-offs:
- ✅ No schema changes
- ❌ CtrlHelloRequest triggers WAL streaming (heavy) — not suitable for periodic use
- ❌ Semantic mismatch: ctrlhello is a connection-setup message, not a state-reporting message
Option C: New lightweight opcode
Add OPCode_stateReport / OPCode_stateAck to capnp schema:
- Dedicated message type with
(applied_lsn, config_tag, in_catch_up)fields - Sent by standby in response to heartbeat, or periodically
- Why: Clean separation of concerns
Trade-offs:
- ✅ Clean protocol design
- ❌ New opcode, new capnp struct, new handler on both sides
4. Effort and Risk Summary
| Req | Effort | Risk | Justification |
|---|---|---|---|
| 1 — Heartbeat state report | S (1-2d) | Low | Extend existing reply; capnp schema change if needed |
| 2 — Per-standby tracking | S (1d) | Low | New fields in ha_state_t or extend standby_conn_t |
| 3 — WAL catch-up wiring | S (1d) | Low | Wire stub to existing apply_wal_entry() |
| 4 — Snapshot transfer | M (3-5d) | Medium | persist_checkpoint exists; transfer protocol is new |
| 5 — Batch WAL transfer | S (1-2d) | Low | Accumulate entries in loop; capnp schema change |
| 6 — COMMITTING gate | S (1d) | Low | Replace ha_repl_ack_received with heartbeat-driven check |
| 7 — Observability | S (alongside) | Low | INFO/WARN log statements |
Total estimated effort: 1-2 weeks. Risk: Low-Medium (snapshot transfer is the main risk).
5. Key Reusable Assets
| Asset | Location | Reuse in |
|---|---|---|
OPCode_nop send/reply loop |
cluster.c:225, ha.c:2880 | Req 1 (heartbeat state report) |
CtrlHelloReply fields |
controller.capnp:835-846 | Req 1 (capnp schema template) |
standby_conn_t + repl_wait_majority_ack |
repl.h:22, repl.c:338 | Req 2 (convergence tracking) |
apply_wal_entry() |
persist.c:685 | Req 3 (WAL catch-up) |
persist_checkpoint() |
persist.c:610 | Req 4 (snapshot creation) |
| WAL streaming loop | config_repl.c:1481 | Req 5 (batch send) |
ha_repl_ack_received() |
ha.c:2197 | Req 6 (quorum math reuse) |
INFO/WARN macros |
xcelog.h | Req 7 (observability) |
6. Research Needed (Design Phase)
- Capnp schema design: Whether to extend
RequestReplywith state fields, add a newstateReportstruct to existing heartbeat messages, or create a new opcode. Evaluate against existing capnp codegen patterns. - Heartbeat reply processing: Currently fire-and-forget; need to add a reply handler on the primary side. Determine best insertion point in the dispatch path.
- Snapshot wire format: How to serialize
persist_checkpointoutput for transfer. COW zarray vs raw file transfer.
Cross-Spec Gap Audit (2026-06-08)
Batch
/kiro-validate-gapacross 9 specs on persistent_ha branch.
Analysis Summary
- ha-repl-hardening is ~98% complete. All 6 task groups (1–6), 27 subtasks (1.1–6.7) are
[x]in tasks.md. Recent commits (aab41fathroughe1517bb) confirm active implementation. The git log shows all tasks through 6.7 implemented with bug fixes applied. - Remaining gaps: Task groups 1–4 and 5–6 all show
[ ]as section headers, but all subtasks within them are[x]. The section checkboxes are cosmetic. - Deferred items: None. The original research's "Research Needed" items (capnp schema design, heartbeat reply processing, snapshot wire format) were resolved during the design phase.
- Cross-spec relationships: This spec extends
ha-config-bootstrapReq 5 (majority-commit WAL) and Req 8 (majority-commit config) with heartbeat-driven convergence. It depends onha-heartbeat-livenessfor the idle-triggered NOP policy on the primary and activity-based liveness detection on the standby. It depends onha-peer-connection-reusefor the promoted connection model —handle_ctrlhellowas reduced to connection setup only (task 5.1), with all replication decisions moved to the heartbeat decision loop. - Single-node mode: Verified working —
n_peers=1, immediate convergence,CTRL_PRIMARYwith no heartbeat wait. - Recommendation: Run
/xce-build controllerto confirm ASAN-clean build, then mark the specimplemented.
Gap Analysis — Validation Pass (2026-06-09)
Validation-phase gap analysis: requirements.md vs code as on disk (including uncommitted changes to ha.c/ha.h/myapp.c/raft.c). All 27 subtasks in tasks.md are
[x]; this pass verifies what the code actually does against each acceptance criterion.
Headline Finding
The control plane is complete; the data plane is not. Every protocol mechanism in this spec (heartbeat stateReport, per-standby tracking, decision loop, batch framing, snapshot chunking, convergence-gated promotion, safe commit point) is implemented and unit/integration tested. But payload bytes are never transmitted or applied:
ha_send_snapshot_to_standbysends every chunk withchunk.payload = NULL—config_repl.c:1638(/* encoding deferred */, explained at config_repl.c:1613-1618: the capnpData↔xce_data_ttagged-union encode path is not wired).- The standby snapshot handler passes
payload_bytes=NULL, payload_len=0—cluster.c:191-197. Consequentlysnap.partis never written, theis_lastsize check (ha.c:1094-1106) fails (orstatfails on a missing file), andpersist_load_snapshotis never reached in production. Snapshot transfer works only in unit tests viaxce_snapshot_chunk_hook. - WAL catch-up applies entries header-only:
ha_apply_single_entry(ha.c:974-984) buildspayload_len=0and callsapply_wal_entry(p, &hdr, NULL).apply_wal_entry(persist.c:691) then only advances the LSN chain — payload-bearing entry types (WAL_JOB_SUBMIT→ capnp job, jobstate entries →wal_jobstate_t) restore nothing.
Safety implication: a standby's applied_lsn advances without the corresponding state, so it
reports "converged" via stateReport, the primary promotes out of COMMITTING, and a later failover
to that standby would serve from a state missing all payload-bearing entries. The Majority
Consistency Invariant (design §Overview) is currently tracked but not actually established.
Cross-spec constraint: the live replication path has the same hole — repl_broadcast
(repl.c:193-234) discards payload and sends nothing ("TCP send wiring deferred to task 7.6",
owned by ha-config-bootstrap). Today no WAL payload bytes move between controllers on any path.
Requirement-to-Asset Map
| Req / AC | Asset (file:line) | Status |
|---|---|---|
| 1.1 heartbeat reply carries (tag, lsn) | cluster.c:273-287 OPCode_nop handler; ha_nop_reply_state_report ha.c:1140; StateReport controller.capnp:788 |
Implemented |
| 1.2 primary records + evaluates | repl_ack_decode_and_dispatch ha.c:2456-2547 → ha_process_standby_state ha.c:1198 |
Implemented |
| 1.3 convergence criteria (tag≥ AND lsn≥) | ha.c:1211-1212 | Implemented |
| 1.4 tag=0 before config push | reads myconf->config_version_tag (0 pre-push) |
Implemented |
| 2.1 init tracking on COMMITTING entry | Arrays zeroed only in ha_init (ha.c:816-819 comment claims the invariant); raft_cb_become_leader (ha.c:445-529) resets repl_ack_count but not standby_converged[]/standby_reported_*[] → stale convergence flags survive a demote→re-elect cycle |
Partial |
| 2.2 update + re-evaluate on reply | ha.c:1198-1223, 2525-2533 | Implemented |
| 2.3 majority threshold ceil(n/2)−1 | ha_check_majority_convergence ha.c:1244-1278 requires all standbys (n_converged >= n_peers−1), not majority. Three conflicting formulas: requirements.md 2.3 = ceil(n/2)−1 (1 standby for n=3); tasks.md 5.2 = ceil(n/2) (2 for n=3); code = n−1 (2 for n=3, 4 for n=5) |
Constraint / deviation (stricter than spec — safe but availability hit) |
| 2.4 INFO on promotion | ha.c:1266-1267 | Implemented |
| 2.5 regression → not-converged | ha.c:1210-1213 | Implemented |
| 3.1 decode + apply WAL entry | Handler wired (cluster.c:161-173 → ha_apply_wal_catchup_entry ha.c:986) but applies header-only, payload dropped (ha.c:974-984) |
Partial (CRITICAL) |
| 3.2 advance applied_lsn | ha_advance_applied_lsn ha.c:960 (atomic, monotonic) |
Implemented |
| 3.3 ERROR + skip on failure | ha.c:1003-1016, 1032-1037 | Implemented |
| 3.4 stays CTRL_STANDBY, no job ops | ha_dispatch_gate CTRL_STANDBY case ha.c:1456-1483 |
Implemented |
| 3.5 LSN order | sequential batch iteration | Implemented |
| 4.1 create checkpoint + transfer | ha_send_snapshot_to_standby config_repl.c:1578 chunks an existing snap-{checkpoint_lsn}.capnp — never calls persist_checkpoint() to create a fresh one; payload=NULL |
Partial |
| 4.2 load snapshot, set applied_lsn | ha_handle_snapshot_chunk ha.c:1058-1131 complete (verify/rename/load/applied_lsn) but unreachable in production (NULL payload, cluster.c:196) |
Partial |
| 4.3 no job ops until loaded | dispatch gate (standby role unchanged) | Implemented |
| 4.4 retry via reconnect | Restart-on-reconnect documented (ha.c:1054, cluster.c:178-180) but no production code unlinks snap.part on reconnect — stale partial file breaks the next transfer's size check |
Partial |
| 4.5 single-node: no snapshot | no standbys → decision loop never fires | Implemented |
| 5.1 multiple entries per message | entries :List(WalCatchupEntry) controller.capnp:850; send_wal_batch config_repl.c:1409 |
Implemented |
| 5.2 configurable max batch size | WAL_STREAM_BATCH_ENTRIES=256 / WAL_STREAM_BATCH_BYTES=64KB are compile-time #defines, not runtime config |
Implemented (minor constraint) |
| 5.3 apply in order, advance after batch | ha.c:992-1027 (last_good_lsn post-batch advance) — but payload gap applies |
Partial |
| 5.4 partial batch sent immediately | config_repl.c:1556-1563 | Implemented |
| 5.5 ordering + durability | Ordering yes; "durably written" weak — entities restored in-memory, no fsync on standby (applied_lsn in-memory-only is a documented design choice), and payload gap applies | Partial |
| 6.1 heartbeats continue in COMMITTING | self-re-arming heartbeat task (fix e1517bb) |
Implemented |
| 6.2 NOT_READY for non-repl opcodes | ha_dispatch_gate CTRL_PRIMARY_COMMITTING allowlist ha.c:1436-1454 |
Implemented |
| 6.3 immediate promotion on majority | ha.c:2529-2533 (before repl dispatch) — subject to 2.3 threshold deviation | Implemented (constraint) |
| 6.4 timeout → WARN + degraded mode | committing_timeout_fn ha.c:2885-2914; default 60000 (HA_DEFAULT_COMMITTING_TIMEOUT_MS ha.h:131) |
Implemented |
| 6.5 single-node immediate | n_peers=1 → threshold 0 (ha.c:1261-1264; fixed in e10cf9a) |
Implemented |
| 7.1 catch-up start INFO | config_repl.c:1445-1447 | Implemented |
| 7.2 converged INFO | ha.c:1216-1222 | Implemented |
| 7.3 snapshot start/complete INFO | config_repl.c:1595, 1663 | Implemented |
| 7.4 majority INFO | ha.c:1266 | Implemented |
| 7.5 timeout WARN w/ counts | ha.c:2905-2908 | Implemented |
| Flow 0 safe commit point (tasks 4.1/4.2) | ha_compute_safe_committed_lsn ha.c:373-432; truncation + committed baseline in raft_cb_become_leader ha.c:456-485 |
Implemented |
Score: 26 of 33 acceptance criteria fully implemented. Requirements 1, 6, 7 (and Flow 0 safety) Implemented; Requirement 2 Partial (init-reset + threshold semantics); Requirements 3, 4, 5 Partial — all blocked on the same payload data-plane gap.
Uncommitted working-tree changes (ha.c/ha.h/myapp.c/raft.c/deploy-ha.sh) are election-protocol
hardening adjacent to this spec: ha_init_raft one-time init (prevents raft state reset on
re-election), term-scoped candidate-agnostic vote denial in raft_on_vote_request with idempotent
re-grant, startup self-vote/stale-vote handling, and watchdog arming on the skip-election path.
These strengthen Election Safety per raft-tla-audit.md and should be committed, but they do not
close any of this spec's acceptance criteria.
Implementation Approach Options (remaining gaps)
Gap 1 — Payload encoding for WalCatchupEntry.payload and SnapshotTransfer.payload (the blocker)
Option A — Extend the typemap codegen to map capnp Data ↔ raw buffer for these structs.
The schema already declares payload :Data on both structs; only the C-side encode/decode of the
xce_data_t tagged union is missing.
- ✅ Fixes both messages at once; follows the established schema/typemap convention; benefits any future Data-carrying message
- ❌ Touches generated-code path — regression risk across all opcodes; needs codegen expertise
Option B — Hand-rolled encode/decode for just these two opcodes.
Bypass the tagged union: serialize lsn/type/len/bytes with direct capn calls (or a length-prefixed
raw layout) in send_wal_batch / ha_send_snapshot_to_standby and their decode counterparts.
- ✅ Localized, no codegen risk, fastest path to a working data plane
- ❌ Two bespoke wire paths diverging from every other message; long-term maintenance debt
Option C — Phased hybrid (recommended).
Phase 1: hand-wire the snapshot payload (raw file bytes — trivially a byte stream, no per-entry
structure) to make Req 4 production-real. Phase 2: solve Data encoding properly in the typemap
for WalCatchupEntry (structured, batched, used on the hot path) and migrate the snapshot path to
it. Until both land, keep the spec phase below implemented or gate promotion on a data-plane
flag.
- ✅ De-risks incrementally; snapshot (rare, big) and WAL (frequent, small) have different shapes
- ❌ Two-step migration; temporary inconsistency between the two paths
Gap 2 — Convergence threshold semantics (Req 2.3 vs code)
- Option A: change code to the requirement's majority formula (
ceil(n_peers/2) − 1standbys). Restores availability (one dead standby in a 3-node cluster no longer forces the 60s degraded timeout) and matches Raft quorum; safe because safe_committed_lsn already derives from a majority. - Option B: keep all-standbys convergence and amend requirements.md + tasks.md with the rationale (strongest consistency at startup; degraded timeout covers minority loss).
- Either way, reconcile the three formulas (requirements 2.3, task 5.2, code) — they currently disagree for any n ≥ 5.
Gap 3 — Small correctness fixes (extend existing components)
- Zero
standby_converged[]/standby_reported_*[]inraft_cb_become_leaderon COMMITTING entry (Req 2.1 — re-election staleness). unlink(snap.part)on standby reconnect/ctrlhello (Req 4.4 — stale partial file).- Create a fresh checkpoint (via delaymgr —
persist_checkpointis blocking I/O, must not run in the vtask/heartbeat-reply context) when SEND_SNAPSHOT fires and the on-disk snapshot is older thanoldest_wal_lsncoverage requires (Req 4.1). - Consider caching
ha_compute_oldest_wal_lsn— currently an opendir/readdir scan per heartbeat reply per standby in vtask context (ha.c:2538-2543). - Optionally promote batch limits to
ha_config_t(Req 5.2 "configurable").
Effort and Risk
| Gap | Effort | Risk | Justification |
|---|---|---|---|
| 1 — Payload data plane (Option C) | M–L (5–10d) | Medium | Snapshot raw-bytes path is S; typemap Data codegen + batched WAL payload apply + ASAN-clean lifetime mgmt (current (xce_data_t *)pbuf cast at config_repl.c:1529 is already suspect) + E2E re-verification is the bulk |
| 2 — Threshold reconciliation | S (0.5–1d) | Low | One comparison + spec text alignment; existing tests pin the 3-node case |
| 3.1 — Tracking reset on COMMITTING | S (<0.5d) | Low | memset 3 arrays at ha.c:510; add re-election unit test |
| 3.2 — snap.part discard | S (<0.5d) | Low | unlink in handle_ctrlhello standby path |
| 3.3 — Fresh checkpoint on SEND_SNAPSHOT | M (2–3d) | Medium | Blocking checkpoint must hop to delaymgr; interaction with concurrent WAL writes |
| 3.4 — oldest_wal_lsn caching | S (0.5d) | Low | Cache + invalidate on segment GC |
| 3.5 — Configurable batch limits | S (0.5d) | Low | Plumb two fields through ha_config_t |
Recommendations
- Do not mark this spec
implemented. All tasks are[x]but Requirements 3, 4, 5 are only control-plane complete; the system currently reports convergence it has not achieved. Reopen targeted tasks (suggest: 7.x series) for the payload data plane rather than re-flagging 2.x/3.5. - Land Gap 3.1 and 3.2 immediately (S, Low) — they are latent correctness bugs independent of the payload work.
- Decide Gap 2 (threshold) explicitly and record it in the Decision Log; today three documents disagree.
- Sequence the payload work after coordinating with ha-config-bootstrap task 7.6
(
repl_broadcastTCP wiring) — the live path and catch-up path should share one entry serialization, or the two specs will produce divergent wire formats for the same WAL entry. - Add an E2E assertion that verifies entity state (e.g., a submitted job visible on the
promoted standby), not just
applied_lsnequality — the current tests 6.5–6.7 pass despite the data-plane gap because they only observe LSN/log convergence.
Research Needed
- How the typemap codegen represents capnp
Data(xce_data_ttagged union table dispatch) and what it takes to encode an arbitrary byte buffer — prerequisite for Gap 1 Option A/C phase 2. - Whether convergence should additionally verify content (e.g., last-entry CRC chained via
wal_entry_hdr_t.crc/prev_crcin stateReport) so header-only application can never report false convergence again. - Ownership boundary with ha-config-bootstrap task 7.6: which spec owns live WAL entry
serialization, and can
send_wal_batchbe reused for it. - Req 5.5 "durably written" on the standby: is in-memory entity restore + in-memory applied_lsn acceptable (current documented design), or should batch apply fsync a standby-side WAL copy.
Gap Re-Audit (2026-06-18)
Post-implementation gap audit. spec.json
phase=implemented; all 41 sub-tasks[x](1.1–7.6 incl. the reopened payload data-plane series). This pass re-verifies every EARS criterion against the code as on disk — specifically whether the two CRITICAL data-plane gaps and the small correctness gaps from the 2026-06-09 pass are now closed, and whether the COMMITTING-gate requirements still describe the live code afterha-single-leader-rolelanded.
Headline Finding
Both 2026-06-09 CRITICAL blockers are CLOSED. The data plane is real.
- WAL catch-up now applies payload-bearing entries.
ha_apply_single_entry(ha.c:1558) does persist-before-apply:persist_wal_write_replicated(p, e->lsn, type, e->raw_payload, len)(fsync, no re-broadcast, idempotent) THENapply_wal_entry(p, &hdr, e->raw_payload).apply_wal_entry(persist.c:1305) restores entities by type —WAL_JOB_SUBMIT→restore_job, job-state→restore_job_state,WAL_WORKER_CTRL→worker_from_capnp,WAL_QUEUE_CTRL→restore_queue_state(persist.c:1342-1389). Header-only application is gone. - Snapshot payload bytes are transmitted.
ha_send_snapshot_to_standby(config_repl.c:1857) stats the real file, chunks 64KB, and ships each chunk viaxce_snapshot_chunk_req_raw(ep, &chunk, buf, chunk_len)(config_repl.c:1928) — rawDatabytes, not NULL. Standbyha_handle_snapshot_chunk(ha.c:1683→ viahandle_snapshot_transfercluster.c:196, dispatched at cluster.c:589) writes chunks tosnap.part, verifies total size onis_last, atomic-renames tosnap-{lsn}.bin, loads viapersist_load_snapshot, setsapplied_lsn.
The Majority Consistency Invariant is now actually established, not merely tracked. A standby
that reports converged holds the entity state behind its applied_lsn.
Architectural supersession (read before re-opening any 6.x task)
CTRL_PRIMARY_COMMITTING no longer exists — grep -rn CTRL_PRIMARY_COMMITTING src/controller
returns zero hits. ha-single-leader-role removed the transitional COMMITTING role:
raft_cb_become_leader now stores CTRL_PRIMARY directly (ha.c:1034) with the comment "there is
no transitional COMMITTING state." New-leader safety is provided by a different, stronger
mechanism than this spec's heartbeat-convergence gate:
- Own-term WAL_NOOP + writable_lsn gate (
ha_arm_own_term_gateha.c:2259) — every WAIT completion is withheld untilcommitted_lsn >= own-term noop LSN, i.e. until this term has committed an entry on a majority. - Per-operation write quorum (
ha_dispatch_gateha.c:2061 →ha_has_write_quorumha.c:2031) — state-changing opcodes return-XCE_ERR_NOT_READYunlessrepl_active_standby_count >= n_peers/2; replication and read-only/fact-bearing opcodes are always admitted (allowlist at ha.c:2078-2106). - Vote-derived safe commit point (
ha_compute_safe_committed_lsnha.c:480, called from raft_cb_become_leader ha.c:908) —safe_committed_lsn= majority-threshold element of granted voters' LSNs;persist_truncate_wal_above(safe_lsn)+committed_lsn=safe_lsn(ha.c:914).
Net effect: the intent of Req 6 (don't serve un-replicated state; gate on majority) is met
more strongly and continuously than the original timeout-bounded COMMITTING design — it is now
enforced per-operation for the life of the term, not just at startup. The original Req 2.3 / 6.1–6.4
wording (COMMITTING role, 60s committing_timeout_ms degraded fallback, immediate
COMMITTING→PRIMARY on majority) describes a design that was deliberately replaced. This is a
documentation drift, not a functional gap.
Requirement-to-Asset Map (re-verified)
| Req / AC | Asset (file:line) | Status |
|---|---|---|
| 1.1 heartbeat reply carries (tag,lsn) | OPCode_nop handler cluster.c:300-301; populates from myconf->config_version_tag + applied_lsn |
CLOSED |
| 1.2 primary records + evaluates | repl_ack_decode_and_dispatch → ha_process_standby_state ha.c:1851 |
CLOSED |
| 1.3 convergence criteria tag≥ AND lsn≥ | ha.c:1864-1865 | CLOSED |
| 1.4 tag=0 before config push | reads live config_version_tag (0 pre-push) |
CLOSED |
| 2.1 init tracking on term entry | FIXED — raft_cb_become_leader memsets standby_reported_tag/_lsn/_converged (ha.c:1043-1045) on CTRL_PRIMARY entry. Closes 2026-06-09 Gap 3.1. |
CLOSED |
| 2.2 update + re-evaluate on reply | ha_process_standby_state ha.c:1851-1876 |
CLOSED |
| 2.3 majority threshold | RECONCILED to n_peers/2 (C int div) per task 7.5 + Decision Log 2026-06-09. ha_has_write_quorum uses connected >= n_peers/2 (ha.c:2058); safe_committed_lsn uses n_granted/2 (ha.c:527). Three-doc disagreement resolved. |
CLOSED |
| 2.4 INFO on promotion/converged | ha.c:1869-1875 (per-standby converged INFO) | CLOSED |
| 2.5 regression → not-converged | ha.c:1863-1866 (recomputed every reply) | CLOSED |
| 3.1 decode + apply WAL entry with payload | FIXED — ha_apply_single_entry ha.c:1558-1589 passes raw_payload to apply_wal_entry; entities restored persist.c:1342-1389. Closes 2026-06-09 CRITICAL. |
CLOSED |
| 3.2 advance applied_lsn | ha_advance_applied_lsn ha.c:1541 (atomic monotonic); batch advances after full batch (ha.c:1656-1659) |
CLOSED |
| 3.3 ERROR + skip on failure | ha.c:1633-1647, 1666-1672 (NULL/failed sub-entries skipped, no crash) | CLOSED |
| 3.4 stays STANDBY, no job ops | ha_dispatch_gate STANDBY case |
CLOSED |
| 3.5 LSN order | sequential batch iteration + persist-before-apply | CLOSED |
| 4.1 fresh checkpoint + transfer | FIXED — ha_dispatch_repl_action_for_standby calls persist_checkpoint(&c->persist) before send (ha.c:1961), -EBUSY falls back to existing. Closes 2026-06-09 Gap 3.3. |
CLOSED |
| 4.2 load snapshot, set applied_lsn | ha_handle_snapshot_chunk ha.c:1683 (verify→rename→persist_load_snapshot→applied_lsn); reachable in production (real payload) |
CLOSED |
| 4.3 no job ops until loaded | dispatch gate (standby role) | CLOSED |
| 4.4 retry via reconnect / discard stale | FIXED (cleaner than proposed) — chunk_index==0 unlinks any stale snap.part (ha.c:1693, 1708-1713) instead of relying on a reconnect-time unlink. Verified by test_snapshot_recv_reconnect_discards_snappart (test_config_repl.c:2045). Closes 2026-06-09 Gap 3.2. |
CLOSED |
| 4.5 single-node: no snapshot | no standbys → decision loop never fires | CLOSED |
| 5.1 multiple entries/message | entries :List(WalCatchupEntry) + send_wal_batch (config_repl.c) |
CLOSED |
| 5.2 configurable max batch | WAL_STREAM_BATCH_ENTRIES=256 / _BYTES=64KB compile-time #defines |
CLOSED (minor: limits are compile-time, not runtime ha_config_t — Req says "configurable maximum"; defensible) |
| 5.3 apply in order, advance after batch | ha_apply_wal_catchup_entry ha.c:1615-1681 (last_good_lsn post-batch advance) — now with real payload |
CLOSED |
| 5.4 partial batch sent immediately | config_repl.c batch flush on range end | CLOSED |
| 5.5 ordering + durability | Ordering preserved; durability now STRONGER than original design — ha_apply_single_entry fsyncs via persist_wal_write_replicated BEFORE apply/ACK (ha.c:1570). Supersedes the "in-memory only" caveat for catch-up. |
CLOSED (exceeds spec) |
| 6.1 heartbeats continue / gate on majority | superseded: continuous per-op ha_has_write_quorum (ha.c:2031) replaces COMMITTING-only check |
CLOSED (via supersession) |
| 6.2 NOT_READY for non-repl opcodes | ha_dispatch_gate ha.c:2061-2110 (allowlist + -XCE_ERR_NOT_READY default) — now enforced for the whole term, not just COMMITTING |
CLOSED (stronger) |
| 6.3 immediate promotion on majority | superseded by own-term gate + safe_committed_lsn (no COMMITTING dwell to exit) | CLOSED (via supersession) |
| 6.4 timeout → WARN + degraded | OBSOLETE — no COMMITTING state ⇒ no committing_timeout_ms. Degraded operation handled by write-quorum gate (rejects writes, keeps serving reads) rather than a one-shot 60s promotion fallback. |
N/A (superseded) |
| 6.5 single-node immediate | ha_has_write_quorum returns true when n_peers<=1 (ha.c:2037); safe_committed_lsn single-node path ha.c:486 |
CLOSED |
| 7.1 catch-up start INFO | config_repl.c (send_wal_batch start log) | CLOSED |
| 7.2 converged INFO | ha.c:1869-1875 | CLOSED |
| 7.3 snapshot start/complete INFO | config_repl.c:1874-1876, 1939-1941 | CLOSED |
| 7.4 majority INFO | per-standby converged INFO (ha.c:1870); promotion is direct-to-PRIMARY INFO (ha.c:1036) | CLOSED |
| 7.5 timeout WARN | OBSOLETE with COMMITTING removal; quorum-loss edge logged once via ha_log_quorum_transition (ha.c:2076) instead |
N/A (superseded by quorum-transition logging) |
| Flow 0 safe commit point | ha_compute_safe_committed_lsn ha.c:480-540; truncate+baseline raft_cb_become_leader ha.c:898-923 |
CLOSED |
Score: 31/33 original acceptance criteria CLOSED; 2 (6.4, 7.5) OBSOLETE by deliberate architectural supersession (CTRL_PRIMARY_COMMITTING removed by ha-single-leader-role). Counting the superseded pair as satisfied-by-replacement (their intent — gate on majority, observe degraded mode — is met more strongly), effective coverage is 33/33. No CRITICAL or HIGH functional gap remains.
Residual / Deferred Items (all LOW, none blocking)
- Documentation drift (HIGH-doc, LOW-code). requirements.md Req 6 + design.md still describe
CTRL_PRIMARY_COMMITTING,committing_timeout_ms, and the 60s degraded-mode timeout, none of which exist in code. The Introduction (requirements.md:5-7) and Adjacent-expectations also reference COMMITTING. A reader of the spec will mis-model the live FSM. Code is correct; the prose is stale. - Req 5.2 "configurable" batch size is compile-time (
#define), not runtime config. Minor; no operator has needed to tune it. Defensible as "configurable at build." ha_compute_oldest_wal_lsnis anopendir/readdirscan per heartbeat reply per standby in vtask context (config_repl.c:1946). Performance-only; fine at current cluster sizes. The 2026-06-09 caching suggestion (Gap 3.4) was never actioned and remains optional.- No covering tests flagged by codegraph for
ha_process_standby_state,ha_evaluate_repl_action,ha_dispatch_repl_action_for_standby,ha_send_snapshot_to_standbyat the unit level (decision-loop logic is exercised via hooks in test_ha_role.c / test_config_repl.c, and convergence via test_convergence_verification.c). Coverage is integration-weighted, not absent — but the pure decision functions lack direct unit pins. - E2E entity-state assertion (2026-06-09 Recommendation 5): tests 6.5–6.7 historically asserted LSN convergence only. With the data plane now real this is the right place to add a "submitted job visible on promoted standby" assertion to guard against future regressions to header-only application. Not done; LOW (data plane verified by unit round-trips per spec.json note).
Effort / Risk for Open Work
| Item | Option | Effort | Risk |
|---|---|---|---|
| 1 — Spec/design doc reconciliation (remove COMMITTING; document own-term gate + write-quorum + safe_committed_lsn as the actual Req 6 mechanism) | A (recommended): update requirements.md Req 6 + Introduction + design.md to describe the superseding mechanism and cross-link ha-single-leader-role / ha-commit-durability. B: leave a one-line "SUPERSEDED" banner pointing to those specs. |
A: S (0.5–1d) · B: XS (<0.5d) | Low (docs only) |
2 — Promote batch limits to ha_config_t |
A: plumb two fields. B: leave as build-time. | S (0.5d) | Low |
3 — Cache oldest_wal_lsn |
A: cache + invalidate on segment GC. B: defer (current scale fine). | S (0.5d) | Low |
| 4 — Direct unit tests for decision functions | A: add ha_evaluate_repl_action truth-table + ha_process_standby_state convergence-flag tests. B: rely on hook/integration coverage. |
S (1d) | Low |
| 5 — E2E entity-state assertion | A: extend test 6.7 to assert a submitted job is present post-failover. B: rely on unit payload round-trips. | S (0.5–1d) | Low |
Recommendation: the spec is functionally complete and may remain implemented. The only item
worth scheduling is Item 1 (doc reconciliation) — the requirements/design now misdescribe the
live FSM after ha-single-leader-role, which will mislead future readers and any
/kiro-verify-completion run that checks prose against code. Items 2–5 are optional hardening.
Gap Analysis (re-validation) — 2026-06-24
Re-validation pass. spec.json
phase=implemented(all 41 sub-tasks[x]). This pass re-verifies every EARS criterion against the code as on disk after the heavyha-term-safetyrework (commits 004a40b…b77d412, tasks 1.1–4.3) plus bug-828/991/1028/1060/1067 fixes landed since the 2026-06-18 audit. Question answered: does this spec still hold, and how much of it is now owned by sibling specs?
Headline Finding
Still functionally complete; now LARGELY SUPERSEDED in mechanism, but its data-plane code is the live load-bearing path. Two distinct things are true at once:
-
The control-plane design of this spec (Req 2.3 / Req 6:
CTRL_PRIMARY_COMMITTINGrole + 60 scommitting_timeout_msdegraded fallback + COMMITTING→PRIMARY-on-majority) is GONE.grep -rn CTRL_PRIMARY_COMMITTING src/controller/*.{c,h}→ 0 hits;committing_timeout→ 0 hits. Removed byha-single-leader-roleand replaced by a stronger, continuous mechanism (own-term WAL_NOOP +writable_lsngateha_arm_own_term_gateha.c:107/1206; per-op write quorumha_has_write_quorumha.c:2781 →-XCE_ERR_NOT_READY; vote-derivedha_compute_safe_committed_lsnha.c:510). This is documentation drift, not a functional gap — identical to the 2026-06-18 finding and still unactioned. -
The data-plane code of this spec (Req 1,3,4,5,7) is intact, hardened, and is the path every sibling spec builds on. It was NOT rewritten away —
ha-term-safetyroutes through it:- WAL catch-up payload apply:
ha_apply_single_entryha.c:2044 →persist_wal_write_replicated(fsync, idempotent) ha.c:2056 THENapply_wal_entryha.c:2081 (entity restore). Config markers also flow here (ha.c:2168). Header-only application remains gone. - Batched streaming:
send_wal_batchconfig_repl.c:2005,WAL_STREAM_BATCH_ENTRIES=256 /_BYTES=64 KB (config_repl.c:1981-1982); batch flush loop config_repl.c:2071-2173. - Snapshot raw payload:
ha_send_snapshot_to_standbyconfig_repl.c:2425 →xce_snapshot_chunk_req_rawconfig_repl.c:2496 (real bytes). - Per-standby convergence:
ha_process_standby_stateha.c:2592 (setsstandby_reported_tag/_lsn/_converged, ha.c:2600-2607); reset on term entry ha.c:1115-1117; fed fromOPCode_replystateReport at ha.c:4620. safe_committed_lsn(this spec's Flow 0, tasks 4.1/4.2) is explicitly cited by code as ha-repl-hardening: ha.c:940 "Task 4.1 (ha-repl-hardening)".
bug-1028 (FIXED 2026-06-23) was inside this very streaming path (
ha_stream_wal_to_standbygained aconst persist_t *pparam) — confirming the path is live and maintained, not dead. - WAL catch-up payload apply:
Which sibling spec now owns what
| Concern | Originating in ha-repl-hardening | Now owned / superseded by |
|---|---|---|
| Heartbeat stateReport (Req 1) | yes | retained as-is; consumed by ha-heartbeat-committed-propagation (adds committed+term stamp) |
| Per-standby convergence tracking (Req 2.1/2.2/2.5) | yes | retained as-is |
Majority threshold n_peers/2 (Req 2.3) |
yes | reconciled + absorbed into ha_has_write_quorum / safe_committed_lsn (raft-commit-safety, ha-term-safety) |
| WAL catch-up payload apply (Req 3) | yes | retained; now also drives ha-term-safety config-marker apply + bug-991 lsn-advance fix |
| Snapshot transfer (Req 4) | yes | retained as-is |
| Batch WAL transfer (Req 5) | yes | retained; bug-1028 hardened the streamer signature |
| COMMITTING gate (Req 6) | yes | SUPERSEDED & DELETED by ha-single-leader-role (own-term gate) + ha-commit-durability (durable-commit fence) |
| Flow 0 safe commit point (tasks 4.1/4.2) | yes | retained; extended by raft-commit-safety (Figure-8) + ha-term-safety (per-entry term, (term,lsn) reconcile) — the LSN-only safe point is now term-aware upstream |
Requirement-to-Asset Map (delta vs 2026-06-18 — only changes noted)
All 31 CLOSED criteria from 2026-06-18 remain CLOSED (assets re-verified, line numbers shifted +~860 in ha.c / +~560 in config_repl.c due to ha-term-safety growth; spot-checked above). No regression.
| Req / AC | Change since 2026-06-18 | Status |
|---|---|---|
| 3.x payload apply | Path unchanged in shape; bug-991 fix made the three live-apply setters advance applied_lsn to the LAST-held LSN (correctness hardening). | CLOSED (hardened) |
| 5.x batch streaming | bug-1028 added const persist_t *p to ha_stream_wal_to_standby (off-by-one / hdr-source fix). |
CLOSED (hardened) |
| Flow 0 safe commit | Now also gated by ha-term-safety per-entry term + bug-1060 promotion-truncate floor max(safe_lsn,durable_tip) (prevents new leader truncating its own committed log) + bug-1067 AE header-term separation. |
CLOSED (strengthened upstream) |
| 6.4 / 7.5 COMMITTING timeout/WARN | Still OBSOLETE (no COMMITTING). | N/A (superseded) |
Score: 31/33 CLOSED, 2 OBSOLETE-by-supersession (6.4, 7.5). Effective 33/33. No CRITICAL/HIGH functional gap. Unchanged from 2026-06-18, now re-confirmed post ha-term-safety.
Disposition: RESCOPE (close as data-plane-complete + supersede the control-plane prose)
This spec should not be reopened for new feature work and should not be deleted. Recommended disposition:
- Keep
phase=implemented. Every acceptance criterion is satisfied or satisfied-by-replacement. - CLOSE-as-superseded the control plane (Req 2.3 + Req 6 entirely). Their mechanism was deliberately replaced. Carry a one-line "SUPERSEDED by ha-single-leader-role / ha-commit-durability" banner on Req 6 and on the Introduction's COMMITTING references.
- RETAIN the data plane (Req 1,3,4,5,7 + Flow 0) as the canonical owner. No sibling spec re-implements WAL-catch-up payload apply, batching, snapshot transfer, or stateReport — they all consume this code. If anything, this spec is now the foundation, not the obsolete layer.
The 2026-06-09 project-memory note ("data plane MISSING, critical, bugs 335-338") is stale — it was closed by the reopened 7.x payload series (audited 2026-06-18) and is unaffected by later work.
Residual / Deferred (all LOW, none blocking — unchanged + re-confirmed)
- Documentation drift (HIGH-doc, LOW-code) — STILL OPEN. requirements.md Req 6 + Introduction +
design.md describe
CTRL_PRIMARY_COMMITTING/committing_timeout_ms/ 60 s degraded fallback, none of which exist. This is the only item worth scheduling. A/kiro-verify-completionrun that checks prose-vs-code will flag Req 6 falsely. - Req 5.2 "configurable" batch size is compile-time
#define, notha_config_t. Defensible. ha_compute_oldest_wal_lsnopendir/readdir per heartbeat reply per standby. Perf-only.- Direct unit tests for
ha_process_standby_state/ha_evaluate_repl_action/ha_send_snapshot_to_standby— coverage is integration-weighted (test_config_repl.c, test_convergence_verification.c), not absent. - E2E entity-state assertion (submitted job visible on promoted standby post-failover) — the
live HA soak (
/xce-test-ha) now exercises this end-to-end, partially discharging the gap; a pinned unit/E2E assertion is still nice-to-have.
Effort / Risk for the one schedulable item
| Item | Option | Effort | Risk |
|---|---|---|---|
| Doc reconciliation: remove COMMITTING from requirements.md Req 6 + Introduction + design.md; document own-term gate + write-quorum + safe_committed_lsn as the real Req 6 mechanism; cross-link ha-single-leader-role / ha-commit-durability / ha-term-safety | A (recommended): rewrite Req 6 + Introduction (S, 0.5–1d). B: one-line SUPERSEDED banner (XS, <0.5d). | A: S · B: XS | Low (docs only) |
Recommendation
- No open implementation work. Spec is functionally complete and is the live owner of the HA replication data plane. Mark project-memory note (2026-06-09 "data plane MISSING") as resolved.
- Schedule only the doc reconciliation (Residual #1). It has been flagged in two prior audits (2026-06-09 doc-drift, 2026-06-18 Item 1) and remains unactioned. Either rewrite Req 6 (Option A) or banner it (Option B). Everything else is optional hardening.
- Treat this spec as RESCOPED: control plane CLOSED-as-superseded; data plane RETAINED-as-owner.
Research Needed
- None new. Prior research items (typemap
Dataencoding, snapshot wire format, ownership boundary with ha-config-bootstrap task 7.6 / liverepl_broadcast) were all resolved by the payload series and ha-term-safety task 3.4 (repl_broadcastnow carries leader term, term-0 live send retired).