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) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-09-30 09:53:14 +00:00
parent 239bcf95cd
commit 65679285ba
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
7 changed files with 674 additions and 244 deletions

View file

@ -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.

View file

@ -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
};

View file

@ -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.

View file

@ -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 <errno.h>
#include <stdlib.h>
@ -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);

View file

@ -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 <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unity/unity.h>
#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();
}

View file

@ -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);

View file

@ -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