Commit graph

908 commits

Author SHA1 Message Date
5929af1078 feat(ha-entity-snapshots): entity-bearing checkpoint write path for jobs (task 2.2)
write_snapshot now Cap'n-Proto-encodes every retained job under nested
table+per-entity RTTI_LOCK_READ (table lock alone does not exclude a
concurrent per-entity writer -- design.md's central correctness finding),
appends length-prefixed job frames after a new format_version marker, and
fixes a previously-unchecked fdatasync return. Any failure anywhere in the
path (encode, write, sync) discards the whole checkpoint -- no partial
file at the final path, proven by a real mid-encode-loop fault-injection
test (forces one specific job's encode to fail among N, asserts nothing
survives).

Also fixed two pre-existing bugs in task 1.1's test fixture: terminal-state
seeded jobs need a non-zero finish_time or job_should_retain's retention
window silently excludes them from any checkpoint; the fixture needs
xstring_table_init or every seeded job's submit/start_info silently
decodes as NULL.

Round 1 of this task also touched load_snapshot/persist.h to make the
round-trip tests pass end-to-end, but review caught a live hazard: it
advanced current_lsn/checkpoint_lsn BEFORE the entity-decode loop ran, so
a decode failure still left the position advanced -- reachable today
through the unmodified ha_handle_snapshot_chunk production path, not
hypothetical. Remediated by fully reverting load_snapshot/persist.h to
byte-identical pre-task-2.2 state and rewriting the round-trip tests to
parse write_snapshot's raw on-disk bytes directly (read_Job/decode_Job)
instead of round-tripping through persist_load_snapshot -- proves the
write side without depending on load_snapshot, which stays task 5.2's
scope entirely, not started here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDYFbrgyDHbvNYcxApKhrX
2026-07-01 06:30:28 +00:00
0f344da89a test(ha-entity-snapshots): RED tests for job checkpoint content (task 2.1)
Three tests against the current sentinel-only write_snapshot/load_snapshot:
job content round-trip and concurrent-mutation-not-torn genuinely FAIL
(RED) since no entity content is written today; write-failure-discard
already passes (write_snapshot's open()-failure path already discards
correctly) -- flagged transparently as a GREEN-from-day-one regression
guard rather than faked RED, per real fault injection (directory removal
-> genuine ENOENT).

Carries forward two notes for task 2.2: fdatasync's return value is
currently unchecked in write_snapshot (persist.c:1505), and a mid-encode-
loop fault-injection test should be added once the per-job encode loop
exists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDYFbrgyDHbvNYcxApKhrX
2026-07-01 05:18:45 +00:00
10f9bbfd9d feat(ha-entity-snapshots): add spec + task 1.1 test fixture
Adds the ha-entity-snapshots Kiro spec (requirements, design, research,
tasks) for task 5.10a / raft-ha-core-safety: making the HA checkpoint an
actually-restorable copy of committed job/queue state instead of a
position-only sentinel, closing the residual chaotic-soak divergence gap.

Task 1.1 (foundation): reusable test fixture snapent_make/snapent_teardown
in a new test_persist_snapshot_entities.c, seeding N jobs (varying state)
and M queues (varying status) into a real persist_t+cluster_t for later
write/load/rotation-guard tests to build on. Build clean; new suite 1/1;
regression spot-checks (test_persist_hooks 19/19, test_commit_barrier_reply
22/22) clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDYFbrgyDHbvNYcxApKhrX
2026-07-01 04:55:55 +00:00
0d70d5e412 fix(controller): debounce RDHUP-triggered failover — stop the term-escalation storm (bug-1310, task 5.10b)
A dead primary's control fd RDHUPs faster than an election settles. The 5.6
guard only drops an RDHUP while the role is instantaneously CTRL_ELECTING/
prevote; an RDHUP landing in the gap after a lost election (role briefly back
to CTRL_STANDBY) re-triggers a fresh ha_begin_failover -> PreVote -> real
election (term++). One bump per RDHUP -> unbounded escalation under a sustained
cadence (observed round 4: 48 begin_failovers, terms 6->24, round overran the
harness window).

Fix: ha_state_t.last_failover_ms (atomic) stamped at ha_begin_failover;
ha_on_primary_ctrl_fd_rdhup drops an RDHUP within 2*election_timeout_ms of the
last trigger (closes the dead fd, returns) so the election's own jittered retry
(5.7) runs without the external RDHUP cadence adding term bumps. First trigger
never debounced; genuine loss still fails over, at most once per window.

Validation: unit-proven — test_rdhup_within_cooldown_is_debounced (recent
last_failover -> stays CTRL_STANDBY) vs test_rdhup_single_node_reaches_primary
(last_failover=0 -> CTRL_PRIMARY) is a differential proof. Soak: 10/10 elect,
terms 4->14 bounded, begin_failover 14/14/12 (was 48), committed_job_loss=0, no
regression. The stochastic storm did not recur this run (debounce fired 0x), so
soak-reproduction of the storm is still pending; the mechanism rests on the
unit test. Election suites green (rdhup 8/0, prevote 12/0, core_safety 24/0,
election 28/0, failover_prevote 6/0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDYFbrgyDHbvNYcxApKhrX
2026-07-01 01:32:27 +00:00
cc80e65288 docs(raft-ha-core-safety): 5.10b deeper — RDHUP failover storm (48 begin_failovers) defeats 5.6 point-in-time suppression
The round-4 term ladder is driven by repeated RDHUP->begin_failover->
PreVote-won->real-election(term++) cycles from the dead primary's socket, not
a behind-node vote loop. 5.6's instantaneous 'suppress while electing' guard is
insufficient under a sustained RDHUP cadence. Fix direction: per-failover
cooldown/debounce + hold the failover latch across the full prevote->election->
settle window. Needs a deterministic dead-primary-cadence repro.
2026-07-01 01:21:48 +00:00
9944c689db docs(wolf): correct bug-1306 note — clobber does NOT extend to QUEUE_CTRL/WORKER_CTRL
Their WAL_MODE_WAIT hook (wal_hook_append_classified) does local-append only
and carries no durability verdict through the hook return, so there is nothing
for a later hook to clobber. JOB was unique in running the full commit barrier
inside the hook. Their commit barrier is the separate ha-commit-barrier-rollout
handler barrier — verify independently, do not mechanically extend the JOB fix.
2026-07-01 01:20:28 +00:00
adda4b8b9c docs(raft-ha-core-safety): root-cause round-4 election-escalation tail (task 5.10b)
freshness-deny escalation loop under a just-restarted-survivor overlap: a
behind-log candidate is freshness-denied every term (18->24 observed), times
out, re-campaigns at term+1 -> ~18 escalations before convergence. NOT RNG
(seed is /dev/urandom); 5.7 jitter bounds retry rate, not the escalation.
Fix direction: a repeatedly-freshness-denied candidate must recognize it is
behind and defer/catch-up instead of laddering the cluster term. Needs a
deterministic 2-survivor repro before a fix (5.5/5.6/5.7 family).
2026-07-01 01:15:16 +00:00
20cfd2751d docs(wolf): record bug-1306 hook-rc-clobber learning + waldump bisect method 2026-07-01 00:43:14 +00:00
15be5d94e8 fix(controller): extract job-submit commit barrier out of the hook fan-out — stop committed-job-loss (bug-1306)
raft-apply-decoupling item #1 (down payment). xce_run_hooks returns only the
LAST-registered hook's rc, so a later JOB AFTER_EVENT subscriber (sched/
apiserver) returning 0 clobbered persist_job_hook_fun's commit -ETIMEDOUT.
handle_jobsubmit then replied 'jobsubmit ok' for an entry that never reached
majority, and a new-term leader legally discarded it (committed-job-loss,
Commit Durability violation).

- persist.c/.h: new persist_submit_wal_and_commit(p, jlist) — the explicit raft
  step (WAL append + majority-commit barrier) returning the commit verdict
  directly. persist_job_hook_fun's WAL_MODE_WAIT branch is now a no-op.
- job.c handle_jobsubmit: call the explicit step, read commit_rc DIRECTLY,
  reject on <0 before firing the now notification-only AFTER hook
  (canonical commit -> notify -> reply).
- tests: +test_job_hook_new_is_notify_only regression pin; 4 hook tests
  relocated to drive the explicit step. persist_hooks 19/0, e2e_hooks 3/0,
  commit_barrier_reply 22/0, commit_durability 38/0.

Proven: 10-round chaotic soak committed_job_loss 5 -> 0 (same election code).
Residual soak FAILs (divergence catch-up lag + a round-4 election-escalation
tail) are NOT job loss — tracked as task 5.10.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDYFbrgyDHbvNYcxApKhrX
2026-07-01 00:42:30 +00:00
6bb9d81225 docs(ha-commit-durability): sharpen bug-1306 root cause — xce_run_hooks clobbers commit -ETIMEDOUT
10-round soak waldump+log bisect: the chaotic committed_job_loss is NOT the
catch-up/entity-snapshot prefix gap. All 3 WALs gapless/CRC-intact/full
payloads; lost job only on ctrl-1@LSN30 term9 vs term-10 NOOP on the majority.
The durable-majority gate worked (commit_submit -> -ETIMEDOUT), but
xce_run_hooks (hook.c:134-157) returns only the LAST-registered hook's rc, so a
later JOB AFTER_EVENT hook (sched/apiserver) returning 0 clobbered persist's
-ETIMEDOUT -> handle_jobsubmit replied jobsubmit ok for an uncommitted entry ->
term-10 leader legally overwrote it. Fix per raft-apply-decoupling item #1:
move the WAL append + commit barrier out of the hook into an explicit
handle_jobsubmit-driven raft step whose rc is read directly (down payment on
the full apply-after-commit inversion). Entity-snapshot gap is a separate,
still-real defect (not this soak's mechanism).
2026-07-01 00:21:23 +00:00
08bdaba3dc docs(raft-ha-core-safety): close election layer — 5.5/5.6 done, bug-1296/1286 fixed, 10-round soak validates
10-round chaotic soak: 10/10 rounds elect in a single term increment (4->13),
zero escalation; round 7+ (old bug-1296 liveloc) clean. Election liveness layer
(tasks 5.5/5.6/5.7) DONE. Remaining soak FAIL (committed_job_loss=5,
divergence=11) is wholly the task 5.9 / ha-commit-durability catch-up gap
(bug-795/828/851/1306) — job 0-0-1782854622-0 lost at round 8, rounds 1-7
job-loss-free.

- tasks.md: 5.5 + 5.6 flipped [x] (subsumed by 5.7 soak validation)
- buglog: bug-1296 FIXED (resonance, confirmed 10-round); bug-1286 FIXED
  (5db734a NULL-guard); bug-1197 SUPERSEDED by bug-1298 refcount
- research.md: Phase 1 closeout / 10-round validation section
2026-06-30 21:26:00 +00:00
ed94852ec6 chore(buglog): bug-1306 — chaotic soak rounds 4-6 committed-job-loss (pre-existing bug-795/828/851 catch-up gap, exposed by 5.8; not a regression) 2026-06-30 14:47:57 +00:00
da06ba437f docs(raft-ha-core-safety): 5.8 validated (bug-1298 UAF fixed via ep refcount); round-4..6 reveals pre-existing commit-loss (task 5.9)
5.8 endpoint refcount fix validated: 182 unit tests/0 fail, 6-round ASAN chaotic
soak ASan-clean, all rounds elect. bug-1298 marked fixed.

The soak now reaches rounds 4-6 for the first time (every prior run crashed at
round 4) and FAILs the verdict on committed_job_loss=4/divergence=7. Evidence shows
this is the pre-existing bug-795/828/851 failover catch-up gap, NOT a regression:
one job missing from round 4 onward across 3 primaries; rounds 1-3 clean; ZERO
WAL/commit/catch-up code changed this session; ASAN clean. Routed to task 5.9 /
ha-commit-durability.
2026-06-30 14:27:59 +00:00
6e05dc1523 fix(libxce): endpoint reference count — kill teardown-vs-dispatch UAF (bug-1298, task 5.8)
The io_worker freed an endpoint (endpoint_killer, cluster.c) on connection
teardown while the vtask dispatcher concurrently read ep->busy
(endpoint_is_task_blocked, netio.c) on a queued vtask — a heap-use-after-free
that ABORTed a controller mid-chaotic-soak (round 4), dropping the cluster
below quorum so no primary could be elected.

Replace the accumulation of ad-hoc ep-lifetime patches (bug-1282 lockless busy,
bug-1197 dispatch_inflight drain + deferred teardown vtask) with one principled
invariant: an endpoint lives until its reference count reaches 0.

  - endpoint_t.refcnt (_Atomic) + heap_alloc flag; endpoint_get starts a heap ep
    at refcnt=1 (the owner ref).
  - endpoint_ref/endpoint_unref: the last unref fini's + frees a heap ep.
    Embedded eps (client ctx->ep, heap_alloc=0) are never freed here — their
    container owns them — so a vtask-balanced ref/unref pair is a safe no-op.
  - vtask_enq takes a per-vtask ref on the bound ep BEFORE publishing the task to
    the dispatcher; run_task / taskq_item_killer release it. So the dispatcher can
    read ep->busy on any queued vtask without the ep being freed underneath it.
  - every HEAP-ep teardown site (endpoint_killer, handle_endpoint_error, the
    vote/prevote connect-fail paths, the vote-ep loser/teardown paths, the standby
    reconnect/eviction paths) now drops its owner ref via endpoint_unref instead of
    raw endpoint_fini+free. Several of those also leaked the ep struct before
    (endpoint_fini-only); unref fixes the leak too.

Validation: controller_unit_test 182 tests / 0 failures (test_ha_vep_teardown
updated — its OFF-path test asserted the old struct leak, now fixed). 6-round
ASAN chaotic soak: ZERO AddressSanitizer reports, ctrl-3 no longer crashes at
round 4, all 6 rounds elect at bounded terms. (The pre-existing iomgr_fini ->
SSL_CTX_free abort under openssl 3.6 in the aggregate test binary is unrelated.)

Note: with the crash gone, the soak now reaches rounds 4-6 for the first time and
surfaces a SEPARATE, pre-existing committed-job-loss/divergence (the bug-795/828
failover catch-up gap; ha-commit-durability domain) — not a refcount regression
(ASAN clean; rounds 1-3 are loss-free).
2026-06-30 14:24:47 +00:00
5be165312a docs(raft-ha-core-safety): 5.7 verified (election resonance fixed); round-4 re-roots to bug-1298 UAF (task 5.8)
5.7 election-timeout resonance FIXED + soak-confirmed: 6-round chaotic soak now
elects with bounded terms (rounds 1-3 at terms 4/5/6, not 25-30+), zero job loss.
The election-liveness layer is done.

Round-4 stall re-roots to a NON-election crash: ctrl-3 ASAN heap-use-after-free
in endpoint teardown — io_worker (endpoint_killer, cluster.c:813) frees an endpoint
while the vtask dispatcher reads ep->busy (endpoint_is_task_blocked, netio.c:1008).
ctrl-3 dies -> cluster drops to 1/3 -> no quorum -> no primary. Same async-io
dispatch surface as bug-1282/1197; UPSTREAM-owned (raft-drive-async / libxce).
Logged bug-1298; tracked as task 5.8.
2026-06-30 13:39:55 +00:00
74784b2b18 fix(raft-ha-core-safety): randomize per-campaign election timeout to [T,2T] (task 5.7)
The per-campaign election watchdog was armed with a FIXED election_timeout_ms
(raft_start_election -> on_schedule_timeout -> raft_cb_schedule_timeout). Under
cold-connection churn a just-restarted survivor opens a fresh TCP+SSL endpoint
per vote round, so its single needed grant can land a hair AFTER a constant
deadline on EVERY campaign -- a resonance: the campaign times out, the term
re-escalates, and the now-stale grant is dropped (DropStaleResponse). Forever.
Terms climb unbounded; the freshest survivor never tallies quorum. This is the
final round-4 chaotic-soak election-liveness stall.

Fix (Ongaro thesis 3.4): re-randomize the watchdog per campaign to the canonical
[T, 2T] interval in the HA adapter, so some campaign's deadline falls AFTER its
grant RTT and the grant lands in-window. Done in raft_cb_schedule_timeout so the
pure (sim-tested) raft library stays deterministic. Independent of
election_jitter_max_ms (which tunes only the sub-second startup/stagger jitter)
so it spreads even when jmax is small/0; never below the configured floor so
genuine-loss failover is not sped up past election_timeout_ms.

Preserves Election Safety (no change to term/vote/DropStaleResponse logic).

new ha_jittered_election_delay() helper + 2 unit tests (RED verified: stubbing
jitter to 0 fails test_5_7_campaign_timeout_is_randomized with the resonance
message; PIN guards floor + modulo-by-zero). Suite: 24 Tests / 0 Failures.
2026-06-30 13:29:15 +00:00
2b182ecde1 docs(raft-ha-core-safety): 5.6 verified (unit) + reduces churn; add task 5.7 (vote-reply term-lag escalation)
5.6's RDHUP-no-preempt guard (81cb09a) is unit-verified and slows the round-4 term
churn, but the soak still stalls — a 5th layer (task 5.7). The freshest survivor
ctrl-2 (15@t5) wins PreVote, enters the real election, and ctrl-3 GRANTS it
(vote_granted) — but ctrl-2 never tallies a majority: every grant arrives as
vote_reply_stale_drop (reply term < ctrl-2's current term). ctrl-2's term
escalates via repeated election timeouts faster than ctrl-3 (cold peer
connection, fresh SSL connect per vote, plus dead-ctrl-1 connect timeouts) can
reply, so each grant is one term stale -> dropped -> timeout -> term++.
Self-perpetuating; DropStaleResponse itself is correct Raft.

Adds task 5.7 (don't re-escalate the term while a vote RTT is outstanding /
warm-reuse the peer vote connection / wider jitter). 5 layers of the round-4
stall now peeled, 4 fixed (deadlock, segfault, advertised-term, RDHUP-preempt);
the cold-connection term-lag remains. Consensus SAFETY intact throughout.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbTHR9CsEYK9RsqmQpQpC
2026-06-30 11:58:43 +00:00
81cb09a8db fix(controller): drop RDHUP while electing — break dueling-candidate term escalation (task 5.6)
After 5.5 let the freshest survivor win PreVote and ENTER the real election,
round 4 still stalled: the dead primary's control fd keeps RDHUP'ing on a
deterministic ~15s cadence, and ha_on_primary_ctrl_fd_rdhup's 5.2 suppress only
catches CTRL_STANDBY — so an ELECTING node fell through to ha_begin_failover and
restarted a fresh PreVote, preempting its own in-progress real election. Every
cycle bumped the term (21->25->28->29...) and kept two survivors dueling in
lockstep, defeating the jittered election-timeout tiebreak.

Guard the RDHUP entry: if role==CTRL_ELECTING or a PreVote round is in progress,
close the dead control fd and return WITHOUT restarting failover — let the
in-progress round finish or time out (its election timeout is jittered, which
desynchronizes the duel so one survivor wins). A node not currently electing
still takes the normal 5.2-suppress / genuine-loss failover path.

RED+PIN in test_ha_core_safety.c (22 Tests / 0 Failures): CORE RED — CTRL_ELECTING
+ RDHUP must not start a new PreVote (Expected 0 Was 1 on baseline); PIN — genuine
loss (STANDBY, not electing) still fails over. Preserves Election Safety, 5.2
suppression, and genuine-loss liveness. Re-running 7.2.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbTHR9CsEYK9RsqmQpQpC
2026-06-30 11:47:09 +00:00
fa53feaf62 docs(raft-ha-core-safety): 5.5 advertised-term fix verified as progress; add task 5.6 (dueling-candidate split-vote)
5.5's real root cause was a stale advertised lastLogTerm (c->persist.term vs
persist_entry_term), not the last_activity hypothesis — fixed in b8453b2 and
VERIFIED: with it, the freshest survivor now wins PreVote and enters the real
election (it could not before). But round 4 still does not complete: a deeper
layer (task 5.6) — under the dead-primary RDHUP cadence (~15s) the two survivors
duel in lockstep and the term escalates unbounded (21->25->28->29...) without
reaching majority. Adds task 5.6 (randomized failover backoff to desync the duel
/ don't re-trigger failover mid-real-election). Consensus SAFETY intact; this is
election LIVENESS under sustained churn. 4 layers of the round-4 stall now peeled
(deadlock, segfault, advertised-term fixed; dueling-candidate remains).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbTHR9CsEYK9RsqmQpQpC
2026-06-30 11:38:21 +00:00
b8453b2d2e fix(controller): advertise true last-log term in vote/prevote — unblock round-4 election (bug-851/task 5.5)
The election cross-deny that stalled round 4 (after bug-1282+bug-1286 fixed): the
freshest survivor (e.g. ctrl-2 tip 15 @ term 5) could not win because it
advertised lastLogTerm = c->persist.term (the persist layer's RUNNING term,
stale/0 after a restart) instead of the term STAMPED ON its last WAL entry. So a
voter (ctrl-3, last entry term 4) saw the candidate's lastLogTerm <= its own and
raft_log_ok rejected the genuinely-fresher candidate -> every round timed out ->
permanent stall. This was a documented stopgap (ha.c "task 1.3" NOTE) and is the
bug-851 term-identity family.

Fix both advertise sites (real-vote raft_cb_send_vote_request + PreVote
raft_cb_send_pre_vote_request): lastLogTerm = persist_entry_term(&c->persist,
ha->raft.log_len) when log_len>0 — the SAME accessor voters use to compute their
own last-log term (on_get_entry_term -> persist_entry_term), so logOk is now
symmetric and the freshest survivor wins. Strengthens Election Safety/Leader
Completeness (only genuinely-fresher candidates are granted); the index source
(ha->raft.log_len, seeded from the recovered WAL tip) is unchanged.

Root-caused from a frozen round-4 soak: ctrl-2 (15@t5) vs ctrl-3 (11@t4); ctrl-3
denied ctrl-2 on logOk (mislabeled "eligibility"). Re-running 7.2 to confirm.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbTHR9CsEYK9RsqmQpQpC
2026-06-30 11:16:40 +00:00
5a36698a90 docs(raft-ha-core-safety): bug-1282+bug-1286 verified fixed; unmask round-4 election cross-deny (task 5.5)
6-round chaotic soak with both fixes: rounds 1-3 elect; round 4 has NO segfault,
NO fdatasync, NO iomgr deadlock — both fixes held. The round-4 stall is now the
pure election-liveness cross-deny that the crash/deadlock masked: the freshest
survivor (floor 15) is the legitimate winner, but the other survivor (floor 12)
denies it on eligibility because it still believes its dead primary is alive
(in_catch_up + heartbeat-not-yet-stale). Suspect 5.2's last_activity re-arm
keeping a node falsely-fresh after primary death.

Adds task 5.5 (release the heartbeat-fresh/in_catch_up deny on genuine primary
death within the failover window) as the remaining 7.2 blocker. bug-1282 and
bug-1286 are fixed and verified; consensus safety unaffected.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbTHR9CsEYK9RsqmQpQpC
2026-06-30 09:33:25 +00:00
5db734aa44 fix(controller): NULL-guard ep->owner in myreq_handler — stop round-4 restart SIGSEGV (bug-1286)
Root-caused via the non-ASAN debug build + gdb: on a chaotic restart an inbound
endpoint dispatches a request (e.g. walCatchupEntry op 37) while ep->owner
(cluster_t) is NULL — not yet wired at accept, or cleared during teardown. The
handler did `c = ep->owner` then `ha_dispatch_gate(&c->ha, ...)`; with c==NULL,
&c->ha = NULL+offsetof(cluster,ha) = 0x3ff0, a garbage pointer ha_dispatch_gate
dereferenced (atomic_load &ha->role) -> SIGSEGV, dropping the node below quorum
and stalling the election.

Guard c==NULL after decode (so the decoded request is freed, no leak): log a
loud WARN and drop the request — there is no cluster context to dispatch through
and no c->myhost to address a reply. The controller stays alive (never
self-terminate on an operational condition); the peer retries once the endpoint
is owned. Backtrace: ha_dispatch_gate(ha=0x3ff0) <- myreq_handler cluster.c <-
vtask_entry <- run_task.

Second of the two round-4 blockers (bug-1282 deadlock already fixed). Re-running
the 7.2 chaotic soak with both fixes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbTHR9CsEYK9RsqmQpQpC
2026-06-30 09:23:51 +00:00
02e9275706 feat(xce-build): add non-ASAN debug + release profiles; root-cause bug-1286 with the debug build
/xce-build now takes a build PROFILE: asan (default, debug+ASAN -> xce.build,
backward-compatible), debug (non-ASAN -O0 -g3 -fno-inline -fno-omit-frame-pointer
-> build.dbg, for gdb/cores), release (-O3 -> build.release). Each profile uses
its own default build dir so variants don't clobber each other; arg2 may still be
a legacy build_dir. asan runs the unit suites; debug/release are build-only (the
suites are ASAN-oriented and the meson test harness is overlay-fs fragile, bug-924)
and print how to run a suite directly.

Why: bug-1282/bug-1286 are heisenbugs ASAN perturbs away (no SIGSEGV) and that
gdb-on-release turns into a hang/timeout. The non-ASAN -O0 -g3 build reproduces
them AND yields a symbolized backtrace under gdb.

It immediately paid off: the debug profile ROOT-CAUSED bug-1286 (the round-4
segfault) — myreq_handler (cluster.c:251) does `c = ep->owner` then
`ha_dispatch_gate(&c->ha,…)`; on chaotic restart a walCatchupEntry (op 37) vtask
runs with ep->owner==NULL, so &c->ha = 0x3ff0 -> deref crash at ha.c:3194. Fix is
a NULL-guard in myreq_handler (+ owner-lifecycle). 7.2 now has one remaining
blocker (bug-1286, root-caused); bug-1282 deadlock is fixed.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbTHR9CsEYK9RsqmQpQpC
2026-06-30 09:15:36 +00:00
e90bf835f9 docs(raft-ha-core-safety): bug-1282 fixed + isolate bug-1283 (the round-4 stall had two causes)
bug-1282 (ABBA iomgr deadlock) is FIXED and verified (commit 25e741d): zero
__lll_lock_wait threads post-fix, unit suite green. But a plain release soak WITH
the fix still segfaults a restarted standby at round 4 — and deadlocks hang, they
do not SIGSEGV, so that crash is a SEPARATE bug (bug-1283), most likely an
endpoint-lifetime UAF under chaotic restart (bug-1197 family).

bug-1283 is a heisenbug (invisible under ASAN and gdb; no usable core in the
sandbox) and likely UPSTREAM raft-drive-async / core libxce endpoint quiescence.
7.2 stays blocked on it. Consensus correctness (Phase-2/5.2/5.3) is unaffected.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbTHR9CsEYK9RsqmQpQpC
2026-06-30 08:25:04 +00:00
25e741d8c7 fix(libxce): break ABBA iomgr deadlock — make endpoint busy flag lockless (bug-1282)
Root-caused via gdb (all-thread backtrace on two independently-wedged controllers
during chaotic failover): an ABBA lock-order deadlock in the async dispatch path.

  io_worker (netio.c): holds ep->lock, blocks on q->lock (vtask_enq -> dataq_enq)
  vdispatcher: dataq_deq holds q->lock, blocks on ep->lock via the
    vtask_eligible -> endpoint_is_task_blocked match callback

endpoint_is_task_blocked took ep->lock while the caller (dataq_deq) held the dataq
q->lock, inverting io_worker's ep->lock -> q->lock order. Under churn the two
orders collide and the iomgr wedges (no more accepts / vote replies).

Fix (mirrors the dispatch_inflight precedent, bug-1197): promote endpoint_t.busy
from a 1-bit bitfield to _Atomic int and make endpoint_block_tasks /
endpoint_unblock_tasks / endpoint_is_task_blocked lockless (release/acquire). The
ep->lock that wrapped each access protected nothing else (busy only gates the
vtask queue), so removing it is safe; the inherent set-then-check TOCTOU is
unchanged (the lock never prevented it — the decision is advisory).

Verified: post-fix gdb soak shows ZERO threads in __lll_lock_wait and
endpoint_is_task_blocked no longer appears in any lock path (deadlock gone);
controller_unit_test all green (no regression). NOTE: a SEPARATE release-only
restart segfault remains and still blocks the raft-ha-core-safety 7.2 gate
(tracked separately) — deadlocks hang, they do not SIGSEGV, so it is a distinct
memory bug.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbTHR9CsEYK9RsqmQpQpC
2026-06-30 08:22:15 +00:00
bfe69b1e85 docs(raft-ha-core-safety): root-cause 7.2 round-N blocker to an ABBA dispatch deadlock; route to raft-drive-async (bug-1282)
Using gdb (SIGQUIT all-thread backtrace on the wedged nodes), nailed the round-4
blocker to a deterministic ABBA lock-order deadlock in src/lib async dispatch —
identical on both wedged survivors:

- io_worker (netio.c:672): holds ep->lock, blocks on q->lock (vtask_enq->dataq_enq)
- vdispatcher dataq_deq (dataq.c:65): holds q->lock, blocks on ep->lock via the
  vtask_eligible (vtaskmgr.c:255) -> endpoint_is_task_blocked (netio.c:1001) cb

io_worker wedges -> node stops accepting/answering PreVotes -> two restarted
survivors can't elect -> round-4 stall. The release SIGSEGV is the same ep/q race
corrupting under churn.

Defect: dataq_deq runs its match callback while holding q->lock, and the callback
takes ep->lock — inverting io_worker's ep->lock->q->lock order. Minimal fix: make
ep->busy atomic + lockless in endpoint_is_task_blocked (or io_worker drops ep->lock
before dataq_enq, or one global lock order).

Ownership: src/lib async-I/O dispatch (vtaskmgr/dataq/netio) = raft-drive-async /
core libxce, a Non-Goal of this spec. Per upstream-ownership protocol, routed to
raft-drive-async (bug-1282); 7.2 stays blocked until the dispatch lock-order is
repaired, then re-run the gate. The gate worked as designed — 7.2 must be GREEN
before raft-drive-async flips dispatch flags ON, and it caught a real dispatch
deadlock. Task 5.4 (root-cause + assign ownership) is DONE. Consensus correctness
(Phase-2/5.2/5.3) is unaffected.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbTHR9CsEYK9RsqmQpQpC
2026-06-30 04:18:19 +00:00
e80abd5cc4 docs(raft-ha-core-safety): unify 7.2 round-N blocker via gdb — one restart-stability bug (SIGSEGV / iomgr accept-hang)
Used the now-available gdb (wrapped the release controller under gdb -batch via
XCE_HA_BIN) to expose the round-4 blocker's second face. Findings:

- The release-build SIGSEGV and a gdb-run accept-loop HANG are the SAME restart-
  time I/O-subsystem corruption: under gdb the memory bug is perturbed away from
  a crash and instead wedges the inbound-accept path (ctrl-3 stops accepting at
  03:42:03 while still firing its own election timer), so two restarted survivors
  can never exchange PreVotes -> permanent round-N stall.
- This is a restart-STABILITY bug, NOT a consensus-logic gap: PreVote/eligibility/
  freshness (incl. 5.3) are correct; the restarted node is simply I/O-dead. It
  predates this session (baseline 21e26a1 failed round 3-4).
- Heisenbug: invisible under ASAN and under gdb; no usable core (read-only
  core_pattern -> non-running systemd-coredump). Needs a non-ASAN DEBUG (-O0 -g)
  build under gdb, or a host with a live coredump pipeline, to backtrace.

Updates research.md, refines task 5.4 (the unified restart-stability bug +
the debug-build path), updates bug-1281. Consensus correctness (Phase-2/5.2/5.3)
is unaffected and validated for every round that completes (2-round soak PASSES;
6-round rounds 1-3 elect; zero job loss; committed prefix byte-identical).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbTHR9CsEYK9RsqmQpQpC
2026-06-30 03:57:51 +00:00
75243fbb86 test(raft-ha-core-safety): capture controller stderr in soak; root-cause 7.2 6-round blocker to a restart SEGFAULT (bug-1281, task 5.4)
run-ha-soak.sh start_node previously sent each controller's stdout+stderr to
/dev/null, hiding all crash output. Capture it per-node (logs/<node>.stderr).
That immediately revealed the true 7.2 6-round blocker.

Findings (research.md §"Task 7.2 on ~/tmp"):
- On the user's non-overlay ~/tmp, a 2-round chaotic soak COMPLETES and 6-round
  rounds 1-3 each elect a higher-term primary — 5.2 + 5.3 hold live, job safety
  holds (zero committed-job loss; committed prefix byte-identical across members).
- The 6-round stall is a RELEASE-BUILD SEGFAULT: a controller segfaults seconds
  after being restarted into the churning cluster (recovery->failover/PreVote),
  dropping the cluster to 1-of-3 so the survivor can't reach quorum. Captured
  stderr: "<pid> Segmentation fault (core dumped) ... --myname ctrl-3".
- NOT consensus, NOT the in_catch_up deadlock (5.3 fixes that, unit-proven), NOT
  bug-924 (a separate fdatasync wedge hit one earlier run only). ASAN-invisible
  heisenbug; no gdb / no usable core in the sandbox, so the backtrace needs a
  debug+core+gdb env.

Adds task 5.4 (root-cause+fix the chaotic-restart segfault) and bug-1281;
re-attributes the 7.2 block from bug-924 to the segfault. Also notes the soak's
divergence over-count (boundary 1+2*njobs vs primary committedLsn) as a separate
Phase-1 accuracy fix. Consensus work (Phase-2/5.2/5.3) is unit-complete and live-
confirmed for every round that completes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbTHR9CsEYK9RsqmQpQpC
2026-06-30 03:33:25 +00:00
5d72133216 docs(raft-ha-core-safety): task 5.3 done (unit) + 7.2 re-attributed to bug-924 env block
5.3 fix (bf88681) lands and is unit-validated (test_ha_core_safety 20 Tests /
0 Failures). Re-ran the 7.2 chaotic soak with all Phase-2+5.2+5.3 fixes: it
still stalls at round 4, but bisecting the frozen logs shows the cause is the
bug-924 WAL-fdatasync wedge, NOT consensus — a survivor's persist flusher spins
on `fdatasync FAILED (errno=22 EINVAL)` and stops answering PreVotes, so the
round times out (presents as an election stall, is a durability-layer wedge).

A file-level repro of the controller's exact WAL pattern (open + posix_fallocate
64MB + write + fdatasync) SUCCEEDS on every available ext4/tmpfs path, so it is
the live overlay-backed WAL fd under chaotic restart — the documented bug-924,
a Non-Goal of this spec (owned by ha-commit-durability, excluded from the
verdict).

Re-attributes the round-4 stall (the first §"Task 7.2" note guessed in_catch_up;
5.3 fixes that real latent eligibility deadlock at unit level but it is not the
soak blocker). 7.2 is now BLOCKED ON ENVIRONMENT, not a consensus NO-GO; the
consensus work this spec owns (Phase-2 payload integrity, 5.2, 5.3) is
unit-complete. research.md §"Task 5.3 + 7.2 re-run"; bug-1272 updated.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbTHR9CsEYK9RsqmQpQpC
2026-06-30 02:56:00 +00:00
bf88681fb0 fix(raft-ha-core-safety): catching-up survivor must grant PreVote when primary gone (task 5.3)
The round-4 election stall surfaced by the 7.2 soak (bug-1272): after several
overlapping kills the only fully-up-to-date node is killed and the two survivors
are both restarted-and-lagging (in_catch_up=1) with no live primary left to catch
up FROM — so the flag never clears. Each holds the full committed prefix and is a
SAFE candidate, yet raft_cb_vote_eligible denied ALL PreVotes while in_catch_up
UNCONDITIONALLY, so they mutually denied and the election deadlocked forever.

Gate the in_catch_up deny on a LIVE primary (primary_ctrl_fd>=0 &&
ha_heartbeat_is_fresh), mirroring the heartbeat-fresh deny one block up. The
deny's purpose is to avoid disrupting a primary that is actively replicating to
us; once the primary is unreachable, raft_log_ok (already applied in Step 2 of
raft_on_pre_vote_request, before this callback) guarantees the candidate's log
>= ours, so granting a fresher candidate is safe (Leader Completeness preserved).

Adds 3 cases to test_ha_core_safety.c driving the real raft_on_pre_vote_request:
CORE RED (catching-up survivor + primary gone must GRANT a fresh candidate),
PIN (still DENY while a live primary feeds it), PIN (STALE candidate still DENIED
by logOk). test_ha_core_safety 20 Tests / 0 Failures; 5.1/5.2 unaffected.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbTHR9CsEYK9RsqmQpQpC
2026-06-30 02:44:45 +00:00
da25ad53e1 docs(raft-ha-core-safety): task 7.2 chaotic soak NO-GO — classify + add follow-up 5.3
First acceptance chaotic soak (6 rounds, real-fs ext4, release binaries rebuilt
at HEAD 01419e1 with Phase-2 payload fix + Phase-5 5.2 suppression). Outcome:

PASS (job safety + landed fixes):
- Zero committed-job loss (all tracked jobs present on the new primary each round).
- Committed prefix [1..11] byte-identical across all 3 members — Phase-2 payload-loss
  signature absent. All counted walcompare divergences are present=2 catch-up-LAG
  MISSING tails of restarted-uncaught followers (tips 19/15/11), not corruption.
- Rounds 1-3 elect a strictly higher-term primary; 5.2 spurious-failover suppression
  confirmed working (round-4 genuine-loss takes the failover path, not suppression).

NO-GO (R8.3 liveness):
- Round-4 election STALL after 3 overlapping kills: the up-to-date leader (tip 19)
  is killed, both survivors are lagging (15, 11) but each holds the full committed
  prefix; their PreVote rounds repeatedly time out with no grant quorum, so no
  primary is elected. Distinct from the 5.2 flap. Contributors: freshness
  mutual-denial, in_catch_up-denies-all-votes (Req 2.5, obs 1240) with no live
  primary to catch up from, and/or unestablished peer vote connections.

Per the chaotic-harness contingency: recorded full classification in research.md
§"Task 7.2"; added task 5.3 (lagging-but-safe survivor must be electable under
overlapping churn); 7.2 marked _Blocked_ on 5.3; logged bug-1272. Frozen evidence
at /home/claude/tmp/ha-7p2-frozen.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbTHR9CsEYK9RsqmQpQpC
2026-06-30 02:26:47 +00:00
01419e1310 fix(raft-ha-core-safety): suppress spurious PreVote failover on transient RDHUP (task 5.2)
A recovered standby whose control channel flaps (transient RDHUP) while a
current-term primary is alive treated each flap as "primary died" and launched
an unwinnable PreVote failover (peers see the live primary and deny, the round
times out, the channel re-establishes, repeat ~15s) — so the node prevote-spun
forever instead of rejoining, wedging the chaotic soak (research.md §"Phase 4/5").

Add a freshness guard at the RDHUP failover-initiation path
(ha_on_primary_ctrl_fd_rdhup): when CTRL_STANDBY and ha_heartbeat_is_fresh,
stay CTRL_STANDBY, re-arm the liveness watchdog (production), and await the
primary's reconnect instead of campaigning — the same guard
ha_failover_stagger_fn already applies on the deferred PreVote path. Genuine
loss (stale heartbeat) falls through to failover unchanged; no term bump / no
role change, so Election Safety is untouched.

Keeping the standby attached lets catch-up complete, which clears in_catch_up
and lets votes flow again — so this one guard also resolves the downstream
catching-up-node vote-deny deadlock (obs 1240).

Flips task 5.1's CORE RED to GREEN: test_ha_core_safety 17 Tests / 0 Failures
(spurious launches=0/5; genuine-loss final_term=8; Election Safety A=3/B=0 →
1 primary). Zero failing suites binary-wide; LSan unchanged vs baseline.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbTHR9CsEYK9RsqmQpQpC
2026-06-30 02:03:50 +00:00
806257afcf test(raft-ha-core-safety): RED tests for election liveness & safety under churn (task 5.1)
Add three Phase-5 cases to test_ha_core_safety.c pinning the spurious-failover
root cause (research.md §"Phase 4/5"):

- CORE RED test_5_1_spurious_failover_suppressed_while_primary_alive: a
  control-channel flap (transient RDHUP) while a current-term primary is alive
  (fresh heartbeat) must NOT launch a PreVote failover. FAILs on baseline
  (Expected 0 Was 5) — the RDHUP-initiation path (ha_on_primary_ctrl_fd_rdhup
  -> ha_begin_failover -> ha_start_prevote_async synchronous branch) has no
  ha_heartbeat_is_fresh guard, unlike the deferred ha_failover_stagger_fn path.
  GREEN when task 5.2 adds that guard.

- LIVENESS test_5_1_genuine_loss_elects_and_advances_term: genuine loss (stale
  heartbeat) still launches a real failover; a failed round re-arms; rapid
  rounds advance the term. Guards 5.2 against over-suppression (Req 6.1/6.2/6.3).

- SAFETY test_5_1_election_safety_one_primary_per_term: two candidates contest
  the same term; the real handle_request_vote grants at most one vote per term
  (A=3/B=0), so at most one committing primary per term. Falsifiable Election
  Safety check mapped to raft/raft.tla (Req 4.1/4.3).

Deterministic (no sleeps; clock reads only), c==NULL unit-test mode, clean
ha_fini teardown. test_ha_core_safety suite: 17 Tests / 1 Failure (the
intended RED); all 2.x/4.x/seam cases still pass; no new ASAN leaks.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dbTHR9CsEYK9RsqmQpQpC
2026-06-30 00:22:33 +00:00
7507a113f8 chore(ha-term-safety): mark tasks 5.2/5.4/5.5 complete — live verification done
5.2: clean failover STRICT_TERM verified — committed prefix byte-identical
     (term,pcrc) across all 3 members; standbys persist non-zero term;
     0 committed-job loss.
5.4: chaotic 3-round soak — zero job loss; committed prefix consistent
     at every walcompare checkpoint; all 3 nodes led; round-3 election
     stall is pre-existing bug-851/795 family (NOT regression).
5.5: build+unit suite green (19 groups 0 failures, ASAN clean).

Ha-term-safety: ALL 21 tasks complete. Spec done.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 08:19:14 +00:00
7bc23a20b6 chore(ha-term-safety): mark task 5.2 complete — clean failover STRICT_TERM verified live
Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 07:20:50 +00:00
af25c483e1 chore(raft-drive-async): mark task 4.3 complete — thread-role compliance verified
Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 04:39:50 +00:00
016a5dc0a1 chore(raft-drive-async): mark task 4.1 complete
Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 04:37:50 +00:00
837c35c255 test(raft-drive-async): network-partition soak primitive + RED/GREEN scenario (task 4.1)
Add in-process partition blackhole primitive (no privilege required):
- Evict partitioned peer from standby_endpoints so reconnect fires
- RED mode: reconnect connect parks in gate → blocks delaymgr tick
  (proves pre-change build heartbeat stalls on unreachable peer)
- GREEN mode: real offload path runs → slowlane_submit non-blocking
  → tick returns promptly → survivor heartbeats continue every tick
- Heal: re-add peer, both standbys receive heartbeats post-heal

Requirements: 7.1, 7.2, 7.3, 7.4
Boundary: PartitionTest

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 04:37:20 +00:00
01b7d6b7d5 test(raft-ha-core-safety): follower-reconvergence discriminator — Phase 4 sound (tasks 4.1/4.2)
Deterministic follower-level test drives the REAL ha_consume_append_entries
with a leader-style backtracking AE sequence against a multi-entry former-
primary divergent tail (12-15 @old term, committed 1..11). Follower
backtracks, conflict-truncates 14/15, accepts leader term-6 entries,
preserves committed prefix. Reviewer adversarially falsified (neuter intake
=> reached=0). VERDICT: follower-side reconvergence + applied-agreement gate
already sound on baseline => NO Phase-4 production fix needed. Redirects the
live-stall root cause to Phase 5 (channel-flap + in_catch_up-blocks-prevote-
reply). 4.1 + apply-gap pins green; 14/0 suite.
2026-06-28 03:42:06 +00:00
4fa04a852b docs(raft-ha-core-safety): retarget Phase 4/5 to RDHUP-flap root cause
Phase-4/5 investigation found the live stall is a recovered standby with a
divergent uncommitted tail (former-primary shape) that never reconverges:
loops on ae_reject_logmismatch, its control channel flaps ~15s, each flap
spawns an unwinnable PreVote failover while a live primary exists.
- Phase 4 (Standby Reconvergence): lead with divergent-tail truncate+catch-up
  (make the AE backtrack + follower-conflict-truncate fire for a former-primary
  tail, not just a short suffix), keep the applied/log_len healthy gate.
- Phase 5 (Election Liveness): retarget from 'missing election-restart' (retry
  works) to spurious-failover suppression — reconnect+catch-up on transient
  RDHUP, don't storm unwinnable PreVotes while a current-term primary is
  reachable. 5.2 now _Depends 4.2_.
2026-06-28 03:24:12 +00:00
8974f2cc97 research(raft-ha-core-safety): Phase-4/5 stall root cause — RDHUP-flap spurious failover
Post-Phase-2 chaotic wedge root-caused from frozen soak logs: primary stable
at term 6, but recovered standby ctrl-2 (divergent uncommitted tail 12-15)
cannot reconverge; its primary control channel RDHUP-flaps ~15s, each flap
spawns an unwinnable PreVote failover (peers see primary alive, no grant) ->
timeout -> reconnect -> repeat, so the cluster never reaches stable 3-of-3.
Fix spans Phase 4 (truncate divergent tail + catch up like ctrl-3 does) and
Phase 5 (don't launch unwinnable failover storm on transient RDHUP while a
current-term primary is reachable; reconnect+catch-up first). RED direction
recorded. (Dispatched investigator hung; reconstructed by parent.)
2026-06-28 03:12:45 +00:00
d89f7e41bc research(raft-ha-core-safety): Phase-2 interim live validation — payload-loss eliminated
Rebuilt release at HEAD (2.2+2.4), chaotic soak on real ext4. Universally-
held committed prefix (1..11) content-identical across all members; the
present-but-empty committed divergence (bug-1.3 signature) is ABSENT. The
fix eliminates the cross-term payload-loss divergence. Remaining issues:
chaotic soak wedges on election stall (Phase 5) before its verdict; tail
MISSING = catch-up lag (Phase 4). Clean soak verdict gated on Phase 5/4,
not Phase 3 (snapshot, off the critical path).
2026-06-27 15:56:50 +00:00
63b9c5a81a fix(raft-ha-core-safety): streamer fail-loud bounded-retry, never ship empty (task 2.4)
Root-cause source fix: ha_stream_wal_to_standby reads the entry body via
stream_read_entry_body (bounded retry max 4, no sleep); on persistent short
pread/malloc-fail it fails loud + ABORTS the stream (flush valid prefix,
drop unreadable entry, -EIO) instead of shipping PLEN=0. Incomplete stream
re-driven by ACK-driven catch-up. With the 2.2 guard this closes the live
cross-term payload-loss divergence. 2.3 CORE RED -> GREEN (fail-loud: no
empty persisted; + retry-recover test ships full 64). Term-blind structural
retirement deferred to 6.2 (empty-over-full safety enforced by 2.2 guard +
pinned). 12/12 core suite; siblings green; no regression introduced.
Phase 2 complete.
2026-06-27 15:42:37 +00:00
ae8291ed56 test(raft-ha-core-safety): cross-term streamer payload-integrity RED (task 2.3)
Minimal XCE_TESTING fault-injection seam in ha_stream_wal_to_standby
(NULL/production-inert) forces a short body read on a cross-term entry.
CORE RED proves the streamer ships PLEN=0 for a full (source_len=64)
cross-term entry: instrumentation source_len=64 SEND_len=0 RECV_len=0 ->
loss is SEND-side (answers task 1.3's open send-vs-transitive question).
2 honest pins (term-blind route guarded by 2.2; below-tip fence). 11
tests/1 RED; production xce_controller links clean without the seam.
2026-06-27 11:24:16 +00:00
056675c8b3 fix(raft-ha-core-safety): (term,lsn,payload-crc)-aware persist guard (task 2.2)
Payload-aware repair: identical payload = idempotent no-op; non-empty
differing payload at the TIP = repair (truncate-to-lsn-1 + re-append,
committed_lsn snapshot/restored under lock); empty-over-stored = keep
(never degrade); BELOW-tip conflict = loud non-promotable fence
(recovery_unrecoverable), never truncate the tail or lower committed_lsn.
read_entry_meta_locked computes payload-crc identically to walcompare.
Turns 2.1 case (a) green; new below-tip regression test pins the
committed-boundary safety. Review round 1 caught + fixed a committed-data-
loss bug (tail-truncate on below-tip conflict). 8/8 target suite, sibling
persist/repl/catchup/durability suites green; persist-before-ACK intact.
2026-06-27 10:57:37 +00:00
3b5eda1429 test(raft-ha-core-safety): payload-aware persist-guard RED + pins (task 2.1)
Case (a) empty-then-full = genuine RED (payload-blind guard at persist.c:822
no-ops the corrective write, Expected 64 Was 0). Cases (b) full-then-empty
and (c) identical-rewrite = baseline-passing pins that constrain the 2.2
fix asymmetry (empty must never overwrite stored full; identical re-stream
stays idempotent). 7 tests/1 RED/no ASAN.
2026-06-27 10:20:02 +00:00
07f08e58f5 test(raft-ha-core-safety): client-probe soak primary-detection (task 1.5)
Release build block-buffers --logfile (flush on signal only), so log-grep
primary/convergence detection false-aborted. Replace all log-grep detection
with client probes (xce_test ctrlstatus): probe_primary/node_probe/
cluster_committed_converged. Both modes use shared helpers; 1.2 verdict
self-test 17/17 intact; bug-854 isolation preserved. Live release-binary
runs reach the verdict (chaotic re-confirms committed_divergence=3 / job
loss 0; sequential PASS). No production code touched. Phase 1 complete.
2026-06-27 10:12:50 +00:00
dc50924833 test(raft-ha-core-safety): harness infrastructure + payload-loss smoke-RED (task 1.4)
New test_ha_core_safety.c with deterministic gate/probe seams (no sleeps,
drain-before-fini per bug-1224): payload-controllable WAL writer, on-disk
(term,pcrc,len) readback probe, walcompare observer, apply-gap + kill/
election injection for phases 2.3/4.1/5.1. Smoke-RED (Expected 64 Was 0)
proves the payload-blind (term,lsn)-only guard at persist.c:822 drops a
corrective full-payload write at a committed LSN. 5 tests/1 RED/4 seam
PASS, no ASAN errors.
2026-06-27 09:49:51 +00:00
9e55f09274 docs(raft-ha-core-safety): retarget Phase 2 to cross-term payload loss (post-1.3)
Task 1.3 refuted the term-blind hypothesis; real defect = cross-term
catch-up payload loss (streamer ships PLEN=0 on short pread/malloc-fail;
(term,lsn)-only guard is payload-blind). Retarget:
- design: new Catch-up Payload Integrity component (config_repl.c core fix);
  Persist guard -> (term,lsn,payload-crc); term-blind demoted to defensive;
  flows/traceability/testing/hypothesis sections updated.
- tasks: Phase 2 retargeted (streamer fail-loud + payload-aware guard);
  new 1.5 (release-log-buffer soak robustness); 7.2 _Depends 1.5_.
- research: old root-cause + synthesis D1/D3 marked SUPERSEDED.
Independent sanity review PASS after adding the 1.5 dependency edge.
2026-06-27 09:38:06 +00:00
21ce4c4f4a research(raft-ha-core-safety): live divergence bisect REFUTES term-blind hypothesis (task 1.3)
Reproduced committed divergence at LSN 9 (real-fs release soak): all 3
members term=3, minority ctrl-1 pcrc=0/PLEN=0 vs majority PLEN=3560 =
PAYLOAD loss, term correct. Bisect: arrived via the reconcile seam under
cross-term catch-up (stored term 3, leader term 4 / bug-1067 path), NOT
the term-blind bypass. Real defect = cross-term catch-up payload loss in
ha_stream_wal_to_standby (silent empty ship on short pread/malloc-fail).
Independently verified against frozen WALs. Secondary: release-build log
buffering breaks soak primary-detection.
2026-06-27 09:18:26 +00:00