CLAUDE.md's TLA+ Action-to-Code Mapping table gains corrected/new rows: production vs. dormant AppendEntries send mechanisms, WAL catch-up and config-push reply integrity, and vote/PreVote failure routing across all failure timings -- each citation independently re-verified against live source. The old AppendEntries row's citations had already drifted onto unrelated code before this task; corrected, not just appended to. Protocol Verification Checklist gains 3 new items for the guarantees this feature closes. ha-term-safety/spec.json's known_limitations entry for the dormant send-path drift gets an additive RESOLVED pointer back to this spec's disposition, so a future reader lands on the answer instead of re-discovering the drift. Requirement 1 (systematic conformance audit deliverable) complete.
14 KiB
Implementation Plan
-
1. Foundation: extend the configuration-push reply schema with a term field
- Add
term @2: UInt64toReplConfigPushReplyinsrc/schema/controller.capnp, alongside the existingaccepted/versionTagfields - Rebuild the controller so the Cap'n Proto codegen
custom_targetregeneratescontroller.capnp.c/.hwith the new field - Observable:
repl_config_push_reply_texposes atermmember usable fromconfig_repl.candcluster.c; existing config-push tests still build and pass unmodified - Requirements: 7.4
- Add
-
2. Core: configuration-push reply integrity
-
2.1 (P) Send an explicit accept/reject reply carrying term on every configuration push outcome
- In
handle_repl_config_push, construct and sendReplConfigPushReply{accepted=0, term=current_term}on each of the four reject early-returns (malformed request, missing bytes/sha256, sha256 mismatch,conf_save_configfailure) instead of returning without a reply - Populate
termon the existing success reply path too - Preserve the existing
-EAGAIN-from-config_activate_committedbehavior unchanged: that path still sendsaccepted=1(activation-deferred is not a rejection) - Add an
XCE_TESTING-only send-hook (mirroringha_wal_ack_send_hook) so a unit test can observe the constructed reply payload without a live endpoint - Observable: every code path through
handle_repl_config_pushnow sends exactly oneReplConfigPushReply, with no path returning silently - Requirements: 2.2, 2.3, 2.5, 7.4
- Boundary: config_repl.c reply construction
- In
-
2.2 (P) Wire term-adoption into the primary-side configuration-push reply handler
- In the existing
ReplConfigPushReplybranch ofmyreq_handler(cluster.c, the code that already checkscack->acceptedand callsrepl_on_config_ackor warns-and-ignores), add a term comparison: whencack->termexceeds the primary's current term, callraft_on_higher_term_observed— the same adoption entry point the heartbeat-reply and WAL-NACK vectors already use - Do not alter the existing accept/reject branching logic itself, only add the term check alongside it
- Observable: a config-push reply carrying a higher term causes the primary to adopt that term and step down from leadership held at the lower term
- Requirements: 7.4, 7.5
- Boundary: cluster.c reply handling
- In the existing
-
2.3 Test configuration-push reply integrity end-to-end
- Unit test: each of the four reject conditions in
handle_repl_config_pushproducesaccepted=0with the current term, via the new send-hook - Unit test: the
-EAGAINactivation-defer path still producesaccepted=1 - Unit test: an inbound config-push reply carrying a higher term triggers adoption via the
cluster.cwiring from 2.2 - Observable: new tests in
test_config_repl.cfail against pre-fix code and pass against the code from 2.1/2.2 - Requirements: 2.2, 2.3, 2.4, 2.5, 7.4, 7.5
- Depends: 2.1, 2.2
- Unit test: each of the four reject conditions in
-
3. Core: vote and pre-vote endpoint failure routing
-
3.1 (P) Split the shared vote-endpoint error handler and route post-connect failures to the production tally
- Split
vote_ep_err_handlerinto a real-vote handler (registered at its existing connection-setup call site) and apre_vote_ep_err_handler(registered at its existing, separate PreVote connection-setup call site) - Extract the identical
CTRL_PRIMARY-role cleanup branch (standby-endpoint deregistration,repl_remove_standby, quorum-transition logging) into one shared helper called by both, unchanged in behavior - Change each handler's election-phase branch to route a post-connect failure through the same production tally path already used for connect-time failures (
vote_reply_dispatchfor the real-vote handler,pre_vote_reply_dispatchfor the pre-vote handler) instead of the legacyhandle_request_vote_reply - Observable: a failure arriving after a vote or pre-vote endpoint has already connected now reaches the same tally counter a connect-time failure or a delivered denial would
- Requirements: 4.2, 4.3, 4.4
- Boundary: ha.c vote endpoint handlers
- Split
-
3.2 Test post-connect vote and pre-vote failure routing
- Extend the existing test-construction helper (
vote_ep_ctx_build_for_test) to synthesize a post-connect failure on a real-vote endpoint and assert it reachesvote_reply_dispatch's tally - Do the same for a PreVote endpoint against
pre_vote_reply_dispatch - Confirm a round where every peer's outcome is now counted (including this failure mode) reaches a quorum decision without waiting for the full round timeout
- Observable: new tests in
test_ha_vote_send.cfail against pre-fix code (tally never updates) and pass against the code from 3.1 - Requirements: 4.2, 4.3, 4.5
- Depends: 3.1
- Extend the existing test-construction helper (
-
4. Core: document the dormant AppendEntries sender and guard it against silent regression
-
4.1 Record the production-authoritative send mechanism and add a non-regression guard
- Extend the
on_send_append_entriesdoc-comment (ha.c, theha_cb_send_append_entries_stubcallback) to state plainly that this callback is reachable only viaraft_send_append_entries, which has no production caller, and thatrepl_broadcast/ha_stream_wal_to_standbyare the actual productionAppendEntries-equivalent send/retry/backoff mechanism - Add an
XCE_TESTING-only call counter toha_cb_send_append_entries_stub - Observable: the counter exists and is queryable from a test; the doc-comment names both the dormant path and the live substitute explicitly
- Requirements: 3.3
- Extend the
-
4.2 Test that the dormant sender stays uncalled in production
- Drive the production heartbeat/replication path end-to-end in a test and assert the new call counter stays at zero
- In the same test, confirm
repl_broadcast/ha_stream_wal_to_standbydid fire for the same scenario, proving the assertion isn't vacuous - Observable: the test fails if a future change accidentally gives
raft_send_append_entriesa production caller - Requirements: 3.3
- Depends: 4.1
- Boundary: test_ha_core_safety.c
-
5. Core: regression-lock tests for already-shipped reply and append behavior
-
5.1 (P) Test that a WAL catch-up rejection actually sends the reply on the wire
- Add a test asserting
ha_send_wal_ack_reply_ex(success=0)is invoked (via the existingha_wal_ack_send_hook) when a WAL catch-up entry is rejected, closing the reply-construction-boundary coverage gap that existed alongside the already-shipped bug-1479 fix - Observable: new test in
test_ha_core_safety.cfails if the reject branch is ever changed back to a silent drop - Requirements: 2.1
- Boundary: test_ha_core_safety.c
- Add a test asserting
-
5.2 (P) Test that one failing payload in a submitted batch propagates to the whole batch
- Add a test asserting
persist_submit_job_payloads_asyncresolves every payload in a batch with the first failure's outcome when one payload fails to persist, rather than completing a subset - Observable: new test in
test_commit_barrier_reply.cfails if a future change lets a batch partially succeed - Requirements: 5.3
- Boundary: test_commit_barrier_reply.c
- Add a test asserting
-
6. Integration: extend the controller's durable conformance record with this feature's findings
- Update
src/controller/CLAUDE.md's "TLA+ Action-to-Code Mapping" table and "Protocol Verification Checklist" with corrected/added rows covering:AppendEntriesreply integrity (WAL catch-up and configuration push), vote/PreVote failure routing across all failure timings, and the dormant-sender resolution from task 4 - Append a one-line pointer to the existing
known_limitationsentry (index 30) in.kiro/specs/ha-term-safety/spec.json, referencing this spec's resolution of the dormanton_send_append_entriessend-path drift - Observable: the CLAUDE.md table cites the current, post-fix code paths for every action this feature touched, with no reference to the pre-fix silent-drop behavior as if it were still current; a reader of
ha-term-safety's known_limitations lands on this spec's resolution instead of re-discovering the drift - Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6
- Depends: 2.1, 2.2, 3.1, 4.1, 4.2
- Update
-
7. Validation: regression confirmation and live verification
-
7.1 (P) Confirm append and commit-source regression coverage is unaffected
- Run
test_commit_barrier_rollout.c,test_commit_barrier_reply.c(including the new test from 5.2), andtest_term_safety_adapter.cand confirm all pass without modification to the already-conformant code they exercise - Observable: test run output confirms client-append-integrity and durable-majority-authoritative behavior are unchanged
- Requirements: 5.1, 5.2, 5.4, 6.1, 6.2, 6.3, 6.4
- Depends: 5.2
- Boundary: test_commit_barrier_rollout.c, test_commit_barrier_reply.c, test_term_safety_adapter.c
- Run
-
7.2 (P) Confirm election, replication, and recovery regression coverage is unaffected
- Run
test_ha_core_safety.c(including the new tests from 5.1 and 4.2),test_ha_vote_send.c(including the new tests from 3.2),test_persist_recovery.c, andtest_config_repl.c(including the new tests from 2.3; also confirms 2.4's existing timeout/unknown-outcome handling, unchanged) and confirm all pass - Run
test_repl_catchup.c,test_repl_unit.c,test_repl_majority.c, andtest_ha_repl_push.cand confirm all pass, closing the coverage gap for the Requirement 3 retry/backoff/catch-up-streaming-ceiling regression lock (ha_raft_on_repl_nack,repl_start_catchup,repl_broadcast) - Observable: test run output confirms the already-fixed retry/backoff, catch-up streaming ceiling, connect-time vote-failure routing, term-adoption vectors, restart/recovery reconcile, and the new dormant-sender guard all remain correct
- Requirements: 2.4, 3.1, 3.2, 3.3, 3.4, 4.1, 4.4, 7.1, 7.2, 7.3, 7.5, 8.1, 8.2, 8.3, 8.4
- Depends: 5.1, 2.3, 3.2, 4.2
- Boundary: test_ha_core_safety.c, test_ha_vote_send.c, test_persist_recovery.c, test_config_repl.c, test_repl_catchup.c, test_repl_unit.c, test_repl_majority.c, test_ha_repl_push.c
- Run
-
7.3 Live soak: configuration-push rejection end-to-end
- Run a rolling chaotic-failover soak where the test driver issues a configuration push whose payload deliberately does not match its advertised sha256, exercising the real
handle_repl_config_pushSHA-256-mismatch reject branch - Observable: the soak log shows the primary receiving and logging the explicit reject rather than a timeout
- Requirements: 2.2, 2.3, 2.5, 7.4, 7.5
- Depends: 2.1, 2.2
- Run a rolling chaotic-failover soak where the test driver issues a configuration push whose payload deliberately does not match its advertised sha256, exercising the real
-
* 7.4 (P) Live soak: vote/PreVote post-connect failure end-to-end (optional, defer if no fault-injection mechanism exists)
- Attempt a soak round that severs an already-established outbound vote connection before a reply arrives (e.g. a firewall-rule toggle or proxy in the soak harness); if no such mechanism exists cheaply in
xce-test-ha, defer this task — the unit tests from 3.2 already give deterministic coverage of the fix itself - Observable, if run: the soak log shows the affected round reaching a quorum decision within the fast-tally path rather than the full election timeout
- Requirements: 4.2, 4.3, 4.5
- Depends: 3.1
- Boundary: xce-test-ha soak harness
- Attempt a soak round that severs an already-established outbound vote connection before a reply arrives (e.g. a firewall-rule toggle or proxy in the soak harness); if no such mechanism exists cheaply in
Implementation Notes
- Task 1: adding a
$C.mapname-tagged capnp field requires a companion edit to the hand-written C struct insrc/include/typemap.h(the codegen writes into that struct by name; it does not synthesize it).ReplConfigPushReply.termrequiredstruct repl_config_push_reply_s.termin typemap.h — design.md's File Structure Plan didn't call this out, but it's the established, required pattern for every existing$C.mapnamestruct in that file, not scope creep. Any future task adding a capnp field should expect the same pairing. - Authoritative test verification recipe (confirmed working):
rm -rf /tmp/xce_*then runASAN_OPTIONS=detect_leaks=0 src/xce.build/controller/controller_unit_testdirectly and read its own stdoutN Tests M Failuresline — meson's aggregate Ok/Fail can show a spurious TIMEOUT/inline FAIL under its own harness even when the direct binary run is clean (1053 Tests 0 Failures confirmed for task 1's baseline). - Task 2.2 (bug-1482):
myreq_handler's first unconditional step isxce_decode(pkt, &request), which itself unconditionallyfree_packet(pkt)+ frees theio_packet_tstruct before returning on every path —pktownership transfers intoxce_decode, the caller must never touch/free it afterward. Any new_for_testwrapper aroundmyreq_handler(orxce_decodecallers generally) must document this, not the reverse ("caller owns/frees pkt") — that wrong assumption is what caused the original test-only wrapper's doc comment to mislead a reviewer into requesting a double-free. - Task 2.2: driving a test through
cluster.c's realmyreq_handler(rather than testing extracted logic in isolation) requires addingcluster.ctocontroller_unit_test_srcinmeson.buildplus link-satisfying fakes for symbols its OTHER unrelated switch-cases reference (task.c/opget.c/sched.c/apiserver.c/myconf.c) — this is pre-existing, established convention in this codebase (test_worker_join.c,test_commit_barrier_reply.calready do it for the same reason), not scope creep. Any new test drivingmyreq_handlerend-to-end should expect this same footprint. - New test-only wrappers exposing a
staticproduction dispatch function (the_for_testpattern, e.g.peer_ctrl_msg_handler_for_test,myreq_handler_for_test) MUST be wrapped in#ifdef XCE_TESTING/#endif— the source file compiles into the production binary too, and an unguarded wrapper ships as a new linked symbol in production with zero production purpose. - Task 3.2 (bug-1483, not fixed): driving a PreVote round to
RAFT_PRE_VOTE_LOSTin a unit test (c==NULL) without first stubbingha_prevote_rearm_liveness_hookfalls through to the realha_arm_heartbeat_timeout_vtask, which callocs into the statics_liveness_ctxand never frees it whenc==NULLshort-circuits the scheduling guard.test_ha_prevote.c's existing LOST-path tests already stub-and-reset this hook — any new PreVote-round test that reaches LOST must do the same. pre_vote_reply_dispatchhas noXCE_TESTINGmock-hook seam (unlikevote_reply_dispatch'sxce_vote_reply_hook) — it callsraft_on_pre_vote_replydirectly. Tests verifying PreVote routing must assert onraft_state_t's own tally fields (pre_vote_replies[],pre_votes_granted,pre_vote_in_progress) instead, pertest_ha_prevote.c's established technique.