Adds apply_job_task_count() to overwrite a standalone primary resreq's minTasks/maxTasks with a job's real requested task count, no-op for COMPOUND/ALTER nested primaries, with a defensive min>max clamp. Closes the gap that lets scheduler-allocation's multi-host allocator ever be reached (bug-1770). Wiring at live ingest call sites follows in subsequent tasks. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
35 KiB
Overview
This feature closes bug-1770: a job's real requested task count is already correctly threaded from the apiserver all the way to xce_sched_job_t.mintasks/maxtasks, but the parsed resource request the scheduler's allocator dispatch actually reads (xce_primary_resreq_t.minTasks/maxTasks) is unconditionally hardcoded to one task. This makes the scheduler-allocation feature's already-implemented, already-tested, already-soak-validated multi-host allocation logic (parjob_allocator) permanently unreachable from any real job submission today.
Users: cluster users submitting multi-task jobs, and cluster operators relying on existing single-task jobs continuing to behave identically.
Impact: changes the resource request the scheduler dispatches on from an always-1 constant to a real, per-job derived value for standalone primary resource requests, at every point a resreq is parsed from a job's submitted or persisted state. No existing component's ownership, wire format, or public interface changes shape — this is a targeted derivation fix plus the validation needed to trust it.
Goals
- A standalone primary resource request's
minTasks/maxTasksreflect the job's real requested task count, at fresh ingest and at snapshot restore, and remain correct across a subsequent resource-request update (a guarantee this feature relies on an existing structural property for, rather than actively enforcing — see Existing Architecture Analysis). - A job requiring more tasks than one host can provide is genuinely evaluated for, and where appropriate allocated across, multiple hosts, through the real scheduler dispatch path.
- A committed multi-host allocation is proven, end to end, to actually launch and run on real (or virtual) workers.
- Zero behavioral change for any job with a requested task count of one, and zero behavioral change for compound/alternative resource requests.
Non-Goals
- Implementing
span[ptile=N]resreq-grammar semantics (remains parsed and discarded, unchanged). - Wiring task count into primary resource requests nested inside a compound/alternative resource request (bug-1768 already makes their resource checking meaningless regardless of task count).
- Fixing the snapshot-restore
resreq_fpcomputation gap (bug-1772, tracked separately) or its sibling inmake_sched_job()(bug-1774) — the latter is dead code today (confirmed:make_sched_job()'s entire call chain, viasched_update_jobs()/sched_update(), has zero live callers anywhere in this codebase), so this feature does not wire it at all, not merely defer fixing it. - Fixing a job's resource-request update never taking effect for scheduling purposes for any field (bug-1773) — this feature's own task-count guarantee across an update holds for that same structural reason, not because this feature actively re-wires anything on update.
- Modifying the controller's or worker's existing multi-host launch code (
mk_launchinfo(),job_start_remote_task(),create_mpi_cmdline()) — this design depends on and validates that code, but does not change it. - Fixing the pre-existing per-host live memory-usage reconciliation limitation for multi-host jobs.
Boundary Commitments
This Spec Owns
- The derivation that sets a standalone primary resource request's
minTasks/maxTasksfrom the job's real requested task count, at every live call site whose parsed resreq actually reaches scheduler dispatch. - The defensive invariant that an invalid (
minTasks > maxTasks) pair cannot reach allocator dispatch, regardless of upstream validation. - A new automated end-to-end test proving a real multi-task job, submitted through the normal submission path, is genuinely allocated across multiple hosts by the scheduler and actually runs to completion.
Out of Boundary
span[...]resreq-grammar syntax (parsed, discarded, unchanged).- Primary resource requests nested inside a compound/alternative resource request (bug-1768).
- The snapshot-restore
resreq_fpgap (bug-1772). - Per-host live memory-usage reconciliation for multi-host jobs (pre-existing, documented limitation).
- The scheduler's plan-generation/allocation algorithm itself (owned by
scheduler-allocation; this spec only makes it reachable). - The controller/worker multi-host launch mechanism's internal implementation (pre-existing; this spec validates it, does not modify it).
Allowed Dependencies
scheduler-allocation'sparjob_allocator/seqjob_allocator/alloc_cache— consumed as-is; this spec must not modify their dispatch logic.- The apiserver's existing
checkNumProcessors()validation andbackend/convert.go'smintasks/maxtasksclamp — relied upon as the primary validation layer; this spec adds a redundant defensive check, not a replacement. - The controller/worker's existing multi-host launch code (
mk_launchinfo(),job_start_remote_task(),create_mpi_cmdline()) — read-only dependency for the new end-to-end test.
Revalidation Triggers
- Any change to
parse_resreq_primary()'s orparse_resreq()'s signature, or toxce_primary_resreq_t's struct shape. - Any change to how a resreq's top-level tag distinguishes a standalone primary from a
COMPOUND/ALTERtree. - Any change to
job_submit_data_t.mintasks/maxtasksor its Go-side validation (checkNumProcessors(),convert.go's clamp). - Any change to
scheduler-allocation's dispatch gate (sched_make_primary_alloc_()'smaxTasks == 1branch) or to bug-1768's compound-nested resource-checking gap, since a future fix there would change what "out of boundary" means for compound resource requests.
Architecture
Existing Architecture Analysis
parse_resreq_primary(xce_primary_resreq_t*, char*, var, char**)andparse_resreq(xce_resreq_t*, char*, var, char**)(src/lib/resreq.c) take no job-context parameter — they cannot see a job's requested task count. Six existing test call sites invoke the bare production signature directly with no job object in scope, ruling out a signature change as the fix mechanism.- Five call sites in total reach this parser in the source tree, but only two are reachable in production today. The scheduler's actual dispatch reads
job->schedobj->u.primary->resreq(scheduler.c:315), a pointer set exactly once, by whichever function constructsjob->schedobj. Exactly three functions ever do this (job->schedobj =grepped exhaustively):core_build_job()(core_events.c),make_sched_job()(schedobj.c), and the snapshot-restore path (snapshot_load.c). Of these,make_sched_job()is dead code: its only caller,sched_update_jobs(), is only called from insidesched_update()'s switch, andsched_update()(scheduler.c) has zero live callers anywhere in this codebase — confirmed by exhaustive grep and corroborated by the codebase's own pre-existing comments describing it as dead/superseded, and by the identical, already-fixed sibling case in bug-1740.core_build_job()(fresh submission, via the driver-translatedXCE_CORE_EVENT_JOBS_PUSHpath) and thesnapshot_load.crestore path are the two genuinely live schedobj-construction sites, and are this spec's only implementation targets. - The remaining two of the five parser call sites are job-update handlers (
sched_update_existing_job()inschedobj.c, theJOB_UPDATEhandler incore_events.c) that replacejob->resreqbut never rebuildjob->schedobj.sched_update_existing_job()is itself dead code (its sole caller is also insidesched_update()'s dead switch).core_events.c'sJOB_UPDATEhandler IS live (reached via the driver-translated path, confirmed by tracingmain.c→driver.c's event translation →xce_core_push_events()) — but it never affects scheduling regardless, sincejob->schedobjisn't rebuilt there either, and no live feature in this codebase today ever actually changes an existing job'ssubmit->resreq(grepped the whole controller: nothing mutates it post-submission). Applying this feature's helper at either update call site would be dead code with zero observable effect. Requirement 1.4's guarantee (task count unaffected by an update) already holds today for this structural reason, independent of this feature. The update path never taking effect for scheduling at all, for any field, is a real, separately-tracked, pre-existing defect (bug-1773). - "Snapshot restore" (Requirement 1.3/1.4's
snapshot_load.csite) is live only via the simulation/replay driver, not live-cluster crash recovery.xce_sched_core_load_snapshot()'s only real caller anywhere insrc/issim.c'ssim_locate_and_load_snapshot(), reachable only throughmain.c's--sim FILEoffline-replay flag; normal-modemain.chas no snapshot-load call or flag at all. A live scheduler restart (including post-failover, e.g.run-ha-soak.sh's own scheduler-restart step) starts with empty in-memory state and rejoins viaon_scheduler_join()→ the same livecore_build_job()path fresh ingest uses — it never touchessnapshot_load.c. This means the HA soak (Requirement 5.2) provides zero exercise of this call site; Requirement 5.6 validates it separately. The fix is still worth wiring here —--simmode drives the identicalparjob_allocator/seqjob_allocatordispatch per this project's own driver-equivalence invariant, so a saved snapshot's task count needs to be correct for simulation/tuning fidelity — but this is a genuinely different (and narrower) liveness scope than "post-restart/post-failover" phrasing might suggest, and bug-1772's own description has been corrected accordingly. - A single job's resreq parse tree can contain more than one primary when
COMPOUND/ALTERsyntax is used;parse_resreq_primary()already runs once per primary node in the tree, uniformly. Only the job's own top-level standalone primary (tagXCE_RESREQ_PRIMARY) is in this spec's boundary — a nested primary insideCOMPOUND/ALTERmust be left exactly as today. job_submit_data_t.mintasks/maxtasks(the job-level source of truth) is already correctly threaded end-to-end from the apiserver'snum_processors/maxnum_processorsfields, validated twice (checkNumProcessors(),convert.go's clamp) before reaching the C layer, and already correctly round-tripped through the WAL/snapshot wire format. This spec's boundary starts exactly where that correct value stops propagating.
Project Invariant Cross-Check
core_events.c (home of core_build_job(), one of this spec's real call sites) is part of sched_core_isolation_lib — the "one core, two consumers" layer shared between the real driver (driver.c) and the simulation driver (sim_loop.c), per this project's own scheduler-allocation task 5.2 lesson (a fix applied only to the real-driver path there caused a real driver/simulation divergence that corrupted xce_sched_sim_driver_t state). Confirmed, not merely assumed: driver.c and sim_loop.c both build XCE_CORE_EVENT_JOBS_PUSH events and call the same shared xce_core_push_events() (core_events.c), whose JOBS_PUSH case calls core_build_job() directly — no parallel, sim-specific job constructor exists anywhere. Wiring the fix in core_build_job() therefore applies identically to real and simulated job ingest with no divergence risk. test_maxtasks_wiring_ingest.c still includes a regression test exercising this path through sim_loop.c's own entry point, to keep this confirmed property under test rather than resting on a one-time design-time check.
Architecture Pattern & Boundary Map
graph TB
ApiServer[Apiserver job submission] --> SubmitData[job_submit_data_t mintasks maxtasks]
SubmitData --> Ingest[Scheduler ingest call sites]
Ingest --> ParseResreq[parse_resreq existing parser]
ParseResreq --> Helper[apply_job_task_count new helper]
Helper --> Primary[xce_primary_resreq_t minTasks maxTasks]
Primary --> Dispatch[Scheduler dispatch existing]
Dispatch --> SeqJob[seqjob_allocator existing]
Dispatch --> ParJob[parjob_allocator existing]
ParJob --> Commit[Multi host allocation committed]
Commit --> Controller[Controller mk_launchinfo existing]
Controller --> Worker[Worker job_start_remote_task existing]
Worker --> Running[Job reaches RUN then DONE]
Architecture Integration:
- Selected pattern: a single new helper function, invoked by each of the two ingest call sites whose result actually reaches dispatch (
core_build_job()and the snapshot-restore path), immediately after its existingparse_resreq()/parse_resreq_primary()call returns. - Domain boundaries:
src/lib/resreq.cowns the new helper and its tag-gating/clamp logic; each scheduler-side call site owns invoking it with whichever job-context task count is already in scope there. - Existing patterns preserved: this mirrors the codebase's own existing idiom of copying a field from
submit->onto a target struct immediately after that struct is built — already done identically forjob->mintasks/maxtasksitself at both of these sites. - New component rationale: one shared helper avoids duplicating the
COMPOUND/ALTERtag-check twice and gives one independently unit-testable unit for the only real logic this fix introduces. - Steering compliance: no new WAL/snapshot field is introduced (see Data Models);
resreq.cis a shared C library untouched by the scheduler-core isolation boundary; the fix does not alterscheduler-allocation's dispatch or allocation logic, only what reaches it.
Technology Stack
| Layer | Choice / Version | Role in Feature | Notes |
|---|---|---|---|
| Shared C library | Existing C (gnu17), src/lib/resreq.c |
New apply_job_task_count() helper |
No new dependency |
| Scheduler (C) | Existing C, src/scheduler/src/ |
Two live ingest call-site wiring | core_events.c, snapshot_load.c |
| Test infrastructure | Existing xce_test debug CLI + xcecd_virtual |
New end-to-end test | Extends the pattern already used by xce-test-resources/xce-test-ha |
File Structure Plan
Modified Files
src/lib/resreq.c— addapply_job_task_count();parse_resreq_primary()'s existingminTasks = 1; maxTasks = 1;hardcode is left in place as the correct default for a freshly-parsed resreq before the helper is applied, and as the permanent behavior for nestedCOMPOUND/ALTERprimaries the helper deliberately skips.src/include/resreq.h— declareapply_job_task_count().src/scheduler/src/core_events.c— call the helper aftercore_build_job()'sparse_resreq()call, usingj->submit->mintasks/maxtasks.core_events.c'sJOB_UPDATEhandler is NOT modified — it's live, butjob->schedobj(what dispatch actually reads) is never rebuilt by an update, so applying the helper to the freshly-parsedjob->resreqthere would be dead code (see Existing Architecture Analysis and bug-1773).src/scheduler/src/snapshot_load.c— call the helper after the restore path'sparse_resreq()call, usingjob->mintasks/maxtasks(already restored earlier in the same function, before this call).src/scheduler/src/schedobj.cis NOT modified —make_sched_job()andsched_update_existing_job()are both dead code (confirmed: unreachable viasched_update(), which has zero live callers), so wiring either would have no production effect.src/scheduler/testsuite/test_parjob_e2e_multislot.c— remove thePATCH_MAXTASKS_FOR_BUG_1770workaround now that real ingest correctly wiresmaxTasks, converting it to drive the same scenario through unmodified real ingest.
New Files
src/lib/testsuite/test_resreq_task_count.c— unit tests forapply_job_task_count()in isolation.src/scheduler/testsuite/test_maxtasks_wiring_ingest.c— white-box tests confirming each of the two wired ingest call sites applies the wiring correctly (including the fresh-ingest-vs-snapshot-restore comparative assertion for Requirement 5.6), plus the structural regression test for Requirement 1.4.src/worker/testsuite/test_worker_launch_type_dispatch.c— white-box test targetingjob_start_remote_task()'s launch-type gate (controller_connector.c) first: confirms it correctly no-ops (defers to MPI's own remote-launch mechanism) for an MPI launch type, and correctly fans the job out via XCE's own per-host dispatch for the default launch type — this is the actual decision point Requirement 4.4 depends on, and today has zero test coverage of any kind. Secondarily confirmsjob_start_local_task()'s branch intocreate_mpi_cmdline()builds the right host list and command. Neither requires a real MPI runtime installed in the test environment (Requirement 4.4)..claude/skills/xce-test-multihost/— new skill (SKILL.md+ driver script), followingxce-test-resources's single-controller-node pattern: deploys one controller/scheduler plus two namedxcecd_virtualworkers, submits a real multi-task job viajobsub, and verifies genuine multi-hostRUN-then-DONEexecution on both hosts, plus one array-job variant.
System Flows
sequenceDiagram
participant User
participant Api as Apiserver
participant Sched as Scheduler ingest
participant Alloc as Scheduler dispatch
participant Ctrl as Controller
participant W1 as Worker host A
participant W2 as Worker host B
User->>Api: submit job with -n 4,6
Api->>Api: checkNumProcessors validate
Api->>Sched: job_submit_data_t mintasks maxtasks
Sched->>Sched: parse_resreq existing
Sched->>Sched: apply_job_task_count new
Sched->>Alloc: standalone primary minTasks maxTasks real value
Alloc->>Alloc: maxTasks greater than one route to parjob_allocator
Alloc->>Ctrl: commit multi host allocation
Ctrl->>W1: launch info host A share
Ctrl->>W2: launch info host B share
W1-->>Ctrl: task started
W2-->>Ctrl: task started
Ctrl-->>Sched: job running
Sched-->>User: job reaches DONE
Key decision visible in the diagram: apply_job_task_count() sits strictly between the existing parser and the existing dispatch gate — nothing downstream of it (allocator, controller, worker) changes; nothing upstream of it (apiserver validation, job_submit_data_t wiring) changes.
Requirements Traceability
| Requirement | Summary | Components | Interfaces | Flows |
|---|---|---|---|---|
| 1.1 | Real task count reaches the standalone primary | TaskCountWiring, IngestTaskCountWiring |
apply_job_task_count |
Sequence diagram, steps 1-6 |
| 1.2 | Default (one task) behavior unchanged | TaskCountWiring |
apply_job_task_count |
— |
| 1.3 | Jobs restored via the simulation/replay driver's snapshot loading derive the same value as fresh ingest | IngestTaskCountWiring (snapshot_load.c site) |
apply_job_task_count |
— |
| 1.4 | A resource-request update does not change the job's dispatched task count | test_maxtasks_wiring_ingest.c (structural regression test; no production change needed — see Existing Architecture Analysis and bug-1773) |
— | — |
| 1.5 | Invalid pair cannot reach allocator dispatch | TaskCountWiring |
apply_job_task_count (clamp) |
— |
| 2.1 | Candidate plans evaluated for maxTasks > 1 | Existing scheduler-allocation dispatch (unmodified) |
— | Sequence diagram, step 7 |
| 2.2 | Multi-host allocation committed when satisfiable | Existing parjob_allocator (unmodified) |
— | Sequence diagram, step 8 |
| 2.3 | maxTasks == 1 unaffected | Existing seqjob_allocator (unmodified) |
— | — |
| 3.1 | Identical single-task results before/after | test_maxtasks_wiring_ingest.c |
— | — |
| 3.2 | No cluster-wide behavior change while maxTasks always 1 | test_maxtasks_wiring_ingest.c |
— | — |
| 3.3 | Compound-nested primaries unaffected | TaskCountWiring's tag gate |
apply_job_task_count |
— |
| 4.1 | Controller ensures every host has launch info | Existing controller (unmodified), validated by xce-test-multihost |
— | Sequence diagram, steps 8-10 |
| 4.2 | Worker starts tasks on every allocated host | Existing worker (unmodified), validated by xce-test-multihost |
— | Sequence diagram, steps 9-12 |
| 4.3 | Scheduler reports running via existing mechanism | Existing scheduler (unmodified), validated by xce-test-multihost |
— | Sequence diagram, step 13 |
| 4.4 | Non-default launch type honored | WorkerLaunchTypeCoverage |
— | — |
| 5.1 | Existing suites pass unmodified | Full regression run | — | — |
| 5.2 | HA soak shows no regression (does not exercise the simulation/replay driver's snapshot-loading path — see 5.6) | Existing xce-test-ha (unmodified) |
— | — |
| 5.3 | Real multi-task job proven end to end | xce-test-multihost |
— | Full sequence diagram |
| 5.4 | No cache collision across differing task counts, scoped to the ingest path with a correct fingerprint | test_resreq_task_count.c / scheduler-allocation's alloc_cache tests (fresh-ingest, core_build_job(), only; snapshot-restore is the documented bug-1772 exception — it never computes resreq_fp, confirmed live and unfixed by this feature; make_sched_job()'s path is dead code and out of consideration entirely) |
— | — |
| 5.5 | Multi-task array job proven end to end | xce-test-multihost (array-job scenario) |
— | Full sequence diagram, array variant |
| 5.6 | Simulation/replay-loaded snapshot derives the same task count as fresh submission | test_maxtasks_wiring_ingest.c (snapshot-restore site, comparative assertion against fresh-ingest for equivalent input) |
apply_job_task_count |
— |
Components and Interfaces
| Component | Domain/Layer | Intent | Req Coverage | Key Dependencies (P0/P1) | Contracts |
|---|---|---|---|---|---|
TaskCountWiring |
Shared C library (src/lib) |
Apply a job's real task count onto its resreq's standalone primary | 1.1, 1.2, 1.5, 3.3 | None (pure function) | Service |
IngestTaskCountWiring |
Scheduler (src/scheduler/src) |
Invoke the helper at every ingest call site whose result actually reaches dispatch | 1.1, 1.3 | TaskCountWiring (P0) |
Service |
xce-test-multihost |
Test infrastructure | Prove real multi-host allocation and execution end to end | 4.1, 4.2, 4.3, 5.3, 5.5 | Existing controller/worker/scheduler binaries (P0) | Batch |
WorkerLaunchTypeCoverage |
Worker (src/worker/src) |
Close a pre-existing, zero-coverage gap in the worker's launch-type dispatch decision | 4.4 | controller_connector.c (P0) |
Service |
Shared C Library (src/lib)
TaskCountWiring
| Field | Detail |
|---|---|
| Intent | Overwrite minTasks/maxTasks on a resreq's own top-level primary with the job's real requested task count, leaving nested/compound primaries untouched. |
| Requirements | 1.1, 1.2, 1.5, 3.3 |
Responsibilities & Constraints
- Sole owner of the tag-gate distinguishing a standalone primary (
XCE_RESREQ_PRIMARY) from aCOMPOUND/ALTERtree. - Sole owner of the defensive
minTasks <= maxTasksclamp. - Never allocates memory, never mutates
select/rusages/rusage_hash/span/same/affinity.
Dependencies
- Inbound:
IngestTaskCountWiring— the two live scheduler-side call sites (P0).
Contracts: Service [x]
Service Interface
/*
* Overwrite minTasks/maxTasks on resreq's own top-level primary with the
* job's real requested task count. No-op if resreq is NULL or if its
* top-level tag is not XCE_RESREQ_PRIMARY -- a COMPOUND or ALTER resreq's
* nested primaries are left exactly as parse_resreq_primary() set them.
*/
void apply_job_task_count(xce_resreq_t* resreq,
uint32_t mintasks,
uint32_t maxtasks);
- Preconditions:
resreqhas already been successfully returned byparse_resreq()/parse_resreq_primary(). - Postconditions: if
resreq->tag == XCE_RESREQ_PRIMARY, itsprimary->minTasks/primary->maxTasksequalmintasks/maxtasks, withmaxtasksraised tomintasksifmintasks > maxtasks(mirroringconvert.go's clamp direction exactly). Otherwiseresreqis unchanged. - Invariants: idempotent — calling it twice with the same inputs produces the same result.
- Type note: parameters are
uint32_t, matchingjob_submit_data_t.mintasks/maxtasksandxce_sched_job_t.mintasks/maxtasks(bothuint32_t, confirmed intypes.h) — the values every call site actually holds.xce_primary_resreq_t.minTasks/maxTasks(the assignment target) are plainint(resreq.h:239-240); this function performs a deliberate, documented narrowing conversion at the write, not a silent one. Safe for realistic task counts (well underINT_MAX), and no existing code path today assigns across thisint/uint32_tboundary, so this is a new, one-time, explicitly acknowledged conversion rather than a pattern already relied upon elsewhere.
Implementation Notes
- Integration: called once, immediately after each of the two live ingest
parse_resreq()/parse_resreq_primary()call sites returns successfully. - Validation:
test_resreq_task_count.ccovers the standalone-primary case, theCOMPOUND/ALTERno-op case, and the defensive clamp case. - Risks: none identified — the "undiscovered sixth call site" risk originally recorded in
research.mdhas since been independently re-verified twice (exhaustive grep, no additional caller found either time).
Scheduler (src/scheduler/src)
IngestTaskCountWiring
| Field | Detail |
|---|---|
| Intent | Invoke apply_job_task_count() at every ingest call site whose result actually reaches dispatch, using whichever job-context task count is already in scope there. |
| Requirements | 1.1, 1.3 |
Responsibilities & Constraints
- Two call sites, each a single new line:
core_build_job()(core_events.c); the restore path (snapshot_load.c). schedobj.cis untouched entirely:make_sched_job()andsched_update_existing_job()are both dead code (unreachable viasched_update(), which has zero live callers).core_events.c'sJOB_UPDATEhandler is deliberately NOT touched either — it's live, but rebuildsjob->resreqand neverjob->schedobj, so the actually-dispatched object never sees the update regardless of whether this helper runs there (bug-1773).
Dependencies
- Outbound:
TaskCountWiring(P0).
Contracts: Service [x]
Service Interface
No new interface — this is two one-line call-site additions to existing functions, not a new function.
Implementation Notes
- Integration: ordering matters only in that the call must happen after
parse_resreq()/parse_resreq_primary()returns and before any downstream code (e.g.core_build_primary_schedobj()'sresreq_fpcomputation) readsminTasks/maxTasks— confirmed both wired sites already satisfy this ordering naturally. - Validation:
test_maxtasks_wiring_ingest.cwhite-box tests each site directly. - Risks: none identified; each site's change is independently reviewable and revertable.
Worker (src/worker/src)
WorkerLaunchTypeCoverage
| Field | Detail |
|---|---|
| Intent | Close a pre-existing, zero-coverage gap in the worker's launch-type dispatch decision, adjacent to (but not caused by) this feature's own fix. |
| Requirements | 4.4 |
Responsibilities & Constraints
- Does not modify
controller_connector.c— pure test addition against existing, unmodified production code. - Primary target:
job_start_remote_task()'s launch-type gate (correct no-op for an MPI launch type, deferring to MPI's own remote-launch mechanism; correct fan-out via XCE's per-host dispatch for the default type) — the real decision point Requirement 4.4 depends on, with no existing coverage of any kind. - Secondary target:
job_start_local_task()'s branch intocreate_mpi_cmdline(), confirming the built host list and command are correct.
Dependencies
- External:
controller_connector.c's existing, unmodified launch-dispatch functions (P0).
Contracts: Service [x]
Service Interface
No new interface — a white-box test file (#include-ing controller_connector.c directly, the same technique already established by test_controller_connector.c) exercising existing static functions.
Implementation Notes
- Integration: no production code touched.
- Validation:
test_worker_launch_type_dispatch.c. - Risks: none — this is a pure test addition against unmodified code, adjacent-gap discovery rather than a fix this feature's own change requires.
Test Infrastructure
xce-test-multihost
| Field | Detail |
|---|---|
| Intent | Prove a real multi-task job is genuinely allocated across multiple hosts by the scheduler's own decision, and actually runs to completion. |
| Requirements | 4.1, 4.2, 4.3, 5.3, 5.5 |
Responsibilities & Constraints
- Deploys a single controller/scheduler node (no HA needed for this proof) plus two named
xcecd_virtualinstances, each sized so neither alone can satisfy the test job's requested task count. - Drives submission via plain
jobsub, neverxce_test'sjobrunmanual-allocation bypass — the scheduler's own decision to span hosts is what this test exists to prove. - Follows the pre-flight zombie-process/port check convention already established for
xce-test-ha.
Dependencies
- External: real
xce_controller,xce_scheduler,xcecd_virtualbinaries (P0, unmodified by this spec).
Contracts: Batch [x]
Batch / Job Contract
- Trigger: manual invocation (
bash .claude/skills/xce-test-multihost/run.sh), same convention asxce-test-ha/xce-test-resources. - Input / validation: none beyond the built binaries; the script deploys its own fixture cluster.
- Output / destination: pass/fail verdict line matching the existing skills'
[xce-test-*] PASS/FAILconvention, plus per-host worker log confirmation. - Idempotency & recovery: each run deploys a fresh, uniquely-named cluster instance; no shared state across runs.
Implementation Notes
- Integration: reuses
run-ha-soak.sh'ssubmit_worker_job()/verify_worker_rejoin_resync()polling idiom (real job, polljobinfoforRUNthenDONE, grep each worker's own log) rather than inventing a new verification mechanism. - Validation: two scenarios — a plain multi-task job, and a multi-task job submitted as part of an array job (Requirement 5.5).
- Risks: soak-style process/port flakiness, mitigated by the same pre-flight check
xce-test-haalready established.
Data Models
Domain Model
No new entities or fields. xce_primary_resreq_t.minTasks/.maxTasks (existing fields) transition from a compile-time constant to a per-job derived value.
Persistence / Save-Restore Completeness
Per persistence.md's "Snapshot Save/Load Field-Completeness" section: minTasks/maxTasks are not independently persisted in the WAL or snapshot wire format. snapshot_wire.c persists job_submit_data_t.mintasks/maxtasks (the job-level source of truth, already correctly round-tripped, confirmed unchanged by this design); snapshot_load.c restores that value and then calls the same parse_resreq() chain fresh — the exact derivation this spec fixes. There is no field being newly persisted, so there is no new round-trip gap to enumerate: fixing the shared derivation fixes fresh-ingest and snapshot-restored jobs identically, by construction.
Error Handling
Error Categories and Responses
Invalid task-count pair (minTasks > maxTasks reaching apply_job_task_count()): defensive-only, since the apiserver already guarantees this invariant twice before any job reaches the C layer. maxtasks is raised to mintasks, mirroring convert.go's clamp direction exactly — no new job-rejecting error path is introduced.
mintasks == 0: not separately validated by this feature, since it's unreachable from either live call site (both convert.go and xce_test's jobsub independently clamp mintasks to at least 1 before a job ever reaches the C layer). A genuinely-zero pair (only reachable via a hand-crafted --sim snapshot bypassing both guards) degrades gracefully through scheduler-allocation's own pre-existing maxTasks <= 1 bail in parjob_allocator.c — the job pends as unsatisfiable rather than reaching an inconsistent dispatch state.
Testing Strategy
- Unit Tests (
test_resreq_task_count.c): standalone primary receives the applied task count;COMPOUND/ALTER-tagged resreq is left untouched;minTasks > maxTasksis defensively clamped. - Integration Tests (
test_maxtasks_wiring_ingest.c): each of the two live ingest call sites applies the wiring correctly (fresh ingest viacore_build_job(), and the simulation/replay driver's snapshot-loading path viasnapshot_load.c); the snapshot-loading test asserts the same task count is derived as an equivalent fresh-ingest input would produce, satisfying Requirement 5.6's comparative claim directly; confirmssim_loop.c's job-ingest path reaches the samecore_build_job()/core_events.ccall site as the real driver, so simulated and real job ingest apply the fix identically with no driver/simulation divergence; a structural regression test drives a real resource-request update (Requirement 1.4) and confirms the job's dispatched task count is unchanged, proving the guarantee holds viajob->schedobjnever being rebuilt (bug-1773) rather than via any new code this feature adds.test_parjob_e2e_multislot.cis updated to drive its multi-host scenario through unmodified real ingest, closing the loop on the bug it originally characterized. - E2E Tests (
xce-test-multihost): a real multi-task job submitted via plainjobsubagainst two real workers is genuinely split across both hosts by the scheduler's own decision and reachesDONEon both (4.1-4.3); a second scenario repeats this for a multi-task array job (5.5). - Non-default launch type (
test_worker_launch_type_dispatch.c, white-box): confirmsjob_start_remote_task()'s launch-type gate correctly no-ops for an MPI launch type and correctly fans out via XCE's own per-host dispatch for the default type — the actual decision point Requirement 4.4 depends on, currently with zero coverage of any kind — and confirmsjob_start_local_task()'s branch intocreate_mpi_cmdline()builds the correct host list and command (4.4), without requiring a real MPI runtime in the test environment. The E2E harness above only exercises the default launch path, whose availability is guaranteed. - Regression: full existing scheduler/controller/worker test suites re-run unmodified (5.1); existing
xce-test-hasequential and chaotic soak re-run unmodified as non-regression evidence (5.2); a cache-key collision test (reusingscheduler-allocation'salloc_cachetest conventions) confirms two otherwise-identical jobs with different task counts don't collide in the unsatisfiable-resreq cache, for the fresh-ingest path (core_build_job()) whereresreq_fpis correctly computed (5.4) — the snapshot-restore path is explicitly excluded from this test per the documented, pre-existing bug-1772 exception (it never computesresreq_fp, confirmed live and unfixed by this feature), not silently skipped;make_sched_job()'s path is dead code and not a live production concern at all.