myxce/.kiro/specs/scheduler-allocator-runner/brief.md
Rongsong Shen b283f3b93c
docs(spec): scheduler-allocator-runner and scheduler-sim-decision-trace requirements
scheduler-allocator-runner: brief and requirements for moving the duplicated
seqjob/parjob my_alloc() body into one framework-owned allocator runner.

scheduler-sim-decision-trace: brief and requirements for a per-job decision
trace in the simulator REPL (converged after 9 review rounds). The
decision-reasoning seed capability in scheduler-sim-diagnostics now points to
this spec.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 08:57:23 +00:00

6.9 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) picks core->seqjob_allocator (maxTasks == 1) or core->parjob_allocator and calls plugin->job_alloc_fn(plugin, ctx).
  • Plan generation is already abstracted behind xce_alloc_plan_iter_t (include/sched/alloc_iter.h); host scan behind xce_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.h init's third parameter is xce_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 single w.

Desired Outcome

  • Neither plugin defines my_alloc(); both register .job_alloc_fn = sched_allocator_run and supply only an xce_sched_allocator_iface_t (optional accepts guard + plan iterator) plus their existing prepare_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.
  • new_requirements.md item 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; } in plugin.h; allocator_iface pointer on xce_sched_plugin_t with the same ownership as picker_iface.
  • sched_allocator_run() = guards -> accepts -> sched_alloc_prepare_shared_() -> plan loop -> sched_try_plan_() (promoted parjob_try_plan_) -> sched_finalize_job_alloc_() (one make_host_alloc per claim -> make_job_alloc -> allocs_shared block).
  • Retype alloc_iter.h init's third parameter to xce_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) retype plan_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; retype xce_alloc_plan_iter_t.init to xce_sched_core_t*; equivalence tests; update issues/new_requirements.md item 4.
  • Out: see Out of Boundary.

Boundary Candidates

  • Plugin ABI (plugin.h iface + plugin template wiring)
  • Framework allocation runner (new plugin_alloc.c or scheduler.c) — shared-resource prep, claim loop/rollback, commit tail
  • Plan-iterator contract (alloc_iter.h retype; seqjob/parjob plan iters)

Out of Boundary

  • Retyping sched_make_allocs() to xce_sched_core_t* and removing the (xce_scheduler_t*)plugin->sched cast (follow-up; callers outside allocators).
  • Collapsing sched_make_seqjob_allocation / sched_make_parjob_allocation dispatch in scheduler.c (optional, only if trivially falls out).
  • The commit override hook.
  • The FIXME: no duration now (NULL duration to sched_make_allocs): carry it over unchanged, don't fix it here.
  • Any change to plan ordering, host ordering, pend-reason content, or preemption.

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_eq used by sim verify).
  • Downstream: any future allocator plugin (it would need only a plan iterator); scheduler-preemption; the follow-up sched_make_allocs retype.

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 (parjob maxTasks <= 1 returns 0 with no fabricated reason, and before the lazy reason is created), bug-1768; -ENOMEM cleanup order of both current tails.
  • Divergences found in discovery that the design MUST resolve explicitly (they are not in the 2026-09-23 proposal):
    1. parjob does xvec_idval_fini/init(&ctx->alloc_req.host) per bucket (parjob_allocator.c:452); seqjob never resets it. Decide whether the shared loop always resets, and prove it is a no-op for seqjob.
    2. seqjob builds its halloc directly from ctx->alloc_req.host; under the shared per-claim snapshot it gains a new -ENOMEM path. It must go through the same rollback (sched_ctx_uncommit_host).
    3. seqjob does not set ctx->nslots/nhosts in its loop. Its values come from seqjob_prepare_allocation (1,1). sched_try_alloc_on_host_() (resource.c) does not read them directly; check which hook does, and that bucket_slots[0] == 1 for seqjob.
    4. seqjob has no maxTasks guard today. A seqjob_accepts (maxTasks == 1) changes behaviour for any direct caller with another value (several testsuite files reference seqjob_allocator). Either make accepts NULL for seqjob or audit those callers.
    5. Proposal-sketch defects to avoid: sched_ctx_primary == NULL typo; claims[plan->nbuckets] VLA (heap-allocate or bound by maxTasks).
  • Claim host_req snapshots 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).