The buglog was one 27K-line JSON file whose fields covered only symptom,
root cause and fix, as free text: 2% of entries named the code version a
bug was found on, 12% the build profile, 36% a way to reproduce, and 58% of
the file was OpenWolf hook auto-detections ("Missing guard clause",
"Function not marked async" on .md files).
- .claude/skills/buglog/bug-record.md: the record standard. Eight
sections (Summary as the title, Description, Environment, Reproduction;
Root cause, Fix, Verification, References once fixed), each with the
content it must contain -- e.g. Environment needs the OS, the libc the
failing binary loads (from its ELF interpreter: glibc 2.42 in the nix
shell vs 2.40 on the host), toolchain, build profile and commit.
- .wolf/bugs/<id>.md: 500 records migrated from buglog.json; every field
verified to round-trip. They carry legacy: true and an explicit "not
recorded" where the old log had no such field; component derived from
file paths, severity from keywords (else unknown), fix_commits from
hashes in status_note/fix that git resolves (91 of 463 fixed).
- .wolf/buglog-archive.json: the 710 hook auto-detections, verbatim.
- .wolf/buglog.json is now an index generated from the records, keeping
the fields OpenWolf's hooks and `openwolf bug search` read.
- scripts/buglog.py rewritten: new (template + Environment collected from
the machine and --binary), check, status (refuses a status the record
doesn't support; fixed needs Root cause, Fix, Verification and
--commit hashes git resolves), note, recur, set, show, list, triage,
env, index. 29 unittest cases in scripts/tests/test_buglog.py.
- scripts/git-hooks/pre-commit runs `check --staged`: incomplete staged
records, a legacy record hand-changed to fixed without the fix-time
sections, a new record claiming legacy: true, or a stale index refuse
the commit.
- scripts/gitea-sync.py reads the records: issue body = the record's
sections plus linked fix commits (origin remote, GITEA_COMMIT_REPO to
override), and a body hash so an issue's text is refreshed when the
record changes.
- bug-1866 rewritten to the full standard as the worked example.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
10 KiB
| id | status | status_updated | status_note | component | severity | timestamp | last_seen | occurrences | files | tags | related | legacy | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| bug-1589 | fixed | 2026-09-24 | worker | unknown | 2026-08-19T03:20:00Z | 2026-09-19T09:00:00Z | 2 |
|
|
|
true |
bug-1589: User-reported, live 3-node deployment log analysis: a 5-element job array…
Description
User-reported, live 3-node deployment log analysis: a 5-element job array (base_jobid=0-7-1787111880-0, array_size=5) was fully expanded and all 5 elements were dispatched by the controller (job.c's dispatch_a_job_ fires for [0]-[4], confirmed in logs/ctrl-1.log) and elected by the scheduler's limit plugin (logs/scheduler.log's limit_job_election fires for [0]-[4]), but only element [0] ever produced an on_job_start log line (job.c:1673) in either log file -- elements [1]-[4] were dispatched but never reported as started. User's own hypothesis: may be related to the worker running in 'virtual host mode' (xcecd_virtual) rather than a real xcecd worker.
Environment
Not recorded (migrated from buglog.json).
Reproduction
Not recorded (migrated from buglog.json).
Root cause
CONFIRMED via source trace + live reproduction (both hypotheses from the prior static-reading pass were REFUTED). (1) Code path: worker/meson.build shows xcecd_virtual == xcecd_srcs + main.c, built with -DVIRTUAL_CLUSTER -DDBUS_MANAGER -- and the REAL xcecd target uses the SAME xcecd_srcs + main.c with -DDBUS_MANAGER only. Because DBUS_MANAGER is defined for BOTH binaries, main.c's run loop (#ifndef DBUS_MANAGER worker_process_update(...) #else dbus_workers_update(...) #endif, main.c:603-616) ALWAYS takes the dbus_workers_update() branch for both -- process_worker_manager.c's worker_process_update() (the 'looks less obviously broken' alternative flagged in the original hypothesis) is dead code, never called, in either build. Separately, ipc_master_init() is #ifndef VIRTUAL_CLUSTER-gated (main.c:571-579) so for xcecd_virtual no IPC-master unix-socket listener is ever created; master_job_status_callback() (main.c:163) and the single global static xcecd_job_t job (main.c:32) are therefore UNREACHABLE for xcecd_virtual (no forked job-runner ever connects to report MSG_JOB_STATUS over that socket) -- the original hypothesis about a missing job.job_id = job_status->job_id assignment there was investigating dead code for this deployment shape; it may still be a latent issue for a REAL xcecd with genuinely concurrent forked job-runners, but is NOT bug-1589's cause and was not touched. (2) Real root cause: dbus_workers_update() (xcecd_dbus_manager.c:178, called from main.c's run loop every ~10ms) iterates xcecd_ctx.worker_list -- one xcecd_worker_t node per concurrently-dispatched job -- with xcecd_worker_t* worker = *worker_list; while (worker) { ... worker = worker->next; }. Several branches inside the loop body used a bare continue that re-tests the SAME worker pointer instead of advancing (the VIRTUAL_CLUSTER JOB_STARTING->JOB_RUNNING branch, xcecd_dbus_manager.c:201 pre-fix; the exit_code==0 and exit_code<0 VIRTUAL_CLUSTER completion-check branches, pre-fix lines 241/268; the real-mode 'job done but running not received' branch, pre-fix line 291) -- and worse, the WORKER_DONE/WORKER_EXITED early check (xcecd_dbus_manager.c:205-209 pre-fix) did worker = worker->next; return true;, which ABORTED THE ENTIRE FUNCTION the moment ANY worker in the list was already finished, discarding the scan of every worker queued behind it (also a latent bug: return true == return 1 == XCECD_ERROR on a function whose only real caller only special-cases XCECD_DB_ERROR, so this wasn't even surfaced as an error). Net effect on a job array: once dbus_workers_update() started making progress on the HEAD of xcecd_ctx.worker_list (element [0], first added via add_worker()), every OTHER concurrently-dispatched element ([1]-[4]) was starved of update_worker_status() -- the function that fires xcecd_reply_startjob() i.e. the on_job_start report -- for as long as element [0] stayed un-reaped from the list (which for a long-running job is indefinite, matching the user's 'never, in either log file' observation for a job array of sleep N-style long jobs; for short/instant jobs the same bug still starves [1]-[4] but only for the ~10ms tick until cleanup_worker_list() reaps the finished head and the next element becomes head in turn). This is a WORKER-side bug only -- the controller's on_job_start()/record_job_start_fact() (job.c:1665-1663) and its job_state_is_backward() guard (job.c:43) were traced and confirmed to correctly key everything by jobid with no cross-job interference; they simply never received a report for [1]-[4] because the worker never sent one.
Fix
FIXED. xcecd_dbus_manager.c's dbus_workers_update(): capture xcecd_worker_t* next = worker->next; once at the top of every loop iteration (right after while (worker) {), and changed every early-exit path in the loop body to worker = next; continue; instead of a bare continue (re-tests the same node forever) or the old worker->worker_status == WORKER_DONE/WORKER_EXITED branch's return true (aborted the whole scan on the first finished worker). This fixes traversal for BOTH VIRTUAL_CLUSTER and real (non-VIRTUAL_CLUSTER) branches of the function -- the real-mode kill(pid,0)!=0 / 'job done but running not received' branch had the identical bare-continue bug and would starve a real xcecd's worker_list the same way under concurrent dispatch. No change to the state-machine logic itself (update_worker_status(), check_worker_sleep_time_elapsed(), the systemd dbus_is_worker_active()/get_systemd_job_exit_code() calls) -- purely a traversal/control-flow fix, matching the 'minimal, correct, no symptom-papering' guidance; considered and rejected converting worker_list to a per-jobid array (process_manager.c's processes[] precedent) since the linked list itself was never the problem, only its traversal. TDD: new src/worker/testsuite/test_dbus_workers_update.c (2 tests, white-box #includes xcecd_dbus_manager.c with -DVIRTUAL_CLUSTER -DDBUS_MANAGER matching xcecd_virtual's real build flags, stubs update_worker_status()/is_systemd_job_active()/get_systemd_job_exit_code()/stop_systemd_job()/make_job_sd_unit_name()/start_systemd_job()/get_systemd_job_pid()/check_worker_sleep_time_elapsed() to isolate the loop's own control flow) wired into src/worker/testsuite/meson.build. RED-verified live: git stash push -- src/worker/src/xcecd_dbus_manager.c, rebuild, both tests FAIL ('Expected 0 Was 1' -- the started-call-seen assertions correctly detect elements [1]-[4] never receiving their start transition, and a DONE head worker correctly blocks the scan); git stash pop, rebuild, both tests PASS (GREEN). Live end-to-end proof (not just the unit test): deployed a fresh 3-controller + scheduler + xcecd_virtual cluster (/xce-deploy-ha pattern, dedicated dir /tmp/xce-deploy-bug1589), submitted jobsub -u rshen -q normal -c "sleep 300" -a 5 (5-element array, rshen used specifically to avoid limits.yaml's 'limit1' 3-slot cap on all-users-except-rshen, matching the user's original unconstrained-concurrency scenario) -- ctrl-1.log showed dispatch_a_job_ AND on_job_start for ALL FIVE elements [0]-[4] (previously only [0] would ever show on_job_start), scheduler.log showed limit_job_election for all 5 (as it already did pre-fix), a follow-up joblist showed all 5 elements in state RUN, and xcecd-worker-1.log showed the Job ID <id> running - sending status to controller line (the exact xcecd_reply_startjob trigger) for 5 DISTINCT job ids within milliseconds of each other. Cluster torn down after capturing evidence (self-started, self-stopped, per boundaries). Note: this repro session also observed pre-existing, unrelated 'Failed to connect to system bus' ERROR spam from dbus_is_worker_active()/get_systemd_job_exit_code() polling every ~10ms per still-running virtual job (expected -- no real dbus session in this sandbox, and VIRTUAL_CLUSTER never creates a real systemd unit to poll in the first place) -- cosmetic log noise, not a correctness bug, pre-existing before this fix and unrelated to bug-1589's actual defect; flagged here for visibility, NOT fixed (out of this bug's scope). Also flagged, NOT fixed (out of scope, no buglog entry created per explicit task boundary): dbus_worker_clean() (xcecd_dbus_manager.c) has an apparently-inverted #ifndef VIRTUAL_CLUSTER return true; guard -- as written, the REAL (non-virtual) xcecd never actually runs its own systemd-unit cleanup body, while VIRTUAL_CLUSTER mode does (but finds nothing to clean, since no real unit ever existed) -- looks backwards from the evident intent, worth a separate look. Full suite after fix: worker 4/4 GREEN (resource_sync, xcecd_startup_order, dbus_workers_update, controller_connector). Controller 1101/1104 (3 unrelated FAILs in test_config_repl.c -- config version trimming/tag lookup/marker supersession, a file this fix never touches and that was confirmed clean/unmodified by this session; controller/ was not touched at all by this fix). Scheduler: bug-1560's known symlink-escape failure (expected, unrelated) plus 3 NEW failures in test_sim_verify.c/test_repl_breakpoints_inspection.c -- traced via git status to src/scheduler/src/driver.c and src/scheduler/src/recording_session.c being concurrently modified, uncommitted, by ANOTHER session's in-flight work in this shared worktree (unrelated 'bug-1591' job.c/driver.c changes observed mid-flight during this session's builds) -- outside this bug's boundary (scheduler/core files are explicitly off-limits for this task) and not caused by this worker-only fix; not investigated further.
Verification
Not recorded (migrated from buglog.json).
References
- Related: bug-1586