myxce/src/scheduler/testsuite/test_alloc_runner.c
Rongsong Shen e3b87d28f3
fix(scheduler): place a task-range job with the largest count that fits (bug-1931)
The parjob plan generator split only maxTasks, so a job submitted with a
task range (minTasks < maxTasks) pended until all maxTasks tasks fit,
even when minTasks did (a tasks 2..5 job pended on a host with 2 free
slots).

The runner now handles a task range in sched_alloc_range_(): it probes
minTasks first and pends after that one search when it does not fit;
otherwise it places maxTasks when that fits, or binary-searches for the
largest count that fits and places it. Fitting is monotone in the count
for balanced plans, so the search is exact for today's plan shapes.
ctx->ntasks carries the count; parjob's job-level pass scales per-task
shared wants and global limits to it, and the generator gets the request
with its task count fixed to it. Fixed counts keep the old path.

The decision trace shows each count tried (task count N (minimum) /
(maximum), placing N tasks) and releases a fitting probe's hosts.
Test fixtures that used minTasks = 1 as filler for an N-task job are now
fixed-count.

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

561 lines
18 KiB
C

/*
* test_alloc_runner.c - scheduler-allocator-runner task 3.1: the framework
* allocation routine's guards and shared-resource precheck (Req 1.3, 1.4,
* 2.1-2.4, 5.1), driven only through sched_allocator_run() with an in-test
* fake allocator contract on a hand-built plugin.
*
* The fake contract counts every call: the acceptance guard returns a
* configurable verdict; plan_init returns a configurable result and
* plan_next yields no plan, so an attempt that gets past the shared
* precheck always ends pending. The job-level shared request is staged by
* the plugin_prepare_allocation() stub below from g_shared_want.
*/
#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/alloc_runner.c"
#include "../src/core.c"
#include "../src/host_iter.c"
#include "../src/resource.c"
/* Hooked sources compile SCHED_TRACE bodies (scheduler-sim-decision-trace). */
#include "../src/sched_trace.c"
#include "../src/sched_trace_scan.c"
#include "../src/sched_trace_capture.c"
#include "../src/sched_trace_outcome.c"
#include "../src/schedobj.c"
/* ---------------------------------------------------------------------- */
/* Link-satisfying stubs (same convention as test_alloc_equivalence_*.c) */
/* ---------------------------------------------------------------------- */
int plugin_register(void* sched, void* plugin)
{
return (void)sched, (void)plugin, 0;
}
void plugin_unregister(void* arg) { (void)arg; }
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;
}
/* Never reached by these tests (no plan is ever satisfied); present only
* because the white-box sources reference it. */
xce_alloc_t** sched_make_allocs(xce_scheduler_t* sched, char* reserve, int nres,
int* idxs, float* vals, int* duration)
{
(void)sched, (void)reserve, (void)nres, (void)idxs, (void)vals;
(void)duration;
errno = ENOSYS;
return NULL;
}
/* Shared request staged by the job-level prepare pass: resource index and
* amount per entry, g_shared_nwant entries. */
#define MAX_WANT 4
static int g_shared_nwant = 0;
static int g_shared_idx[MAX_WANT];
static float g_shared_val[MAX_WANT];
/* Resets the request vectors as plugin.c does, then stages g_shared_want
* on the job-level pass (ctx->worker NULL). */
int plugin_prepare_allocation(xce_sched_core_t* sched, xce_sched_ctx_t* ctx)
{
int i;
(void)sched;
xvec_idval_fini(&(ctx->alloc_req.host));
xvec_idval_init(&(ctx->alloc_req.host), 8);
if (ctx->worker != NULL) {
return 0;
}
xvec_idval_fini(&(ctx->alloc_req.shared));
xvec_idval_init(&(ctx->alloc_req.shared), 8);
for (i = 0; i < g_shared_nwant; i++) {
TEST_ASSERT_TRUE(xvec_idval_store(&(ctx->alloc_req.shared),
g_shared_idx[i], g_shared_val[i])
> 0);
}
return 0;
}
/* ---------------------------------------------------------------------- */
/* Fake allocator contract */
/* ---------------------------------------------------------------------- */
static int g_accepts_calls = 0;
static int g_accepts_result = 1;
static int g_init_calls = 0;
static int g_init_result = 0;
static int g_next_calls = 0;
static int g_fini_calls = 0;
static int fake_accepts(const xce_primary_resreq_t* primary)
{
(void)primary;
g_accepts_calls++;
return g_accepts_result;
}
static int fake_plan_init(xce_alloc_plan_iter_t* iter,
xce_primary_resreq_t* resreq, xce_sched_core_t* core)
{
(void)resreq, (void)core;
g_init_calls++;
iter->private = NULL;
return g_init_result;
}
static xce_alloc_plan_t* fake_plan_next(xce_alloc_plan_iter_t* iter)
{
(void)iter;
g_next_calls++;
return NULL;
}
static void fake_plan_fini(xce_alloc_plan_iter_t* iter)
{
(void)iter;
g_fini_calls++;
}
/* Mutable so a test can drop one member; reset by setUp() */
static xce_sched_allocator_iface_t g_iface;
/* make_pendreason() failure injection (-Wl,--wrap=make_pendreason in
* meson.build): when armed, the next call fails with ENOMEM and disarms. */
static int g_pendreason_fail = 0;
xce_pendreason_t* __real_make_pendreason(uint64_t jobid);
xce_pendreason_t* __wrap_make_pendreason(uint64_t jobid)
{
if (g_pendreason_fail) {
g_pendreason_fail = 0;
errno = ENOMEM;
return NULL;
}
return __real_make_pendreason(jobid);
}
/* Restores the full fake contract and clears its call counters */
static void fake_contract_reset(void)
{
g_accepts_calls = 0;
g_accepts_result = 1;
g_init_calls = 0;
g_init_result = 0;
g_next_calls = 0;
g_fini_calls = 0;
g_iface.accepts = fake_accepts;
g_iface.plan_init = fake_plan_init;
g_iface.plan_next = fake_plan_next;
g_iface.plan_fini = fake_plan_fini;
}
void setUp(void)
{
g_shared_nwant = 0;
g_pendreason_fail = 0;
fake_contract_reset();
}
void tearDown(void) { }
/* ---------------------------------------------------------------------- */
/* Fixture */
/* ---------------------------------------------------------------------- */
typedef struct {
xce_scheduler_t sched;
xce_resource_t* lic_a; /* shared, 10 in total */
xce_resource_t* lic_b; /* shared, 2 in total */
xce_sched_plugin_t plugin;
xce_primary_resreq_t req;
xce_sched_obj_t obj;
xce_primary_schedobj_t primary_obj;
xce_sched_ctx_t ctx;
} rt_fixture_t;
static xce_resource_t* rt_register_shared(var resmgr, const char* name)
{
xce_resource_t res;
memset(&res, 0, sizeof(res));
res.name = (char*)name;
res.flags = XCE_RESF_GLOBAL;
return makeResource(resmgr, &res);
}
/* Core with two shared pools, the hand-built plugin over the fake contract,
* and a ctx over one single-task primary with its job-level prepare pass
* run (the shared request staged from g_shared_want). */
static void rt_fixture_init(rt_fixture_t* f)
{
int i;
int idx[2];
float total[2] = { 10.0f, 2.0f };
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->lic_a = rt_register_shared(f->sched.core.resmgr, "lic_a");
f->lic_b = rt_register_shared(f->sched.core.resmgr, "lic_b");
TEST_ASSERT_NOT_NULL(f->lic_a);
TEST_ASSERT_NOT_NULL(f->lic_b);
idx[0] = f->lic_a->idx;
idx[1] = f->lic_b->idx;
put_resvals(&(f->sched.core.sharedInst), 2, idx, total);
f->plugin.name = (char*)"fake_allocator";
f->plugin.sched = &(f->sched);
f->plugin.core = &(f->sched.core);
f->plugin.allocator_iface = &g_iface;
f->req.minTasks = 1;
f->req.maxTasks = 1;
f->primary_obj.resreq = &(f->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)));
sched_ctx_set_primary(&(f->ctx), &(f->req));
}
static void rt_fixture_fini(rt_fixture_t* f)
{
free_JobAlloc_ptr(&(f->ctx.alloc));
free_PendReason_ptr(&(f->ctx.reason));
sched_ctx_fini(&(f->ctx));
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();
}
/* Stages a shared request of `a` lic_a and `b` lic_b (0 = not requested)
* and reruns the job-level prepare pass. */
static void rt_want_shared(rt_fixture_t* f, float a, float b)
{
g_shared_nwant = 0;
if (a > 0.0f) {
g_shared_idx[g_shared_nwant] = f->lic_a->idx;
g_shared_val[g_shared_nwant++] = a;
}
if (b > 0.0f) {
g_shared_idx[g_shared_nwant] = f->lic_b->idx;
g_shared_val[g_shared_nwant++] = b;
}
sched_ctx_set_primary(&(f->ctx), &(f->req));
}
static int rt_run(rt_fixture_t* f)
{
return sched_allocator_run(&(f->plugin), &(f->ctx));
}
static float rt_shared_uncomit(rt_fixture_t* f, xce_resource_t* r)
{
return ((float*)f->ctx.uncomitted.shared->data)[r->idx];
}
static void rt_assert_no_shared_uncomit(rt_fixture_t* f)
{
TEST_ASSERT_EQUAL_FLOAT(0.0f, rt_shared_uncomit(f, f->lic_a));
TEST_ASSERT_EQUAL_FLOAT(0.0f, rt_shared_uncomit(f, f->lic_b));
}
static void rt_assert_calls(int accepts, int init, int fini)
{
TEST_ASSERT_EQUAL_INT(accepts, g_accepts_calls);
TEST_ASSERT_EQUAL_INT(init, g_init_calls);
TEST_ASSERT_EQUAL_INT(fini, g_fini_calls);
}
/* ---------------------------------------------------------------------- */
/* Guards (Req 5.1, 1.3, 1.4, 2.1) */
/* ---------------------------------------------------------------------- */
/* Missing plugin or ctx: -EINVAL, the ctx untouched, no contract call. */
static void test_missing_plugin_or_ctx_is_einval(void)
{
rt_fixture_t f;
rt_fixture_init(&f);
TEST_ASSERT_EQUAL_INT(-EINVAL, sched_allocator_run(NULL, &(f.ctx)));
TEST_ASSERT_EQUAL_INT(-EINVAL, sched_allocator_run(&(f.plugin), NULL));
TEST_ASSERT_NULL(f.ctx.reason);
rt_assert_calls(0, 0, 0);
rt_fixture_fini(&f);
}
/* Each missing field (core, sched, primary, iface) in turn: -EINVAL,
* ctx->reason still NULL, no contract call. */
static void test_missing_field_is_einval_without_reason(void)
{
rt_fixture_t f;
int i;
rt_fixture_init(&f);
rt_want_shared(&f, 3.0f, 0.0f);
for (i = 0; i < 4; i++) {
xce_sched_core_t* core = f.plugin.core;
void* sched = f.plugin.sched;
xce_primary_resreq_t* primary = f.ctx.primary;
const xce_sched_allocator_iface_t* iface = f.plugin.allocator_iface;
f.plugin.core = (i == 0) ? NULL : core;
f.plugin.sched = (i == 1) ? NULL : sched;
f.ctx.primary = (i == 2) ? NULL : primary;
f.plugin.allocator_iface = (i == 3) ? NULL : iface;
TEST_ASSERT_EQUAL_INT_MESSAGE(-EINVAL, rt_run(&f), "missing field");
TEST_ASSERT_NULL(f.ctx.reason);
f.plugin.core = core;
f.plugin.sched = sched;
f.ctx.primary = primary;
f.plugin.allocator_iface = iface;
}
rt_assert_calls(0, 0, 0);
rt_assert_no_shared_uncomit(&f);
rt_fixture_fini(&f);
}
/* Each missing plan_* member in turn: -EINVAL, no reason, no call. */
static void test_missing_plan_member_is_einval_without_reason(void)
{
rt_fixture_t f;
int i;
rt_fixture_init(&f);
for (i = 0; i < 3; i++) {
fake_contract_reset();
g_iface.plan_init = (i == 0) ? NULL : fake_plan_init;
g_iface.plan_next = (i == 1) ? NULL : fake_plan_next;
g_iface.plan_fini = (i == 2) ? NULL : fake_plan_fini;
TEST_ASSERT_EQUAL_INT_MESSAGE(-EINVAL, rt_run(&f), "missing plan_*");
TEST_ASSERT_NULL(f.ctx.reason);
rt_assert_calls(0, 0, 0);
}
rt_fixture_fini(&f);
}
/* A rejecting guard: pending, no reason record, no shared check or count,
* no plan generator (Req 1.4). */
static void test_rejecting_accepts_pends_without_checks(void)
{
rt_fixture_t f;
rt_fixture_init(&f);
rt_want_shared(&f, 3.0f, 1.0f);
g_accepts_result = 0;
TEST_ASSERT_EQUAL_INT(0, rt_run(&f));
TEST_ASSERT_NULL(f.ctx.reason);
TEST_ASSERT_NULL(f.ctx.alloc);
rt_assert_calls(1, 0, 0);
rt_assert_no_shared_uncomit(&f);
rt_fixture_fini(&f);
}
/* No guard accepts every request and reaches plan_init (Req 1.3). */
static void test_null_accepts_reaches_plan_init(void)
{
rt_fixture_t f;
rt_fixture_init(&f);
g_iface.accepts = NULL;
TEST_ASSERT_EQUAL_INT(0, rt_run(&f));
TEST_ASSERT_NOT_NULL(f.ctx.reason);
rt_assert_calls(0, 1, 1);
rt_fixture_fini(&f);
}
/* An accepting guard: the reason record is created after it (Req 2.1). */
static void test_reason_created_after_accepting_guard(void)
{
rt_fixture_t f;
rt_fixture_init(&f);
TEST_ASSERT_EQUAL_INT(0, rt_run(&f));
TEST_ASSERT_NOT_NULL(f.ctx.reason);
TEST_ASSERT_NOT_NULL(f.ctx.reason->reasons);
TEST_ASSERT_EQUAL_INT(0, f.ctx.reason->reasons->main_reason);
TEST_ASSERT_EQUAL_INT(0, f.ctx.reason->reasons->nres_reasons);
rt_assert_calls(1, 1, 1);
rt_fixture_fini(&f);
}
/* An existing reason record (e.g. from an earlier primary of the same
* COMPOUND job) is reused, not replaced. */
static void test_existing_reason_is_reused(void)
{
rt_fixture_t f;
xce_pendreason_t* before;
rt_fixture_init(&f);
before = make_pendreason(0);
TEST_ASSERT_NOT_NULL(before);
f.ctx.reason = before;
TEST_ASSERT_EQUAL_INT(0, rt_run(&f));
TEST_ASSERT_EQUAL_PTR(before, f.ctx.reason);
rt_fixture_fini(&f);
}
/* The reason record cannot be created: -ENOMEM with ctx->reason still
* NULL, no shared count and no plan generator (Req 2.4). */
static void test_reason_oom_returns_enomem_without_checks(void)
{
rt_fixture_t f;
rt_fixture_init(&f);
rt_want_shared(&f, 3.0f, 1.0f);
g_pendreason_fail = 1;
TEST_ASSERT_EQUAL_INT(-ENOMEM, rt_run(&f));
TEST_ASSERT_NULL(f.ctx.reason);
TEST_ASSERT_NULL(f.ctx.alloc);
rt_assert_calls(1, 0, 0);
rt_assert_no_shared_uncomit(&f);
rt_fixture_fini(&f);
}
/* ---------------------------------------------------------------------- */
/* Shared precheck (Req 2.2, 2.3, 3.4) */
/* ---------------------------------------------------------------------- */
/* bug-1893: two shared resources requested, only lic_b short (3 of 2):
* exactly one host-less NO_RESOURCE reason naming lic_b, nothing counted,
* no plan generator. */
static void test_shared_short_one_reason_per_unmet_entry(void)
{
rt_fixture_t f;
xce_res_reason_t* rr;
rt_fixture_init(&f);
rt_want_shared(&f, 3.0f, 3.0f);
TEST_ASSERT_EQUAL_INT(0, rt_run(&f));
TEST_ASSERT_NULL(f.ctx.alloc);
TEST_ASSERT_NOT_NULL(f.ctx.reason);
TEST_ASSERT_EQUAL_INT(0, f.ctx.reason->reasons->main_reason);
TEST_ASSERT_EQUAL_INT(0, f.ctx.reason->reasons->nhost_reasons);
TEST_ASSERT_EQUAL_INT(1, f.ctx.reason->reasons->nres_reasons);
rr = f.ctx.reason->reasons->res_based_reasons[0];
TEST_ASSERT_NOT_NULL(rr);
TEST_ASSERT_EQUAL_STRING("", rr->host);
TEST_ASSERT_EQUAL_STRING("lic_b", rr->resource);
TEST_ASSERT_EQUAL_INT(PREASON_NO_RESOURCE, rr->reason);
rt_assert_calls(1, 0, 0);
rt_assert_no_shared_uncomit(&f);
rt_fixture_fini(&f);
}
/* Shared met, plan_init fails: pending, plan_fini not called, and the
* requested amounts stay counted at their resource indices. */
static void test_shared_met_counted_and_kept_when_plan_init_fails(void)
{
rt_fixture_t f;
rt_fixture_init(&f);
rt_want_shared(&f, 3.0f, 1.0f);
g_init_result = -ENOMEM;
TEST_ASSERT_EQUAL_INT(0, rt_run(&f));
TEST_ASSERT_NULL(f.ctx.alloc);
TEST_ASSERT_EQUAL_INT(0, f.ctx.reason->reasons->nres_reasons);
rt_assert_calls(1, 1, 0);
TEST_ASSERT_EQUAL_INT(0, g_next_calls);
TEST_ASSERT_EQUAL_FLOAT(3.0f, rt_shared_uncomit(&f, f.lic_a));
TEST_ASSERT_EQUAL_FLOAT(1.0f, rt_shared_uncomit(&f, f.lic_b));
rt_fixture_fini(&f);
}
/* Shared met, plan_init succeeds with no plan: pending, plan_fini called
* exactly once, and the requested amounts stay counted. */
static void test_shared_met_counted_and_kept_when_no_plan(void)
{
rt_fixture_t f;
rt_fixture_init(&f);
rt_want_shared(&f, 3.0f, 1.0f);
TEST_ASSERT_EQUAL_INT(0, rt_run(&f));
TEST_ASSERT_NULL(f.ctx.alloc);
rt_assert_calls(1, 1, 1);
TEST_ASSERT_EQUAL_FLOAT(3.0f, rt_shared_uncomit(&f, f.lic_a));
TEST_ASSERT_EQUAL_FLOAT(1.0f, rt_shared_uncomit(&f, f.lic_b));
rt_fixture_fini(&f);
}
/* The helpers directly: a shared request of nothing is met and counts
* nothing; the reason helper keeps an existing record. */
static void test_helpers_empty_shared_is_met(void)
{
rt_fixture_t f;
rt_fixture_init(&f);
TEST_ASSERT_EQUAL_INT(0, sched_alloc_ensure_reason_(&(f.ctx)));
TEST_ASSERT_NOT_NULL(f.ctx.reason);
TEST_ASSERT_EQUAL_INT(
1, sched_alloc_prepare_shared_(&(f.sched.core), &(f.ctx), 1));
rt_assert_no_shared_uncomit(&f);
rt_fixture_fini(&f);
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_missing_plugin_or_ctx_is_einval);
RUN_TEST(test_missing_field_is_einval_without_reason);
RUN_TEST(test_missing_plan_member_is_einval_without_reason);
RUN_TEST(test_rejecting_accepts_pends_without_checks);
RUN_TEST(test_null_accepts_reaches_plan_init);
RUN_TEST(test_reason_created_after_accepting_guard);
RUN_TEST(test_existing_reason_is_reused);
RUN_TEST(test_reason_oom_returns_enomem_without_checks);
RUN_TEST(test_shared_short_one_reason_per_unmet_entry);
RUN_TEST(test_shared_met_counted_and_kept_when_plan_init_fails);
RUN_TEST(test_shared_met_counted_and_kept_when_no_plan);
RUN_TEST(test_helpers_empty_shared_is_met);
return UNITY_END();
}