15 tests drive the real parjob plugin through job_alloc_fn: success, short shared, pending on hosts, mid-plan rollback, task counts 0/1, missing inputs, six OOM steps with their debits, COMPOUND. Values the runner changes on purpose sit in *_today tests. Spec: the task showed that parjob records host reasons only for a bucket search's first rejected candidate (the rejected host's request stays in alloc_req.host and the host iterator pre-filters later hosts against it). Decision: the runner clears alloc_req.host before every candidate, for both allocators, so every short host gets its reasons (Req 3.6/3.7); publishing reasons only when they change is a follow-up. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
11 KiB
Brief: scheduler-allocator-runner
Problem
Scheduler plugin authors and maintainers. seqjob_allocator.c (my_alloc,
line 213, ~215 lines) and parjob_allocator.c (my_alloc, line 545, ~220
lines) each own the entire allocation algorithm, although only plan
generation is plugin-specific. ~100+ lines are duplicated verbatim (lazy
pend-reason, sched_ctx_get_uncomit_shared, sched_check_shared_satisfy +
PREASON_NO_RESOURCE fill, add-uncommitted-shared, plan-iterator loop
skeleton, commit tail with sched_make_allocs + -ENOMEM cleanup chain).
Every bug fix (bug-1882, bug-1767, bug-1768) has to land twice, and a new
allocator plugin must copy the whole my_alloc().
Driver: issues/new_requirements.md item 4.
Current State
- Framework dispatch
sched_make_primary_alloc_()(scheduler.c:235) pickscore->seqjob_allocator(maxTasks == 1) orcore->parjob_allocatorand callsplugin->job_alloc_fn(plugin, ctx). - Plan generation is already abstracted behind
xce_alloc_plan_iter_t(include/sched/alloc_iter.h); host scan behindxce_host_iter_t. - The picker side already uses a framework-owned iface pattern
(
xce_job_picker_iface_t,plugin->picker_iface,plugin.h:88). alloc_iter.hinit's third parameter isxce_scheduler_t*, unused by both implementations ((void)sched); sim mode has no real scheduler.- parjob has
parjob_try_plan_/parjob_rollback_claims_/parjob_snapshot_host_req_claim bookkeeping; seqjob keeps a singlew.
Desired Outcome
- Neither plugin defines
my_alloc(); both register.job_alloc_fn = sched_allocator_runand supply only anxce_sched_allocator_iface_t(optionalacceptsguard + plan iterator) plus their existingprepare_allocation. - One framework function owns shared-resource prep, the bucket -> host-claim loop with rollback, and the commit tail.
- Allocation decisions are bit-for-bit identical to today for every existing
test and sim-parity fixture. Intentional differences, all outside the
decision and main pend reason: pending seqjob jobs may gain per-host
NO_RESOURCEreasons (Constraints item 1); spurious shared reasons are recorded for the first time (bug-1893, item 6); seqjob gets the-EINVALguard and OOM rollback (Req 5 AC 1/5). new_requirements.mditem 4 closed.
Approach
Framework runner, lean (chosen 2026-09-28). The design proposal of
2026-09-23 (§5), with the optional commit hook left out because nothing
uses it yet:
xce_sched_allocator_iface_t { accepts; plan_init; plan_next; plan_fini; }inplugin.h;allocator_ifacepointer onxce_sched_plugin_twith the same ownership aspicker_iface.sched_allocator_run()= guards ->accepts->sched_alloc_prepare_shared_()-> plan loop ->sched_try_plan_()(promotedparjob_try_plan_) ->sched_finalize_job_alloc_()(onemake_host_allocper claim ->make_job_alloc->allocs_sharedblock).- Retype
alloc_iter.hinit's third parameter toxce_sched_core_t*. - Staged migration keeps the suite green: (1) extract shared helpers
used by both existing
my_alloc()s; (2) runner + iface, parjob first; (3) seqjob; (4) retypeplan_init; (5) close item 4.
Rejected: adding the commit override hook (an unused extension point that
would need its own tests); only extracting helpers (would not meet item 4's
goal that plugins no longer export my_alloc()).
Scope
- In: new allocator iface + framework runner; shared prep/try-plan/
finalize helpers; delete both
my_alloc()s and parjob's claim helpers; retypexce_alloc_plan_iter_t.inittoxce_sched_core_t*; equivalence tests; the bug-1893 loop-bound fix (Constraints item 6); updateissues/new_requirements.mditem 4. - Out: see Out of Boundary.
Boundary Candidates
- Plugin ABI (
plugin.hiface + plugin template wiring) - Framework allocation runner (new
plugin_alloc.corscheduler.c) — shared-resource prep, claim loop/rollback, commit tail - Plan-iterator contract (
alloc_iter.hretype; seqjob/parjob plan iters)
Out of Boundary
- Retyping
sched_make_allocs()toxce_sched_core_t*and removing the(xce_scheduler_t*)plugin->schedcast (follow-up; callers outside allocators). - Collapsing
sched_make_seqjob_allocation/sched_make_parjob_allocationdispatch inscheduler.c(optional, only if trivially falls out). - The
commitoverride hook. - bug-1892 (
unsatisfy_()reads the index-keyed uncommitted vector by list position; wrong admission for later primaries of COMPOUND/ALTER jobs). It lives inresource.c's satisfy checks, which the runner calls unchanged; fix it separately. Equivalence tests for COMPOUND/ALTER must not encode its wrong outcomes as expected values. - The
FIXME: no duration now(NULL duration tosched_make_allocs): carry it over unchanged, don't fix it here. - Any change to plan ordering, host ordering, pend-reason content, or preemption, except the decided seqjob host-reason change in Constraints item 1.
Upstream / Downstream
- Upstream:
scheduler-allocation(plan/host iterators, Req 4 combinatorial parjob, Req 9 no-regression);scheduler-core(plugin/picker iface pattern, sim mode);libxce-alloc-preason-equality(job_alloc_eqused by sim verify). - Downstream: any future allocator plugin (it would need only a plan
iterator);
scheduler-preemption; the follow-upsched_make_allocsretype.
Existing Spec Touchpoints
- Extends: none (scheduler-allocation is implementation-complete; its Req 9 is inherited as a constraint, not reopened).
- Adjacent:
scheduler-allocation,scheduler-core,scheduler-sim-discrete-clock(in flight, touches sim_loop — avoid overlapping files),libxce-alloc-preason-equality.
Constraints
- TDD: tests in
src/scheduler/testsuite/first; full ASAN build via/xce-build scheduler. - Behavioral equivalence is mandatory. Preserve exactly: bug-1882 (half-built
alloc never left on
ctx->alloc), bug-1767 (parjobmaxTasks <= 1returns 0 with no fabricated reason, and before the lazy reason is created), bug-1768;-ENOMEMcleanup order of both current tails, plus the seqjob OOM rollback of item 2 (Req 5 AC 5). - Divergences found in discovery that the design MUST resolve explicitly
(they are not in the 2026-09-23 proposal):
- Decided 2026-09-29, revised 2026-09-30 (task 2.2 finding, user
decision): the runner clears
ctx->alloc_req.hostbefore every candidate host (at the start of each bucket and after every rejected candidate), for both allocators. The per-host request used by the authoritative check is rebuilt for every candidate anyway (plugin_prepare_allocation()withctx->workerset,plugin.c:639). What the clear changes is the host iterator's pre-filter (host_iter.c:143), which readsalloc_req.hostbefore that rebuild and silently skips hosts that fail. The 2026-09-29 plan (clear once per bucket, as parjob does) turned out not to be enough: task 2.2's characterization showed that a rejected candidate leaves its own request behind, so parjob today records reasons only for the first rejected candidate of a bucket search, and a once-per-bucket clear would give seqjob the same partial reasons. With a clear before every candidate, the pre-filter only applies the select expression, and every selection-matching unclaimed short host gets its host/resource reasons, in both allocators. Accepted consequences (intentional changes): host choice, main reason and decisions are unchanged (a host the stale pre-filter rejects also fails the authoritative check while the per-host request is host-independent; where a limit makes it host-specific, the stale filter could wrongly skip a host, which the clear fixes). Pending jobs of both allocators carry more host/resource reasons (res_based_reasons; notmain_reason, so the unsatisfiable cache and sim parity are unaffected). The per-host prepare fan-out, including the limit plugin's lazy host-limit instantiation, now runs on every short host (decision-neutral; one prepare + check per short host per bucket search). Larger reason sets are published and persisted; publishing reasons only when they change is a follow-up (user, 2026-09-30), out of scope here. Rejected: clearing once per bucket (partial reasons, still stale-state dependent); keeping today's per-plugin behaviour. - Decided (requirements Req 5 AC 5): seqjob builds its halloc directly
from
ctx->alloc_req.host; under the shared per-claim snapshot it gains a new-ENOMEMpath, which goes through the same rollback (sched_ctx_uncommit_host) as parjob's. - Decided 2026-09-29: for seqjob,
ctx->nslotsandctx->nhostsare always 1. Their only consumer issched_fill_limit_requests()inplugin_prepare_allocation()(plugin.c:682job-level pass,:699per-host pass);sched_try_alloc_on_host_()does not read them. Today seqjob sets 1/1 only in its job-level prepare pass (seqjob_allocator.c:92) and relies on the values carrying into the per-host passes. The shared loop setsctx->nslots = plan->bucket_slots[b]andctx->nhosts = 1before every bucket's host scan (as parjob does,parjob_allocator.c:492); seqjob's plan is always one bucket of one slot (seqjob_allocator.c:163), so seqjob sees exactly 1/1 in every pass. The seqjob plan iterator must keepnbuckets == 1andbucket_slots[0] == 1(assert in tests). - Decided (requirements Req 5 AC 3): seqjob's
acceptsis NULL. seqjob has nomaxTasksguard today, and aseqjob_accepts(maxTasks == 1) would change behaviour for direct callers with another value (48 testsuite files reference the allocators). - Proposal-sketch defects to avoid:
sched_ctx_primary == NULLtypo;claims[plan->nbuckets]VLA (heap-allocate or bound bymaxTasks). - Decided 2026-09-29 (requirements review), corrected 2026-09-30 (task
2.1): the shared pend-reason loop iterates the unmet list
(
ctx->unmeet.count) and skips only onvget() < 0, fixing bug-1893. Both currentmy_alloc()s skip onvget() <= 0, butvgetreturns 0 on success, so today no sharedNO_RESOURCEreason is ever recorded (the loop also runs toalloc_req.shared.count). After the fix a job pending on shared resources names each short one; main reason and decisions are unaffected.
- Decided 2026-09-29, revised 2026-09-30 (task 2.2 finding, user
decision): the runner clears
- Claim
host_reqsnapshots are owned by the framework: fini on every exit path (success, bucket failure, OOM). - Verify with:
test_seqjob_*,test_parjob_*,test_seqjob_rewrap_equivalence,test_maxtasks_wiring_*,test_sim_*_parity,test_sim_live_allocation,test_snapshot_load_allocator_regression;/xce-audit-invariants scheduler(driver state-equivalence).