Commit graph

1075 commits

Author SHA1 Message Date
5f67be4fa3 test(raft-sim-conformance): conformance Restart persistence test (task 4.3) 2026-06-24 14:05:19 +00:00
b5b7e01b92 fix(raft): vote-reply per-peer dedup + stale-term drop (bug-1095/1096)
raft_on_vote_reply double-counted duplicated vote-responses (counter ignored
from_idx) and failed to drop stale-term replies, allowing two leaders at the
same term (bug-851 split-brain family). Mirror the existing pre-vote per-peer
dedup pattern (new vote_replies[] set) and add DropStaleResponse (term <
currentTerm). Surfaced by the raft-sim-conformance oracle (task 4.2).

47 raft / 264 overall lib tests pass.
2026-06-24 13:56:24 +00:00
e15bbf8190 feat(raft-sim-conformance): seeded scheduler raft_sim_step + per-step invariant check (task 4.2)
Harness surfaced a real upstream raft.c split-brain bug (bug-1095/1096,
bug-851 family) in raft_on_vote_reply: counter-based vote tally double-counts
a duplicated vote-response and no stale-term drop -> two leaders same term.
Out of this spec's boundary (raft.c protocol); recorded as Implementation Note.
2026-06-24 13:37:03 +00:00
90e7e9c999 feat(raft-sim-conformance): action trace recorder + raft_sim_dump_trace (task 4.1) 2026-06-24 13:10:31 +00:00
f0be46fb32 feat(raft-sim-conformance): raft_sim_check_all first-failure composition (task 3.5) 2026-06-24 13:00:10 +00:00
a38d9a7271 feat(raft-sim-conformance): State Machine Safety invariant checker (task 3.4) 2026-06-24 12:51:58 +00:00
63a9740c85 feat(raft-sim-conformance): Leader Completeness invariant checker (task 3.3) 2026-06-24 12:44:18 +00:00
d9b5fd51e0 test(raft-sim-conformance): focused Log Matching checker test for R3.2 (task 3.2) 2026-06-24 12:35:36 +00:00
25b14a6dca feat(raft-sim-conformance): Election Safety invariant checker (task 3.1) 2026-06-24 12:28:16 +00:00
b450e60d13 feat(raft-sim-conformance): bag-only Drop/Duplicate/Reorder ops (task 2.2) 2026-06-24 12:19:15 +00:00
ae4495104a feat(raft-sim-conformance): bag push/deliver routes vote+AE RPCs through library handlers (task 2.1) 2026-06-24 12:10:11 +00:00
631854303c feat(raft-sim-conformance): seeded PRNG + in-memory adapter callbacks + conformance init (task 1.2) 2026-06-24 11:55:11 +00:00
35c012ddc2 feat(raft-sim-conformance): add conformance-harness data types to raft.h (task 1.1) 2026-06-24 11:40:38 +00:00
2b15dfb67a docs(raft-sim-conformance): fix mermaid pipe + semicolon syntax in design diagrams
Mermaid parse errors in design.md's diagrams:
- Literal `|` in flowchart node labels is read as an edge-label delimiter:
  system-flow `term | 0` -> `term &#124; 0`; checker `|{i:...}|` ->
  `&#124;{i:...}&#124;` (entity renders as a literal `|`, meaning preserved).
- `;` is a mermaid statement separator and truncated the text it appeared in:
  sequence msgs `commit_index += ; apply()` -> `commit_index advanced, apply()`
  and `(R5); halt` -> `(R5), then halt`; flowchart label `responses;<br/>` ->
  `responses,<br/>`.

Valid edge-label pipes (-->|violation|) and all `;` in prose/tables outside the
mermaid fences are unchanged. All three diagrams now render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSxwb2Xt5F84ATgiAZnH8V
2026-06-24 10:43:48 +00:00
af099dca3e test(ha-convergence-recovery): close Task 8 — live /xce-test-ha 8-round PASS
Ran the live 3-node E2E (the last open task). Result:
  PASS: 8 rounds, 30 jobs submitted, zero job loss; all 3 nodes led
  (ctrl-1,ctrl-2,ctrl-3)  (exit 0)

Live-covered: kill-primary -> promoted standby owns all confirmed jobs
(zero loss across 8 failovers, jobs 3->30, expect-job ok each round);
restart ex-primary -> converged rejoin, 3-of-3 each round, no phantom jobs;
deterministic leadership coverage (ctrl-3 forced via RequestVote-freshness
fence). The soak also served as the live regression for bug-1077 (~40
failover client round-trips, no SEGV) after rebuilding the controller (the
deployed binaries predated that fix).

Not exercised live (remain unit-covered, tasks 2/4/5): withhold-ACK barrier
single-outcome (test_demotion_convergence) and isolate/heal quorum-WARN
(test_convergence_verification). All 8 tasks now [x]; spec closed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSxwb2Xt5F84ATgiAZnH8V
2026-06-24 10:17:11 +00:00
05febbc9a7 docs(specs): gap re-validation 2026-06-24 (5 HA/raft specs)
/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
2026-06-24 10:04:06 +00:00
50b976aedc docs(controller): NOTICE — config upgrade must snapshot state, not re-derive
Add an in-situ NOTICE at the config-upgrade restart site
(do_upgrade_config_with_bytes, step 9). Today an upgrade commits the
config-change marker, then each member restarts and re-materializes its
post-upgrade state by replaying its own pre-upgrade WAL under the NEW config;
replay is config-dependent (lost_and_found re-homes orphaned entities), so
members can diverge in materialized state (bug-857). The notice records the
intended fix — materialize a full entity-bearing snapshot at the marker's
(term,lsn) boundary, embed the new config, and install the SAME snapshot on
every member via snapshot_transfer_s — and its hard dependency on
entity-bearing-snapshots. Comment-only; anchors to
.kiro/specs/config-upgrade-snapshot/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSxwb2Xt5F84ATgiAZnH8V
2026-06-24 09:45:13 +00:00
8da8fd03dd fix(libxce): bug-1077 ctrlstatus crash from shared reply-union type confusion
ctx->reply.u is a single untagged union shared across opcodes (debugmsg /
get(dataset_t*) / jobid / clusterconf / ctrlstatus alias the same 8 bytes).
Commit 61e1391 added speculative free-guards
`if (ctx->reply.u.ctrlstatus) free_CtrlStatus_ptr(...)` (and the clusterconf
twin). A non-NULL slot does NOT imply a CtrlStatus*: a failover-aborted
reslist/joblist leaves ctx->reply.u.get == &getreply (a dead STACK pointer)
because xce_get_with_condition/xce_resval only cleared the slot on the success
path, not the xce_get_req<0 / cbuf_get<0 early returns. The next ctrlstatus
reply (worker thread) freed that foreign pointer as a CtrlStatus -> ASan
SEGV / stack-use-after-return in free_CtrlStatus (controller.capnp.c:11638).

Fix (root cause, both layers):
- Revert the unsafe free-guards in xce_handle_reply: MOVE_PTR overwrite only,
  never free/deref an untagged union slot by type.
- Clear ctx->reply.u.get = NULL on ALL exit paths of xce_get_with_condition
  and xce_resval via a single `goto done` cleanup (forget-proof; the bug was
  one forgotten failover path). Also drops the now-unused controller.capnp.h.
- Defense-in-depth: handle_get_reply drops a reply when the slot is NULL.

Regression: expose xce_handle_reply and add
test_handle_reply_ctrlstatus_does_not_free_foreign_slot, which poisons the
shared slot with a foreign heap value and proves the ctrlstatus reply handler
does not free it (RED reproduced the exact free_CtrlStatus_ptr double-free;
GREEN after fix). xcelib_test 244/0, controller_unit_test 166/0, all binaries
build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSxwb2Xt5F84ATgiAZnH8V
2026-06-24 09:44:43 +00:00
61e1391cd0 fix bug-1060 bug-1067 2026-06-24 17:05:43 +08:00
6067ba6c94 feat(xce-test-ha): support deploying HA cluster to a specific directory
Add an optional [dir] argument to the /xce-test-ha skill so the 3-node
soak cluster can be deployed to a caller-chosen path.

- run-ha-soak.sh: deploy dir resolves positional $1 > $XCE_HA_TOP > default;
  relative paths absolutized in pure bash (this host's busybox realpath
  lacks -m and would mangle nonexistent/relative paths).
- Fail-loud rm -rf guard: writes a .xce-ha-deploy sentinel at deploy time and
  refuses to wipe a non-empty dir lacking it (protects a typo'd home/source
  path); empty/nonexistent dirs stay safe to (re)deploy into.
- SKILL.md: document /xce-test-ha [rounds] [dir] and the safety semantics.

Verified: guard refuses an unmarked non-empty dir (dir survives, exit 1);
full 5-round soak into a custom dir PASS (18 jobs, zero job loss), cluster
landed in the named dir while the default dir was untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSxwb2Xt5F84ATgiAZnH8V
2026-06-23 22:13:40 +00:00
0eafed3999 save wolf status 2026-06-23 07:08:43 +00:00
2655edd2b4 log(bug-1028): post-failover availability — new primary stuck in catch-up loop, not serving (distinct from bug-828 data safety / bug-854 client discovery) 2026-06-23 07:04:07 +00:00
4338a97d0e docs(steering): sync HA protocol with ha-term-safety — per-entry term, (term,lsn) reconcile, commit-source ownership, bug-828 truncate rule 2026-06-23 04:48:06 +00:00
88823c59a7 test(ha-term-safety): harness post-failover client probes target a live node (bug-854 workaround)
The post-failover wait-primary/expect-job probes used the full masters list led
by the just-killed node, triggering libxce bug-854 (positional discovery can't
fail over from a dead-first master). Now they use a live-first list excluding the
killed primary.

NOTE: this exposes a deeper, separate post-failover AVAILABILITY issue (not data
loss): the new primary stays stuck in a catch-up loop with a lagging standby
(standby[0] lsn=5 < committed_lsn=6) and does not serve clients, so wait-primary
still times out. That is a catch-up/serving-readiness liveness concern orthogonal
to bug-828's committed-job DATA SAFETY, which is fixed and verified (walcompare
committed-prefix identical, jobs preserved on disk).
2026-06-23 04:12:32 +00:00
51d8a4c981 docs(ha-term-safety): record bug-828 FIXED (premature pre-election truncate) + bug-854 residual 2026-06-23 04:08:18 +00:00
b77d412256 fix(ha-term-safety): bug-828 FIXED — remove premature pre-election WAL truncate (committed-job-loss on clean failover)
ROOT CAUSE (confirmed via live instrumentation — NOT a durability violation):
ha_enter_real_election truncated the WAL to last_known_committed_lsn BEFORE the
vote. That watermark lags the true commit point (the primary committed LSN 2-5 on
a durable majority then died before a heartbeat carried committed=5 to the
standby), so the cutoff was 1; persist_truncate_wal_above -> clamp_durable_lsn
lowered the standby's durable_lsn 5->1, physically destroying durably-committed
entries before the election even computed the safe point. The candidate then
advertised floor=1, safe_committed_lsn=1, and the post-election truncate kept only
LSN 1. This directly violated the documented rule: 'Never use last_known_committed
_lsn for truncation — it may be stale.'

FIX: delete the premature truncate. The candidate keeps its full durable log; the
ONLY truncates are now the authoritative votes-derived post-election
persist_truncate_wal_above(safe_committed_lsn) (raft_cb_become_leader) + the
library on_truncate_from (term,lsn) reconcile — both before any commit under the
new leader.

SAFE (reviewer-verified): a divergent uncommitted tail can only exist at a
stale/lower term (single-writer); the library's term-primary (lastLogTerm,
lastLogIndex) freshness gate denies such a candidate — correct now that task 3.4
fixed the term axis (the prerequisite). No divergent entry can win-and-commit; no
bug-339 election-loop. 177 unit tests + a RED-proven regression test.

LIVE: clean failover now preserves committed jobs — walcompare committed-prefix
identical across all 3 members, both jobs present on disk, across multiple runs.
(Residual harness EXIT 1 is the SEPARATE pre-existing libxce client bug-854:
client can't reach the live primary when the dead node is first in the masters
list — a client-discovery defect, orthogonal to server-side data safety.)
2026-06-23 04:06:18 +00:00
ff869e4fc0 docs(ha-term-safety): characterize remaining bug-828 slice (election watermark tangle) 2026-06-23 03:15:23 +00:00
91b9a445f0 fix(ha-term-safety): bug-828 partial — revive dead safe-commit-from-votes + advertise real durable tip
Clean failover was truncating committed jobs because the post-election
safe-commit-from-votes computation was dead: vote_reply_dispatch decoded each
granting voter's advertised peerLastLogLsn but discarded it, so ha_compute_safe
_committed_lsn saw only the self-vote (the 'from 1 granted voters' fingerprint)
and truncated to self_lsn.

Part 1: vote_reply_dispatch now records the granting voter's (lsn,tag,grant) into
ctx->votes[] via a record-only ha_record_vote_reply seam (no double-promotion;
raft_on_vote_reply stays the sole production promotion driver) — so safe-commit
reflects the true voted majority.
Part 2: the candidate/voter advertise the real DURABLE (fsync'd) tip; persist_wal
_write_replicated advances durable_lsn after its fail-loud inline fdatasync.

Reviewer-verified SAFE (advertises only durable positions; safe-commit bounded by
the durable majority, preserving more committed entries, never marking a
non-majority-durable LSN safe). 177 unit tests; new test_ha_vote_gate + election
tests green.

PARTIAL: does NOT yet make the live clean-failover pass. A deeper slice of
bug-828/795 remains — a catch-up'd standby still under-advertises its durable
position in the live run (the documented architectural catch-up/durability gap),
so a job-less node can still win and truncate. Tracked for a dedicated effort.
2026-06-23 03:13:02 +00:00
14c41cb298 docs(ha-term-safety): record Option A resolution + newly-exposed bug-828 failover job-loss 2026-06-23 00:29:06 +00:00
9e5e293605 obs(ha-term-safety): promote committed_lsn advance to INFO + harness polls authoritative commit signal
The commit-engine committed_lsn advance was TRACE-only; promoted to INFO (fires only
on a real advance, not noisy) so operators/tests can observe the authoritative primary
committed watermark. The clean-failover harness now polls this 'commit: committed_lsn
N -> M' line (not the lagging ha_process_standby_state heartbeat) with generous catch-up
headroom.

This made the live RED->GREEN visible: with Option A, committed_lsn reaches 5 on a
stable cluster ('jobs committed on a majority (committed_lsn=5 >= 5)') even via catch-up
delivery — the commit-stall is fixed.
2026-06-23 00:28:14 +00:00
83fbed2370 fix(ha-term-safety): remove task-2.4 commit min-ceiling (Option A) + fix bug-991 catch-up off-by-one
Two coupled fixes for a live commit-stall (committed_lsn pinned at the own-term
NOOP while a 3-node cluster was durable to LSN 5 — jobs acked but never committed):

bug-991: the WAL catch-up applied_lsn was advanced to lsn+1 (next-to-apply) in
the three live/catch-up apply paths, but recovery/checkpoint/init and the leader's
catch-up streamer treat it as last-held. The over-advertise made the streamer skip
an LSN -> library logOk wedge -> no WalAck. Fixed to last-held (= lsn).

Option A (design reconsideration of REVIEW ISSUE 3): removed the
committed = min(durable-majority, raft_commit_index) ceiling. Under single-writer
the durable-majority watermark is already (term,lsn)-current-term-safe (term-carrying
replication + (term,lsn) reconcile/truncate-before-durable + own-term NOOP gate +
post-election truncate-to-safe), so the library commit_index was a redundant,
starvation-prone gate that pinned committed below the safe durable majority whenever
the library lagged. The durable-majority barrier + own-term writable gate are now
authoritative; raft_commit_index is inert-advisory.

Safety: reviewer-verified the durable-majority watermark is (term,lsn)-safe and
Figure-8 holds via the own-term gate (new RED->GREEN test); bug-851 cannot recur.
Live: commit trace 4->5 + walcompare committed-prefix identical across 3 nodes.
177 unit tests pass.
2026-06-23 00:21:06 +00:00
df4bc4f86a test(ha-term-safety): live-harness fixes + record bug-991 (commit stall via catch-up + 2.4 ceiling)
Ran clean_failover_preservation.sh on a real fs ($HOME/tmp/xce-ha-deploy, ext4).
PAYLOAD run CONFIRMED bug-990 fixed live: standbys persist the leader's real
term (2) for the committed prefix; walcompare OK.

Harness fixes: robust failover detection (any post-kill CTRL_PRIMARY promotion,
45s settle) + a wait-for-commit-before-kill gate (jobsub returns on accept not
commit, so the prior harness raced replication vs kill).

That commit-wait gate exposed bug-991: on a STABLE cluster, jobs replicate to
all members (durable LSN 5) but committed_lsn pins at 1. Catch-up-delivered
entries don't advance the library commit_index, so task-2.4's ceiling
min(durable-majority, raft_commit_index) starves commit below the durable
majority. A real regression from the commit-source switch (2.4+3.3), invisible
to the steady-state-only unit tests.
2026-06-22 21:10:48 +00:00
5fa394f598 docs(ha-term-safety): record final verification state — build+unit gate green (175/0); 5.2/5.4/5.5 live runs env-blocked 2026-06-22 15:48:53 +00:00
efe621c124 feat(ha-term-safety): retire term-0 repl_broadcast live send; carry leader term on replication (task 3.4)
Closes bug-990: the live replicator repl_broadcast wire-encoded term=0
(entry.term was never set), so standbys persisted hdr.term=0 and took the
legacy direct-apply path instead of the library (term,lsn) intake. Now the
live broadcast and catch-up streamer carry the leader's real term + AE prev
tuple; ha_apply_wal_catchup_entry routes every term-carrying entry through
raft_on_append_entries_request -> on_persist_entry (leader term stamped),
retiring the term-0 direct-apply. Standbys now persist the leader's real term
and reconcile by (term,lsn); the live commit chain is end-to-end.

848/849/850 + persist-before-ACK + commit-barrier + bug-960 preserved.
Proven at integration level; the definitive live STRICT_TERM failover run is
env-blocked here (bug-924 overlayfs) and recommended on a real fs.
2026-06-22 15:45:14 +00:00
87242aa8e9 spec(ha-term-safety): add task 3.4 — retire term-0 repl_broadcast, drive live library send (closes bug-990) 2026-06-22 15:02:21 +00:00
fda15336d5 test(ha-term-safety): clean-failover live harness; mark 5.2/5.4/5.5 blocked on bug-990 (live-send retirement)
Task 5.2 harness deploys a real 3-node localhost cluster, kills the primary,
promotes a standby, restarts the old primary, and asserts convergence via
walcompare --committed + per-job expect-job. Runs GREEN in PAYLOAD mode
(guards 848/849/850 + no committed-job loss).

The term-identity half of R5.1 is BLOCKED (bug-990): live replication still
uses term-0 repl_broadcast; the library send loop (on_send_append_entries) has
no live caller, so standbys persist hdr.term=0. The live-send retirement was
deferred across tasks 3.2/3.3 into verification tasks 5.2/5.4 that cannot
contain the production cutover. Needs a dedicated implementation task.
2026-06-22 13:27:21 +00:00
a166ecb45b test(ha-term-safety): cross-member walcompare divergence tool (task 5.3)
Diffs member WALs by (lsn -> term, pcrc) over the committed prefix, exits non-zero
on any committed-prefix divergence with the diverging LSN + per-member entry
identities. Reuses the canonical waldump decoder via a new per-entry visitor seam
(no second WAL parser). Detects same-LSN/diff-term and same-LSN/diff-content
divergences (the bug-851 class) that an LSN-range check would mask.
2026-06-22 13:05:23 +00:00
d39500ba48 test(ha-term-safety): end-to-end adapter callback + marker-path integration test (task 5.1) 2026-06-22 12:03:20 +00:00
be4c1f0721 feat(ha-term-safety): out-of-band config bytes, hash-verify-before-activate, serve-fence (task 4.3)
Wires config_committed_marker to a production consumer (config_activate_committed):
verifies pushed bytes' sha256 against the committed marker before activation, fences
(loud, non-serving, stay-alive) on mismatch, and activates the latest committed marker
on recovery. Enforced via ha_dispatch_gate + promotion gate.

Fixes bug-981: a 2nd+ config upgrade could permanently fence a standby on a
push-before-commit ordering. Adds an off-iomgr delaymgr retry scheduled from the
commit-advance hook (atomic read + non-blocking enqueue only on the ACK thread) that
re-activates and clears the stale fence once the new marker commits and bytes verify.
2026-06-22 11:47:15 +00:00
c3201a2b1e feat(ha-term-safety): append config markers via unified log + derive committed config (task 4.2)
Wires do_upgrade_config_with_bytes to append WAL_CONFIG_MARKER through
raft_log_append (live term-stamped consensus record), demotes committed_config_tag
to a non-authoritative cache, and derives the committed config from the latest
committed marker. Preserves the derive across checkpoint compaction via an optional,
backward-compatible config-ref tail in the snapshot sentinel.
2026-06-22 10:34:10 +00:00
f447ad4183 feat(ha-term-safety): add WAL_CONFIG_MARKER entry type + reference payload codec (task 4.1) 2026-06-22 09:37:23 +00:00
f1783f7952 feat(ha-term-safety): drive ACK path through raft_on_append_entries_reply + advance_commit (task 3.3)
Includes fix for bug-960: endpoint-type-aware peer-index resolution
(promoted-vote relative vs heartbeat-standby absolute) with fail-loud
bounds check, preventing an out-of-bounds match_index[] write / wrong-peer
false-quorum on the replication ACK path.
2026-06-22 09:25:44 +00:00
754e586f8f feat(ha-term-safety): route AppendEntries intake through library + leaderCommit wire (task 3.2) 2026-06-22 08:42:35 +00:00
6a519a928b feat(ha-term-safety): drive raft_log_append on primary append path (task 3.1) 2026-06-22 08:09:47 +00:00
3dd8493fff feat(ha-term-safety): wire five callbacks + library init restore (task 2.5) 2026-06-22 07:47:30 +00:00
3470bdfb71 feat(ha-term-safety): on_commit_advanced barrier trigger + commit-gate ownership (task 2.4) 2026-06-22 07:33:04 +00:00
2c5d298cb5 feat(ha-term-safety): on_send_append_entries callback body (task 2.3)
Replaces the stub: builds a WalCatchupEntry AppendEntries to standbys[peer_idx].ep
carrying prev_log_index/prev_log_term; for n_entries==1 sources the entry at
prev_log_index+1 from the local WAL (term via persist_entry_term, type+payload via
ha_read_wal_entry_at); n_entries==0 emits a zero-entry heartbeat. Async send
(xce_wal_batch_req_raw), payload freed on all paths. 2 tests via a capture seam.

Review-applied: corrected the misleading repl.c comment — leader_commit is accepted
but NOT yet wire-encoded (deferred to task 3.2: WalCatchupEntry leaderCommit field +
standby intake). Reviewer APPROVED; per-group 0 failures.
2026-06-22 06:01:30 +00:00
ed2443533f feat(ha-term-safety): on_get_entry_term + on_truncate_from callback bodies (task 2.2)
on_get_entry_term → persist_entry_term (recorded term, 0=absent, legacy-0→1; caveat
documented per 1.3 review). on_truncate_from → persist_truncate_wal_above(p, index-1)
(delete LSN>=index), returns rc for library/leader retry; non-promotable fence intact;
replaces the committed-boundary heuristic on the library-driven path. New
truncate_term_index_locked keeps the in-memory per-LSN term index consistent on both
truncate paths (under lock, cutoff-aligned) → no stale-term read after truncation.
3 tests, reviewer APPROVED, per-group 0 failures.
2026-06-22 05:29:06 +00:00
8f3d3ce8f4 feat(ha-term-safety): on_persist_entry callback body (task 2.1)
Replaces the iter-1 stub: persist_set_term(p, term) stamps the leader/library term
into hdr.term, then role-routes the write — primary via persist_wal_async, standby
via persist_wal_write_replicated(p, index, ...). Persist failure returns non-zero
(no false ACK; durable_lsn unchanged); bug-850 contiguity guard + persist-before-ACK
preserved beneath. In-flight raft_entry_blob_t {type,payload_len} envelope added to
wal_types.h (never enters the 40-byte header). 3 tests, reviewer APPROVED.

Deferred (recorded in tasks.md): task 3.1 must retire the hand-rolled primary append
so current_lsn tracks the library index; task 3.2 must not call on_persist_entry on
heartbeats. Per-group 0 failures (pre-existing test_commit_barrier_reply leak aside).
2026-06-22 04:53:18 +00:00
d540ebd183 feat(ha-term-safety): WAL term seam + recovery read-back + legacy-0 rule (task 1.3)
persist_set_term sets the live election term so appends stamp hdr.term (fixes the
G1 term-0 stamping); replay_wal builds an in-memory per-LSN term index (freed in
persist_fini, no on-disk change); persist_entry_term reads it back (0=absent).
Legacy-0 -> WAL_LEGACY_TERM=1 at read-back only (raw 0 retained so present-legacy-0
is distinguishable from absent). 40-byte wal_entry_hdr_t preserved (value fix, R1.7).
5 new tests (test_term_safety_adapter.c); reviewer APPROVED. Feeds task 2.2.

All controller test groups report 0 failures; the meson full-suite timeout is the
bug-924 overlayfs /tmp fdatasync artifact, not this task.
2026-06-22 04:32:29 +00:00