From 65679285bac02cd000d3f00cc5783d8cd4bc9d4c Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Wed, 30 Sep 2026 09:53:14 +0000 Subject: [PATCH] feat(scheduler-allocator-runner): seqjob allocates through the framework runner (task 4.2) seqjob registers sched_allocator_run with seqjob_alloc_iface (no accepts guard, plus its one-bucket plan iterator); its private my_alloc() is gone. New fixtures pin the intended changes: a reason for every short host, the bug-1893 shared reason, the host-stage OOM debit release, and -EINVAL for missing inputs; seqjob's plan is asserted to be one bucket of one slot. No other test expectation changed. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../specs/scheduler-allocator-runner/tasks.md | 3 +- src/scheduler/src/plugins/seqjob_allocator.c | 226 +------ src/scheduler/testsuite/meson.build | 15 + .../testsuite/test_alloc_equivalence_seqjob.c | 50 +- .../test_alloc_equivalence_seqjob_changes.c | 607 ++++++++++++++++++ .../testsuite/test_seqjob_allocator.c | 11 +- .../test_seqjob_rewrap_equivalence.c | 6 +- 7 files changed, 674 insertions(+), 244 deletions(-) create mode 100644 src/scheduler/testsuite/test_alloc_equivalence_seqjob_changes.c diff --git a/.kiro/specs/scheduler-allocator-runner/tasks.md b/.kiro/specs/scheduler-allocator-runner/tasks.md index f8f0b358..84260f17 100644 --- a/.kiro/specs/scheduler-allocator-runner/tasks.md +++ b/.kiro/specs/scheduler-allocator-runner/tasks.md @@ -67,7 +67,7 @@ - _Depends: 2.2, 3.3_ - _Requirements: 1.1, 1.2, 1.5, 1.6, 2.2, 3.6, 3.7, 5.2, 7.1_ -- [ ] 4.2 Switch the sequential-job allocator +- [x] 4.2 Switch the sequential-job allocator - First add the seqjob intentional-change fixtures to the seqjob characterization tests and see them fail against the old code: a candidate host that the stale per-host request left by the job-level pass used to skip is now checked; complete per-host "no resource" reasons for every selection-matching short host with the main reason unchanged; bug-1893 (the 2.1 fixture that records no shared reason today now expects one host-less reason naming the short resource); release of the claimed host's debit when result building runs out of memory (this rewrites the 2.1 host-allocation out-of-memory fixture's per-host debit assertion to the new expected value) - Give seqjob its allocator contract with no acceptance guard, register the framework routine as its allocation entry point, and remove its private allocation routine - After the switch, add seqjob missing-input fixtures (invalid-argument, no pend-reason record) @@ -104,3 +104,4 @@ - The runner ignores `add_job_preason`'s return value, as the old allocators did (a failed add drops that reason). - Incremental rebuilds in `src/build.alloc` can miss a file restored after a temporary RED edit if its mtime ends up older than the object; `touch` restored files before the GREEN run (seen in 3.1). - Stale references for 5.1 (from 4.1 review): test_parjob_combinatorial.c lines ~6, 56, 106, 668, 806 and helper `drive_my_alloc`; scheduler.h:415; sim_alloc.c:48; test_host_iter.c:665; test_parjob_e2e_multislot.c:138 (plus seqjob ones after 4.2). +- Stale references for 5.1 (from 4.2 review): sim_alloc.c:36-175, scheduler.c:166, test names `test_my_alloc_*` and helper `drive_my_alloc` in test_seqjob_allocator.c / test_seqjob_rewrap_equivalence.c, and their my_alloc() comments. diff --git a/src/scheduler/src/plugins/seqjob_allocator.c b/src/scheduler/src/plugins/seqjob_allocator.c index 61d2f0cc..c09bdc60 100644 --- a/src/scheduler/src/plugins/seqjob_allocator.c +++ b/src/scheduler/src/plugins/seqjob_allocator.c @@ -163,10 +163,10 @@ static int seqjob_plan_iter_init(xce_alloc_plan_iter_t* iter, priv->bucket_slots[0] = 1; priv->plan.nbuckets = 1; priv->plan.bucket_slots = priv->bucket_slots; - /* plan.ctx is set by the caller (my_alloc(), below) on every plan - * returned from next() -- tasks.md's task 2.1 implementation note: - * init()'s own fixed (iter, resreq, sched) signature has no ctx - * parameter to thread it through. */ + /* plan.ctx is set by the caller (the framework runner, + * sched_allocator_run()) on every plan returned from next(): + * init()'s fixed (iter, resreq, core) signature has no ctx parameter + * to thread it through. */ priv->plan.ctx = NULL; priv->served = 0; @@ -210,211 +210,16 @@ static void seqjob_plan_iter_fini(xce_alloc_plan_iter_t* iter) iter->private = NULL; } -static int my_alloc(xce_sched_plugin_t* plugin, xce_sched_ctx_t* ctx) -{ - xce_sched_core_t* core = plugin->core; - /* Kept alongside `core` above ONLY for sched_make_allocs() below - * (scheduler.c, out of this task's boundary) -- that function's own - * signature still needs a genuine xce_scheduler_t*, but its body only - * ever reads sched->core.resmgr/lock/nextid (scheduler.c's own - * sched_make_allocs()/sched_get_id() bodies), so a caller with no real - * xce_scheduler_t* of its own (simulation mode) can safely hand it a - * narrow, resmgr-aliased stand-in (sim_alloc.c) -- never used for - * anything plugin-registry/fan-out related, which is the SEPARATE - * concern `core` above (task 4.3) exists to fix honestly. */ - xce_scheduler_t* sched = (xce_scheduler_t*)plugin->sched; - int rc; - xce_pendreason_t* reason = ctx->reason; - xvec_t* uncomit = NULL; - xce_sched_worker_t* w = NULL; - xce_alloc_plan_iter_t plan_iter_storage; - xce_alloc_plan_iter_t* plan_iter = &plan_iter_storage; - xce_alloc_plan_t* plan; - - if (reason == NULL) { - reason = make_pendreason(0); - ctx->reason = reason; - if (reason == NULL) { - return -ENOMEM; - } - } - - rc = sched_ctx_get_uncomit_shared(&uncomit, ctx); - if (rc < 0) { - return rc; - } - - /* scheduler-sim-live-allocation task 4.3: sched_check_shared_satisfy() - * takes core directly (already core-typed since task 1.3) -- read - * from the plugin's own honest `core` field, not derived from - * plugin->sched. */ - /* bug-1884: the check (re)initializes unmeet itself */ - xvec_idval_fini(&(ctx->unmeet)); - rc = sched_check_shared_satisfy(core, uncomit, - &(ctx->alloc_req.shared), &(ctx->unmeet)); - if (rc < 0) { - return rc; - } - - if (rc == 0) { - int i; - - for (i = 0; i < ctx->alloc_req.shared.count; i++) { - xce_resource_t* r = NULL; - - rc = vget(core->resmgr, ((int*)ctx->unmeet.idxs.data)[i], - (void**)&r); - if (rc <= 0) { - continue; - } - - add_job_preason(reason->reasons, PREASON_NO_RESOURCE, NULL, - r->name); - } - - return 0; - } - - /* add uncomitted shared */ - rc = xvec_idval(uncomit, &(ctx->alloc_req.shared), XVEC_ADD_FLOAT); - if (rc < 0) { - return rc; - } - - /* - * Req 9.1 / Composition Algorithm (requirements.md): plan_iter -> - * bucket loop -> host_iter, degenerate to seqjob's trivial single- - * plan / single-bucket / first-matching-host case. Replaces the old - * sched_collect_workers() + xlist_find(sched_try_alloc_on_hosts, ...) - * inline worker-collection loop -- no allocator-specific worker- - * collection logic remains here, only the shared iterators. - */ - plan_iter->init = seqjob_plan_iter_init; - plan_iter->next = seqjob_plan_iter_next; - plan_iter->fini = seqjob_plan_iter_fini; - - if (plan_iter->init(plan_iter, ctx->primary, core) != 0) { - /* An infrastructure error (e.g. OOM) is not a resource- - * availability determination -- treat it the same as "no - * candidates", matching alloc_iter.h's own error-propagation - * contract for a failing init(). */ - return 0; - } - - while ((plan = plan_iter->next(plan_iter)) != NULL) { - int b; - - w = NULL; - - /* Composition Algorithm's bucket loop -- degenerates to exactly - * one iteration since seqjob's plan always has nbuckets == 1. */ - for (b = 0; b < plan->nbuckets; b++) { - xce_host_iter_t host_iter_storage; - xce_host_iter_t* host_iter = &host_iter_storage; - - /* tasks.md task 2.1 implementation note: thread ctx onto - * every plan next() returns -- init()'s own fixed signature - * has no ctx parameter for this. */ - plan->ctx = ctx; - - host_iter->init = sched_host_iter_init; - host_iter->next = sched_host_iter_next; - host_iter->fini = sched_host_iter_fini; - - if (host_iter->init(host_iter, plan, core->workers) == 0) { - xce_sched_worker_t* cand; - - while ((cand = host_iter->next(host_iter)) != NULL) { - /* scheduler-sim-live-allocation spec, task 4.3: - * sched_try_alloc_on_host_() takes core directly - * (already core-typed since task 2.1) -- this is the - * exact call whose own internal plugin_prepare_ - * allocation() fan-out (resource.c) must reach the - * SAME core->plugins registry every other plugin - * (e.g. the limit plugin) is registered against, so - * it reads the plugin's honest `core` field, not a - * throwaway wrapper derived from plugin->sched. */ - if (sched_try_alloc_on_host_(cand, core, ctx) - > 0) { - w = cand; - break; - } - } - - host_iter->fini(host_iter); - } - /* host_iter->init() failing is treated the same as "no - * candidates" for this bucket -- an infrastructure error, not - * a resource-availability determination (alloc_iter.h). */ - - if (w == NULL) { - break; /* this bucket found no satisfying host */ - } - } - - if (w != NULL) { - break; /* plan satisfied -- seqjob only ever has one plan */ - } - } - - plan_iter->fini(plan_iter); - - if (w != NULL) { - xce_host_alloc_t** hallocs = NULL; - xce_host_alloc_t* halloc = NULL; - xce_alloc_t** allocs = NULL; - - /* bug-1882: any constructor failing (OOM) is -ENOMEM with - * ctx->alloc left NULL -- the caller moves ctx->alloc onto the job - * whatever the rc, so a half-built allocation must never stay - * there. The debits this attempt made live in ctx's scratch and - * are dropped by sched_ctx_fini(). */ - allocs = sched_make_allocs( - sched, "", ctx->alloc_req.host.count, ctx->alloc_req.host.idxs.data, - ctx->alloc_req.host.vals.data, /* FIXME: no duration now */ NULL); - if ((allocs == NULL) && (ctx->alloc_req.host.count > 0)) { - return -ENOMEM; - } - - halloc = make_host_alloc(w->name, ctx->alloc_req.host.count, allocs); - if (halloc == NULL) { - free_ResAlloc_list(ctx->alloc_req.host.count, allocs); - return -ENOMEM; - } - - hallocs = make_host_allocs(1, halloc); - if (hallocs == NULL) { - free_HostAlloc_ptr(&halloc); - return -ENOMEM; - } - - ctx->alloc = make_job_alloc(0, 1, hallocs); - if (ctx->alloc == NULL) { - free_HostAlloc_list(1, hallocs); - return -ENOMEM; - } - - if (ctx->alloc_req.shared.count > 0) { - ctx->alloc->allocs_shared - = sched_make_allocs(sched, "", ctx->alloc_req.shared.count, - ctx->alloc_req.shared.idxs.data, - ctx->alloc_req.shared.vals.data, - /*FIXME: no duration now*/ NULL); - if (ctx->alloc->allocs_shared == NULL) { - free_JobAlloc_ptr(&(ctx->alloc)); - return -ENOMEM; - } - ctx->alloc->nres_shared = ctx->alloc_req.shared.count; - } else { - ctx->alloc->allocs_shared = NULL; - ctx->alloc->nres_shared = 0; - } - - return 1; - } - - return 0; -} +/* seqjob's allocator contract (scheduler-allocator-runner Req 1.2, 5.3): + * no acceptance guard -- every primary that reaches seqjob is accepted, as + * before -- and the one-bucket plan iterator above. Static, not freed by + * the registry; it lives as long as the plugin (Req 1.6). */ +static const xce_sched_allocator_iface_t seqjob_alloc_iface = { + .accepts = NULL, + .plan_init = seqjob_plan_iter_init, + .plan_next = seqjob_plan_iter_next, + .plan_fini = seqjob_plan_iter_fini +}; /* * seqjob_allocator_new_plugin - scheduler-sim-live-allocation spec, task @@ -440,7 +245,8 @@ xce_sched_plugin_t* seqjob_allocator_new_plugin(void) .begin_session_fn = NULL, .end_session_fn = NULL, .job_elect_fn = NULL, - .job_alloc_fn = (plugin_job_allocate_fn_t)my_alloc, + .job_alloc_fn = (plugin_job_allocate_fn_t)sched_allocator_run, + .allocator_iface = &seqjob_alloc_iface, .data = NULL }; diff --git a/src/scheduler/testsuite/meson.build b/src/scheduler/testsuite/meson.build index 0924cd06..4c5546d8 100644 --- a/src/scheduler/testsuite/meson.build +++ b/src/scheduler/testsuite/meson.build @@ -1729,6 +1729,18 @@ sched_alloc_equivalence_seqjob_test = executable( '-Wl,--wrap=make_job_alloc'], install: false) +# scheduler-allocator-runner task 4.2 (Req 2.2, 3.6, 3.7, 5.1): seqjob's +# intentional changes and missing-input guards. Same white-box stack and +# wraps as sched_alloc_equivalence_seqjob_test. +sched_alloc_equivalence_seqjob_changes_test = executable( + 'sched_alloc_equivalence_seqjob_changes_test', + ['test_alloc_equivalence_seqjob_changes.c'], + include_directories:[incdir], + dependencies: [scheduler_test_deps], + link_args: ['-Wl,--wrap=make_host_alloc', + '-Wl,--wrap=make_job_alloc'], + install: false) + # scheduler-allocator-runner task 2.2 (Req 4.5, 5.1, 5.2, 5.4, 5.8, 7.2): # parjob characterization through the plugin's registered job_alloc_fn, # against today's allocator and (from task 4.1) the framework runner. Same @@ -2052,6 +2064,9 @@ test('scheduler seqjob allocator test', test('scheduler alloc equivalence seqjob test', sched_alloc_equivalence_seqjob_test, suite: 'scheduler') # Leak detection stays on (as for the seqjob characterization above). +test('scheduler alloc equivalence seqjob changes test', + sched_alloc_equivalence_seqjob_changes_test, suite: 'scheduler') +# Leak detection stays on (as for the seqjob characterization above). test('scheduler alloc equivalence parjob test', sched_alloc_equivalence_parjob_test, suite: 'scheduler') # Leak detection stays on: every fixture tears down its ctx and core. diff --git a/src/scheduler/testsuite/test_alloc_equivalence_seqjob.c b/src/scheduler/testsuite/test_alloc_equivalence_seqjob.c index a3457acb..3f5e4288 100644 --- a/src/scheduler/testsuite/test_alloc_equivalence_seqjob.c +++ b/src/scheduler/testsuite/test_alloc_equivalence_seqjob.c @@ -2,9 +2,8 @@ * test_alloc_equivalence_seqjob.c - scheduler-allocator-runner task 2.1: * characterization of the sequential-job allocator (Req 4.5, 5.4, 5.8, * 7.2), driven only through the registered plugin->job_alloc_fn of - * seqjob_allocator_new_plugin(), never the static routine by name. The - * expectations must still hold once task 4.2 replaces that routine with the - * framework runner (one more white-box include below). Each fixture asserts + * seqjob_allocator_new_plugin() (the framework runner since task 4.2), + * with the Req 2.2 and 5.5 changes of task 4.2 pinned. Each fixture asserts * result code, hosts, per-host records, shared records, per-host and shared * uncommitted amounts and pend reasons. * @@ -14,11 +13,10 @@ * libxce make_host_alloc/make_job_alloc (meson.build) failing on call N. * The host-array allocation is not injected (its origin changes; task 3.3). * - * Kept out on purpose: multi-host pending (Req 3.7), more shared requests - * than short ones (bug-1893), COMPOUND layouts where the positional - * uncommitted read (bug-1892) matters. Values the switch changes on purpose - * (Req 5.5 host-stage debit, Req 2.2 shared reasons) sit in their own - * `_today` test functions. + * Multi-host pending (Req 3.7), more shared requests than short ones + * (bug-1893), the plan shape and missing inputs (Req 5.1) are in + * test_alloc_equivalence_seqjob_changes.c. COMPOUND layouts where the + * positional uncommitted read (bug-1892) matters are kept out. */ #include #include @@ -34,12 +32,12 @@ #include "xcealloc.h" #include "xstring.h" -/* White-box sources under test. Task 4.2 adds the framework runner - * ("../src/alloc_runner.c") as one more line of this list. */ +/* White-box sources under test. */ #include "../src/core.c" #include "../src/resource.c" #include "../src/schedobj.c" #include "../src/host_iter.c" +#include "../src/alloc_runner.c" #include "../src/plugins/seqjob_allocator.c" /* ---------------------------------------------------------------------- */ @@ -515,18 +513,17 @@ static void test_seqjob_pending_on_short_shared_resource(void) eq_finish(&f, &p); } -/* 2, resource reasons, TODAY's behaviour: none. The reason loop skips - * every unmet entry because vget() returns 0 on success and the loop - * tests `rc <= 0`. Requirement 2.2 changes this in task 4.2 (one host-less - * NO_RESOURCE reason naming "licenses"). */ -static void test_seqjob_pending_on_short_shared_no_reason_today(void) +/* 2, resource reasons (Req 2.2, bug-1893): exactly one host-less + * NO_RESOURCE reason (host stored as ""), naming "licenses". */ +static void test_seqjob_pending_on_short_shared_names_it(void) { eq_fixture_t f; eq_primary_t p; xce_sched_worker_t* a; TEST_ASSERT_EQUAL_INT(0, eq_run_short_shared(&f, &p, &a)); - eq_assert_reason_counts(&f, 0, 0, 0); + eq_assert_reason_counts(&f, 0, 0, 1); + eq_assert_res_reason(&f, 0, "", "licenses"); eq_finish(&f, &p); } @@ -561,7 +558,7 @@ static void test_seqjob_pending_single_host_short_of_mem(void) } /* 4a. Out of memory building the claimed host's allocation records - * (sched_make_allocs() call 1). Per-host debit: see the _debit_today test. */ + * (sched_make_allocs() call 1). Per-host debit: see _releases_debit. */ static void test_seqjob_oom_host_records_returns_enomem(void) { eq_fixture_t f; @@ -578,8 +575,8 @@ static void test_seqjob_oom_host_records_returns_enomem(void) eq_finish(&f, &p); } -/* 4b. Out of memory in make_host_alloc(). Per-host debit: see the - * _debit_today test. */ +/* 4b. Out of memory in make_host_alloc(). Per-host debit: see + * _releases_debit. */ static void test_seqjob_oom_host_alloc_returns_enomem(void) { eq_fixture_t f; @@ -596,10 +593,9 @@ static void test_seqjob_oom_host_alloc_returns_enomem(void) eq_finish(&f, &p); } -/* 4a/4b per-host debit, TODAY's behaviour: the claimed host's debit is - * left in the attempt context. Requirement 5.5 changes this in task 4.2 - * (the debit of a claim not turned into a host allocation is released). */ -static void test_seqjob_oom_host_stage_leaves_debit_today(void) +/* 4a/4b per-host debit (Req 5.5): the claim was not turned into a host + * allocation, so the claimed host's debit is released. */ +static void test_seqjob_oom_host_stage_releases_debit(void) { int stage; @@ -616,8 +612,8 @@ static void test_seqjob_oom_host_stage_leaves_debit_today(void) } TEST_ASSERT_EQUAL_INT(-ENOMEM, eq_run_standard(&f, &p, &a)); - TEST_ASSERT_EQUAL_FLOAT(2.0f, eq_host_uncomit(&f, a, f.mem)); - TEST_ASSERT_EQUAL_FLOAT(1.0f, eq_host_uncomit(&f, a, f.slots)); + TEST_ASSERT_EQUAL_FLOAT(0.0f, eq_host_uncomit(&f, a, f.mem)); + TEST_ASSERT_EQUAL_FLOAT(0.0f, eq_host_uncomit(&f, a, f.slots)); eq_finish(&f, &p); } @@ -780,11 +776,11 @@ int main(void) UNITY_BEGIN(); RUN_TEST(test_seqjob_success_one_host_with_shared); RUN_TEST(test_seqjob_pending_on_short_shared_resource); - RUN_TEST(test_seqjob_pending_on_short_shared_no_reason_today); + RUN_TEST(test_seqjob_pending_on_short_shared_names_it); RUN_TEST(test_seqjob_pending_single_host_short_of_mem); RUN_TEST(test_seqjob_oom_host_records_returns_enomem); RUN_TEST(test_seqjob_oom_host_alloc_returns_enomem); - RUN_TEST(test_seqjob_oom_host_stage_leaves_debit_today); + RUN_TEST(test_seqjob_oom_host_stage_releases_debit); RUN_TEST(test_seqjob_oom_job_alloc_returns_enomem_debit_stays); RUN_TEST(test_seqjob_oom_shared_records_returns_enomem_debit_stays); RUN_TEST(test_seqjob_task_count_zero_reaches_plugin); diff --git a/src/scheduler/testsuite/test_alloc_equivalence_seqjob_changes.c b/src/scheduler/testsuite/test_alloc_equivalence_seqjob_changes.c new file mode 100644 index 00000000..ca46ac22 --- /dev/null +++ b/src/scheduler/testsuite/test_alloc_equivalence_seqjob_changes.c @@ -0,0 +1,607 @@ +/* + * test_alloc_equivalence_seqjob_changes.c - scheduler-allocator-runner task + * 4.2: the sequential-job allocator's intentional changes and missing-input + * guards, driven through the registered plugin->job_alloc_fn of + * seqjob_allocator_new_plugin() (the rest of its characterization is in + * test_alloc_equivalence_seqjob.c, whose stubs and fixture this file + * mirrors). + * + * - every selection-matching short host gets its (host, resource) + * NO_RESOURCE reasons; main reason, decision and debits unchanged + * (Req 3.6, 3.7) + * - with two shared resources requested and one short, exactly one + * host-less reason, naming the short one (Req 2.2, bug-1893) + * - seqjob's plan is one bucket of one slot whatever the task count + * (brief.md Constraints item 3) + * - a missing core, scheduler back-reference, primary or allocator + * contract is -EINVAL with no pend-reason record (Req 5.1) + */ +#include +#include +#include + +#include + +#include "preason.h" +#include "resobj.h" +#include "resreq.h" +#include "sched/alloc_iter.h" +#include "scheduler.h" +#include "xcealloc.h" +#include "xstring.h" + +/* White-box sources under test. */ +#include "../src/core.c" +#include "../src/resource.c" +#include "../src/schedobj.c" +#include "../src/host_iter.c" +#include "../src/alloc_runner.c" +#include "../src/plugins/seqjob_allocator.c" + +/* ---------------------------------------------------------------------- */ +/* Link-satisfying stubs (as test_alloc_equivalence_seqjob.c) */ +/* ---------------------------------------------------------------------- */ + +int plugin_register(void* sched, void* plugin) +{ + (void)sched; + (void)plugin; + return 0; +} + +void plugin_unregister(void* arg) { (void)arg; } + +/* plugin.c's fan-out, seqjob's hook only; resets as plugin.c (bug-1883). */ +int plugin_prepare_allocation(xce_sched_core_t* sched, xce_sched_ctx_t* ctx) +{ + xce_sched_plugin_t fake_plugin; + + if (ctx->worker == NULL) { + xvec_idval_fini(&(ctx->alloc_req.shared)); + xvec_idval_init(&(ctx->alloc_req.shared), 8); + } + xvec_idval_fini(&(ctx->alloc_req.host)); + xvec_idval_init(&(ctx->alloc_req.host), 8); + + memset(&fake_plugin, 0, sizeof(fake_plugin)); + fake_plugin.sched = sched; + fake_plugin.core = sched; + + return seqjob_prepare_allocation(&fake_plugin, ctx); +} + +int plugin_job_update(void* s, xce_sched_job_t* j) +{ + return (void)s, (void)j, 0; +} + +int plugin_queue_update(void* s, xce_sched_queue_t* q) +{ + return (void)s, (void)q, 0; +} + +int plugin_job_rusage_update(void* s, xce_sched_job_t* j, xce_job_rusages_t* r) +{ + return (void)s, (void)j, (void)r, 0; +} + +/* scheduler.c's sched_make_allocs(): real make_alloc() records (duration -1 + * for a NULL duration array, instid 0); NULL for nres <= 0 as the real one. + * Counted so a test can assert no result was built. */ +static int g_make_allocs_calls = 0; + +xce_alloc_t** sched_make_allocs(xce_scheduler_t* sched, char* reserve, + int nres, int* idxs, float* vals, + int* duration) +{ + xce_alloc_t** allocs; + int i; + + g_make_allocs_calls++; + if ((sched == NULL) || (nres <= 0) || (idxs == NULL) || (vals == NULL)) { + errno = EINVAL; + return NULL; + } + + allocs = (xce_alloc_t**)calloc(nres, sizeof(*allocs)); + if (allocs == NULL) { + return NULL; + } + + for (i = 0; i < nres; i++) { + xce_resource_t* r = NULL; + + vget(sched->core.resmgr, idxs[i], (void**)&r); + if (r == NULL) { + continue; + } + allocs[i] = make_alloc(r->name, reserve, 0, vals[i], + (duration != NULL) ? duration[i] : -1); + if (allocs[i] == NULL) { + free_ResAlloc_list(nres, allocs); + errno = ENOMEM; + return NULL; + } + } + + return allocs; +} + +/* The target links with the same --wrap flags as the seqjob + * characterization target; no injection is needed here. */ +xce_host_alloc_t* __real_make_host_alloc(char*, int, xce_alloc_t**); +xce_job_alloc_t* __real_make_job_alloc(uint64_t, int, xce_host_alloc_t**); + +xce_host_alloc_t* __wrap_make_host_alloc(char* host, int nalloc, + xce_alloc_t** allocs) +{ + return __real_make_host_alloc(host, nalloc, allocs); +} + +xce_job_alloc_t* __wrap_make_job_alloc(uint64_t jobid, int nhost, + xce_host_alloc_t** hallocs) +{ + return __real_make_job_alloc(jobid, nhost, hallocs); +} + +void setUp(void) { g_make_allocs_calls = 0; } +void tearDown(void) { g_make_allocs_calls = 0; } + +/* ---------------------------------------------------------------------- */ +/* Fixture */ +/* ---------------------------------------------------------------------- */ + +#define HOST_A "eq-host-a" +#define HOST_B "eq-host-b" +#define HOST_C "eq-host-c" +#define HOST_D "eq-host-d" + +typedef struct { + xce_scheduler_t sched; + xce_resource_t* slots; + xce_resource_t* mem; + xce_resource_t* rack; + xce_resource_t* lic; /* shared, pool set per fixture */ + xce_resource_t* tok; /* shared, pool 10 */ + xce_sched_plugin_t* plugin; + xce_sched_obj_t obj; + xce_primary_schedobj_t primary_obj; + xce_sched_ctx_t ctx; + int ctx_ready; +} eq_fixture_t; + +/* One primary request: `mem` per task, `lic` per job (0 = not requested), + * optional `rack == N` select. */ +typedef struct { + xce_primary_resreq_t req; + rusage_value_t mem_rusage; + rusage_value_t lic_rusage; + rusage_value_t tok_rusage; + xce_select_t select; + xce_select_node_t sel_res, sel_num, sel_eq; +} eq_primary_t; + +static xce_resource_t* eq_register_resource(var resmgr, const char* name, + int flags) +{ + xce_resource_t res; + + memset(&res, 0, sizeof(res)); + res.name = (char*)name; + res.flags = flags; + + return makeResource(resmgr, &res); +} + +/* Shared pools "licenses" (`lic_total`) and "tokens" (10), none in use. */ +static void eq_fixture_init(eq_fixture_t* f, float lic_total) +{ + int idx[2]; + float val[2]; + int i; + + memset(f, 0, sizeof(*f)); + TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(f->sched.core))); + /* xce_sched_core_fini() finalizes these; both drivers calloc them */ + for (i = 0; i < 2; i++) { + f->sched.core.pend_updates[i] = (dataq_t*)calloc(1, sizeof(dataq_t)); + TEST_ASSERT_NOT_NULL(f->sched.core.pend_updates[i]); + dataq_init(f->sched.core.pend_updates[i], NULL); + } + + f->slots = eq_register_resource(f->sched.core.resmgr, "slots", 0); + f->mem = eq_register_resource(f->sched.core.resmgr, "mem", 0); + f->rack = eq_register_resource(f->sched.core.resmgr, "rack", 0); + f->lic = eq_register_resource(f->sched.core.resmgr, "licenses", + XCE_RESF_GLOBAL); + f->tok = eq_register_resource(f->sched.core.resmgr, "tokens", + XCE_RESF_GLOBAL); + TEST_ASSERT_NOT_NULL(f->slots); + TEST_ASSERT_NOT_NULL(f->mem); + TEST_ASSERT_NOT_NULL(f->rack); + TEST_ASSERT_NOT_NULL(f->lic); + TEST_ASSERT_NOT_NULL(f->tok); + + idx[0] = f->lic->idx; + val[0] = lic_total; + idx[1] = f->tok->idx; + val[1] = 10.0f; + put_resvals(&(f->sched.core.sharedInst), 2, idx, val); + + f->plugin = seqjob_allocator_new_plugin(); + TEST_ASSERT_NOT_NULL(f->plugin); + TEST_ASSERT_NOT_NULL(f->plugin->job_alloc_fn); + f->plugin->sched = &(f->sched); + f->plugin->core = &(f->sched.core); +} + +static xce_sched_worker_t* eq_add_host(eq_fixture_t* f, const char* name, + float slots, float mem, float rack) +{ + xce_sched_worker_t* w; + int idx[3] = { f->slots->idx, f->mem->idx, f->rack->idx }; + float total[3] = { slots, mem, rack }; + float inuse[2] = { 0.0f, 0.0f }; + + w = (xce_sched_worker_t*)calloc(1, sizeof(*w)); + TEST_ASSERT_NOT_NULL(w); + w->name = makeString((char*)name); + TEST_ASSERT_NOT_NULL(w->name); + zarray_init(&(w->resvals), 8, (zarray_item_killer_t)kill_sched_resval); + zarray_init(&(w->resInUse), 8, (zarray_item_killer_t)kill_sched_resval); + put_resvals(&(w->resvals), 3, idx, total); + put_resvals(&(w->resInUse), 2, idx, inuse); + + TEST_ASSERT_TRUE(sched_add_worker(&(f->sched.core), w) >= 0); + + return w; +} + +static void eq_primary_init(eq_primary_t* p, int max_tasks, float mem, + float lic, const eq_fixture_t* f, int rack) +{ + memset(p, 0, sizeof(*p)); + p->req.minTasks = (max_tasks > 0) ? 1 : 0; + p->req.maxTasks = max_tasks; + xlist_init(&(p->req.rusages), 4); + + if (mem > 0.0f) { + p->mem_rusage.res = (char*)"mem"; + p->mem_rusage.value = mem; + p->mem_rusage.per_flag = PER_TASK; + xlist_store(&(p->req.rusages), &(p->mem_rusage)); + } + if (lic > 0.0f) { + p->lic_rusage.res = (char*)"licenses"; + p->lic_rusage.value = lic; + p->lic_rusage.per_flag = PER_JOB; + xlist_store(&(p->req.rusages), &(p->lic_rusage)); + } + if (rack > 0) { + p->sel_res.type = SEL_RESNAME; + p->sel_res.u.resname.res_idx = f->rack->idx; + p->sel_res.u.resname.res_type = RES_TYPE_NUMERIC; + p->sel_num.type = SEL_NUM; + p->sel_num.u.numval = (double)rack; + p->sel_eq.type = SEL_EQ; + p->sel_eq.u.binop.left = &(p->sel_res); + p->sel_eq.u.binop.right = &(p->sel_num); + p->select.root = &(p->sel_eq); + p->req.select = &(p->select); + } +} + +/* ctx over a PRIMARY obj for `p`, job-level prepare pass included, exactly + * as sched_make_primary_allocation() drives it. */ +static void eq_ctx_begin_primary(eq_fixture_t* f, eq_primary_t* p) +{ + f->primary_obj.resreq = &(p->req); + f->obj.tag = XCE_RESREQ_PRIMARY; + f->obj.u.primary = &(f->primary_obj); + + TEST_ASSERT_EQUAL_INT(0, sched_ctx_init(&(f->ctx), &(f->sched.core), + &(f->obj))); + f->ctx_ready = 1; + sched_ctx_set_primary(&(f->ctx), &(p->req)); +} + +static int eq_alloc(eq_fixture_t* f) +{ + return f->plugin->job_alloc_fn(f->plugin, &(f->ctx)); +} + +static void eq_finish(eq_fixture_t* f, eq_primary_t* p) +{ + if (f->ctx_ready) { + free_JobAlloc_ptr(&(f->ctx.alloc)); + free_PendReason_ptr(&(f->ctx.reason)); + sched_ctx_fini(&(f->ctx)); + } + f->plugin->fini_fn(f->plugin); + xce_sched_core_fini(&(f->sched.core)); + /* xce_sched_core_init() creates the process-wide string table on every + * call and xce_sched_core_fini() leaves it; release it per fixture */ + xstring_table_fini(); + xlist_fini(&(p->req.rusages)); +} + +/* ---------------------------------------------------------------------- */ +/* Assertion helpers */ +/* ---------------------------------------------------------------------- */ + +static float eq_host_uncomit(eq_fixture_t* f, xce_sched_worker_t* w, + xce_resource_t* r) +{ + xvec_t* v = (xvec_t*)zarray_get(&(f->ctx.uncomitted.hosts), w->idx); + + return (v == NULL) ? 0.0f : ((float*)v->data)[r->idx]; +} + +static float eq_shared_uncomit(eq_fixture_t* f, xce_resource_t* r) +{ + return ((float*)f->ctx.uncomitted.shared->data)[r->idx]; +} + +static void eq_assert_reason_counts(eq_fixture_t* f, int main_reason, + int nhost, int nres) +{ + TEST_ASSERT_NOT_NULL(f->ctx.reason); + TEST_ASSERT_NOT_NULL(f->ctx.reason->reasons); + TEST_ASSERT_EQUAL_INT(main_reason, f->ctx.reason->reasons->main_reason); + TEST_ASSERT_EQUAL_INT(nhost, f->ctx.reason->reasons->nhost_reasons); + TEST_ASSERT_EQUAL_INT(nres, f->ctx.reason->reasons->nres_reasons); +} + +static void eq_assert_res_reason(eq_fixture_t* f, int i, const char* host, + const char* res) +{ + xce_res_reason_t* rr = f->ctx.reason->reasons->res_based_reasons[i]; + + TEST_ASSERT_NOT_NULL(rr); + TEST_ASSERT_EQUAL_STRING(host, rr->host); + TEST_ASSERT_EQUAL_STRING(res, rr->resource); + TEST_ASSERT_EQUAL_INT(PREASON_NO_RESOURCE, rr->reason); +} + +/* ---------------------------------------------------------------------- */ +/* Req 3.6 / 3.7: a reason for every selection-matching short host */ +/* ---------------------------------------------------------------------- */ + +/* Hosts in iteration order: A (no free slot), B (short of mem), D (rack 2, + * outside the job's rack-1 select), C (short of mem). Before the switch, + * A was pre-filtered against the job-level request (slots 1) and C against + * the request B left behind (mem 4, slots 1), both silently: one reason, + * (B, mem). Now the per-host request is empty before every candidate, so + * A, B and C each reach the per-host check; D is still skipped by the + * select filter alone and gets no reason. Decision, main reason and debits + * are unchanged. */ +static void test_seqjob_every_short_host_gets_reasons(void) +{ + eq_fixture_t f; + eq_primary_t p; + xce_sched_worker_t* w[4]; + int i; + + eq_fixture_init(&f, 10.0f); + w[0] = eq_add_host(&f, HOST_A, 0.0f, 8.0f, 1.0f); + w[1] = eq_add_host(&f, HOST_B, 4.0f, 1.0f, 1.0f); + w[2] = eq_add_host(&f, HOST_D, 4.0f, 1.0f, 2.0f); + w[3] = eq_add_host(&f, HOST_C, 4.0f, 1.0f, 1.0f); + eq_primary_init(&p, 1, 4.0f, 3.0f, &f, 1); + eq_ctx_begin_primary(&f, &p); + + TEST_ASSERT_EQUAL_INT(0, eq_alloc(&f)); + TEST_ASSERT_NULL(f.ctx.alloc); + + eq_assert_reason_counts(&f, 0, 0, 3); + eq_assert_res_reason(&f, 0, HOST_A, "slots"); + eq_assert_res_reason(&f, 1, HOST_B, "mem"); + eq_assert_res_reason(&f, 2, HOST_C, "mem"); + + for (i = 0; i < 4; i++) { + TEST_ASSERT_EQUAL_FLOAT(0.0f, eq_host_uncomit(&f, w[i], f.mem)); + TEST_ASSERT_EQUAL_FLOAT(0.0f, eq_host_uncomit(&f, w[i], f.slots)); + } + TEST_ASSERT_EQUAL_FLOAT(3.0f, eq_shared_uncomit(&f, f.lic)); + TEST_ASSERT_EQUAL_INT(0, g_make_allocs_calls); + + eq_finish(&f, &p); +} + +/* Two short hosts, then one that fits: A (no free slot) and B (short of + * mem) each get their reasons, C is claimed; the decision is unchanged. + * Before the switch A was pre-filtered silently: one reason, (B, mem). */ +static void test_seqjob_short_hosts_before_claim_get_reasons(void) +{ + eq_fixture_t f; + eq_primary_t p; + xce_sched_worker_t* w[3]; + + eq_fixture_init(&f, 10.0f); + w[0] = eq_add_host(&f, HOST_A, 0.0f, 8.0f, 1.0f); + w[1] = eq_add_host(&f, HOST_B, 4.0f, 1.0f, 1.0f); + w[2] = eq_add_host(&f, HOST_C, 4.0f, 8.0f, 1.0f); + eq_primary_init(&p, 1, 4.0f, 0.0f, &f, 0); + eq_ctx_begin_primary(&f, &p); + + TEST_ASSERT_EQUAL_INT(1, eq_alloc(&f)); + TEST_ASSERT_NOT_NULL(f.ctx.alloc); + TEST_ASSERT_EQUAL_INT(1, f.ctx.alloc->nhosts); + TEST_ASSERT_EQUAL_STRING(HOST_C, f.ctx.alloc->allocs[0]->host); + + eq_assert_reason_counts(&f, 0, 0, 2); + eq_assert_res_reason(&f, 0, HOST_A, "slots"); + eq_assert_res_reason(&f, 1, HOST_B, "mem"); + + TEST_ASSERT_EQUAL_FLOAT(0.0f, eq_host_uncomit(&f, w[0], f.slots)); + TEST_ASSERT_EQUAL_FLOAT(0.0f, eq_host_uncomit(&f, w[1], f.mem)); + TEST_ASSERT_EQUAL_FLOAT(4.0f, eq_host_uncomit(&f, w[2], f.mem)); + TEST_ASSERT_EQUAL_FLOAT(1.0f, eq_host_uncomit(&f, w[2], f.slots)); + + eq_finish(&f, &p); +} + +/* ---------------------------------------------------------------------- */ +/* Req 2.2 / bug-1893: only the short shared resource is named */ +/* ---------------------------------------------------------------------- */ + +/* "licenses" (3 wanted, pool 2) is short, "tokens" (1 wanted, pool 10) is + * met: exactly one host-less reason (host stored as ""), naming licenses. + * No host is tried and nothing is counted. */ +static void test_seqjob_pending_on_short_shared_names_short_one(void) +{ + eq_fixture_t f; + eq_primary_t p; + xce_sched_worker_t* a; + + eq_fixture_init(&f, 2.0f); + a = eq_add_host(&f, HOST_A, 4.0f, 8.0f, 1.0f); + eq_primary_init(&p, 1, 2.0f, 3.0f, &f, 0); + p.tok_rusage.res = (char*)"tokens"; + p.tok_rusage.value = 1.0f; + p.tok_rusage.per_flag = PER_JOB; + xlist_store(&(p.req.rusages), &(p.tok_rusage)); + eq_ctx_begin_primary(&f, &p); + TEST_ASSERT_EQUAL_INT(2, f.ctx.alloc_req.shared.count); + + TEST_ASSERT_EQUAL_INT(0, eq_alloc(&f)); + TEST_ASSERT_NULL(f.ctx.alloc); + + eq_assert_reason_counts(&f, 0, 0, 1); + eq_assert_res_reason(&f, 0, "", "licenses"); + + TEST_ASSERT_NULL(zarray_get(&(f.ctx.uncomitted.hosts), a->idx)); + TEST_ASSERT_EQUAL_FLOAT(0.0f, eq_shared_uncomit(&f, f.lic)); + TEST_ASSERT_EQUAL_FLOAT(0.0f, eq_shared_uncomit(&f, f.tok)); + TEST_ASSERT_EQUAL_INT(0, g_make_allocs_calls); + + eq_finish(&f, &p); +} + +/* ---------------------------------------------------------------------- */ +/* brief.md Constraints item 3: seqjob's plan shape */ +/* ---------------------------------------------------------------------- */ + +/* The runner sets nslots to bucket_slots[b] and nhosts to 1 per bucket; + * seqjob relies on its one plan being one bucket of one slot, for every + * task count that can reach it. */ +static void eq_check_plan_shape(int max_tasks) +{ + xce_alloc_plan_iter_t it; + xce_alloc_plan_t* plan; + xce_primary_resreq_t req; + + memset(&req, 0, sizeof(req)); + req.maxTasks = max_tasks; + req.minTasks = (max_tasks > 0) ? 1 : 0; + + TEST_ASSERT_EQUAL_INT(0, seqjob_plan_iter_init(&it, &req, NULL)); + + plan = seqjob_plan_iter_next(&it); + TEST_ASSERT_NOT_NULL(plan); + TEST_ASSERT_EQUAL_INT(1, plan->nbuckets); + TEST_ASSERT_NOT_NULL(plan->bucket_slots); + TEST_ASSERT_EQUAL_INT(1, plan->bucket_slots[0]); + + TEST_ASSERT_NULL(seqjob_plan_iter_next(&it)); + TEST_ASSERT_NULL(seqjob_plan_iter_next(&it)); + + seqjob_plan_iter_fini(&it); + TEST_ASSERT_NULL(it.private); +} + +static void test_seqjob_plan_is_one_bucket_of_one_slot(void) +{ + eq_check_plan_shape(1); + eq_check_plan_shape(0); + eq_check_plan_shape(2); +} + +/* ---------------------------------------------------------------------- */ +/* Req 5.1: missing inputs */ +/* ---------------------------------------------------------------------- */ + +enum { EQ_NO_CORE, EQ_NO_SCHED, EQ_NO_PRIMARY, EQ_NO_IFACE }; + +/* The standard satisfiable job with one input removed just before the + * call: -EINVAL, no pend-reason record, nothing built or debited. The + * input is put back before teardown. */ +static void eq_check_missing_input(int which) +{ + eq_fixture_t f; + eq_primary_t p; + xce_sched_worker_t* a; + xce_primary_resreq_t* primary; + int rc; + + eq_fixture_init(&f, 10.0f); + a = eq_add_host(&f, HOST_A, 4.0f, 8.0f, 1.0f); + eq_primary_init(&p, 1, 2.0f, 3.0f, &f, 0); + eq_ctx_begin_primary(&f, &p); + primary = f.ctx.primary; + + switch (which) { + case EQ_NO_CORE: + f.plugin->core = NULL; + break; + case EQ_NO_SCHED: + f.plugin->sched = NULL; + break; + case EQ_NO_PRIMARY: + f.ctx.primary = NULL; + break; + default: + f.plugin->allocator_iface = NULL; + break; + } + + rc = eq_alloc(&f); + + f.plugin->core = &(f.sched.core); + f.plugin->sched = &(f.sched); + f.plugin->allocator_iface = &seqjob_alloc_iface; + f.ctx.primary = primary; + + TEST_ASSERT_EQUAL_INT(-EINVAL, rc); + TEST_ASSERT_NULL(f.ctx.reason); + TEST_ASSERT_NULL(f.ctx.alloc); + TEST_ASSERT_NULL(zarray_get(&(f.ctx.uncomitted.hosts), a->idx)); + TEST_ASSERT_EQUAL_FLOAT(0.0f, eq_shared_uncomit(&f, f.lic)); + TEST_ASSERT_EQUAL_INT(0, g_make_allocs_calls); + + eq_finish(&f, &p); +} + +static void test_seqjob_missing_core_is_einval(void) +{ + eq_check_missing_input(EQ_NO_CORE); +} + +static void test_seqjob_missing_sched_is_einval(void) +{ + eq_check_missing_input(EQ_NO_SCHED); +} + +static void test_seqjob_missing_primary_is_einval(void) +{ + eq_check_missing_input(EQ_NO_PRIMARY); +} + +static void test_seqjob_missing_iface_is_einval(void) +{ + eq_check_missing_input(EQ_NO_IFACE); +} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_seqjob_every_short_host_gets_reasons); + RUN_TEST(test_seqjob_short_hosts_before_claim_get_reasons); + RUN_TEST(test_seqjob_pending_on_short_shared_names_short_one); + RUN_TEST(test_seqjob_plan_is_one_bucket_of_one_slot); + RUN_TEST(test_seqjob_missing_core_is_einval); + RUN_TEST(test_seqjob_missing_sched_is_einval); + RUN_TEST(test_seqjob_missing_primary_is_einval); + RUN_TEST(test_seqjob_missing_iface_is_einval); + return UNITY_END(); +} diff --git a/src/scheduler/testsuite/test_seqjob_allocator.c b/src/scheduler/testsuite/test_seqjob_allocator.c index cac09463..e1af08e5 100644 --- a/src/scheduler/testsuite/test_seqjob_allocator.c +++ b/src/scheduler/testsuite/test_seqjob_allocator.c @@ -49,6 +49,7 @@ #include "../src/resource.c" #include "../src/schedobj.c" #include "../src/host_iter.c" +#include "../src/alloc_runner.c" #include "../src/plugins/seqjob_allocator.c" /* ---------------------------------------------------------------------- */ @@ -299,9 +300,10 @@ test_my_alloc_allocates_single_matching_host_through_shared_iterators(void) * plugin->core (honestly typed, task 1.2) for every core-safe call, * alongside the still-needed plugin->sched (sched_make_allocs(), * scheduler.c, unchanged). */ - plugin.core = &(sched.core); + plugin.core = &(sched.core); + plugin.allocator_iface = &seqjob_alloc_iface; - rc = my_alloc(&plugin, &ctx); + rc = sched_allocator_run(&plugin, &ctx); TEST_ASSERT_EQUAL_INT(1, rc); TEST_ASSERT_NOT_NULL(ctx.alloc); @@ -423,10 +425,11 @@ static void test_my_alloc_reports_enomem_when_allocs_cannot_be_built(void) memset(&plugin, 0, sizeof(plugin)); plugin.sched = &sched; - plugin.core = &(sched.core); + plugin.core = &(sched.core); + plugin.allocator_iface = &seqjob_alloc_iface; g_make_allocs_fail = 1; - rc = my_alloc(&plugin, &ctx); + rc = sched_allocator_run(&plugin, &ctx); TEST_ASSERT_EQUAL_INT(-ENOMEM, rc); TEST_ASSERT_NULL(ctx.alloc); diff --git a/src/scheduler/testsuite/test_seqjob_rewrap_equivalence.c b/src/scheduler/testsuite/test_seqjob_rewrap_equivalence.c index 125ea905..c7030c17 100644 --- a/src/scheduler/testsuite/test_seqjob_rewrap_equivalence.c +++ b/src/scheduler/testsuite/test_seqjob_rewrap_equivalence.c @@ -91,6 +91,7 @@ #include "../src/resource.c" #include "../src/schedobj.c" #include "../src/host_iter.c" +#include "../src/alloc_runner.c" #include "../src/plugins/seqjob_allocator.c" /* ---------------------------------------------------------------------- */ @@ -324,9 +325,10 @@ static int drive_my_alloc(xce_scheduler_t* sched, xce_sched_obj_t* obj, * plugin->core (honestly typed, task 1.2) for every core-safe call, * alongside the still-needed plugin->sched (sched_make_allocs(), * scheduler.c, unchanged). */ - plugin.core = &(sched->core); + plugin.core = &(sched->core); + plugin.allocator_iface = &seqjob_alloc_iface; - return my_alloc(&plugin, ctx); + return sched_allocator_run(&plugin, ctx); } /* assert_uncommitted_host_debit - reads ctx->uncomitted.hosts[w->idx] for