Commit graph

1507 commits

Author SHA1 Message Date
d84a793578
docs: document graceful job kill/stop and job.terminate_interval (bug-1957)
- src/docs/xce.md: explicit signal and force kill are REST-only (signal,
  force_kill); xcecli has no signal option yet.
- src/docs/deployment.md: app.yaml row lists the job block
  (max_array_size, terminate_interval) and trusted proxies.
- xcecli jobs kill/stop/resume help describes the signal sequences.
- REST: control endpoint annotations and the signal/force_kill model fields
  describe the behaviour (swagger output not regenerated: swag fails on an
  unrelated annotation).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 10:07:42 +00:00
97f7c800f1
fix(apiserver): validate, default and document job.terminate_interval (bug-1957)
The loader now stores the effective interval (10 s when unset or 0, LSF's
JOB_TERMINATE_INTERVAL default) and refuses a negative one; app_schema.json
describes the setting; src/docs/xce.md documents kill/stop behaviour and
the interval.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 10:04:47 +00:00
367af11af1
fix(worker): graceful job kill and catchable stop, as LSF (bug-1957)
A plain kill sent one SIGTERM and never escalated, and a plain stop sent
one uncatchable SIGSTOP. Following LSF's TERMINATE and SUSPEND actions, a
plain kill now sends SIGINT, then SIGTERM, then SIGKILL, and a plain stop
sends SIGTSTP (the job can catch it, prepare and stop itself), then SIGSTOP
if it has not stopped; each next signal follows after the grace period and
only while the job is alive (or not yet stopped). A resume cancels a
pending stop; the job's exit ends any sequence. An explicit signal is
delivered once; a force kill is SIGKILL at once.

- JobSignal gains grace (capnp @2, xce_signal_t.grace): > 0 asks for the
  sequence, in seconds between signals.
- apiserver: job.terminate_interval (default 10 s, LSF's
  JOB_TERMINATE_INTERVAL); jobCtrlParams sends kill = SIGKILL and stop =
  SIGSTOP with that grace when no signal is given; the handler no longer
  pre-fills a default signal.
- worker: grace travels through the job-control task, the kept-signal table
  (pending_signal_*_ex) and the IPC message; the job process runs the
  sequence (job_signal_seq_*, uv timer) and checks stop state in /proc.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 10:00:36 +00:00
d3da314041
fix(worker): keep signals for remote tasks that are still starting (bug-1955)
Only a head-host job start opened the pending-signal window (bug-1908), so a
kill for a remote task of a multi-host job that this host had accepted
(runtasks) but not yet registered was dropped with "Failed to find worker".
handle_runtasks() now opens the same window before queuing the task and
closes it if queuing fails; the signal is delivered when the task runs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 09:10:13 +00:00
148557a5d5
fix(worker): deliver the job-control signal the user asked for (bug-1954)
worker_signal_callback() sent SIGTERM for a KILL request without force and
SIGSTOP for any TSTP request. job_signal_to_deliver() (new job_signal.c) now
delivers the requested signal; only a forced terminate escalates to SIGKILL.
The apiserver's default signal for a plain stop becomes SIGSTOP (it was
TSTP and relied on the worker's substitution), so a plain stop still
suspends jobs that ignore SIGTSTP.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 09:07:38 +00:00
e00526f9f5
fix(apiserver): validate named and array submissions like any other (bug-1953)
preCheckJobRequest() returned from inside its job-name branch, so every
named or array job skipped the description, project and resreq checks,
resreq validation and the shell-limit conversion that follow. The name now
only sets the job count and array flag, and every submission runs the rest.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 08:59:34 +00:00
27ef506058
docs(apiserver): document security defaults changed by bug-1945..1952
deployment.md: auth.yaml now holds bcrypt hashes of the (still public)
dev credentials with startup warnings; JWT secret from XCE_JWT_SECRET or a
random per-process secret; logout revocation is per process;
security.trusted_proxies for reverse proxies.
security.md steering: JWT parsing, revocation, secret resolution,
trusted proxies, middleware abort rule, bcrypt and role-change rule.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 08:58:20 +00:00
a80768f31d
fix(apiserver): only rbac:control scope all may change user roles (bug-1952)
Cause: AddRolesForUser and DeleteRolesForUser let an rbac:control grant
narrower than all (e.g. scope self, an accepted domain type for rbac
permissions) change roles whenever the target user was the caller, so a
user holding only rbac:control/self could grant itself cluster_admin
(confirmed by TestAddRolesForUser_SelfScopeCannotGrantItselfRoles: HTTP
200 and cluster_admin assigned).

Fix: canChangeUserRoles requires an rbac:control grant with scope all
(checked over every grant, before deduplication prefers a narrower scope)
to grant or revoke roles for any user, the caller included; anything else
gets 403. Chosen over a "cannot grant permissions you do not hold" rule
because a scoped role change has no safe meaning here and the subset
check across domains would be complex and easy to get wrong.

Tests: TestAddRolesForUser_SelfScopeCannotGrantItselfRoles,
TestDeleteRolesForUser_SelfScopeRejected, TestUserRoleChanges_AllScopeAllowed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 08:58:20 +00:00
4ce4f0b798
fix(apiserver): revoke the token on logout (bug-1946)
Cause: Logout only deleted tokens/<user>/token, a file that token
validation (ValidateHandler / GetUsernameFromToken) never reads, so a
logged-out token stayed valid until its expiry (24 h by default).

Fix: an in-memory revocation set (controllers/token_revocation.go) keyed
by the token's jti (generateJWT now sets a random uuid jti, so two logons
in the same second give different tokens) or, for tokens without a jti,
by SHA-256 of the token; each entry is kept until the token's expiry and
expired entries are purged on every revocation. Logout revokes the token;
the shared ParseTokenClaims (used by ValidateHandler, RequirePermission
and handlers via GetUsernameFromToken) rejects revoked tokens. A missing
token file no longer fails logout. Scope: per process, not persisted and
not shared between apiservers.

Tests: TestLogout_RevokesToken (middleware), TestTokenRevocations_*,
TestIntegration_PostLogoutTokenRejected (replaces the design-gap test).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 08:58:20 +00:00
19a1a6f023
fix(apiserver): never sign or accept JWTs with the shipped default secret (bug-1945)
Cause: config/app.yaml shipped secretKey "xce-api-key" and
resolveJWTSecret used it whenever XCE_JWT_SECRET was unset, so anyone
could forge tokens for any user on such a deployment.

Fix: resolveJWTSecret uses XCE_JWT_SECRET, else the config value, but
treats an empty value or the old public default as unset; then it
generates a random 256-bit per-process secret and InitConf logs a warning
(secret never logged): tokens do not survive a restart and apiservers
sharing one endpoint must set the same XCE_JWT_SECRET. The shipped
app.yaml no longer carries a secret. The controllers test secret is no
longer the old default.

Tests: TestResolveJWTSecret_NeverUsesKnownDefaultOrEmpty,
TestShippedAppYAML_NoJWTSecret, TestInitConf_NoJWTSecretGeneratesAndWarns,
TestInitConf_JWTSecretFromEnvNoWarning.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 08:58:20 +00:00
c4c63f11e7
fix(apiserver): trust X-Forwarded-For only from configured proxies (bug-1948)
Cause: the router used gin.Default() without SetTrustedProxies, and gin
then trusts forwarding headers from every peer, so ClientIP() returned
whatever X-Forwarded-For a client sent: the IP whitelist and the IP bound
into JWTs could be spoofed.

Fix: new app.yaml option security.trusted_proxies (IP or CIDR list,
default empty = trust no proxy, validated at startup) passed to
SetTrustedProxies in Router(); an invalid list falls back to trusting
none. The nginx samples note that the proxy must be listed there.

Tests: TestRouter_IgnoresForwardedForByDefault,
TestRouter_HonoursForwardedForFromTrustedProxy, TestResolveTrustedProxies.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 08:58:20 +00:00
42a402a5b4
fix(apiserver): abort the request on every middleware failure (bug-1949)
Cause: ReturnError only writes the response and never aborts the gin
chain; RequirePermission (token error) and WhitelistIPMiddleware (invalid
client IP) returned after ReturnError without ctx.Abort(), so the route
handler still ran after the rejection.

Fix: ctx.Abort() after ReturnError at both sites (and, for clarity, on the
ValidateHandler body-check path, which validateRequestBody already
aborted). RequirePermission now answers a bad token with 401 instead of
500. ReturnError stays non-aborting (documented): handlers such as
ControlHost call it from helpers and then test ctx.IsAborted(), so a
global abort would change handler flows, and it adds nothing in handlers.

Tests: TestRequirePermission_AbortsOnInvalidToken,
TestWhitelistIPMiddleware_AbortsOnFailure.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 08:58:19 +00:00
42dd1e7a5e
fix(apiserver): pin JWT parsing to the HS256 signing algorithm (bug-1950)
Cause: the ValidateHandler parse callback (middleware isValidToken)
returned the secret without looking at token.Method, and
GetUsernameFromToken accepted any HMAC variant, so tokens signed with
HS384/HS512 were accepted although the server only signs HS256.

Fix: one JWTSigningMethod (HS256) used for signing, plus a shared
JWTKeyFunc (rejects any other alg) and JWTValidMethods parser option used
by both parse paths.

Tests: TestIsValidToken_PinsSigningAlgorithm (middleware),
TestGetUsernameFromToken_PinsSigningAlgorithm (controllers).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 08:58:19 +00:00
f91642dbc0
fix(apiserver): bcrypt passwords and default-credential warnings in config-file auth (bug-1947)
Cause: ConfigFileProvider.Authenticate compared the stored plaintext
password with != (timing side channel, passwords readable from the file),
and config/auth.yaml shipped admin/admin, user1/user1 etc. as plaintext
with nothing reporting that a default credential was active.

Fix: auth.yaml passwords may be bcrypt hashes (golang.org/x/crypto/bcrypt,
v0.23.0 as already pinned in go.mod, now vendored); plaintext entries are
still accepted but compared in constant time (SHA-256 + subtle), and an
unknown user costs the same bcrypt work. The provider reports startup
warnings (SecurityWarner, logged by main) for every plaintext entry and for
every shipped default credential still active. The shipped auth.yaml and
auth.yaml.example now store bcrypt hashes of the same dev passwords, so
tests/regression/run.sh (admin/admin) keeps working. tools/encrypt gains
-action=hash to generate entries.

Tests: TestConfigFileProvider_BcryptPassword,
TestConfigFileProvider_SecurityWarnings, TestShippedAuthConfig_HashedDefaults.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 08:58:19 +00:00
d3ce298232
fix(apiserver): reject auth.yaml without a config_file block (bug-1951)
Cause: NewConfigFileProvider read cfg.ConfigFile.Users without checking
that the optional config_file block was present, so provider: config_file
with no block panicked with a nil-pointer dereference at startup.

Fix: return a clear configuration error when the block is missing.
Test: TestNewConfigFileProvider_Errors/missing_config_file_block.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 08:58:19 +00:00
8568c8ab80
fix(controller): send job control to every host of a multi-host job (bug-1942)
Cause: job_ctrl (job.c) sent kill/stop/resume only to allocs[0]->host.
The tasks on the other hosts are started through runtasks (by the head
worker for a parallel job, or by xrun as the MPI launcher's rsh agent)
and run under their own worker entry on each host; the head worker
signals only its local process group and forwards nothing, so they kept
running.

Fix: job_ctrl sends the request to every allocated host through
job_ctrl_to_host. The head decides the result as before; an unconnected
remote host is logged and skipped (its tasks belong to host failure
handling). Sends are non-blocking enqueues. Test:
testsuite/test_job_ctrl_hosts.c.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 08:43:53 +00:00
f244661b14
fix(controller): allow control of running jobs on connected hosts of any status (bug-1941)
Cause: job_ctrl (job.c) sent kill/stop/resume of a running job only when
the host's status was exactly 0, so any status bit (CLOSED,
CLOSED_BY_ADMIN, BUSY, LOCKED...) refused the request, and the refusal
returned -errno, i.e. whatever errno happened to hold (often 0 or
-ENOENT).

Fix: new job_ctrl_to_host sends the request whenever the worker is
connected (endpoint present, no XCE_WORKER_UNAVAIL); an unknown or
unconnected worker fails with -E_XCE_BAD_WORKER and an ERROR naming the
host. Nothing else changes for jobs on dead hosts. Test:
testsuite/test_job_ctrl_hosts.c.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 08:43:53 +00:00
4d2e80c4bb
fix(apiserver): send the requested signal, force kill and checkpoint options (bug-1940)
Cause: ControlJob (backend/clib.go) looked its content argument up as a
signal name, but the job control handlers pass a JSON JobControlEvent, so
the lookup always missed and every kill sent TERM (stop TSTP, resume
CONT); sig.force and ckpt.kill were hard-coded to 0 and the checkpoint
method was never set.

Fix: new pure jobCtrlParams (backend/convert_pure.go) decodes the event
(or a bare signal name/number), validates the signal against
JobSignalMapTable and returns an error instead of falling back, and maps
force_kill to xce_signal_t.force (kill) or xce_ckpt_options_t.kill
(checkpoint) plus the checkpoint method. ControlJob fills the C structs
from it. Tests: backend/jobctrl_params_test.go.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 08:43:53 +00:00
b9747ad2ff
fix(worker): parse job file transfers under the apiserver's "transfer_files" key (bug-1937)
Cause: the apiserver writes file transfers to exdata as "transfer_files"
(controllers/jobs.go processJobRequest), but parse_exdata() only matched
"transfer_file", so the transfers were logged as an unknown key and
dropped.

Fix: parse_exdata() accepts "transfer_files" and still the older
"transfer_file"; if both are present the first list wins instead of
leaking it. New worker unit test test_exdata_parse.c parses the exdata
JSON the apiserver produces (transfers, env, interactive, limits).
Stage-in/out itself is still not implemented in the worker.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 08:38:26 +00:00
d01ced9bd0
fix(apiserver): send exdata, exec cwd and exec home with submitted jobs (bug-1936)
Cause: modelJobToSubmit() filled only cmdline, execuser, launch_type and
pre/post-exec of job_startinfo_t, so the extended data the submit handler
builds (env, *_limit incl. -W run limit, file transfers, interactive mode),
the cwd and the exec home never reached the controller or the worker.

Fix: map the start info in a pure submitStartInfo() (exdata from
ExtendData, execcwd from ExecCWD or CWD, exechome from ExecHome) that
modelJobToSubmit copies into the C struct; decode exdata back in
cJobToModel/cJobArrayToModels so the store keeps ExtendData and CWD.
Reject extend_data the worker's parse_exdata() cannot take (not a JSON
object, over EXDATA_LENGTH-1 bytes) and drop an empty transfer list
instead of sending a null.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 08:38:26 +00:00
7a9d75d661
fix(scheduler): keep a moved job's own priority (bug-1944)
Both job-fields update paths, the core JOB_UPDATE event (core_events.c,
used by the live and sim drivers) and sched_update_existing_job()
(schedobj.c), set job->priority to the new queue's priority when a job
changed queue, while ingest gives every job 0. builtin_fcfs orders a
queue's jobs by job priority first, so a moved job overtook every job
already pending in the target queue (and snapshot restore, which sets 0,
disagreed with live for moved jobs).

Moving now only re-points job->queue; queue priority still orders the
queues themselves. test_job_move_priority.c moves a job into a priority-10
queue and checks it keeps priority 0 and does not take the only slot from
the queue's first job; test_job_update.c's in-place update case asserted
the old behaviour and now expects the job's priority to be kept.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 08:37:11 +00:00
8b89a0ce6b
fix(scheduler): load user/host groups in the simulator too (bug-1943)
Only live's sched_init_config() (scheduler.c) registered the config's
named user and host groups into core.ugmgr/hgmgr. The simulator takes its
config from the replay snapshot (snapshot_load.c) and later pushes through
the core CONFIG_PUSH event (core_events.c); neither loaded groups, so a
limit whose users name a group ("ug1") never matched in sim, while the
same limit matched in live.

New xce_core_load_groups() (core.c) is the one loader, called by
sched_init_config(), the CONFIG_PUSH event and snapshot restore, each
before limit consumers are built. snapshot.c's field notes now say the
groups are rebuilt from the snapshot's config.
test_sim_group_limit_parity.c checks a ug1-scoped limit matches the same
job on both drivers and that CONFIG_PUSH alone registers ug1 and hg1.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 08:37:11 +00:00
1668a43998
fix(scheduler): exclude admin-closed hosts from new placements (bug-1939)
No scheduler code read XCE_WORKER_CLOSED / XCE_WORKER_CLOSED_BY_ADMIN:
sched_host_iter_next() (host_iter.c), the candidate-host source for both
the seqjob and the parjob allocator, filtered only on the select
expression and resource availability, so jobs kept landing on a host an
admin had closed (parjob's skip_bad_worker_ hook is empty and unused).

sched_host_iter_next() now skips a host with either closed bit before
opening its trace scope, which covers every new placement for serial and
parallel jobs on the live and simulation drivers. Accounting of jobs
already running on the host is unchanged. No new pend-reason code: a
closed host is simply not a candidate. test_host_admin_close.c covers a
serial and a parallel job on both fixture shapes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 08:37:11 +00:00
f51a19d3d5
fix(scheduler): hold jobs of inactive queues at dispatch (bug-1938)
sched_queue_is_ready() in core.c was a `return 1` stub, so the picker
(builtin_fcfs and the legacy fcfs) took jobs from every queue and admin
queue inactivation never reached dispatch.

It now returns false for a queue with QUEUE_STAT_INACT_ADMIN or
QUEUE_STAT_INACT_WINDOW set, so an inactive queue's jobs stay pending.
QUEUE_STAT_CLOSED is deliberately not a gate (LSF semantics: closing only
blocks new submissions; already-pending jobs still dispatch). The status
reaches the core through the same queue add/update paths in both drivers,
so live and sim behave identically. test_queue_admin_control.c covers
open, closed and inactive queues on both fixture shapes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 08:37:11 +00:00
4a38bb3ca9
fix(apiserver): enforce self_ug job scopes and accept project filters (bug-1934, bug-1935)
bug-1934: every job-permission check read a single scope and compared it
only with "self", so a job:view or job:control grant scoped to self_ug acted
like "all" (list, get by ID, control), and self + self_ug collapsed to self.
userJobScope() now collects all of a user's grants of a job permission
before deduplication and adds them up (all; self; self_ug = the user and
everyone sharing a user group). The job list appends a submit_user filter
for narrower scopes, get-by-ID keeps covered rows in a new slice (removing
from the ranged slice skipped rows) and answers 403, both control paths
and job output check the owner against the scope. User groups now reach
the apiserver: the config push converts cluster_config_t.ugroups and
OnUserGroupsUpdated() replaces the new store (nested groups, all,
exclusions). The default basic_user now also sees its user-group mates'
jobs, as its configuration says.

bug-1935: project_name and license_project are accepted job filters, so
xcecli jobs list -P works.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 06:56:57 +00:00
6ecd418d21
fix(scheduler): resolve limit host and project scopes against their own groups (bug-1933)
The limit plugin called build_limit() with the host-group and
project-group managers swapped, so a limit's hosts: members were looked up
among the project groups (a host-group name became a literal host name and
the limit never applied) and its projects: members among the host groups.
Pass them in the order build_limit() declares.

New test test_limit_scopes_resolve_against_own_group_kind; the test's
main() now initializes the interned-string table that group names need.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 03:42:59 +00:00
e3b87d28f3
fix(scheduler): place a task-range job with the largest count that fits (bug-1931)
The parjob plan generator split only maxTasks, so a job submitted with a
task range (minTasks < maxTasks) pended until all maxTasks tasks fit,
even when minTasks did (a tasks 2..5 job pended on a host with 2 free
slots).

The runner now handles a task range in sched_alloc_range_(): it probes
minTasks first and pends after that one search when it does not fit;
otherwise it places maxTasks when that fits, or binary-searches for the
largest count that fits and places it. Fitting is monotone in the count
for balanced plans, so the search is exact for today's plan shapes.
ctx->ntasks carries the count; parjob's job-level pass scales per-task
shared wants and global limits to it, and the generator gets the request
with its task count fixed to it. Fixed counts keep the old path.

The decision trace shows each count tried (task count N (minimum) /
(maximum), placing N tasks) and releases a fitting probe's hosts.
Test fixtures that used minTasks = 1 as filler for an N-task job are now
fixed-count.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 08:51:44 +00:00
0317c7af98
fix(scheduler): cache hits keep the full pend reason; sim uses the cache (bug-1932)
The session-level unsatisfiable-resreq cache stored only the exhausted
attempt's main reason (an int). The allocators record shortfalls as
per-host/per-resource entries and leave main_reason at 0, so every cache
hit in the live scheduler pended its job with an empty reason. The
simulator registered no cache at all (its table lived on xce_scheduler_t),
so it searched every identical pending job again and its traces differed
from production.

- The cache table moves to xce_sched_core_t; cache_lookup/insert/clear and
  the hook adapters take the core, with no cast to a driver struct.
  sched_init() builds it, cache_clear() rebuilds it per session,
  xce_sched_core_fini() destroys it.
- A cache entry is a deep copy of the attempt's full xce_job_preason_t
  plus the job that proved the request unsatisfiable (core->alloc_jobid,
  set by sched_allocator()). A hit moves a copy into ctx->reason; a miss
  leaves ctx->reason untouched.
- main.c passes the same three adapters to the simulator through new
  sched_sim_args_t fields; sched_sim_driver_start() sets them on the sim
  core (plain field writes: white-box tests include sim.c alone).
- Decision trace: a hit renders "no plan generated: skipped, same request
  as <job> proved unsatisfiable earlier this session" (new
  XCE_TRACE_NOPLAN_KNOWN_UNSAT via sched_trace_known_unsat()); the outcome
  shows the restored reasons.
- sim_verify keeps its main-only PEND compare (old recordings carry
  main-only hits); comments and the scheduler-sim-live-allocation design
  note updated.

Tests: full-reason assertions in the cache dispatch tests (RED before:
"a cache hit must restore the original attempt's FULL pend reason"),
test_alloc_cache.c ported to the new API (deep copy, clear frees, -EEXIST
frees), a sim-registered clear test, a renderer test, and a sim e2e
scenario with two identical unsatisfiable jobs (204, 205) where 205 is
skipped with 204's full reasons (RED before: no skip line).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 08:02:18 +00:00
a64d0846c7
build(xceadmin): run the live E2E tests against this build's xce_controller
The three live E2E tests (test_e2e_live_push, test_e2e_live_ha_push x2)
skipped with 'no xce_controller binary found': they run from the build
dir's repo mirror, where controller discovery only finds a sibling
xce.build/build.release/build.dbg controller dir that happens to exist
when pytest starts, and nothing ordered the controller link before the
xceadmin target. The xceadmin custom_target now depends on the
controller and passes its path, which meson-build.sh exports as
XCE_CONTROLLER_BIN (a caller's value wins; left unset in the Nix
package sandbox so nix build keeps skipping them). xceadmin: 373 passed,
none skipped.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 06:41:09 +00:00
9f9b63dd24
fix(scheduler): record each pend reason once per job (bug-1927)
The scheduler appended one NO_RESOURCE/MATCH_FAIL entry per host per
placement plan, so a pending parallel job carried plans x hosts copies
(~93k entries in the 1000-host bounds test). libxce gains
add_job_preason_once() with a per-context hash index keyed on
(host, resource, code) that keeps first-occurrence order; the
scheduler's three builders use it. add_job_preason() is unchanged for
other callers. The parjob equivalence test now expects each short host
once; the bounds test runs in ~2 s instead of ~19 s.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 06:30:58 +00:00
7aef6b85c9
fix(scheduler): break jobid= filters accept every jobid form the REPL prints (bug-1926)
repl_u64_str_eq() compared the packed jobid's raw decimal string with
the filter text, so a breakpoint on the dashed jobid the REPL prints
never fired. The filter is now parsed once when the breakpoint is set
(repl_parse_jobsel_arg, moved to repl_fmt.c) and matched with
repl_jobsel_matches: dashed ids, X[k], X[a-b] and raw decimal ids all
work, a bare array-aligned id also matches its elements, and an
unparseable value is rejected instead of creating a dead breakpoint.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 04:06:53 +00:00
ba6f42cdc2
fix(scheduler): REPL iterator leaks, affected jobids in round summaries, raw ids for trace on job
bug-1928: repl_count_jobs() and repl_info_jobs() never destroyed their
job-table iterators (16 B per continue round); both now do, and the
baseline and trace wiring tests run with leak detection on.
bug-1929: continue-mode round summaries listed only pending jobs, so a
dispatch-only round printed an empty jobids= and rounds dispatching
different jobs collapsed together; rounds now record and list every
affected job (scheduler-sim-repl-verbosity Req 2.1, 3.2). The baseline
transcript changes only in its two jobids= lines.
bug-1930: trace on job now accepts the raw decimal jobid form that info
trace and info job accept, through a shared parser.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 03:37:03 +00:00
5ca48206b2
test(scheduler-sim-decision-trace): close validation coverage gaps; sync design.md
Feature validation round 1: tests for trace off keeping recall (1.5),
no logging from REPL trace sources (8.7), reconstructed records in the
trace file (8.1) and first in continue mode (7.3), output after trace
off matching an untraced run (10.4), and tracing enabled only from the
REPL (11.1). design.md now reflects the REPL-side file split, the added
API, the dropped file tag and the test files; duplicate trace
declarations removed from repl_internal.h.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 17:33:26 +00:00
1b78fa9638
test(scheduler-sim-decision-trace): bounds under large traces; update the scheduler guide
Task 6.4: a 1000-host, 64-task parallel job traced at full level stores
exactly the line bound with exact omitted counts and counters; 10,000
traced decisions keep the trace store within 64 MiB without evicting the
newest. The scheduler guide lists the REPL trace files and test rules
and carries measured file sizes. Full all-components ASAN build passes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 17:11:52 +00:00
2e733994c1
test(scheduler-sim-decision-trace): prove tracing never changes scheduling
Task 6.3: the contention and extra replays run with tracing off, all
jobs at summary and full level, and a job subset, in step and continue
mode; decisions, full pend reasons, --sim-output recordings, snapshots,
--sim-verify results and per-step accounting dumps are identical.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 16:49:26 +00:00
7ac988a7f5
test(scheduler-sim-decision-trace): error-path trace scenarios with the real allocator
Task 6.2: injects plan-claims growth, snapshot, finalize, host-scan,
per-host uncommitted lookup, allocation-function and unknown-jobid
faults and asserts each traced outcome. Also fixes two deferred OOM
paths: a failed limit row keeps the host's verdict bits, and an array
listing that cannot be built is reported instead of silently truncated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 16:28:17 +00:00
4d1a245fd9
fix(scheduler-sim-decision-trace): render an unset pend main reason as none
The pend outcome printed 'main reason: code 0' for every pend the
allocator builds, because 0 (libxce's 'no main reason set') fell through
to the unknown-code fallback. Found by the 6.1 end-to-end review.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 16:07:53 +00:00
625663af59
test(scheduler-sim-decision-trace): end-to-end trace scenarios over the contention fixture
Task 6.1: drives the real simulator REPL with tracing on for all and
selected jobs at both levels, asserting every Requirement 2-6 section,
ordering in step and continue mode and at breakpoints, info trace at a
breakpoint, after finish, for arrays and after a units change, and a
byte-identical trace file copy.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 16:06:22 +00:00
fcca4c9cfa
fix(scheduler): stop sim threads before releasing trace settings on quit (bug-1925)
repl_cmd_quit() (quit and EOF) released the trace settings while the
picker and allocator threads could still run; at a mid-session
breakpoint the allocator reads core->trace_ctl unlocked on every job
pop, a possible use-after-free just before exit. quit now cancels and
joins both threads first, as sched_sim_driver_run() does on return.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 15:44:53 +00:00
a1cb9cba4b
feat(scheduler-sim-decision-trace): wire tracing into the simulation driver
Task 5.3: session hooks around each sim session; trace-only decisions
are handled first and skip verification, recording, echo and
breakpoints; traced decisions are emitted after the echo gate and before
the breakpoint check; reconstructed dispatches get reconstructed
records; unclaimed traces are freed at both decision free sites.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 15:35:20 +00:00
6bac16812c
feat(scheduler-sim-decision-trace): wire the trace commands into the REPL
Task 5.2: trace is routed with the raw line and info trace through info;
help gains a Trace section; set output rejects open trace files; quit
and EOF shut tracing down; the continue-mode rollup reports a traced
round on its own; repl.h gains the interactive-only trace gates. The
baseline transcript changes only in the help and info-usage lines.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 15:15:04 +00:00
8931833132
feat(scheduler-sim-decision-trace): compile trace sources into REPL/sim tests and the sim isolation checks
Task 5.1: the 26 white-box tests that include repl.c or sim_loop.c now
include the REPL trace sources (and the core trace sources where no
hooked source brings them); sched_sim_isolation_lib carries all trace
sources and the sim isolation test scans the REPL trace files.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 14:56:31 +00:00
4df29606f6
feat(scheduler-sim-decision-trace): emit traces inline and apply settings at session boundaries
Task 4.6: session begin makes the next settings current, stamps the
session number and publishes them to the core; session end withdraws and
releases them. Emission marks the round traced, flushes the collapsed
range and reprints the suppressed echo in continue mode, renders to the
console and the trace file with identical bytes, and stores the record.
sim_print_decision is exported unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 14:42:27 +00:00
27cad0b0b7
feat(scheduler-sim-decision-trace): manage the trace file copy and output-file conflicts
Task 4.5: a named trace file is opened for appending at command time;
an open failure or a path that is the sim output file or the scheduler
log (same device and inode) is rejected with settings unchanged; files
close when their last settings holder releases them; set output gets a
conflict check; shutdown flushes and closes every trace file and frees
the store.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 14:26:10 +00:00
e01ffe4c9c
feat(scheduler-sim-decision-trace): pass the scheduler log path to the simulation driver
Task 4.4: the --logfile path (or the default, now SCHED_DEFAULT_LOG_PATH)
reaches the sim driver as an owned copy so the trace file checks can
reject the scheduler log.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 14:13:50 +00:00
aa4c43c897
feat(scheduler-sim-decision-trace): parse the trace command and manage trace settings
Task 4.3: trace on all|job SEL... [level] [lines N] [file PATH], off and
status, parsed from the raw line; invalid arguments are named and leave
the settings unchanged; refcounted next/current settings; status shows
the settings in effect or pending, whole arrays as <base>[*], and stored
trace memory against its bound.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 14:05:36 +00:00
73416e3e91
feat(scheduler-sim-decision-trace): store the latest trace per job and serve info trace
Task 4.2: a new trace replaces a job's earlier one; above 64 MiB the
oldest traces are evicted (never the newest) and marked discarded until
a newer capture; info trace renders a job's or an array's stored traces
in the current units mode, or reports discarded / no trace with the
job's state / unknown job.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 13:50:02 +00:00
fe445e9453
feat(scheduler-sim-decision-trace): render trace records to text
Task 4.1: hierarchy, summary-level position summaries, truncation marker
with the fixed totals block, 50-host outcome cap with aggregates and
separate limit charges, no-decision and reconstructed records, amounts
in the REPL units mode, identical bytes to one or two sinks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 13:30:06 +00:00
16f40fa9dc
feat(scheduler-sim-decision-trace): hook the allocator runner
Task 3.4: the runner records no-plan reasons, the cluster-wide check,
plans (tried, discarded, not tried) and their ends, positions, claimed
skips and per-host candidate verdicts, rollbacks, commit or finalize
failure, and search end. A declined or generator-failed requirement now
counts once as 'no plan generated'. Runner results are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 12:04:16 +00:00
d4bee4b5d0
feat(scheduler-sim-decision-trace): hook the host pre-filter, host scan and satisfaction check
Task 3.3: the pre-filter records selection mismatches and internal
errors as host verdicts, a scan stopped by an error marks the position
as a scan error, and unsatisfy_() hands every checked row to the current
record. Iterator and check results are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 11:42:19 +00:00