Commit graph

1904 commits

Author SHA1 Message Date
08e6c5e817
chore(buglog): bug-448, bug-1626, bug-1631 fixed (0c1bac44)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 03:05:20 +00:00
0c1bac4426
fix(controller): job_ctrl lock/reference and dispatch retry requeue (bug-448, bug-1626, bug-1631)
bug-448: job_ctrl() took a reference on the job but only one error path
released it; every successful call leaked one (refcount 1 -> 3 after two
calls). It is now released on every exit path, including the key_find
rc<0 one.

bug-1626: job_ctrl()'s not-started branch changes the job (mark_job_update
/ mark_job_fail -> apply_job_*_transition, which require RTTI_LOCK_WRITE)
while holding only RTTI_LOCK_READ, so it could run concurrently with
readers. It now takes the write lock. (dispatch_a_job_ already did.)

bug-1631: dispatch_a_job_()'s four retryable-failure branches never reset
state/allocs, correct for a first dispatch (still PEND) but not for a job
that had already been dispatched once: it stayed RUN holding its old
allocation. Each branch now calls record_job_reconcile_pending_fact() --
a no-op without a WAL fact for a PEND job; for a RUN job it returns it to
PEND, releases allocs and appends a durable WAL_JOB_UPDATE -- and notifies
after unlocking when it applied, the same pattern as the resync reconcile
path (cfe66dde). Not reachable until something re-dispatches RUN jobs
(e.g. preemption), fixed now so it is correct when that lands.

- job_get_refcount() (jobj.c) read accessor for the leak test.
- test_apply_pump.c: test_job_ctrl_releases_its_job_reference and
  test_job_ctrl_mutates_not_started_job_under_write_lock (both RED before).
- test_dispatch_fail_retry.c now asserts PEND + allocs released after a
  post-success retryable failure (RED before).

Controller suite 1236/0; all 111 meson suites pass.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 03:05:20 +00:00
d43ae58edc
chore(buglog): bug-1870 fixed (63fd3d2a)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 15:16:46 +00:00
63fd3d2afd
fix(controller): checkpoints no longer punch a hole in the replicated log (bug-1870)
persist_checkpoint() and persist_checkpoint_marker_triggered() wrote a
local-only WAL_CHECKPOINT marker that consumed an LSN of the replicated log
(current_lsn++) but was never replicated. A checkpoint taken by one member
only -- the primary's periodic max-wal-records checkpoint, or the one before
a snapshot send -- left a hole no standby had: the leader's next entry
carried prev_log_index = the marker's LSN, logOk failed on every standby
forever, commits timed out and writes stalled ("could not submit 110 jobs"
in the --force-snapshot soak; reproduced on the pre-bug-1867 baseline). Even
the config-marker checkpoint, taken on every member, stamped the marker with
each member's own current_lsn at apply time, which need not agree.

Nothing reads the marker except to skip it, so checkpoints simply stop
writing it. The recovery, streamer and bug-1475 log_len skip paths stay, for
WALs written by older binaries; persist_test_write_legacy_checkpoint_marker()
(XCE_TESTING only) reproduces such a WAL for the tests that cover them.

- new test_bug1870_leader_only_checkpoint_leaves_no_log_hole (RED before):
  a leader-only checkpoint takes no LSN and a standby that never
  checkpointed catches up past it via the real streamer/receive path.
- test_checkpoint_writes_marker_to_wal inverted to ..._writes_no_marker_...
- bug-1475 x2 and the apply-correctness trailing-marker test now build
  their legacy marker explicitly.

Controller suite 1234/0. HA soaks: --force-snapshot PASS (113 jobs, real
snapshot install -- also live-verifies bug-1867), --config-upgrade-wal-trim
PASS, --sequential 6 rounds PASS (SIMVERIFY 7 files, 0 mismatches).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 15:16:46 +00:00
725301dd92
chore(buglog): bug-1867 fixed (e4bb492a); log bug-1870 (checkpoint marker never replicated)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 09:55:24 +00:00
e4bb492ae6
fix(controller): checkpoints keep a job's exit code (bug-1867)
The capnp Job struct -- the checkpoint/entity-snapshot form of xce_job_t --
had no exit-info field, so capnp_encode_job() never wrote job.exit_info
and a job restored from a snapshot (instead of WAL replay) came back with
exit_info NULL, for DONE/EXIT and failed jobs alike. bug-1689/bug-1754
made the WAL facts carry the code; anything older than the last
checkpoint's WAL trim still lost it.

- controller.capnp: struct JobExitInfo $C.mapname("job_exitinfo_t")
  { exitcode }, and Job.exitInfo @12 $C.mapname("exit_info"). The
  generator now emits encode/decode/free for it inside encode_Job /
  decode_Job / free_Job; restore_job()'s create_job() already takes
  exit_info via MOVE_PTR. Null stays null (no fabricated exit 0). A new
  ordinal, so older snapshots/recordings decode with exit_info NULL.

New test_snapent_checkpoint_exit_info_roundtrip (RED before): exit codes
0 and 137 and a NULL survive both the raw checkpoint frames and a real
persist_load_snapshot restore. Full /xce-build all (asan): 111 meson
suites, 0 failures; all Go packages pass.

Not live-verified: the --force-snapshot HA soak cannot reach its snapshot
step because of a pre-existing checkpoint-marker replication stall,
reproduced on the baseline without this change (bug-1870).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 09:55:23 +00:00
a565779d94
chore(buglog): bug-1747 fixed (1d6c04c2)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 09:33:55 +00:00
1d6c04c28a
fix(controller): persist a failed job's finish_time through WAL_JOB_FAIL (bug-1747)
The FAIL half of bug-1747: apply_job_fail_transition() stamps
finish_time = time(NULL) at APPLY time, and WAL_JOB_FAIL carried no time,
so a failed job's finish_time changed on every restart replay / standby
catch-up, and a worker-reported failure found on rejoin could never keep
the worker's own time.

- WAL_JOB_FAIL's trailer grows to int32_t code + uint64_t finish_time
  (seconds). Decoded by length: 16 B (pre-bug-1754), 20 B (code only,
  bug-1754) and 28 B (code + time) all apply as before for what they carry.
- record_job_fail_fact_at(c, job, failinfo, finish_time_ms), the fail-side
  sibling of record_job_finish_fact_at(): 0 = receipt time. on_job_fail()
  uses receipt time; resync's finishedFail branch passes the worker-
  reported finish_time_ms (Req 4.2 now holds for failures too).
- Both apply seams adopt the wire time after the transition.

4 new tests in test_wal_job_fail_fact.c (3 RED before the fix) and the
resync round-trip now asserts the worker-reported failure time (RED
before). Controller suite 1232/0. Factstream HA soak (2 rounds, 60 jobs)
PASS.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 09:33:55 +00:00
21e72726f0
chore: sync bug-1508/1868/1869 to Gitea (#287-#289), refresh STATUS.md
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 09:25:46 +00:00
78fcacfac3
chore(buglog): bug-1869 fixed (5df33e69)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 08:13:24 +00:00
5df33e69d9
fix(worker): xcecd's fixed /tmp files never follow planted links (bug-1869)
xcecd runs as root and kept files at fixed names in world-writable /tmp,
opened by path:

- main.c created <logdir or /tmp>/xcecd_worker.log and then chmod(path,
  0666)'d it: a symlink planted there made root chmod 0666 any file
  (e.g. /etc/shadow).
- process_worker_manager.c appended xcecd_worker's stdout/stderr to
  /tmp/xcecd_worker.log through any planted symlink/hardlink.
- xcecd_prepare_restart() created /tmp/.xcecd.restarting through a
  dangling symlink (an empty root-owned file anywhere), and
  xcecd_check_restart_type() trusted access() on it, so any user could make
  an unplanned restart look planned.

xcecd_tmpfile.h:
- xcecd_open_owned_file(): reuses an existing file only if it is our own
  regular single-link file (so a legitimate log keeps being appended to);
  otherwise unlinks whatever is there and creates a fresh file with
  O_EXCL|O_NOFOLLOW. O_NONBLOCK so a planted FIFO cannot hang startup.
- xcecd_owned_file_exists(): lstat-based; only our own regular file counts
  as the restart flag. Anything else at the name is removed and ignored.
- The worker log's 0666 mode is now set with fchmod() on the fd.

New test_xcecd_tmpfile.c (7 tests; with the old behaviour the symlink,
dangling-symlink and hardlink cases FAIL and the FIFO case hangs);
worker suite 12/12. Sequential HA soak (5 rounds, xcecd_virtual) PASS.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 08:13:24 +00:00
5f53211f9d
chore(buglog): bug-1868 fixed (7864a3da); log remaining root /tmp symlink opens
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 07:55:23 +00:00
7864a3dab7
fix(worker): don't follow links planted at a job's output file name (bug-1868)
uv_proc_exec() created /tmp/job_<jobid>_task<N>.out as root with
O_WRONLY|O_CREAT|O_TRUNC. The name is predictable and /tmp is world-
writable, so a local user could plant a symlink (or hardlink) there before
the job started and make the root worker truncate and write job output
into the target. Only the host's fs.protected_* sysctls stood in the way.

xcecd_create_job_output() (job_output_path.h) now unlinks whatever is at
the name and creates a fresh file with O_EXCL|O_NOFOLLOW|O_CLOEXEC, mode
0600 as before; O_EXCL never follows a symlink and fails if anything
reappears, so a race can only fail the create, never redirect it (retried
3 times). A re-run job still starts with an empty file, as O_TRUNC gave.

New test_job_output_path.c (4 tests, 2 RED before the fix); worker suite
11/11.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 07:55:23 +00:00
2741143e7f
chore(buglog): log uv_proc output-file symlink follow (sibling of bug-1508)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 07:40:17 +00:00
404fc5c7fd
chore(buglog): bug-1508 fixed (7723a4f4)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 07:40:04 +00:00
7723a4f444
fix(worker): fetchfile only reads the job's own output file (bug-1508)
handle_fetchfile() runs as root in xcecd and open()ed whatever filepath
the request carried. Any libxce client could read any file on a worker
(/etc/shadow, keys) with a fetchfile RPC, and so could any REST user: the
apiserver resolves the path from the job's user-supplied out_file/err_file,
so submitting a job with out_file=/etc/shadow and then reading its output
passed the apiserver's ownership check and read the file as root.

- job_output_path.h: the per-task output name uv_proc.c writes,
  <dir>/job_<jobid>_task<N>.out, now shared by the writer (xcecd_worker)
  and the reader (xcecd), plus xcecd_is_job_output_path(), an exact
  string match for that job's names only.
- fetchfile_open_checked(): rejects any other path with -EACCES, opens
  with O_NOFOLLOW|O_NONBLOCK|O_CLOEXEC and requires a regular file with a
  single link, so a symlink, hardlink, FIFO or directory planted at the
  output name is refused.

Three new tests in test_controller_connector.c (2 RED before the fix);
worker suite 10/10. Sequential HA soak (5 rounds, xcecd_virtual) PASS.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 07:40:04 +00:00
ff4e2cb84e
chore: gitea-sync --ids, sync recent bugs (#264-#286), refresh STATUS.md
gitea-sync.py: --ids syncs named bugs regardless of dates (OR'd with
--since). Bulk triage bumped last_seen/status_updated across most of the
buglog, so --since 2026-09-01 matched ~1150 bugs while recently fixed older
bugs (bug-1798..1804, 1714, 1754, 1850, 1854) could never be reached.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 07:26:45 +00:00
e3f5a813c6
chore(buglog): bug-1754 fixed (6876e4f6); log snapshot exit_info gap
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 03:33:15 +00:00
6876e4f6f2
fix(controller): persist a failed job's exit code through WAL_JOB_FAIL (bug-1754)
A worker reports every nonzero job exit as a FAIL (xcecd_job_finished ->
xcecd_notify_job_fail(jobid, stage, exit_code)), not a FINISH. The deferred
WAL_JOB_FAIL fact carried only {jobid, STAGE_FLAG|stage} and no apply seam
touched job.exit_info, so exit_info stayed NULL for every failed job, live
and after replay, and HOOK_DETAIL_JOB_FAIL subscribers saw code 0.

- persist_submit_job_fail_fact(): header + int32_t code trailer, used by
  record_job_fail_fact(). Old entries (no trailer) decode as before.
- apply_job_fail_exit_code(): records the code in exit_info for pre-exit
  stages only; a POST_EXEC code is the script's and leaves exit_info alone.
  Kept out of apply_job_fail_transition() so mark_job_fail()'s internal
  -errno failures (not replayed with a code) stay live/replay consistent.
- Both apply seams adopt the code. apply_wal_entry()'s WAL_JOB_FAIL case
  also stops assigning the wire state 0 for STAGE_FLAG entries and runs the
  real transition, as apply_committed_entry() already does.

New test_wal_job_fail_fact.c (5 tests, 3 RED before the fix); the two
test_worker_resync.c assertions that pinned exit_info == NULL now assert the
worker-reported code. Controller suite: 1228 tests, 0 failures.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 03:32:54 +00:00
1ba643b8be
chore(buglog): bug-1714, bug-1840 cannot-reproduce on current builds; log bug-1866
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 15:42:10 +00:00
d87b78dcae
chore(buglog): bug-1850, bug-1865 fixed (soak-verified)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 15:20:45 +00:00
8d5bcdbbc6
chore(buglog): bug-1854 fixed (663ea63c); bug-1850, bug-1865 committed pending soak
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 15:09:49 +00:00
663ea63cc5
fix(scheduler): re-pushed resource definitions are not errors (bug-1854)
Every scheduler (re)join re-pushes the whole config, and both apply paths
-- sched_init_resources() (scheduler.c) and core_apply_config_resources()
(core_events.c) -- re-added each resource with makeResource(), which
refuses an existing name: one ERROR per resource per rejoin (33 in
data/xce_scheduler.log), and a genuinely changed definition was dropped
just as silently.

New libxce resourceApplyDef(): under one write lock, register a new
resource (ADDED), or compare an already-registered one on its
scheduling-relevant fields -- valtype, ordtype, flags, interval -- and
return SAME or CHANGED. Both call sites log SAME at debug, CHANGED as a
WARN that the definition is not applied to the running scheduler
(updating a registered resource in place would disturb its index and
accounting), and only real failures as ERROR.

Test test_resource_reapply_same_is_noop_changed_is_reported (EEXIST on
an identical re-push before). Scheduler suite 87/87, lib 1/1. Re-verify
of data/scheduler-replay-20260925-133708.replay: 0 add-resource ERRORs
(33 before), 0 changed-definition WARNs, 0 mismatches, same 60 DISPATCH /
908 PEND decisions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 15:09:49 +00:00
6b1862f8d3
fix(lib): retry a NOT_READY controller in place instead of failing over (bug-1850)
xce_failover_classify() mapped E_XCE_NOT_READY with NOMASTER to
RETRY_BACKOFF, so xcecli_retry_next() rotated to the next master,
reconnected and fired the rejoin hook. But a NOT_READY reply comes from a
controller that answered on a working connection -- here a healthy primary
that had briefly lost its write quorum (both standbys NACKing, bug-1864).
The scheduler abandoned it for the standbys, rejoined, and got a full
re-push built from lagging committed state (21:39:24-27 in data/logs).

New action XCE_FO_RETRY_SAME (appended; existing values unchanged):
NOT_READY backs off (same jittered schedule, same -E_XCE_NOT_READY on
budget exhaustion) and re-sends on the current connection -- no rotate,
reconnect or rejoin. NOMASTER keeps rotating after backoff. If the
controller is really a standby it answers REDIRECT/STANDBY/NOMASTER next
time, and if it dies the transport error rotates, as before.

Tests (test_libxce_retry.c): taxonomy row NOT_READY -> RETRY_SAME;
decide backs off and exhausts to -E_XCE_NOT_READY; the real
xcecli_retry_next keeps cur_master on NOT_READY (rotated before) while
STANDBY still rotates; backoff-growth test moved to NOMASTER. Failover
tests 56/56; lib, controller 5/5, worker 10/10, scheduler 87/87,
harness 5/5.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 15:04:47 +00:00
910b43cd37
fix(controller): never close() the primary channel's raw fd (bug-1865)
handle_ctrlhello stored the netio endpoint's fd (primary_ctrl_fd = ep->fd)
and ha_on_primary_ctrl_fd_rdhup close()d it in three branches. netio owns
that socket and closes it on endpoint teardown, so by the time the
liveness poll fired the RDHUP the number could be reused -- in a chaotic
soak by the standby's WAL segment: HA's close() shut the WAL, fdatasync
failed 2305 times (EBADF, then EINVAL once a socket took the number),
durable_lsn stuck, and one round's jobsub failed EIO.

handle_ctrlhello now also holds a reference on the endpoint
(primary_ctrl_ep), which keeps its socket open and its fd number ours.
ha_drop_primary_ctrl_conn() replaces the raw closes: shutdown() the
still-owned socket so netio sees the hang-up and tears it down itself,
then release the reference. A bare fd with no endpoint is only forgotten.

Test test_rdhup_never_closes_an_fd_netio_already_released: real
ctrlhello -> netio teardown -> file opened on the freed number -> RDHUP;
the file was closed before, survives now. test_rdhup_closes_and_nulls_fd
updated to the new contract (peer reads EOF, fields cleared). Controller
suite 5/5.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 14:55:04 +00:00
98c52b7d96
chore(buglog): bug-1864 fixed (0d3bb965); log bug-1865
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 14:44:14 +00:00
0d3bb965e5
fix(lib): keep per-endpoint message order in the vtask dispatcher (bug-1864)
The dispatcher checked each queued task's endpoint busy flag one item at a
time, while the vcpu that finished the endpoint's previous message cleared
the flag without the taskq lock (a lock there deadlocked, bug-1282). Within
one scan, task N was skipped (endpoint still busy), the endpoint freed up,
and task N+1 of the SAME endpoint was dequeued first. Standby controllers
then handled WAL catch-up entry 258 before 257 and NACKed (log mismatch):
26 reorders -> ~576 NACKs in a 45-min production-like run, both standbys
at once at 21:39:24, primary lost its write quorum and answered
jobdispatch/join with E_XCE_NOT_READY, the scheduler client failed over and
rejoined, and the rejoin JOBS_PUSH carried lagging committed state
("refusing resync state ... existing job is RUN" WARNs).

dataq gains dataq_deq_ctx() (a per-scan matcher context). The scan is now
vtask_deq_eligible(taskq, is_blocked): it remembers every endpoint found
busy and keeps all later tasks of that endpoint ineligible for the rest of
the scan (past 64 distinct busy endpoints, every later IO task waits for
the next scan) -- per-endpoint FIFO without any new lock.

Test lib/testsuite/test_vtask_order.c injects is_blocked to free the
endpoint mid-scan: task N+1 was dequeued before the fix, none after; other
endpoints and FIFO/generic tasks unaffected. Suites: lib 1/1, controller
5/5, worker 10/10, scheduler 87/87, harness 5/5. Chaotic HA soak 2/2 PASS
with 0 WAL reorders (2 and 5 per run before), no ASAN.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 14:44:14 +00:00
6247bf052b
chore(buglog): bug-1766 fixed (601b245e)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 13:27:08 +00:00
601b245e5a
fix(scheduler): a failed COMPOUND leaves no partial allocation (bug-1766)
When a later primary of a COMPOUND failed, sched_make_compound_alloc_
still moved the earlier primaries' merged allocation onto the schedobj,
so a job decided PEND carried a partial allocation that the next
successful round overwrote and leaked. The merged allocation is now
published only when every primary succeeds and freed otherwise. No
resource-state rollback is needed: the primaries' uncommitted debits live
in ctx.uncomitted, per-attempt scratch freed by sched_ctx_fini(), and
schedobj->alloc's only reader is the dispatch path. The ALTER loop's
per-alternative cleanup (added with bug-1769) is now redundant and removed.

Test: "1*{..} + 5*{..}" on 2x2 slots PENDs with schedobj->alloc NULL
(held the first primary's allocation before). Scheduler suite 87/87.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 13:27:08 +00:00
b63bb581d4
chore(buglog): point fix references at the re-signed commit hashes
The 16 commits since 534895c5 were re-signed (rebase --exec amend -S; tree
unchanged), which changed their hashes. Update the buglog and the
scheduler-allocation completion note to the new ones.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 13:15:43 +00:00
428dd4f740
chore(buglog): bug-1863 fixed (71287ef1)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 13:15:04 +00:00
aea3473823
fix(scheduler): allocate every primary of a COMPOUND, not only the last (bug-1863)
Each primary's allocator (seqjob/parjob my_alloc) sets ctx->alloc to a
fresh job allocation covering only its own hosts, so a multi-primary
COMPOUND ("1*{A} + 2*{B}") dispatched only the last primary's hosts --
leaking the earlier allocations while every primary's hosts had been
debited in ctx.uncomitted.

sched_make_compound_alloc_ now folds each primary's allocation into one
job allocation via the new libxce merge_job_alloc(): a host placed by
several primaries becomes one entry with per-resource values added
(matched on resname, instid and reserve, so distinct instances are never
summed); new hosts and shared allocations are appended/merged the same
way. Partial-failure rollback remains bug-1766.

Tests: "1*{..} + 2*{..}" on 2x2 slots now allocates 3 slots across two
distinct hosts; "1*{..} + 1*{..}" yields one host entry with 2 slots
(both kept only the last primary before). Scheduler suite 87/87, lib 1/1.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 13:15:04 +00:00
a6a0502d62
chore(buglog): bug-1768 fixed (34c52ccb); log bug-1863
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 13:15:04 +00:00
b4e9a09925
fix(scheduler): check slots/rusage for primaries nested in COMPOUND/ALTER (bug-1768)
seqjob_prepare_allocation() and parjob_prepare_allocation() returned 0
whenever ctx->schedobj was not PRIMARY, so a primary nested in a COMPOUND
(or ALTER) job never had its slot count or rusage checked: a 5-task
compound dispatched onto 4 slots, and an ALTER job was "placed" by its
first alternative with an empty (0-slot) allocation. Both hooks now
compute the want from ctx->primary -- the primary being allocated, set by
sched_ctx_set_primary() for nested primaries and identical to
obj->u.primary->resreq for PRIMARY jobs.

sched_ctx_set_primary() also clears ctx->worker, so the next primary of a
compound computes its job-level want rather than a per-host want left
over from the previous primary's host (PRIMARY jobs set it once, with
worker already NULL).

Tests (real ingest + live allocator): a 5-task COMPOUND on 2x2 slots now
PENDs; "{5*{..}} || {1*{..}}" falls back to the second alternative (1
slot). Both failed before. Scheduler suite 87/87.

Follow-up logged: bug-1863 (multi-primary COMPOUND keeps only the last
primary's allocation).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 13:15:04 +00:00
3991d6d4f5
chore(buglog): bug-1769, bug-1771 fixed (43974a21)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 13:15:04 +00:00
1ecd348f03
fix(scheduler): ALTER allocation type confusion (bug-1769); NULL queue in legacy fcfs hooks (bug-1771)
bug-1769: every builder stores xce_compound_schedobj_t* (24 bytes) in
obj->u.alters, but sched_make_alter_allocation() read them as
xce_sched_obj_t* (176 bytes) and ran the compound allocation on them -- a
heap-buffer-overflow read of ->u.compound and write of ->alloc for any real
`{..} || {..}` resreq. The compound allocation now takes its resreq
explicitly (sched_make_compound_alloc_), and each ALTER alternative is
tried on behalf of the ALTER obj itself; a failed alternative's partial
allocation is dropped before the next (rollback itself is bug-1766).
sched_ctx_init() rejected ALTER objs ("bad sched obj type"), so ALTER jobs
could never allocate at all -- it now accepts them like COMPOUND (all
downstream checks only distinguish PRIMARY).

bug-1771: plugin_commit_decision() fans every DISPATCH out to the legacy
"xce_sched_fcfs" plugin even when it never tracked the job; fcfs_del_job()
(and fcfs's job-add hook) dereferenced job->queue->name with no NULL
check, SEGV'ing the allocator for a job whose queue was never registered.

Tests (test_parjob_e2e_multislot.c, real ingest + live allocator thread):
test_alter_resreq_job_dispatches_via_real_allocator (ASAN
heap-buffer-overflow before, DISPATCH after) and
test_dispatch_of_job_with_unregistered_queue_does_not_crash (SEGV at
fcfs.c:318 before). Scheduler suite 87/87.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 13:15:03 +00:00
4ba0f32766
chore(scheduler-allocation): mark implementation complete
All 16 subtasks were already done; check the 6 parent tasks and set
phase implementation-complete. Verified 2026-09-24/25: scheduler suite
87/87 (ASAN), live multi-host e2e PASS, HA soak sequential + chaotic PASS.
Out-of-scope follow-ups stay open: bug-1766, bug-1768, bug-1769,
bug-1771, bug-1860.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 13:15:03 +00:00
250f7d1ac2
chore(buglog): bug-1862 fixed (f0762962)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 13:15:03 +00:00
c29518b21e
fix(worker): lock the worker list against vtask-thread readers (bug-1862)
The main loop adds and reaps worker-list entries (add_worker,
cleanup_worker_list -> remove_worker) while controller request handlers on
vtask threads walk the same list with no lock. A rejoin resync overlapping
a job reap read a freed node (ASAN heap-use-after-free in
find_worker_by_jobid), killing the worker mid chaotic HA soak.

- xcecd_worker_list_lock/unlock: add_worker links a fully built entry under
  it; remove_worker unlinks under it and frees after.
- handle_jobstatequery holds it from reading the head through the query
  page (the report send is a non-blocking xce_request); job control and
  remote task start copy what they need under it instead of keeping the
  entry pointer.
- The finished-job retention store (appended by the main loop, read by
  resync) gets its own mutex around its public entry points.

Test: test_build_report_safe_against_concurrent_list_mutation -- UAF 3/3
with no-op lock stubs, clean 5/5 with the fix; worker suite 10/10; chaotic
HA soak 2/2 PASS, 6/6 resync jobs reconciled, no ASAN reports.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 13:15:03 +00:00
4d77af7323
chore(buglog): bug-1429, bug-1861 fixed (574a5f84); bug-1859 fixed (444fd504); log bug-1860, bug-1862
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 13:15:03 +00:00
7c98c056c9
fix(worker): don't let a failover outage expire a finished job's retained outcome (bug-1859)
The finish notify is fire-and-forget, so the retained outcome is the only
way a controller learns a finish lost in a failover. Its 60s window ran
from the finish time, so an 84s outage expired it before the rejoin
resync could report it; the controller got "unrecognized" and requeued a
completed job (then stuck: scheduler kept RUN, see bug-1860).

Expiry is now measured from max(finish_time, last reconnect)
(xcecd_on_reconnect), and nothing expires while checking_connection's
hello probe reports the connection unhealthy. The reported finish_time_ms
is unchanged.

Verified: worker suite 10/10 (two new retention tests, red first); chaotic
HA soak 3/3 PASS (was 1 in 3 failing).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 13:15:03 +00:00
b5d59253e5
fix(controller): stop a failed test from hanging the unit suite (bug-1429); mkstemp for upgrade temp file (bug-1861)
A failing TEST_ASSERT longjmps out and skips the fixture teardown, leaving
the commit engine's sweep thread and persist's fsync_worker asleep on a
static struct. The next test re-inits it underneath them, corrupting the
condvar, and a later teardown blocks in pthread_cond_destroy/join until
the 300s meson timeout. The failure line itself was lost because stdout
was fully buffered.

- controller_test: line-buffer stdout; tearDown() reaps what a failed test
  leaked via new XCE_TESTING registries (persist_test_reap_unstopped, then
  commit_test_reap_unfinalized_engines -- persist_stop before engine fini).
  Clean run reaps 0; an injected failure now fails in 2s instead of hanging.
- persist: flusher_flush_once reports only its own durable advance, so
  fsync_worker no longer re-fires the durable callback for an advance a
  concurrent persist_flush made (flaky "must not re-fire" test). Regression
  test test_flusher_pass_does_not_claim_concurrent_flush_advance.
- tests: jap observer is atomic and waits (bounded) for a continuation that
  may resolve on the flusher thread; pump catch-up tests wait for trailing
  replies instead of a single scan.
- config_repl: upgrade decode temp file via mkstemp instead of a
  pthread_self()-derived /tmp name (same value across processes, opened
  without O_EXCL -- collisions and a symlink target).

Verified: controller_unit_test 1222/1222; 24 parallel runs (6x4), no
hangs or ASAN errors.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 13:15:03 +00:00
44c1981cff
chore(buglog): mark bug-1858 fixed (393a07c2); log bug-1859
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 13:15:03 +00:00
9e9f2798a0
fix(scheduler): release interned queue resreq with dereference, not free (bug-1858)
q->resreq holds an interned makeString() value (pointer past the xstring
object header). core_upsert_queue() and sched_update_existing_queue()
released the old value with FREEUP()/free() when a queue update carried a
new resreq, an ASAN bad-free. Seen as HA soak replay-parity errors=1: the
sim replay of a snapshot-restored queue followed by a config push aborted
in core_upsert_queue.

Both sites now dereference() the old value (NULL-guarded). Regression
tests cover each site; the existing queue-update test had skipped the
resreq branch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 13:15:03 +00:00
441008cd30
chore(buglog): mark bug-1857 fixed (e216cb9a)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 13:15:03 +00:00
3cc2f6e3d8
fix(xce-test-multihost): deploy with a fixture config set that clears limits (bug-1857)
Since bug-1790's fix (9caf1424) counts a job's full task total against
global limits, the repo's limit1 (all users except rshen share 3 slots)
pends xce_test's root-submitted 33-task job forever, so the multihost
e2e fixture failed even though the allocator is correct.

deploy-ha.sh gains an optional XCE_DEPLOY_CONFIG_DIR (default unchanged:
src/configs). run-multihost-verify.sh copies src/configs into its deploy
dir, replaces limits.yaml with an empty list, and deploys with that
override.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 13:15:03 +00:00
534895c5f6
save wolf status 2026-09-24 19:23:24 +08:00
b0f2aea466
chore(gitea-sync): record first status-driven Gitea sync
- buglog: gitea_issue / gitea_synced_status for the 20 bugs created and the
  30 already-synced entries (#25/#35 reopened as partial)
- .kiro/specs/*/spec.json: "gitea" milestone/issue mapping for all 66 specs
  (65 milestones + 195 issues created, 158 closed at creation)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 10:22:05 +00:00
e915f8def4
fix(gitea-sync): survive live Gitea replies; point project memory at /buglog
gitea-sync.py fixes found on the first live run (bug-1855 follow-up):
- 204/empty-body replies (label DELETE) no longer crash in json.loads
- every request has a 60s timeout, so a stalled call fails instead of hanging
- spec.json sync state is inserted as a trailing "gitea" block instead of
  re-serializing the file (15 hand-formatted spec.json files would reflow)
- implementation issue body labels generated vs approved correctly; the
  validation body uses the same done-phase set as the close rules

Project memory now routes all buglog writes through the buglog skill:
CLAUDE.md key rule, a cerebrum Do-Not-Repeat entry (with the older
duplicate-id / json.dump / tags-vs-fix lessons tagged as superseded), and
xce-audit-invariants step 5.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 10:21:51 +00:00
19104f1940
feat(buglog): status field, buglog skill, status-driven gitea-sync
The buglog had no status field, so "is this bug open?" meant reading free
text, and gitea-sync closed any issue whose `fix` text was non-empty --
including "NOT FIXED"/"PENDING"/"PARTIALLY FIXED" ones (bug-1855).

- Backfill `status` (+ status_updated) on all 1,193 entries. The 85 entries
  whose text said not-fixed/pending/out-of-scope were checked against HEAD
  and git history; the rest are `fixed`. Result: 36 open, 10 partial,
  2 investigating, 5 cannot-reproduce, 4 wontfix, 1 duplicate.
  Insert-only diff: existing lines are untouched.
- .claude/rules/buglog-status.md defines the status values (kept out of
  openwolf.md, which `openwolf update` overwrites).
- scripts/buglog.py + buglog skill: add (next free id), status, note,
  recur, list, show, triage. Edits only the touched entry's lines; a full
  json.dumps rewrite churns ~1,200 lines of mixed \u-escaping.
- gitea-sync (script + skill): state and a single bug:<status> label come
  from `status`; reopen supported; issues shared by several entries follow
  the most-open one; only status labels are swapped; per-entry write-back
  with gitea_synced_status so unchanged entries cost no API calls; list-typed
  `fix` no longer crashes; spec issues close on done phases (spec.json has
  no implementation approval); --since no longer floods Gitea after the
  backfill.
- Disable OpenWolf buglog auto-detection: its hook numbers new entries
  len(bugs)+1, which collides with existing ids (bug-1856, upstream).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 09:40:48 +00:00