fix(scheduler): populate job-wide total want for per-host resources during global phase (bug-1790)

Closes the gap bug-1789 explicitly carved out: a GLOBAL limit built on a
per-host resource other than "slots" (e.g. a future absolute global mem
cap) needs ctx->alloc_req.host to carry that resource's job-wide total
want during the global phase, but only "slots" ever got that treatment
(via seqjob/parjob_prepare_allocation()'s own unconditional store).

Adds calc_wanted_host_resval_job_totals() (resreq.c), sharing its loop
body with calc_wanted_global_resvals() via a new static helper
parameterized only by which XCE_RESF_GLOBAL direction to match -- it
computes the SAME job-wide-total formulas (PER_TASK: value*nslots, PER_JOB:
value, PER_HOST: value*nhosts) for non-global resources instead of global
ones. seqjob/parjob_prepare_allocation()'s global-phase branch now calls
it into ctx->alloc_req.host alongside the existing calc_wanted_global_
resvals() call into .shared.

Safety was traced, not assumed: plugin_prepare_allocation() only resets
.host at the top of a per-host call, never during the global phase, and
sched_fill_limit_requests() for glimits reads .host later in the SAME
global-phase invocation -- transferring the value into the limit's
permanent .shared record before any later per-host call ever resets
.host. An earlier candidate fix (reusing calc_wanted_host_resvals()
directly) was rejected during design: its PER_HOST formula and, for
parjob, its per-bucket nslots don't give a job-wide total.

New regression tests in test_seqjob_allocator.c/test_parjob_
combinatorial.c prove a non-"slots" per-host resource ("mem") now gets
correctly populated; parjob's test uses maxTasks=3 specifically to prove
the formula uses the job's whole nslots, not a bucket's share.

Full suite (xce-build.sh all asan --incremental): 92/94, both failures
pre-existing and unrelated (bug-1762, bug-1733's already-documented
xcealloc.c leak). Not live-verified -- no limits.yaml entry exercises
this path yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GLfB5uWRdmx2WiZSFC7Fm7
This commit is contained in:
Rongsong Shen 2026-09-08 02:57:51 +00:00
parent 028d8f6ac7
commit 9caf1424b9
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
7 changed files with 265 additions and 8 deletions

View file

@ -731,9 +731,21 @@ int format_resreq(char** buf, xce_resreq_t* resreq)
return rc;
}
int calc_wanted_global_resvals(xvec_idval_t* resvals, var resmgr,
xlist_t* rusages, rusage_per_type_t perflag,
int nhosts, int nslots)
/*
* calc_wanted_job_total_resvals_ - shared body for calc_wanted_global_resvals()
* and calc_wanted_host_resval_job_totals(): computes each matching rusage
* entry's JOB-WIDE TOTAL want (PER_TASK: value*nslots, PER_JOB: value,
* PER_HOST: value*nhosts -- nslots/nhosts are the whole job's totals, not
* any one host's share). `match_global` selects which resources are
* "matching": nonzero processes only XCE_RESF_GLOBAL resources (the
* original calc_wanted_global_resvals() behavior), zero processes only
* non-GLOBAL (per-host) ones instead.
*/
static int calc_wanted_job_total_resvals_(xvec_idval_t* resvals, var resmgr,
xlist_t* rusages,
rusage_per_type_t perflag,
int nhosts, int nslots,
int match_global)
{
int i;
@ -760,8 +772,14 @@ int calc_wanted_global_resvals(xvec_idval_t* resvals, var resmgr,
continue;
}
if ((r->flags & XCE_RESF_GLOBAL) == 0) {
continue;
if (match_global) {
if ((r->flags & XCE_RESF_GLOBAL) == 0) {
continue;
}
} else {
if ((r->flags & XCE_RESF_GLOBAL) != 0) {
continue;
}
}
perflag1 = v->per_flag;
@ -790,6 +808,47 @@ int calc_wanted_global_resvals(xvec_idval_t* resvals, var resmgr,
return 0;
}
int calc_wanted_global_resvals(xvec_idval_t* resvals, var resmgr,
xlist_t* rusages, rusage_per_type_t perflag,
int nhosts, int nslots)
{
return calc_wanted_job_total_resvals_(resvals, resmgr, rusages, perflag,
nhosts, nslots, 1);
}
/*
* calc_wanted_host_resval_job_totals - bug-1790: like
* calc_wanted_global_resvals(), but for resources NOT flagged
* XCE_RESF_GLOBAL (per-host resources) -- computes each such resource's
* JOB-WIDE TOTAL want, not any one host's own share (calc_wanted_host_
* resvals(), below, computes that instead, for the real per-host capacity
* check). Exists so a GLOBAL limit built on top of a per-host resource
* (e.g. a future absolute global mem cap, "slots" already being the one
* bug-1789 fixed) can read a real aggregate instead of an always-missing
* value.
*
* seqjob_prepare_allocation()/parjob_prepare_allocation()'s global-phase
* branch (ctx->worker == NULL) calls this into ctx->alloc_req.host
* immediately alongside its existing calc_wanted_global_resvals() call.
* This is safe despite .host normally being per-host/per-bucket-varying
* data (see parjob_allocator.c's own per-bucket reset): plugin_prepare_
* allocation() (plugin.c) only resets .host at the START of a PER-HOST
* call (ctx->worker != NULL), never during the global phase, so the value
* written here survives long enough for sched_fill_limit_requests()
* (resource.c) to read it -- called later in the SAME global-phase
* invocation, for obj->glimits -- before any later per-host call wipes
* and correctly repopulates .host with that specific host's own share via
* calc_wanted_host_resvals().
*/
int calc_wanted_host_resval_job_totals(xvec_idval_t* resvals, var resmgr,
xlist_t* rusages,
rusage_per_type_t perflag, int nhosts,
int nslots)
{
return calc_wanted_job_total_resvals_(resvals, resmgr, rusages, perflag,
nhosts, nslots, 0);
}
int calc_wanted_host_resvals(xvec_idval_t* resvals, var resmgr,
xlist_t* rusages, rusage_per_type_t perflag,
int nhosts, int nslots)