Commit graph

92 commits

Author SHA1 Message Date
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
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
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
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
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
294b9ce8eb chore(raft-drive-async): task 4.2 regression sweep — no new regression (baseline-confirmed)
Verdict: raft-drive-async introduces NO new regression.

- Unit: controller_unit_test EXIT=0, xcelib_test 282/0 (all HA suites green;
  dispatch/snapshot/configpush flags default OFF, inert).
- Live kill-based failover soak on a real fs (~/tmp), release + ASAN builds:
  zero committed-JOB loss every completed round (jobs 3->12 monotonic).
- The 6-round CHAOTIC soak does not run clean to round 6 (fails ~round 4), but a
  baseline comparison against pre-Phase-3 binaries (commit 21e26a1) fails round 4
  IDENTICALLY (election stall / reconverge). The round-4 committed-prefix
  divergence at LSN 9 is the documented pre-existing bug-851/795 family
  (chaotic-failover silent divergence), present before Phase 3 and out of scope
  for this spec. Logged as bug-1225 (baseline-confirmed).

Also: make the xce-test-ha soak driver's controller/xce_test paths
env-overridable (XCE_HA_BIN / XCE_HA_XT, backward-compatible defaults) — used to
run the baseline binaries through the same driver for the regression comparison.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S6fDCtXkkEgupu7rUah1y2
2026-06-27 03:04:23 +00:00
d2830ce514 test(raft-drive-async): receiver-side dispatch liveness integration tests (task 3.4b)
With the dispatch flags flipped ON in-test, prove the three receiver-side
liveness properties (production defaults stay OFF):

- P1: while a snapshot (and config-push) transfer is parked in flight on the
  slow lane, another endpoint's peer-control message is still processed
  promptly on the vtask pool (different thread), with the transfer still
  parked — the offloaded transfer does not block the drive path.
- P2: a peer-control ack (classified VTASK_LIVENESS) runs promptly even while
  the pool is saturated to the LivenessAdmission reservation boundary with
  REQ_BLOCK tasks (one more REQ_BLOCK fast-rejects with -E_XCE_NOT_READY,
  proving real saturation). Narrowed to the liveness-dispatch-under-saturation
  assertion; commit-advance itself is covered by the commit-engine tests.
- P3: while one endpoint's peer-control body is parked (holding a worker + that
  ep busy), another endpoint's body runs concurrently on a different worker —
  per-endpoint FIFO does not serialize across endpoints.

All deterministic (cond-var gates, bounded polls). Fixed a flaky teardown-race
hang (bug-1224): the tests released a parked vtask body then immediately called
the cancel-based vtaskmgr_fini while the just-released body was still unwinding
(P2's REQ_BLOCK bodies racing blk_lock teardown the worst). Fix adds end-of-body
completion counters polled to the full expected count before fini, so every
pool body has fully returned (worker idle) before the cancel. slowlane_fini is
graceful (join) so the lane side was already safe.

Verified: 5/5 deterministic isolated runs; full controller suite EXIT=0;
ASan-clean. Completes task 3.4 (3.4a UAF fix + 3.4b tests); the production
flag-flip remains deferred to after the 4.1 partition soak + 4.2 regression
sweep.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S6fDCtXkkEgupu7rUah1y2
2026-06-27 00:36:22 +00:00
6d510d8cf4 fix(raft-drive-async): endpoint-quiescence for bug-1197 vote-ep teardown (task 3.4a)
When the (still-default-OFF) peer-control dispatch flag is ON,
raft_cb_become_follower runs as a vtask pool worker and frees every peer
vote-ep while other pool workers may still hold those eps' data — a UAF
(bug-1197). This makes that path quiescence-safe so the flag can later be
flipped ON (deferred to the 4.x prod-flip gate; production stays OFF here).

Mechanism (no libxce-wide endpoint refcount; reuses the existing per-ep FIFO +
busy serialization):

- Deferred teardown-as-vtask: the loser loop, when dispatch is ON and the pool
  is wired, does per vote-ep endpoint_mark_tearing_down -> iomgr_unmonitor ->
  enqueue a VTASK_IO/VTASK_LIVENESS teardown vtask bound to that ep. Per-ep busy
  FIFO makes it run strictly after any in-flight/queued same-ep peer-control
  vtask; the body NULLs iod->ep before endpoint_fini+free(ep) to disarm
  run_task's post-body endpoint_unblock_tasks (which would touch the freed
  ep->lock). OFF / pool-unwired path is byte-for-byte the legacy endpoint_fini.

- RCU-like drain closes the dispatch/teardown TOCTOU window: new _Atomic int
  endpoint_t.dispatch_inflight (+ tearing_down:1 bit). The ON dispatch brackets
  its flag-check+enqueue with fetch_add/fetch_sub (seq_cst);
  endpoint_mark_tearing_down sets the flag under ep->lock, releases, then spins
  until dispatch_inflight drains to 0. So every dispatch that read the flag
  false finishes its enqueue before mark returns (FIFO-before the teardown
  vtask); any later dispatch reads the flag set and drops the packet. ep->lock
  is deliberately NOT held across the enqueue (it inverts with the taskq lock in
  vtask_eligible); the counter is the lock-free handshake. Drain is bounded: the
  held window is just a calloc + non-blocking enqueue on the single io_worker.

Still open before the production flag flip (4.x): the borrowed-ep race in
snapshot_lane_fn/configpush_lane_fn is distinct (slowlane thread vs io_worker
RDHUP teardown, not the vtask-pool FIFO) and needs its own per-standby-ep
discipline.

Tests: test_ha_vep_teardown.c — 5 ASan tests (ordering, late-event drop, run_task
unblock disarm, OFF-path unchanged, deterministic mark-drains-inflight-dispatch).
xcelib_test 282/0; controller_unit_test EXIT=0; ASan-clean. Adversarial
cpp-review: no CRITICAL/HIGH; handshake formally sound, flag-ON path race-free.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S6fDCtXkkEgupu7rUah1y2
2026-06-26 17:37:26 +00:00
6c0523b69b feat(raft-drive-async): snapshot SlowLane offload + WAL-trim cursor guard (task 3.2)
Part B (LIVE, default-ON): trim_wal_segments now bounds WAL-segment deletion
by repl_trim_lsn(&p->repl, active_seg_lsn) = min(active segment LSN, every
active standby acked_lsn, every valid disconnected-slot last_disconnected_lsn),
after repl_expire_disconnect_slots ages out stale slot guards. floor <=
active_seg_lsn structurally, so the guard can only preserve more segments,
never delete one a live standby still needs — closes the bug-795/828
trim-vs-cursor race for connected/recently-disconnected standbys (bug-1147
stopgap; empty/cursor=0 join still needs entity-bearing snapshots).

Part A (DORMANT, g_drive_async_snapshot_offload default OFF): ha_snapshot_send_
dispatch routes HA_REPL_SEND_SNAPSHOT inline (flag OFF — byte-for-byte legacy,
on io_worker) or to slowlane_submit+snapshot_lane_fn (flag ON). Send body
factored into ha_snapshot_do_send. Per-peer single-flight via
ha_state_t.snapshot_inflight[] CAS + slowlane key; in-flight flag cleared on
all four terminal paths, mirroring the reconnect offload (task 2.1).

Lands dormant like task 3.1: snapshot_lane_fn borrows a standby endpoint it
does not own, so flag-ON would race io_worker RDHUP teardown (cross-thread UAF,
bug-1197 family). Flag OFF keeps send+teardown serialized on the io_worker.
Task 3.4 is the single gate that flips both this and the peerctrl flag ON after
establishing endpoint-lifetime discipline; bug-1198 stays open by design until
then. The offload flag was initially authored default-ON (would ship the UAF)
and corrected to OFF in ha.c and the ha.h accessor doc (bug-1210).

Tests (controller 196/0, 0 new leaks): test_snapshot_offload_runs_on_lane,
test_snapshot_inline_when_flag_off, test_snapshot_offload_dedups_per_peer,
test_trim_guard_preserves_standby_segments,
test_trim_without_standbys_proceeds_normally. Adversarial cpp-review: GO.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S6fDCtXkkEgupu7rUah1y2
2026-06-26 15:43:24 +00:00
e17a6a613c feat(raft-drive-async): peer-control vtask dispatch shim, dormant (task 3.1)
Lands the PeerCtrlDispatch machinery behind a DEFAULT-OFF flag
(g_drive_async_peerctrl_dispatch_enabled); production behavior is
UNCHANGED. peer_ctrl_msg_handler becomes a thin shim that takes
ownership of ep->fsm_in.pkt and either runs the routing body inline
(flag OFF, byte-for-byte the prior path) or dispatches it to the vtask
pool via vtask_enq_classified (flag ON). The 3 routers now take pkt as
an explicit param (no cross-thread ep->fsm_in.pkt re-read). Per-endpoint
arrival order is enforced by bind_vtask's endpoint_block_tasks.

Packet ownership: shim clears ep->fsm_in.pkt at dispatch (sole-owner
transfer); peer_ctrl_vtask_entry NULLs iod->pkt before the body frees
it once. New vtask_t.owns_io_pkt (default 0) lets taskq_item_killer free
the carried packet for an un-executed queued task at vtaskmgr_fini WITHOUT
double-freeing the libxce endpoint-owned path (bug-1199).

Adversarial review found two FLAG-ON hazards, documented with loud
comments (flag is OFF so production is unaffected), to be resolved before
task 3.4 flips the flag ON:
  - bug-1197 (CRITICAL): raft_cb_become_follower endpoint_fini()s peer
    vote-eps from a pool worker, racing the io_worker's read of ep->data;
    needs iomgr_unmonitor before endpoint_fini in every pool-reachable
    teardown.
  - bug-1198 (HIGH): peer_ctrl_classify tags all work VTASK_LIVENESS, but
    the CTRL_PRIMARY repl-ack path can run a blocking snapshot checkpoint
    inline; resolved by task 3.2 (offload snapshot to SlowLane).

Per spec, 3.1 is not 'done' until the task 3.4 deadlock gate is green.
Tests: 3 new peerctrl tests; controller 196/196, lib 282/0; 0 net leaks.
2026-06-26 11:12:04 +00:00
295769d8ae add wolf state 2026-06-25 14:30:46 +08: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
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
181688498e chore(ha-term-safety): record env unblock — flake refreshed, controller suite green
bug-918 resolved (c-capnproto flake repin); clean controller build passes full
suite (Ok:1 Fail:0). Earlier wal_catchup failures were a stale incremental-build
artifact, not fsync/regression. Validation gate reliable for remaining tasks.
2026-06-22 03:54:56 +00:00
20e59d0b42 checkpoint: bug-851 root cause + raft TLA+ conformance spec family
Investigation & fixes (failover committed-job loss):
- root-cause bug-851 on an owned 3-node cluster: chaotic failover causes
  silent committed-prefix divergence (WAL hdr.term always 0; two leaders
  per term; no (term,lsn) reconcile). Evidence in repro-851/ + INVESTIGATION.
- live-validated fixes 848/849/850 (recovery-on-promotion, advertise real
  applied_lsn, persist-layer contiguity guard); add waldump WAL inspector.

Specs (strict raft.tla conformance — library owns the log):
- TLA-gap-analysis.md: impl vs raft/raft.tla, gaps G1-G6.
- raft-log-replication, raft-commit-safety, raft-sim-conformance: extend
  src/lib/raft to own log/commit/verification via callbacks (specs generated,
  cross-spec review passed).
- ha-term-safety: controller-side adapter + config marker (R1-R7); design done.
- config-upgrade-snapshot: R8 descoped here (deps entity-bearing-snapshots, bug-857).
- roadmap + buglog (851/854/857) + .wolf knowledge updated.
2026-06-21 14:02:15 +00:00
7d70820aac docs(ha-commit-durability): root-cause findings for failover committed-job loss (bug-828)
WAL-byte decode proves REAL data loss (not commit-index): committed prefix
entries physically absent from live nodes (ctrl-2 WAL starts LSN 16, ctrl-3 LSN
13), present only on the down node. Mechanism: empty-rejoin catch-up streams
[watermark..committed] not [1..], no snapshot to backfill the prefix -> permanent
gap. Predates libxce-failover. Full findings + open forks + xce_test waldump
tooling plan in INVESTIGATION-failover-job-loss.md. Not yet fixed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 13:51:40 +00:00
cdc6e2377a spec(controller-status): mark implemented (E2E pass; primaryName bug fixed); log upstream dead-first-master failover finding 2026-06-20 06:04:41 +00:00
480772c108 spec(ha-commit-durability): 6.1 answered — durability loss + liveness are REAL (reproduce off overlay), not an fdatasync artifact; Phase 6 now at 6.2 root-cause 2026-06-20 03:19:57 +00:00
c205b5b25d spec(ha-commit-durability): sharpen bug-795 — durability loss reproduces in rolling failover (trustworthy soak) + >60s election liveness finding 2026-06-20 01:28:26 +00:00
aa615a184a chore(buglog): strengthen bug-786 (stale reply id); log bug-795 cold-restart-all durability gap (upstream) 2026-06-19 17:03:43 +00:00
ffc2c590f9 fix(libxce-failover): correct endpoint reconnect on rotate — re-bind iomgr + re-arm handlers (fixes endpoint_connect SEGV during live failover)
xce_failover_rotate reconnected with endpoint_fini+endpoint_init+endpoint_connect
but never re-bound ep->iomgr (reset by endpoint_init) nor re-armed the owner/
msg/err handlers + iomgr_monitor. endpoint_connect then dereferenced a NULL
iomgr (->ssl_ctx) → SEGV at netio.c:672 during live master failover. Added
xce_ctx_reconnect() in xcectx.c that mirrors xcecli_start's establishment
(without restarting iomgr/vtaskmgr or touching ctx->state); rotate delegates to
it. Found via live 3-node E2E. bug-610.
2026-06-17 06:53:46 +00:00
16f4ba67c7 docs(libxce-failover): log bug-609 (known-dead EINVAL) + record 6.2 live-E2E partial status 2026-06-17 04:46:45 +00:00
8c11655e53 fix(persist): interruptible fsync_worker sleep so persist_stop joins promptly
fsync_worker slept the full wal_sync_ms in one uninterruptible nanosleep()
before re-checking fsync_stop. persist_stop sets the flag and immediately
pthread_join()s, so the join blocked until the in-flight sleep returned —
up to a full wal_sync_ms per teardown. Tests set wal_sync_ms=5000 ("slow
fsync — no races"), so each persist_stop cost ~5s; across the ~7 suites
that use it the cumulative teardown time blew past the 300s controller-test
meson cap, killing the suite with SIGTERM.

Replace the nanosleep with pthread_cond_timedwait on a new fsync_mutex/
fsync_cond (added to persist_t, init in persist_init, destroyed in
persist_fini). persist_stop now sets fsync_stop and signals fsync_cond
under fsync_mutex before joining, so the worker wakes immediately. fdatasync
still runs under p->lock, released before re-acquiring fsync_mutex — no lock
nesting. The "commit: WAIT deadline expired" log lines were unrelated
intentional negative tests (50-300ms), not the cause.

Full controller suite now completes in ~10s (was >300s TIMEOUT); 0 failures.

Logged as bug-604.
2026-06-16 08:24:28 +00:00
c45f80ab2b commit missed 2026-06-16 15:42:43 +08:00
5a2fae1d37 feat(ha-commit-barrier-rollout): config barrier with production ACK ingestion (task 5.2 + r1)
configAck branch: per-push majority (acked_config_lsn >= push_tag,
1+count > n_peers/2 over r->ce->n_peers, n_peers<=1 immediate,
ce==NULL -EINVAL); legacy >0 counting and n_active==0 shortcut dead.
push_config_to_standbys returns the barrier result; upgrade failure ->
send_upgrade_error (rc forwarded), no success ACK, no restart,
committed_config_tag success-only.

Remediation r1 (two CRITICAL review findings):
- F1: wire production config-ACK ingestion — replconfigpushreplymsg
  decode branch in repl_ack_decode_and_dispatch -> repl_on_config_ack
  on the promoted-vote ep; + defensive cluster.c OPCode_reply case.
  Closes the unlanded half of ha-config-bootstrap 8.1; without this the
  barrier would brick every multi-member config upgrade. No schema change.
- F2: keep the barrier off the iomgr thread — export
  push_config_transport_only (non-blocking) for the 5 ha.c + join.c
  streaming sites; barrier path now vtask-only (do_upgrade_config).

Wire-path test drives real capnp ACK bytes through the production decode
branch. 30/30 rollout, 6/6 repl_majority, 46/46 config_repl; census 0
FAIL; pre-existing fixture leak only (less than baseline).
2026-06-13 03:46:29 +00:00
732e836868 test(ha-commit-barrier-rollout): fact-retention-across-quorum-loss proof (task 4.3)
Green integration proof: 4 facts driven during total quorum loss append
RETAIN with rc 0 in <1000ms (vs 5000ms timeout — wrongly-applied WAIT
tripwire), pending with retained-depth=4 logged, present exactly once
on disk (real WAL header scan); reconnect + one covering ACK commits
the suffix with current_lsn unchanged (no redelivery). Completes the
bug-396 fix evidence (umbrella Req 2.8). Census 495/495.
2026-06-12 15:29:47 +00:00
7103fa643d test(ha-commit-barrier-rollout): receipt-ACK verdict pins (task 3.3)
4 green pins: handle_jobdispatch replies receipt-only with zero WAL
appends through a real 2-alloc dispatch cycle (notify outcomes intact,
reply-precedes-processing); set/clr_preason and do_update_op rusage
stay receipt-style with RETAIN-pending fact appends ACK-committed.
Umbrella Reply-Ordering Contract cited per pin. DISCOVERY bug-451
(reviewer-verified): RUSAGE hook va_list type confusion — 4-byte OOB
read + garbage state in WAL_JOB_RUSAGE payloads; fix deferred to 7.1.
Census 488; zero failures.
2026-06-12 11:16:51 +00:00
facdb90917 feat(ha-commit-barrier-rollout): commit-first-then-reply in all four handlers (task 3.2)
Uniform pattern (ctrl fn -> persist_commit_barrier -> reply) in
do_jobcontrol_op/do_queuecontrol_op/do_hostcontrol_op; join barrier
between AFTER_EVENT and success reply (failure -> error reply, skip
on_worker_join). FIXES bug-447: key_find found-rc normalized inside
job_ctrl/queue_ctrl/worker_ctrl — queue/host control now actually
append WAL and replicate (was silently unpersisted); idempotent no-ops
now return 0. handle_jobsubmit untouched. 12/12 + 15/15; census zero
failures.
2026-06-12 10:28:25 +00:00
e1c3c173f4 test(ha-commit-barrier-rollout): RED commit-first-then-reply tests per opcode (task 3.1)
New test_commit_barrier_reply.c drives do_jobcontrol_op /
do_queuecontrol_op / do_hostcontrol_op / handle_worker_join directly
(pthread harness, ep->outq reply capture, wired engine n_peers=3).
12 cases: 8 RED (reply-before-commit + bug-447 pins), 4 validation
pins green. DISCOVERY bug-447 (reviewer-confirmed): key_find found-rc
leak means queue/host control never append WAL in production — 3.2's
rc normalization is the fix point. Census 484 = 472 + 12; zero
baseline regressions.
2026-06-12 10:00:38 +00:00
95a10cedf3 test(ha-commit-engine): full regression green + legacy-path audit (task 6.1)
Fixed the 3 bug-411 baseline failures honestly: stale timeout
expectation updated to the reject-on-timeout contract (engine-wired
fixture, killing power retained); two healthy-primary gate fixtures
populated with standby_endpoints (gate logic untouched). Census: 46
groups / 467 tests / 0 failures standalone under default ASAN.
Audit (reviewer-confirmed, recorded in tasks.md): no production walAck
path reaches the deprecated acked_lsn>0 counting or n_active==0
short-circuit except the documented LSN-0 first entry. Leak inventory
for 6 groups documented as follow-up.
2026-06-12 03:32:19 +00:00
d5a0f9c64d test(ha-commit-engine): RED tests for per-LSN majority evaluation (task 1.1)
commit.h umbrella-verbatim interface + inert stub commit.c (test target
only); 7 Unity tests covering per-LSN vs acked_lsn>0 counting, majority
median, monotonicity, append clamp, lagging standby, RETAIN immediacy.
All 7 fail RED as intended. Documented 3 pre-existing masked suite
failures (bug-411) and the per-group exit-code masking root cause.
2026-06-11 14:17:09 +00:00
06135954ff chore: baseline snapshot — prior-session work + ha-consistency-invariants spec family
Pre-existing uncommitted work: ha-repl-hardening payload data plane (7.x),
heartbeat liveness fix (Req 1.3), connection-reuse ASAN fixes, doc cleanup.
New: ha-consistency-invariants umbrella (requirements/design/research) and
its four implementation sub-specs (specs generated via batch, cross-spec
review applied), steering roadmap + product.md Raft+CP decision.
2026-06-11 13:21:50 +00:00
18e351ee1b feat(ha-wal-replication): repl_broadcast TCP send (task 1.1)
Replace no-send stub with real WAL entry transmission via
xce_wal_batch_req_raw. For each non-catch-up registered standby,
serialize the entry header+payload into a wal_catchup_entry_t
and enqueue. Keep the MAX_PENDING_ACKS eviction guard.

5 new tests: no-standbys, one-standby-send, skip-catch-up,
evict-overloaded, send-failure-continues. Controller suite green.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-06-10 08:11:05 +00:00
cdd446fc17 docs(specs): gap-analysis validation pass across all 9 HA-family specs
Appended "Gap Analysis — Validation Pass (2026-06-09)" to each spec's
research.md with requirement-to-asset maps, status tags, and effort/risk.

Key outcomes:
- ha-repl-hardening: REOPENED with task series 7.1-7.7 — replication
  payload data plane is missing (snapshot chunks and WAL catch-up ship
  header-only; standbys converge on LSN without holding entity state)
- worker-join-refactor, resval-id-decoupling: marked implemented
  (all criteria re-verified, full test suite green)
- buglog: 335/336 (scheduler dequeue, open), 337/338/339 (fixed in
  previous commit); cerebrum Decision Log corrected (term-only vote
  deny was recorded as intentional but was a regression)
2026-06-10 01:26:33 +00:00
fa66d1ba9c finalize configuration replication 2026-06-09 16:40:13 +08:00
e31673dd88 chore: checkpoint all prior work for clean state
Captures all uncommitted work from ha-config-bootstrap,
ha-heartbeat-liveness, and associated spec/tooling updates:

- src: ha.c, ha.h, cluster.c, config_repl.c, myapp.c, raft.c —
  heartbeat liveness, RDHUP integration, committing phase fixes
- tests: test_ha_role.c, test_config_repl.c, test_ha_rdhup_integration.c,
  test_p1_integration.c, test_p3_hardening.c, scheduler test
- specs: election-iomgr-refactor, ha-heartbeat-liveness,
  ha-peer-connection-reuse, raft-library (new); ha-config-bootstrap
  and election-iomgr-refactor task/research updates
- tooling: .wolf/ state, ha/data cluster fixtures, raft/raft.tla,
  src/build symlink, xce-build skill update
2026-06-04 13:04:16 +00:00