Commit graph

899 commits

Author SHA1 Message Date
781b0f7ef0 chore: update OpenWolf session state + cerebrum learning (Unity test-harness fix)
Records the controller_unit_test nested UNITY_BEGIN/UNITY_END fix
(commit a81f2a2) in cerebrum.md for future sessions, plus routine
anatomy/memory/token-ledger session bookkeeping.
2026-07-17 14:49:25 +00:00
a81f2a249c fix(controller-tests): make Unity test-file attribution and counters consistent suite-wide
Only 21 of 86 test_*.c group functions called their own nested
UNITY_BEGIN()/UNITY_END(). Each nested call reset Unity's global
NumberOfTests/TestFailures counters, so controller_test.c's final
`return UNITY_END();` -- the value meson test actually gates on --
only ever reflected whichever small window of tests ran since the
LAST nested reset, silently discarding pass/fail evidence for
everything before it (measured: 16 of 972 total tests in one run).
The same nested-BEGIN calls also caused PASS/FAIL banners to show
the wrong source file for tests in files that don't reset it
themselves (Unity.TestFile just carries over from whichever group
last called UNITY_BEGIN()).

Fix: replace every nested UNITY_BEGIN() with UnitySetTestFile(__FILE__)
(sets file attribution only, no counter reset) and drop the matching
UNITY_END(); add the same UnitySetTestFile(__FILE__) as the first
statement to the other group functions that had neither. The 2
genuinely standalone binaries (test_job_array_sched_push.c,
test_job_array_apiserver_push.c -- each has its own int main(), not
registered via controller_test.c's TEST() macro) are untouched.

Verified: single consolidated summary now reads
"972 Tests 0 Failures 0 Ignored OK", exactly matching the
ground-truth RUN_TEST call-site count across all 83 registered
group files (976 total minus 4 belonging to the 2 standalone
binaries). Every PASS/FAIL banner now names its own true source
file. Full xce-build.sh all regression pass clean (Ok: 9, Fail: 0).

No production code touched -- test-harness plumbing only.
2026-07-17 14:49:12 +00:00
e6956ecf46 chore: update OpenWolf session state 2026-07-17 14:16:50 +00:00
6592f9cbf1 chore: update OpenWolf session state + cerebrum learnings (meson parallel-timeout false-positive, /tmp/xce_* cleanup) 2026-07-17 13:07:00 +00:00
3f54ef307c chore: update OpenWolf session state (ha-term-safety + controller-status validation) 2026-07-17 11:01:58 +00:00
c08a34866d docs(controller-status): record first /kiro-validate-impl pass -- GO
All tasks (1.1-6.4) were already [x] with no prior validate_impl
record. Fixed stale delta.impl_status (still said task 6.4 blocked on
sandbox overlayfs despite tasks.md recording it VERIFIED live since
2026-07-02).

Full suite 9/9 green, 36/36 requirement acceptance criteria mapped,
cross-task wire contract verified field-for-field end to end (schema
-> typemap -> handler -> dispatch -> libxce client -> CLI), two live
smoke runs (3-round sequential HA soak + targeted 3-node deploy
exercising Req 10/11 directly) both PASS. One transient test failure
during the run traced to a concurrent second session competing for
the same build directory, not a regression -- reproduced clean 3x in
isolation.
2026-07-17 11:01:48 +00:00
84fde22bbd docs(ha-term-safety): record first /kiro-validate-impl pass -- GO
All 19 tasks were already [x] with no prior validate_impl record.
Full suite 9/9 green, 46/46 requirement acceptance criteria mapped,
fresh 6-round live chaotic-multi-kill soak PASS (zero committed
divergence/job loss), src/lib/raft protocol-ownership boundary
respected across all 35 tagged commits.

Three known_limitations recorded, all non-blocking: two are
design.md describing an originally-planned architecture that shipped
differently after reviewer-approved safety proofs (commit-source
switch stayed on the durable-majority barrier + own-term WAL_NOOP
gate rather than switching to raft_commit_index; AppendEntries send
side still goes through repl_broadcast, not raft_send_append_entries)
-- behavior is correct, docs just weren't updated to match. Third is
a stale TODO comment in ha.c whose own body already says IMPLEMENTED.
2026-07-17 10:53:06 +00:00
1a0960bfcf chore: update OpenWolf session state (bug-1445 fix) 2026-07-17 10:42:23 +00:00
ff01db3665 fix: bug-1445 -- first-boot promotion check races its own first replay
raft_cb_become_leader()'s eligibility gate read
persist_apply_resource_fenced() before this process's own first
cluster_init_phase2()/persist_recover()/replay_wal() call ever ran --
so a node whose first-ever replay hit a resource-exhaustion apply
failure could still become CTRL_PRIMARY, because the fence that
replay would engage didn't exist yet when the gate checked it. Every
LATER promotion evaluation was already correctly protected; this was
a narrow gap specific to a process's single very-first election.

Fix: re-check persist_apply_resource_fenced() immediately after
cluster_init_phase2() completes on first boot, refusing promotion
(same WARN + ha_arm_promotion_fence_timer() + return pattern as the
existing refusal branch) if that first replay just engaged the fence.

Confirmed ha_elect_evaluate_and_signal (the other election path) is
dead in production for the promotion outcome -- every live call site
of handle_request_vote_reply passes vote_granted=0 since bug-828
redirected real grant recording to the record-only, non-signaling
ha_record_vote_reply -- so raft_cb_become_leader is the sole live
promotion path and this fix's scope is complete.

TDD: new test test_first_boot_promotion_evaluates_own_first_replay_fence
wires ha_phase2_hook to actually call persist_recover(), reproducing
production ordering exactly. RED-confirmed against the unfixed code
at the precise predicted assertion; GREEN-confirmed after the fix.
Full controller_unit_test regression (bundles every HA-adjacent
testsuite/test_*.c) green, 0 new failures.

Originally documented (not fixed) in job-array-single-wal-record
task 7.2's Implementation Notes; logged as bug-1445 and fixed here,
outside that spec's own task list per its persist.c-only boundary.
2026-07-17 10:42:13 +00:00
5ddc20f359 docs: log bug-1445 for job-array-single-wal-record's first-boot promotion gap
Root cause and reproduction path were already documented inline in
tasks.md task 7.2's Implementation Notes but never had a standalone
buglog entry. Cross-referenced from spec.json's known_limitations.

Not fixed -- suggested shape captured in the bug entry: a small,
single-call-site ha.c ordering fix (move the first promotion-gate
evaluation to run after the first replay_wal(), or have that one gate
call treat "not yet replayed" as non-promotable), not a new Kiro spec.
2026-07-17 10:10:16 +00:00
fb701c24a1 chore: update OpenWolf session state (job-array-single-wal-record validation pass) 2026-07-17 10:06:10 +00:00
9cf61c9fc1 docs(job-array-single-wal-record): record fresh /kiro-validate-impl GO, close stale notes
spec.json: phase -> implementation-complete, new GO decision (2026-07-17)
covering the full feature plus phase 7's residual-gap closure; prior
decision preserved as prior_decision; bug-1443 known_limitations entry
retired (closed by task 7.1) and replaced with the one limitation that
is still genuinely open (task 7.2's first-boot promotion-ordering gap).

tasks.md: task 3.1's implementation note updated from "open, unowned
gap" to "closed by task 7.1" -- it predated that fix and had gone
stale.

Validation evidence: full C suite 9/9 green (incl. libxce test post
rtti-fix), Go suite green (88.1% backend coverage), clean TBD/secrets
grep, live array-soak smoke PASS (O(1) replay time confirmed at
2-element vs 10,000-element scale), cross-task integration verified at
every seam phase 7 touched, 10/10 requirement groups covered, zero
design drift, zero blocked tasks.
2026-07-17 09:46:16 +00:00
ccee18b0c9 chore: update OpenWolf session state 2026-07-17 09:28:59 +00:00
2cfb454119 docs(job-array-single-wal-record): mark task 1.3's rtti leak note FIXED
Upstream librtti repin (see prior commit) resolves the SIGABRT this
note warned future tasks to expect and ignore -- updated so it no
longer reads as a live gotcha.
2026-07-17 09:28:52 +00:00
8c7d26a9ce chore(nix): repin librtti to fix makeInstance leak-on-init-failure
librtti master 7212ba1 -> 53cf11e (revCount 20->21): makeInstance() was
not freeing its calloc'd allocation when a type's .init() returned
negative, causing a 240-byte LeakSanitizer leak (SIGABRT under
abort_on_error=1) on any job_init() failure path -- most visibly
libxce test's test_job_init_propagates_makeJobsub_failure.

Verified fixed: fresh rebuild resolves the new rev, full libxce test
suite green, and test_jobj_hardening passes clean under
ASAN_OPTIONS=detect_leaks=1:abort_on_error=1 with no leak stack.
2026-07-17 09:28:47 +00:00
9176164f4d chore: update OpenWolf session state (anatomy, cerebrum, memory, token ledger) 2026-07-17 07:31:11 +00:00
b469b174a2 docs(job-array-single-wal-record): sync spec docs with phase-7 residual-gap closure
brief/requirements updated to reflect the reopened-and-now-closed
post-GO gap-closure phase (tasks 7.1-7.3); adds research.md capturing
the investigation trail; roadmap.md reflects current spec status.
2026-07-17 07:31:06 +00:00
53bd686897 fix(job-array-single-wal-record): free jobArray dataset on scheduler receive path
sched_handle_dataset()'s Data_data_jobArray case never freed
dataset->rows (the decoded xce_job_submit_t*) after sched_on_job_array()
consumed it -- free_Dataset() is an empty no-op for every table type, so
nothing else owned this call. One bounded leak per delivered array push.

Free it via the existing free_JobSubmit_ptr() teardown on both the
success and error path, mirroring the sibling list-shaped cases'
ownership-transfer convention.

Rewrote test_job_array_expand.c's init_job_array() fixture to
heap-allocate + strdup its fields (matching the real wire-decode shape)
since freeing the old stack/string-literal fixture crashed. Redelivery
test now builds a fresh descriptor per delivery, matching how a real
redelivery is always a freshly-decoded wire message.

Closes task 7.3 (job-array-single-wal-record), the last of the three
post-GO residual gaps reopened 2026-07-16.
2026-07-17 07:13:12 +00:00
ea8d9d3c4f fix(job-array-single-wal-record): extend Live Apply Resource Fence to boot replay (task 4.2's gap)
replay_wal() now halts logical apply and engages persist_mark_apply_resource_fenced()
on a negative apply_committed_entry() return during boot/restart replay, mirroring
persist_apply_pump_run()'s existing live-path halt-and-fence behavior (task 4.1).
Physical scan bookkeeping (current_lsn, CRC chain) stays unconditional per entry,
consistent with persist_recover()'s own established current_lsn/last_applied_lsn
divergence precedent.

Went through one remediation round: round 1 exercised the wrong ha.c gate (periodic
re-alert timer); round 2 corrected the test to drive the real election-time gate
(raft_cb_become_leader_with_cluster_for_test()), with teeth independently confirmed
via a temporary fence-clear RED/GREEN cycle. Round-2 reviewer APPROVED.

Documents a real, NOT-fixed residual gap found during this task: a node's very
first-ever promotion evaluation in a fresh process runs before that node's own
first persist_recover()/replay_wal() call, so this fence is invisible to that one
promotion check (ha.c:1303-1408 gate runs before ha.c:1427-1439's first-boot
recovery). Every promotion evaluation after a process's first one is protected.
Out of this task's persist.c-only boundary; flagged for a dedicated ha.c follow-up.
2026-07-16 16:39:07 +00:00
5a8136263e docs: mark bug-1443 FIXED (task 7.1, commit dcf9f39) 2026-07-16 15:08:44 +00:00
dcf9f39c22 fix(job-array-single-wal-record): close bug-1443's bulk-insert partial-visibility gap
restore_job_array()'s bulk-insert pass could fail partway through
(key_add() on realloc growth exhaustion), leaving an already-inserted
prefix permanently visible in c->jobmgr but excluded from
out_new_jobs -- so those elements existed and were individually
addressable but never reached HOOK_DETAIL_JOB_NEW, never scheduled or
pushed, not on this attempt or any later successful retry. A literal
violation of Requirements 2.3 and 9.4, tracked as bug-1443.

Adds kvmgr_reserve()/xzarray_reserve(), new purely-additive primitives
in shared kvmgr.c/zarray.c (following the existing kvmgr_capture_keys()
template -- bare extern function, no RTTI vtable slot, caller-holds-
write-lock contract), that pre-grow c->jobmgr's backing store before
the bulk-insert pass begins, closing the dominant N-scaling failure
trigger structurally.

Found a genuine off-by-one in the existing, unmodified
xzarray_ensure_items(): its strict less-than sufficiency check means
reserving exactly array_size and then performing array_size real
stores still triggers one more growth cycle on the last store.
Reserving array_size + 1 is the exact minimal correct compensation,
independently re-derived and confirmed by review with concrete
arithmetic.

For the now-much-narrower residual case (kvtab_add()'s own small
per-entry allocation, or a content collision -- both non-N-scaling,
honestly documented as still theoretically possible): restore_job_array()
now populates out_new_jobs with whatever prefix was genuinely inserted
before a failure, and apply_committed_entry() fires HOOK_DETAIL_JOB_NEW
for that non-empty partial prefix even when the overall return is
negative, while still propagating the negative rc so the LSN correctly
halts and the Live Apply Resource Fence still engages for the
remainder. A genuinely-inserted element is never left un-pushed again.

Task 7.1 of job-array-single-wal-record (Requirements 2.3, 9.4, 9.6).
2026-07-16 15:08:02 +00:00
575f928bbc plan(job-array-single-wal-record): reopen spec for residual gap closure
User directed (2026-07-16) that this feature's own open residual gaps
be fixed: bug-1443 (restore_job_array()'s bulk-insert partial-visibility
gap), task 4.2's boot-replay fence gap, and task 5.3b's bounded leak.
Unrelated pre-existing bugs 1437/1438 explicitly excluded from this
reopening per user's scope decision.

Adds Phase 7 (tasks 7.1-7.3) to tasks.md:
- 7.1: add a purely-additive kvmgr_reserve() capacity-reservation
  primitive (mirroring the existing kvmgr_capture_keys() template) so
  restore_job_array()'s bulk-insert pass can pre-grow its backing store
  before mutating, closing the dominant N-scaling failure trigger; plus
  make a genuinely-inserted partial prefix always get notified even in
  the now-much-narrower residual failure case
- 7.2: extend the Live Apply Resource Fence's halt-and-fence protection
  to replay_wal() (boot/restart replay), reusing the same primitives
  task 4.1/4.2 already built for the live path
- 7.3: free dataset->rows in sched_handle_dataset()'s jobArray case

spec.json phase reverted to tasks-generated to reflect the reopening.
2026-07-16 13:14:03 +00:00
85ce5da61c docs: track recurrences of pre-existing bugs 1437/1438 during re-validation
Both unrelated to job-array-single-wal-record's own code:
- bug-1437 (vendored rtti makeInstance leak): recurred via this
  feature's own test_job_init_propagates_makeJobsub_failure, which is
  simply the first test to drive job_init() through the failure path
  that trips the latent characteristic. Confirmed same signature, not
  a new leak, not a regression.
- bug-1438 (capnp parallel-build race): recurred on a clean build with
  a different symptom (missing header vs. corrupted content) that
  confirms the root cause is a missing meson dependency edge on the
  controller.capnp.h custom_target, not just a non-atomic-write timing
  fluke -- useful for whoever eventually fixes it, out of this spec's
  scope.
2026-07-16 12:30:19 +00:00
2a769407f5 docs(job-array-single-wal-record): backfill bug-1444 for task 6.0a's replay_wal fix
A stale, long-orphaned background agent (dispatched early in this
session for task 6.1, interrupted by an API error, and only resuming
~8 hours later after task 6.0a/6.1/6.2/6.3 and the validate-impl GO
decision were already committed) independently re-discovered and
filed the exact bug commit 16747aa already fixed, before noticing the
fix was already on the branch. Its filed entry (bug-1444) accurately
describes the error/root-cause but was left marked NOT FIXED.

Corrected the fix field to reflect the actual resolution (commit
16747aa, task 6.0a) and cross-referenced it. Kept as its own entry
rather than deleting it -- task 6.0a's fix was documented in
tasks.md's Implementation Notes but had never been logged to
buglog.json, so this backfills that gap. Distinct from bug-1443
(the still-open bulk-insert partial-visibility gap in the same
feature).

No code changes; the stale agent's independently-derived
run-ha-soak.sh/soak-verdict.sh/waldump.c changes were confirmed
byte-identical to HEAD and made no git writes.
2026-07-16 10:41:13 +00:00
2b0a37acc7 docs(job-array-single-wal-record): record /kiro-validate-impl GO decision
Feature-level validation ran 4 parallel dimensions: requirements
coverage (38 ACs, 33 full, 5 partial -- all tracing to the already-known
bulk-insert partial-visibility gap plus an admitted commit-barrier-count
inference), design/boundary alignment (ALIGNED, no drift), cross-task
integration (SEAMS_SOUND, wire format/arithmetic/contracts verified
consistent end to end), and mechanical checks including a live smoke
boot (fresh single-node deploy, real array submission, one WAL record,
all elements queryable) -- PASS.

The coverage pass sharpened restore_job_array()'s known bulk-insert
gap: it's a literal, code-proven violation of Requirements 2.3 and 9.4,
not just a soft nuance -- a resource-exhaustion failure during the
bulk-insert pass can permanently orphan array elements that exist and
are addressable but never get scheduled. Filed as bug-1443
(.wolf/buglog.json) for tracking. User decision: GO, ship with the
gap tracked rather than blocking on a fast-follow fix -- the trigger
condition is narrow and every other validation dimension is clean.
Closing bug-1443 needs a new kv.c/kvmgr.c primitive or a re-derive-
and-renotify path, explicitly out of this spec's approved scope.

spec.json phase -> implementation-complete.
2026-07-16 10:26:07 +00:00
1cfdac47b6 test(job-array-single-wal-record): full regression pass - GO
Final task of the spec. Ran the complete unit, integration, and soak
suite (controller_unit_test, all scheduler binaries, xcelib_test, Go
apiserver tests under both CGO_ENABLED=0/1, and every soak mode this
spec added) -- all clean, zero new job loss, divergence, or liveness
stall.

Designed and executed a genuine empirical byte-identical baseline
comparison for the non-array (array_size=1) path: built the commit
immediately preceding this spec's first commit in an isolated
worktree, deployed independent clusters from both the baseline and
current-tree binaries, and did a byte-for-byte comparison of the raw
on-disk WAL entries for an identical single-job submission.
Independently reproduced end-to-end by the reviewer with a consistent
result -- every differing byte maps to inherently run-specific fields
(timestamp/crc/jobid/sub_time), zero differences in term/lsn/type/
payload_len or the remaining ~6485 payload bytes. Corroborated by a
structural code-diff argument (the single-job WAL-encode path is
untouched, purely-additive diff only) and the pre-existing task 2.3
unit test asserting the same invariant.

Confirmed the three self-flagged residual gaps (task 3.1 bulk-insert
orphan prefix, task 4.2 boot-replay fence, task 5.3b bounded leak)
remain the complete and only set of open items across all 24
completed tasks.

Task 6.3 of job-array-single-wal-record (Requirements 8.1, 8.2, 8.4).
Overall verdict: GO. This completes the spec's implementation phase.
2026-07-16 07:42:21 +00:00
c6138496bd test(job-array-single-wal-record): chaotic-failover array retry scenario
Extends the HA soak harness with --array-chaos-retry: submits an
array, kills the primary essentially immediately (before commit is
known), retries the identical submission against the new primary, and
hard-asserts exactly one committed instance results -- no silent loss,
no double-submit, no partial duplication across the array's full
index range.

Confirms (no production code changes needed) that the existing
single-job cross-primary dedup mechanism (bug-1419: client-side jobid
minted once and never re-sent on retry; server-side
jobid_range_exists()/inflight_range_try_add(), both genuinely
range-shaped over [base,base+array_size); xce_failover_classify()'s
idempotent-replay rule) generalizes correctly to arrays, treating the
whole range as one dedup unit rather than per-element.

No mutation-test RED phase against production code was performed
(correctly out of this task's boundary); reviewer compensated by
extracting the harness's own assertion logic into a standalone
mutation-harness with stubbed collaborators, confirming real
fail()-gated teeth on all four outcomes (clean/silent-loss/
double-submit/partial-duplication).

Live-verified across 4 independent fresh cluster deployments (2 by
implementer, 2 by reviewer), array sizes 6/10/15, 17 total chaotic
rounds, zero failures, genuine election/term diversity each time.

Task 6.2 of job-array-single-wal-record (Requirements 8.2, 8.3).
2026-07-16 05:27:24 +00:00
0d8986a6a9 test(job-array-single-wal-record): large-array HA soak scenario
Extends the HA soak harness with a new --array-soak mode: small (N=2)
and large (N=10000) array submission sub-rounds, each with one clean
kill/failover, asserting one WAL record per submission regardless of
array size, per-element addressability (status/cancel/completion)
spread across low/mid/high indices post-failover, and a real
millisecond-resolution replay-time bound comparing large-array vs
small-array kill-to-addressable latency.

waldump.c gains a WAL_JOB_ARRAY_SUBMIT case in wal_type_name() so the
scenario can grep an exact type name for its live WAL-record-count
delta check.

Went through one remediation round: the replay-time measurement
initially used whole-second resolution (collapsed to 0s/n-a on a real
run) and never asserted on the comparison, giving zero actual evidence
for the O(1)-replay-time requirement. Fixed with $EPOCHREALTIME
(millisecond resolution -- this sandbox's BusyBox date silently
mangles the more common `date +%s%3N` idiom) and a real bound
assertion that hard-fails the run on a genuine O(N) regression,
independently verified to have real teeth.

Live-verified 3 times with genuinely discriminating numbers each time;
final run: small=210ms, large=163ms -- large-array replay was not
slower, live evidence of the feature's O(1) claim.

Task 6.1 of job-array-single-wal-record (Requirements 4.3, 5.2, 5.3,
8.2, 8.3). Depends on and reuses task 6.0a's own soak verification.
2026-07-16 04:43:55 +00:00
16747aa96d fix(job-array-single-wal-record): replay_wal's job-count tally was blind to array submissions
replay_wal() (persist.c ~3902) tallied *new_job_submits_out only for
hdr.type == WAL_JOB_SUBMIT. WAL_JOB_ARRAY_SUBMIT entries were
dispatched and correctly materialized N real jobmgr entries via
apply_committed_entry()'s array case, but never counted --
persist_verify_apply_correctness() then saw real jobmgr growth exceed
the undercounted expectation and declared APPLY_DIVERGENT, engaging a
PERMANENT, non-self-healing fence (distinct from task 4.2's
self-healing Live Apply Resource Fence).

Live-reproduced during task 6.1's soak attempt on 2 of 3 nodes in a
single small-array (N=2) sub-round via two different trigger paths.
Deterministic, not size-dependent -- the first array submission to a
live cluster would fence any node that restarts or hits an
election-path recovery fallback before its next checkpoint, shrinking
the promotable pool. A pre-existing comment at persist.c ~3863-3874
(predating this spec) had already flagged this exact code region as
needing re-verification before tasks 4.1/6.1+ went live; this is that
re-verification, and it failed.

Fix adds a WAL_JOB_ARRAY_SUBMIT branch tallying apply_rc (the genuine
per-record element count apply_committed_entry()'s array case already
returns) when apply_rc > 0, mirroring the existing dedup-safe pattern
for WAL_JOB_SUBMIT. Traced the full return-value space of
restore_job_array()/apply_committed_entry()'s array case to confirm
every tallied value is provably equal to real jobmgr growth -- the fix
cannot overcount and so cannot mask a genuine divergence.

Verified via a from-scratch live 3-node HA soak re-run (--array-soak),
independently by both implementer and reviewer: zero APPLY_DIVERGENT
across all three original trigger paths, one-WAL-record/one-commit-
barrier confirmed for N=2 and N=10000, per-element post-failover
status/cancel/completion confirmed.

Task 6.0a of job-array-single-wal-record (Requirements 3.1, 3.2, 4.1,
9.4). Inserted mid-run after live soak testing surfaced the bug; user
confirmed sequencing (fix now, blocking task 6.1+).
2026-07-16 04:09:43 +00:00
b7b90e9d21 test(job-array-single-wal-record): end-to-end integration test for the compact push
Cross-daemon integration test, test-only, spanning the scheduler (C)
and API-server (Go/CGO) receive paths built in tasks 5.1-5.4/5.3b.

Drives real xce_encode()/xce_decode() wire bytes through the actual
production dispatch chain on both daemons -- extract_data() ->
scheduler_req_handler() -> sched_handle_dataset() ->
sched_on_job_array() and go_push_handler() -> handleDataset() ->
cJobArrayToModels()/dispatchJobArray() -- skipping only vtask_entry()'s
~10 lines of static glue, mirrored verbatim in test code (the
if (callnext) treats-any-nonzero-as-proceed behavior that caused
5.3b's bug is reproduced exactly).

Redelivery is tested via genuinely independent fresh decodes of
separately-encoded bytes, not a decode-once-call-twice shortcut.
Bug-catching capability proven by temporarily reverting task 5.3b's
fix and confirming both the C and Go tests fail meaningfully, then
restoring to green -- confirmed independently by both the implementer
and reviewer.

Task 5.5 of job-array-single-wal-record (Requirements 6.1-6.5, 8.4).
This completes Phase 5 (Compact Scheduler/API-Server Push).
2026-07-15 17:27:34 +00:00
9e0d87fe50 feat(job-array-single-wal-record): API-server-side (Go) local expansion of the compact push
Adds the Go/CGO receive path for Data.jobArray, mirroring task 5.3's
scheduler-side C implementation: rawJobArrayToModels() (pure-Go
base_jobid + i expansion), cJobArrayToModels() (CGO adapter sibling to
cJobToModel), dispatchJobArray() (dedup via the same store.Jobs.Has()
the ordinary Data_data_jobs path already writes through, single
confirmation log only when at least one element was newly
dispatched), and a new case C.Data_data_jobArray in handleDataset.

Elements route through OnJobUpdated, matching the ordinary single-job
push's own routing. The CGO boundary needed no changes -- both
C.xce_job_submit_t and C.Data_data_jobArray were already exposed from
prior tasks, including task 5.3b's extract_data() fix which this
receive path depends on identically to the scheduler's.

store.ArrayJobs (the array-level rollup aggregate) is deliberately
left unpopulated -- confirmed out of this spec's scope, deferred to a
future spec per requirements.md's Boundary Context.

Task 5.4 of job-array-single-wal-record (Requirements 6.2, 6.4, 6.5, 8.4).
2026-07-15 15:56:48 +00:00
1cbd7df1a8 fix(job-array-single-wal-record): wire dispatcher was silently dropping compact array pushes
extract_data() (src/lib/request.c), the shared reassembly step every
inbound push passes through inside vtask_entry() before any receiver's
handler runs, had no case for Data_data_jobArray -- it hit
default: rc=-EINVAL. Because vtask_entry()'s callnext check treats any
nonzero return (including a negative errno) as "reassembly complete,
proceed," this never surfaced as an error: the real payload was freed
and the receiver silently handed an empty, zeroed dataset_t, with no
log anywhere in the chain. Tasks 5.1-5.3 were each individually
correct and tested in isolation, but the feature was dead on the real
wire end-to-end.

Adds case Data_data_jobArray: passing the single, unchunked
descriptor straight through (no pagination needed, unlike the
existing LIST-shaped cases) and nulls the source pointer after
transfer -- verified this is not cosmetic, since free_Request()'s
pushmsg case would otherwise double-free it via free_Data()'s live
Data_data_jobArray branch.

Proven with a real xce_encode()/xce_decode() round-trip through the
fixed extract_data() -> sched_handle_dataset() -> sched_on_job_array()
(task 5.3, unmodified) chain, not just a unit-level check on
extract_data() alone. Also confirms task 5.4's future API-server
receive path shares this exact fix, since both go through the same
xcecli_ctx_init() -> vtask_entry() -> extract_data() pipeline.

One self-flagged, non-blocking residual gap (pre-existing since task
5.3, out of this task's request.c-only boundary): dataset->rows for
the jobArray case is never deep-freed downstream, leaking one bounded
xce_job_submit_t per successfully-delivered array push commit.

Task 5.3b of job-array-single-wal-record (Requirements 6.1, 6.2, 6.4, 8.4).
Inserted mid-run after independent review confirmed the gap; user
confirmed sequencing (fix now, before task 5.4).
2026-07-15 10:59:39 +00:00
d16847185e feat(job-array-single-wal-record): scheduler-side local expansion of the compact push
Adds sched_on_job_array() (handler.c), a sibling case to
Data_data_schedjobs decoding the new Data.jobArray variant and
expanding it locally into N ordinary schedulable tasks via
base_jobid + i, routed through the existing unmodified per-job
pipeline (sched_on_jobs()). Each element gets its own deep clone
(clone_job_submit_data/clone_job_startinfo) of the shared submit
spec, since per-element teardown (free_Job_ptr) frees it
independently and a shared pointer would double-free.

Dedup reuses sched_get_job() against the same kvtab sched_add_job()
guards, rather than a new mechanism. A confirmation log line fires
once per successful expansion (skipped on a fully-duplicate
redelivery).

Adds a new task (5.3b) discovered during this task's review: the
shared wire dispatcher extract_data() (src/lib/request.c) has no case
for Data_data_jobArray and silently drops every real array push
before any receiver's handler runs -- outside every current task's
boundary, confirmed to affect the future API-server receive path
(task 5.4) identically. design.md's File Structure Plan corrected to
record this. User confirmed sequencing: fix it now, before 5.4.

Task 5.3 of job-array-single-wal-record (Requirements 6.3, 6.4, 6.5, 8.4).
2026-07-15 10:10:14 +00:00
e0cec75284 feat(job-array-single-wal-record): build the compact API-server push
Adds apiserver_push_jobarray() (apiserver.c), mirroring task 5.1's
xcesched_push_jobarray() exactly: same (base jobid, array_size, one
spec) Data.jobArray derivation, same direct xce_request()/OPCode_push
transport. apiserver_job_new_fun() branches on l->count > 1; the
existing single-job path (apiserver_push_joblist()) is byte-for-byte
unchanged.

Additionally, and unlike the scheduler (single target), replicates
apiserver_push_joblist()'s existing per-connected-server fan-out loop
(N_APISERVER_MAX iteration, same lock discipline) -- verified against
2 simulated connected servers, each receiving exactly one push.

New standalone apiserver_push_unit_test binary (same link-collision
rationale as task 5.1's sched_push_unit_test), correctly omitting
egress.c since apiserver_push_joblist() has no WAL-commit filter.

Task 5.2 of job-array-single-wal-record (Requirements 6.2, 6.4).
2026-07-15 08:30:27 +00:00
5e7a4aa392 feat(job-array-single-wal-record): build the compact scheduler push
Adds xcesched_push_jobarray() (sched.c), encoding one compact
(base jobid, array_size, one spec) Data.jobArray descriptor -- reusing
the JobSubmit capnp struct already added to the Data union in task 1.2
-- instead of N individually-encoded job structs. sched_job_new_fun()
branches on l->count > 1 to call it; the existing single-job path
(l->count == 1 -> xcesched_push_joblist()) is byte-for-byte unchanged.

Sends via direct xce_request()/OPCode_push/Request_body_pushmsg rather
than the existing xce_push_data_req() helper, which hard-rejects any
payload outside jobs/workers/resources/resvals and cannot carry a
single non-list descriptor. design.md corrected to record this
transport deviation for task 5.2 to follow.

New standalone sched_push_unit_test binary (registered in
meson.build): sched.c/dataex.c/egress.c were never linked into
controller_unit_test, and two existing test files there ship local
fakes of on_scheduler_join()/notify()/collect_confvars() that would
collide with the real definitions at link time.

Task 5.1 of job-array-single-wal-record (Requirements 6.1, 6.3, 6.4).
2026-07-15 08:03:04 +00:00
1888498401 test(job-array-single-wal-record): end-to-end fault-injection test for the live fence
New test_job_array_fence.c exercises the full Requirement 9.4-9.6
engage-refuse-clear-recover sequence through real production seams:
forced array-expansion failure -> fence engages -> promotion refused
at a real ha.c gate site -> zero partial elements visible -> a second,
independent node's progress is unaffected -> clearing the failure and
retrying self-heals the fence and re-opens promotion eligibility.

Deliberately drives this through task 3.1's construction-phase
(scratch-build) seam rather than the bulk-insert-phase seam: the
bulk-insert seam has a known, separately-tested residual gap (an
already-inserted element prefix can survive a mid-pass key_add()
failure with no key_del to undo it), so asserting "no partial set"
against it would misrepresent an already-documented open gap as
resolved. That gap remains open and unowned (see tasks.md
Implementation Notes) -- Phase 4 delivers the generic halt/fence/gate
machinery for it, not the partial-visibility guarantee itself.

Task 4.4 of job-array-single-wal-record (Requirements 9.4, 9.5, 9.6).
This completes Phase 4 (Live Apply Resource Fence).
2026-07-15 04:54:45 +00:00
8cb8c25d4d feat(job-array-single-wal-record): gate promotion on the live apply fence
Threads !persist_apply_resource_fenced(&c->persist) as a required
conjunct into both existing promotion-eligibility call sites in ha.c:
the periodic fence-release check (promotion_fence_timeout_fn) and the
election-time check (raft_cb_become_leader). A node with the fence
engaged is refused promotion at both points; once it self-clears
(task 4.2), the node is eligible again with no other action.

Task 4.3 of job-array-single-wal-record (Requirements 9.4, 9.5).
2026-07-15 04:21:35 +00:00
b47cc660ba feat(job-array-single-wal-record): add live, self-healing apply resource fence
Adds a new atomic LSN field on persist_t (distinct from the existing
boot-only unrecoverable-fence flag) plus three primitives:
persist_apply_resource_fenced(), persist_mark_apply_resource_fenced(),
persist_clear_apply_resource_fence(). Wired into
persist_apply_pump_run(): engages on task 4.1's halt condition, clears
unconditionally on the walk's next successful advance — provably
equivalent to "clears only when the same stuck LSN's retry succeeds"
since the walk is strictly sequential and never advances past a
failed position.

Known residual gap, out of this task's boundary (documented in
tasks.md Implementation Notes): replay_wal() during boot/restart
replay does not yet share this protection, so a node crashing while
fenced can silently re-pass the same failure on restart. Flagged for
a follow-up task, not fixed here.

Task 4.2 of job-array-single-wal-record (Requirements 9.4, 9.5).
2026-07-15 03:54:20 +00:00
cc7e99c2f7 feat(job-array-single-wal-record): make apply pump respect apply failures
persist_apply_pump_run() no longer discards apply_committed_entry()'s
return value. On a negative rc for any WAL type (not just the new
array type), the walk halts at that LSN — no last_applied_lsn advance,
no pending-apply hand-off — logs a loud error, and retries from the
same position on the next kick, mirroring the existing
apply_pump_read_entry failure branch's shape.

Investigated whether this newly turns the pre-existing job-fact
-ENOENT path into a permanent-wedge hazard: it does not, since
c->jobmgr never deletes entries and facts are only ever applied after
their causally-prior submit already succeeded, so a live -ENOENT there
can only mean genuine divergence — halting is the correct behavior.

Task 4.1 of job-array-single-wal-record (Requirement 9.4).
2026-07-15 03:24:28 +00:00
107e3839d9 test(job-array-single-wal-record): close remaining array expansion gaps
Adds 2 targeted tests after confirming tasks 3.1/3.2/3.3 already
cover most of this task's observable criteria:
- N-way expansion swept across sizes {2,3,100,1000} with exact-
  endpoint non-leakage checks (existing coverage only tested size 5)
- Requirement 5.2 (individually addressable exactly as a per-element
  job) exercised through the real production command dispatch
  (job_ctrl(), not the raw key_find() every existing test used) --
  confirms an array-expanded element can be cancelled by identity,
  siblings stay untouched, and an out-of-range jobid is rejected

Idempotent no-op, forced-failure-then-retry, and partial-reapply
were already fully covered by 3.1/3.2 -- no new tests needed.
Requirement 9.4's node-fencing behavior is confirmed out of this
task's boundary (Live Apply Resource Fence, task 4.x); restore_job_
array()'s own atomicity contract that the fence depends on is
already proven. No production code changed -- test-only.

Task 3.4 of job-array-single-wal-record.
2026-07-15 02:56:20 +00:00
b0c7cfe548 feat(job-array-single-wal-record): wire array expansion into apply_wal_entry
Adds case WAL_JOB_ARRAY_SUBMIT to apply_wal_entry() (standby
catch-up dispatch), calling restore_job_array() hook-free, matching
the existing WAL_JOB_SUBMIT case's shape exactly.

Review found apply_wal_entry() has zero production call sites -- a
2026-07-10 refactor (ha_standby_advance_committed_and_apply) moved
standby entity materialization onto the same apply_committed_entry()
pump the primary uses; the code comment claiming otherwise predated
that refactor and was never updated. Requirement 3.1/3.2/4.1 are
fully delivered by task 3.2 alone. This task's wiring is kept as
defensive parity with WAL_JOB_SUBMIT's existing, equally-dead case
in the same switch, not as an independent delivery path -- design.md
and tasks.md corrected accordingly.

Task 3.3 of job-array-single-wal-record.
2026-07-15 02:30:06 +00:00
0c4573b54a feat(job-array-single-wal-record): wire array expansion into live apply
Adds case WAL_JOB_ARRAY_SUBMIT to apply_committed_entry() (the live
apply pump / boot replay dispatch seam): calls restore_job_array()
(task 3.1) instead of restore_job(), and on success fires
HOOK_DETAIL_JOB_NEW exactly once carrying every newly created
element in restore_job_array()'s own out_new_jobs list -- never once
per element. No hook fires on an idempotent no-op (-EEXIST) or any
genuine construction failure, honestly propagating task 3.1's
documented bulk-insert-phase residual gap rather than papering over
it. apply_wal_entry() (standby catch-up) is untouched -- task 3.3.

Task 3.2 of job-array-single-wal-record.
2026-07-14 14:46:03 +00:00
7c755dc4d7 feat(job-array-single-wal-record): implement restore_job_array primitive
Adds the shared N-way array expansion primitive: decodes a committed
WAL_JOB_ARRAY_SUBMIT payload, scratch-builds every element via a
throwaway kv-table (never touching c->jobmgr during construction),
then makes the whole set visible in one bulk pass under a single
write-lock acquisition -- no window where a concurrent reader can
observe a partially-constructed array. Idempotent per index (skips
already-existing jobids; whole-record no-op if all exist). Includes
an XCE_TESTING-guarded fault-injection seam. Not yet wired into any
live dispatch path (tasks 3.2/3.3).

Two review rounds: round 1 found the bulk-insert pass silently
claimed success even when key_add() itself could fail on allocation
exhaustion (c->jobmgr grows via realloc, no reservation primitive
exists anywhere in kv.c/kvmgr.c/zarray.c to pre-check this). Fixed
by checking key_add()'s return and reporting failure honestly
instead of a false success. The underlying table has no key_del, so
an already-inserted prefix on this specific failure mode can remain
genuinely visible while the call reports failure -- documented as a
residual gap in design.md and tasks.md for tasks 3.2/4.x to account
for (that prefix is never pushed to scheduler/API server, even on a
later successful retry, since it's never included in out_new_jobs).

Task 3.1 of job-array-single-wal-record.
2026-07-14 14:16:29 +00:00
78eb983a74 test(job-array-single-wal-record): close remaining array append/commit gaps
Adds 3 targeted tests after confirming tasks 2.1/2.2 already cover
most of this task's observable criteria:
- one-entry/one-continuation swept across sizes {2,3,100,1000}
  (existing coverage only tested size 5)
- payload-size boundedness at the real idgen()-enforced ceiling
  (MAX_ARRAY_IDX+1 = 32768), plus an absolute byte-cap assertion
  (existing coverage only compared 2 vs 10000)
- array_size==1 baseline: explicit WAL-byte readback proving the
  entry type is WAL_JOB_SUBMIT (not WAL_JOB_ARRAY_SUBMIT) and the
  payload decodes via struct Job, not struct JobSubmit (existing
  coverage only proved "old tests still pass" implicitly)

No production code changed -- test-only.

Task 2.3 of job-array-single-wal-record.
2026-07-14 10:53:20 +00:00
d38ca72e76 feat(job-array-single-wal-record): wire array submissions into handle_jobsubmit
Branches handle_jobsubmit() on array_size: ==1 continues through
today's exact unmodified single-job path; >1 routes through a new
handle_jobsubmit_array() helper that reuses jobsubmit_async_ctx_t
with pending=1 and calls task 2.1's persist_submit_job_array_async()
instead of looping N single-element appends. Range validation
(jobid_range_exists/inflight_range_try_add) unchanged, same order,
strictly before any WAL append, for both branches.

Retires two pre-existing tests whose premise (N independent
per-element WAL entries with mixed outcomes) is now structurally
impossible with one entry per array; restores array-scoped
job-non-existence coverage for the failed-commit case that an
earlier draft of this change had dropped (Req 2.2/9.2).

Task 2.2 of job-array-single-wal-record.
2026-07-14 10:32:59 +00:00
7c4afbbb83 feat(job-array-single-wal-record): implement single-record array append/commit
Adds persist_submit_job_array_async(): encodes one JobSubmit-shaped
payload (base jobid, array size, one shared spec), appends exactly
one WAL_JOB_ARRAY_SUBMIT entry, and registers exactly one commit
continuation regardless of array size -- reuses the existing
job_commit_adapter_ctx_t/job_commit_resolve_adapter machinery, no
per-element loop. Not yet wired into handle_jobsubmit (task 2.2).

Also fixes a test-helper bug (WAL offset back-computed from the
post-append cursor, only valid for zero-payload entries) that
corrupted state across tests when exercising a nonzero-payload
array-submit entry.

Task 2.1 of job-array-single-wal-record.
2026-07-14 09:50:00 +00:00
76862bd9aa feat(job-array-single-wal-record): harden jobj.c job construction primitives
- job_init() now checks makeJobsub()/makeString(ckptinfo) return
  values and propagates failure instead of storing NULL with a
  success return (Req 10.1).
- Root-caused the previously observed makeJobsub() NULL-with-EINVAL
  failure to a test-binary-only hazard: multiple controller test
  files each called job_obj_init() from their own setUp() with no
  cross-file coordination, re-running jobsub_table_init() against an
  already-live lock/table. Fixed via atomic_flag-guarded idempotent
  job_obj_init()/job_obj_fini(), plus a distinct -ENODEV return from
  makeJobsub()/makeJobstartinfo() when the table isn't ready
  (previously fell through to undefined behavior) (Req 10.2).
- Verified (test) MOVE_PTR fields are always NULL on a freshly-built
  submission payload -- no code change needed (Req 10.3).
- Fixed job_destroy()'s accounts leak via the existing
  free_job_account() helper, replacing the stale FIXME (Req 10.4).
- Verified (inspection + test) struct Job's capnp schema has no
  ckptinfo field, so decode_Job/free_Job have nothing to leak there
  -- no code change needed (Req 10.5).

Independently re-verified: reviewer reproduced a pre-existing,
unrelated rtti-library leak-on-init-failure against a stashed
pre-task copy of jobj.c to confirm it predates this change.

Task 1.3 of job-array-single-wal-record.
2026-07-14 08:29:45 +00:00
bc4e70b790 feat(job-array-single-wal-record): add compact array push capnp variant
Adds Data union variant jobArray@6, reusing JobSubmit's existing
(jobid, arraySize, submit) shape verbatim -- no new struct, no
existing variant renumbered. Not yet wired into any push path
(tasks 5.1/5.2 own that).

Task 1.2 of job-array-single-wal-record.
2026-07-14 07:11:51 +00:00
2e62e73d84 feat(job-array-single-wal-record): add WAL_JOB_ARRAY_SUBMIT record type
Adds the array-shaped WAL entry type (distinct from WAL_JOB_SUBMIT,
value 11 in the unused WAL_JOB_* gap) and classifies it WAIT-mode via
wal_classify, matching the single-job type's commit-barrier semantics.
No dispatch/apply wiring yet -- foundation for task 2.x/3.x.

Task 1.1 of job-array-single-wal-record.
2026-07-14 06:47:14 +00:00
895a204438 docs(spec): ha-consistency-invariants — Criterion 2.9 now satisfied
The umbrella's implementation_status note still described the Facts-Apply
invariant as an open documented deviation owned by raft-apply-decoupling's
D1. raft-apply-decoupling has since closed it: task 4 ("Fact apply-after-
commit split") moved WAL_MODE_RETAIN fact state mutation + reactions to
the post-commit apply_committed_entry() seam (persist.c:2721) via
apply_job_update_transition(); the old apply-on-receipt path
(apply_wal_entry()) has zero production call sites. D1 (block-but-durable
under quorum loss) is implemented and soak-tested (task 7.2: kill primary
mid-fact-stream). Update the note so it doesn't read as outstanding work.
2026-07-13 08:08:41 +00:00