xce_alloc_plan_iter_t.init takes xce_sched_core_t* instead of xce_scheduler_t*; both built-in generators, both allocators' call sites and the contract-test stub follow. The parjob purity test now shows an identical plan sequence for a NULL and a poisoned core. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1243 lines
48 KiB
C
1243 lines
48 KiB
C
/*
|
|
* test_parjob_combinatorial.c - tasks 4.1 + 4.2 (scheduler-allocation
|
|
* spec): unit tests for parjob_allocator.c's REAL combinatorial
|
|
* xce_alloc_plan_iter_t implementation (Req 2 AC 1-6, Req 4 AC 1) AND
|
|
* (task 4.2, this pass) the real per-bucket allocation loop
|
|
* (my_alloc()/parjob_try_plan_()/parjob_prepare_allocation(), Req 4 AC
|
|
* 2/4/5/6/7).
|
|
*
|
|
* Task 4.1's own tests below never call my_alloc() -- they drive
|
|
* parjob_plan_iter_init()/_next()/_fini() (static, hence the white-box
|
|
* #include) directly, and needed no fixture beyond a bare
|
|
* xce_primary_resreq_t, since the pre-4.2 my_alloc()/parjob_prepare_
|
|
* allocation() stub bodies made no calls into resource.c/schedobj.c/
|
|
* core.c/host_iter.c at all. Left completely unmodified below.
|
|
*
|
|
* Task 4.2 replaces that stub with a real implementation that DOES call
|
|
* into those files (sched_try_alloc_on_host_(), sched_ctx_uncommit_host(),
|
|
* sched_host_iter_init/next/fini(), sched_ctx_get_uncomit_shared(),
|
|
* sched_check_shared_satisfy(), plugin_prepare_allocation()'s fan-out),
|
|
* so this pass adds the SAME white-box #include combination
|
|
* test_seqjob_allocator.c (task 3.1)/test_seqjob_rewrap_equivalence.c
|
|
* (task 3.2) already established (core.c + resource.c + schedobj.c +
|
|
* host_iter.c), plus the matching link-satisfying stubs (a
|
|
* plugin_prepare_allocation() that delegates to the real
|
|
* parjob_prepare_allocation() under test, and a sched_make_allocs()
|
|
* stand-in that avoids white-box-including the rest of the 1382-line
|
|
* scheduler.c) -- and a real fixture (registered "slots" resource, real
|
|
* sched_add_worker() hosts) for the new bucket-loop tests.
|
|
*
|
|
* Covers (task 4.1, unchanged):
|
|
* - one-plan-per-call generation (Req 2 AC 2)
|
|
* - most-preferred-first ordering: ascending host-count k = 1..N
|
|
* (Req 2 AC 3, Req 4 AC 1)
|
|
* - the canonical balanced partition rule, including an uneven split
|
|
* (N=7 across k=3 hosts -> [3,2,2]) (Req 4 AC 1)
|
|
* - descending bucket order within one plan, by construction (Req 2
|
|
* AC 2's "not a uniform pair" + the Composition Algorithm's
|
|
* descending-order correctness property)
|
|
* - a stable end-of-iteration signal once k > N, stable across
|
|
* repeated post-exhaustion calls (Req 2 AC 4)
|
|
* - purity: plan shape/order is a pure function of maxTasks alone,
|
|
* never inspecting live worker/scheduler state (Req 4 AC 1's "never
|
|
* inspects live worker state" clause) -- proved by passing a NULL
|
|
* xce_sched_core_t* to init() and confirming plans still generate
|
|
* correctly
|
|
* - fini() releases the iterator's private state (Req 2 AC 5)
|
|
* - Req 2 AC 6: next() calls pthread_testcancel() (or an equivalent
|
|
* real cancellation point) -- proved behaviorally, same technique
|
|
* test_host_iter.c already established for host_iter_next().
|
|
*
|
|
* Covers (task 4.2, new):
|
|
* - multi-bucket success through the real my_alloc() end to end (Req
|
|
* 4 AC 2/AC 3), including per-bucket resource want scaling with that
|
|
* bucket's own slot count, not a fixed value (Req 4 AC 5/AC 6)
|
|
* - the descending-vs-ascending worked example from requirements.md's
|
|
* Composition Algorithm, driven directly through parjob_try_plan_()
|
|
* with a hand-built plan for each order against the identical host
|
|
* pool (Req 4 AC 2)
|
|
* - a mid-plan bucket failure correctly reversing the earlier
|
|
* successful bucket's provisional debit before the next candidate
|
|
* plan is tried, proved by that same host being successfully
|
|
* reclaimed by the later, winning plan (Req 4 AC 2, "Rollback scope")
|
|
* - Req 4 AC 4: the same not-satisfied signal (0) once every plan is
|
|
* exhausted
|
|
*/
|
|
#include <errno.h>
|
|
#include <pthread.h>
|
|
#include <rtti.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
#include <unity/unity.h>
|
|
|
|
#include "resobj.h"
|
|
#include "resreq.h"
|
|
#include "sched/alloc_iter.h"
|
|
#include "scheduler.h"
|
|
#include "xcealloc.h"
|
|
|
|
/* Pull in the real xce_sched_core_init()/sched_add_worker() (core.c),
|
|
* sched_meet_host_selection()/sched_check_host_satisfy()/
|
|
* sched_try_alloc_on_host_()/sched_ctx_uncommit_host()/put_resvals()
|
|
* (resource.c), the real sched_ctx_init()/sched_ctx_get_uncomit_*()
|
|
* (schedobj.c), the shared host_iter.c implementation, and
|
|
* parjob_allocator.c itself (the file under test) into this TU -- same
|
|
* combination test_seqjob_allocator.c (task 3.1) established for its own
|
|
* sibling plugin. The plan-iterator and (new, task 4.2) bucket-loop
|
|
* functions are all `static`, so this white-box #include is the only way
|
|
* to reach them directly.
|
|
*/
|
|
#include "../src/core.c"
|
|
#include "../src/host_iter.c"
|
|
#include "../src/plugins/parjob_allocator.c"
|
|
#include "../src/resource.c"
|
|
#include "../src/schedobj.c"
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Link-satisfying stubs */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
/* plugin_register()/plugin_unregister() are referenced by
|
|
* register_parjob_allocator() (parjob_allocator.c) and core.c
|
|
* respectively, but no test below calls register_parjob_allocator() --
|
|
* the plan iterator and my_alloc()/parjob_try_plan_() are exercised
|
|
* directly. Never actually invoked by anything this file's tests
|
|
* exercise; provided only so the TU links. */
|
|
int plugin_register(void* sched, void* plugin)
|
|
{
|
|
(void)sched;
|
|
(void)plugin;
|
|
return 0;
|
|
}
|
|
|
|
void plugin_unregister(void* arg) { (void)arg; }
|
|
|
|
/* g_job_level_prepare_calls - task 4.3 (Req 4 AC 7): counts how many times
|
|
* the fan-out stub immediately below is invoked with ctx->worker == NULL --
|
|
* the job-level shared/global-resource prepare pass. requirements.md's Req
|
|
* 4 AC 7 requires this pass to fire exactly once per primary resreq,
|
|
* independent of which plan/bucket-shape is later tried -- this counter
|
|
* lets test_shared_resource_debited_once_regardless_of_host_count() below
|
|
* confirm that dispatch-count claim dynamically (not just by inspection)
|
|
* across a real multi-bucket success. Each test that reads it resets it
|
|
* first, so counts never leak across tests. */
|
|
static int g_job_level_prepare_calls = 0;
|
|
|
|
/* Delegates to the real parjob_prepare_allocation() under test, standing
|
|
* in for plugin_prepare_allocation()'s (plugin.c) real fan-out across
|
|
* every registered plugin -- exactly one (parjob's own) in this file's
|
|
* fixtures. Mirrors plugin.c:591's own reset-before-fan-out step, same
|
|
* rationale as test_seqjob_allocator.c's identical stub. */
|
|
int plugin_prepare_allocation(xce_sched_core_t* sched, xce_sched_ctx_t* ctx)
|
|
{
|
|
xce_sched_plugin_t fake_plugin;
|
|
|
|
if (ctx->worker == NULL) {
|
|
g_job_level_prepare_calls++;
|
|
xvec_idval_fini(&(ctx->alloc_req.shared));
|
|
xvec_idval_init(&(ctx->alloc_req.shared), 8);
|
|
} else {
|
|
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;
|
|
/* scheduler-sim-live-allocation spec, task 4.3: parjob_prepare_
|
|
* allocation() now reads plugin->core (honestly typed, task 1.2)
|
|
* instead of plugin->sched -- `sched` here is already an
|
|
* xce_sched_core_t* (this stub's own signature), so no cast is
|
|
* needed. */
|
|
fake_plugin.core = sched;
|
|
|
|
return parjob_prepare_allocation(&fake_plugin, ctx);
|
|
}
|
|
|
|
int plugin_job_update(void* s, xce_sched_job_t* j)
|
|
{
|
|
(void)s;
|
|
(void)j;
|
|
return 0;
|
|
}
|
|
int plugin_queue_update(void* s, xce_sched_queue_t* q)
|
|
{
|
|
(void)s;
|
|
(void)q;
|
|
return 0;
|
|
}
|
|
int plugin_job_rusage_update(void* s, xce_sched_job_t* j, xce_job_rusages_t* r)
|
|
{
|
|
(void)s;
|
|
(void)j;
|
|
(void)r;
|
|
return 0;
|
|
}
|
|
|
|
/* Avoids white-box-including the rest of scheduler.c just to build the
|
|
* leaf xce_alloc_t records my_alloc()'s success path constructs -- same
|
|
* rationale/shape as test_seqjob_allocator.c's identical stub. This
|
|
* file's own quantitative per-host assertions go through
|
|
* assert_uncommitted_host_debit() (real ctx->uncomitted state, untouched
|
|
* by this stub) rather than these fake xce_alloc_t's fields, exactly like
|
|
* the seqjob equivalence tests already do. */
|
|
/* bug-1882: arm to make the stub's Nth call (0-based) fail like the real
|
|
* sched_make_allocs() does when make_alloc() cannot build a record. */
|
|
static int g_make_allocs_fail_at = -1; /* -1 == disabled */
|
|
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;
|
|
|
|
(void)sched;
|
|
(void)reserve;
|
|
(void)idxs;
|
|
(void)vals;
|
|
(void)duration;
|
|
|
|
if ((g_make_allocs_fail_at >= 0)
|
|
&& (g_make_allocs_calls++ == g_make_allocs_fail_at)) {
|
|
errno = ENOMEM;
|
|
return NULL;
|
|
}
|
|
|
|
if (nres <= 0) {
|
|
return NULL;
|
|
}
|
|
|
|
allocs = (xce_alloc_t**)calloc(nres, sizeof(*allocs));
|
|
if (allocs == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
for (i = 0; i < nres; i++) {
|
|
allocs[i] = (xce_alloc_t*)calloc(1, sizeof(xce_alloc_t));
|
|
}
|
|
|
|
return allocs;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Fixture helpers */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
/* register_slots_resource - the real makeResource() (resobj.c), giving
|
|
* "slots" a resmgr-assigned idx -- identical to test_seqjob_allocator.c's
|
|
* own helper; parjob_prepare_allocation()'s key_find(resmgr, "slots",...)
|
|
* call needs this to be a genuinely registered resource. */
|
|
static xce_resource_t* register_slots_resource(var resmgr)
|
|
{
|
|
xce_resource_t res;
|
|
|
|
memset(&res, 0, sizeof(res));
|
|
res.name = strdup("slots");
|
|
|
|
return makeResource(resmgr, &res);
|
|
}
|
|
|
|
/* register_global_resource - task 4.3 (Req 4 AC 7): like
|
|
* register_slots_resource() above, but flagged XCE_RESF_GLOBAL (xce/
|
|
* types.h) -- the scheduler's job-level/shared-resource pool
|
|
* (sched->core.sharedInst/sharedInUse), distinct from a per-host resource
|
|
* like "slots". resreq.c's calc_wanted_global_resvals()/
|
|
* calc_wanted_host_resvals() dispatch on exactly this flag: a GLOBAL
|
|
* resource is only ever computed by the ctx->worker == NULL job-level
|
|
* prepare pass, never by the per-host pass. makeResource()'s own
|
|
* makeInstance()->resobj_init() memcpy()s the whole xce_resource_t
|
|
* (including .flags) into the resmgr-owned copy, so setting it here is
|
|
* sufficient. */
|
|
static xce_resource_t* register_global_resource(var resmgr, const char* name)
|
|
{
|
|
xce_resource_t res;
|
|
|
|
memset(&res, 0, sizeof(res));
|
|
res.name = strdup(name);
|
|
res.flags = XCE_RESF_GLOBAL;
|
|
|
|
return makeResource(resmgr, &res);
|
|
}
|
|
|
|
/* add_worker_with_slots - a real sched_add_worker() (core.c) worker
|
|
* carrying `slots_total` free "slots" (idx == the resmgr-assigned index),
|
|
* identical to test_seqjob_allocator.c's own helper. */
|
|
static xce_sched_worker_t* add_worker_with_slots(xce_sched_core_t* core,
|
|
const char* name,
|
|
int slots_idx,
|
|
float slots_total)
|
|
{
|
|
xce_sched_worker_t* w;
|
|
int idx[1];
|
|
float val[1];
|
|
int rc;
|
|
|
|
w = (xce_sched_worker_t*)calloc(1, sizeof(*w));
|
|
TEST_ASSERT_NOT_NULL(w);
|
|
|
|
w->name = strdup(name);
|
|
TEST_ASSERT_NOT_NULL(w->name);
|
|
|
|
rc = zarray_init(&(w->resvals), 8, NULL);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
rc = zarray_init(&(w->resInUse), 8, NULL);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
idx[0] = slots_idx;
|
|
val[0] = slots_total;
|
|
put_resvals(&(w->resvals), 1, idx, val);
|
|
val[0] = 0.0f;
|
|
put_resvals(&(w->resInUse), 1, idx, val);
|
|
|
|
rc = sched_add_worker(core, w);
|
|
TEST_ASSERT_TRUE(rc >= 0);
|
|
|
|
return w;
|
|
}
|
|
|
|
static void init_primary_obj(xce_sched_obj_t* obj,
|
|
xce_primary_schedobj_t* primary_obj,
|
|
xce_primary_resreq_t* primary)
|
|
{
|
|
memset(primary_obj, 0, sizeof(*primary_obj));
|
|
primary_obj->resreq = primary;
|
|
|
|
memset(obj, 0, sizeof(*obj));
|
|
obj->tag = XCE_RESREQ_PRIMARY;
|
|
obj->u.primary = primary_obj;
|
|
}
|
|
|
|
/* drive_my_alloc - the exact production sequence a job's allocation
|
|
* attempt runs: sched_ctx_init() -> sched_ctx_set_primary() (the real
|
|
* call that stages ctx->alloc_req.shared via plugin_prepare_allocation()'s
|
|
* ctx->worker==NULL branch, exactly as sched_make_primary_allocation()
|
|
* does today) -> my_alloc(). Returns my_alloc()'s return code; ctx is left
|
|
* initialized (caller must sched_ctx_fini()). Identical to
|
|
* test_seqjob_rewrap_equivalence.c's own helper. */
|
|
static int drive_my_alloc(xce_scheduler_t* sched, xce_sched_obj_t* obj,
|
|
xce_primary_resreq_t* primary, xce_sched_ctx_t* ctx)
|
|
{
|
|
xce_sched_plugin_t plugin;
|
|
|
|
/* scheduler-sim-live-allocation spec, task 2.1: ctx->sched is now
|
|
* honestly typed xce_sched_core_t* -- pass &(sched->core), not sched. */
|
|
TEST_ASSERT_EQUAL_INT(0, sched_ctx_init(ctx, &(sched->core), obj));
|
|
sched_ctx_set_primary(ctx, primary);
|
|
|
|
memset(&plugin, 0, sizeof(plugin));
|
|
plugin.sched = sched;
|
|
/* scheduler-sim-live-allocation spec, task 4.3: my_alloc() now reads
|
|
* 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);
|
|
|
|
return my_alloc(&plugin, ctx);
|
|
}
|
|
|
|
/* assert_uncommitted_host_debit - reads ctx->uncomitted.hosts[w->idx] for
|
|
* resource residx and asserts it equals `expected`. Identical to
|
|
* test_seqjob_rewrap_equivalence.c's own helper. */
|
|
static void assert_uncommitted_host_debit(xce_sched_ctx_t* ctx,
|
|
xce_sched_worker_t* w, int residx,
|
|
float expected)
|
|
{
|
|
xvec_t* uncomit = NULL;
|
|
int rc;
|
|
|
|
rc = sched_ctx_get_uncomit_host(&uncomit, ctx, w->idx);
|
|
TEST_ASSERT_EQUAL_INT(1, rc);
|
|
TEST_ASSERT_NOT_NULL(uncomit);
|
|
TEST_ASSERT_EQUAL_FLOAT(expected, ((float*)uncomit->data)[residx]);
|
|
}
|
|
|
|
static void bind_plan_iter(xce_alloc_plan_iter_t* iter)
|
|
{
|
|
memset(iter, 0, sizeof(*iter));
|
|
iter->init = parjob_plan_iter_init;
|
|
iter->next = parjob_plan_iter_next;
|
|
iter->fini = parjob_plan_iter_fini;
|
|
}
|
|
|
|
static void assert_plan_eq(xce_alloc_plan_t* plan, const int* expect,
|
|
int nexpect)
|
|
{
|
|
int i;
|
|
|
|
TEST_ASSERT_NOT_NULL(plan);
|
|
TEST_ASSERT_EQUAL_INT(nexpect, plan->nbuckets);
|
|
for (i = 0; i < nexpect; i++) {
|
|
TEST_ASSERT_EQUAL_INT(expect[i], plan->bucket_slots[i]);
|
|
}
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Tests */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
/*
|
|
* Req 2 AC 2/AC 3, Req 4 AC 1: for maxTasks = 7, successive next() calls
|
|
* return exactly one plan per call, in ascending host-count order
|
|
* (k = 1..7), each plan the canonical balanced ceil/floor partition with
|
|
* buckets in descending order. k=3 is the uneven-split worked example
|
|
* from requirements.md: [3,2,2], not [2,2,3]/[2,3,2]/[5,1,1].
|
|
*/
|
|
static void
|
|
test_plans_ordered_ascending_host_count_with_balanced_partitions(void)
|
|
{
|
|
xce_primary_resreq_t resreq;
|
|
xce_alloc_plan_iter_t iter_storage;
|
|
xce_alloc_plan_iter_t* iter = &iter_storage;
|
|
xce_alloc_plan_t* plan;
|
|
|
|
/* Expected balanced partition for maxTasks=7, per host-count k=1..7,
|
|
* descending bucket order within each -- worked by hand against
|
|
* requirements.md's "(N mod k) buckets get ceil(N/k), the rest get
|
|
* floor(N/k)" rule. */
|
|
static const int expect_k1[] = { 7 };
|
|
static const int expect_k2[] = { 4, 3 };
|
|
static const int expect_k3[] = { 3, 2, 2 }; /* the uneven-split example */
|
|
static const int expect_k4[] = { 2, 2, 2, 1 };
|
|
static const int expect_k5[] = { 2, 2, 1, 1, 1 };
|
|
static const int expect_k6[] = { 2, 1, 1, 1, 1, 1 };
|
|
static const int expect_k7[] = { 1, 1, 1, 1, 1, 1, 1 };
|
|
|
|
memset(&resreq, 0, sizeof(resreq));
|
|
resreq.minTasks = 1;
|
|
resreq.maxTasks = 7;
|
|
|
|
bind_plan_iter(iter);
|
|
TEST_ASSERT_EQUAL_INT(0, iter->init(iter, &resreq, NULL));
|
|
|
|
plan = iter->next(iter);
|
|
assert_plan_eq(plan, expect_k1, 1);
|
|
|
|
plan = iter->next(iter);
|
|
assert_plan_eq(plan, expect_k2, 2);
|
|
|
|
plan = iter->next(iter);
|
|
assert_plan_eq(plan, expect_k3, 3);
|
|
|
|
plan = iter->next(iter);
|
|
assert_plan_eq(plan, expect_k4, 4);
|
|
|
|
plan = iter->next(iter);
|
|
assert_plan_eq(plan, expect_k5, 5);
|
|
|
|
plan = iter->next(iter);
|
|
assert_plan_eq(plan, expect_k6, 6);
|
|
|
|
plan = iter->next(iter);
|
|
assert_plan_eq(plan, expect_k7, 7);
|
|
|
|
/* Req 2 AC 4: exhausted once k would exceed maxTasks (here k=8 > 7). */
|
|
TEST_ASSERT_NULL(iter->next(iter));
|
|
/* Stable across repeated post-exhaustion calls. */
|
|
TEST_ASSERT_NULL(iter->next(iter));
|
|
TEST_ASSERT_NULL(iter->next(iter));
|
|
|
|
iter->fini(iter);
|
|
TEST_ASSERT_NULL(iter->private);
|
|
}
|
|
|
|
/*
|
|
* A clean divisor case (maxTasks = 6): every partition splits evenly, so
|
|
* every bucket in every plan carries the same slot count -- descending
|
|
* order is trivially satisfied (all equal), and this exercises rem == 0
|
|
* for every k that evenly divides 6 (k=1,2,3,6) as well as k=4,5 where
|
|
* rem != 0.
|
|
*/
|
|
static void test_plans_for_evenly_divisible_task_count(void)
|
|
{
|
|
xce_primary_resreq_t resreq;
|
|
xce_alloc_plan_iter_t iter_storage;
|
|
xce_alloc_plan_iter_t* iter = &iter_storage;
|
|
xce_alloc_plan_t* plan;
|
|
|
|
static const int expect_k1[] = { 6 };
|
|
static const int expect_k2[] = { 3, 3 };
|
|
static const int expect_k3[] = { 2, 2, 2 };
|
|
static const int expect_k4[] = { 2, 2, 1, 1 };
|
|
static const int expect_k5[] = { 2, 1, 1, 1, 1 };
|
|
static const int expect_k6[] = { 1, 1, 1, 1, 1, 1 };
|
|
|
|
memset(&resreq, 0, sizeof(resreq));
|
|
resreq.minTasks = 1;
|
|
resreq.maxTasks = 6;
|
|
|
|
bind_plan_iter(iter);
|
|
TEST_ASSERT_EQUAL_INT(0, iter->init(iter, &resreq, NULL));
|
|
|
|
plan = iter->next(iter);
|
|
assert_plan_eq(plan, expect_k1, 1);
|
|
plan = iter->next(iter);
|
|
assert_plan_eq(plan, expect_k2, 2);
|
|
plan = iter->next(iter);
|
|
assert_plan_eq(plan, expect_k3, 3);
|
|
plan = iter->next(iter);
|
|
assert_plan_eq(plan, expect_k4, 4);
|
|
plan = iter->next(iter);
|
|
assert_plan_eq(plan, expect_k5, 5);
|
|
plan = iter->next(iter);
|
|
assert_plan_eq(plan, expect_k6, 6);
|
|
|
|
TEST_ASSERT_NULL(iter->next(iter));
|
|
TEST_ASSERT_NULL(iter->next(iter));
|
|
|
|
iter->fini(iter);
|
|
}
|
|
|
|
/*
|
|
* maxTasks = 1: exactly one plan (a single host, single slot), then
|
|
* stable exhaustion. Degenerate but required boundary case -- parjob is
|
|
* only ever reached for maxTasks > 1 in production (sched_make_primary_
|
|
* alloc_()'s dispatch), but the iterator itself makes no such assumption.
|
|
*/
|
|
static void test_single_task_yields_single_plan(void)
|
|
{
|
|
xce_primary_resreq_t resreq;
|
|
xce_alloc_plan_iter_t iter_storage;
|
|
xce_alloc_plan_iter_t* iter = &iter_storage;
|
|
xce_alloc_plan_t* plan;
|
|
static const int expect_k1[] = { 1 };
|
|
|
|
memset(&resreq, 0, sizeof(resreq));
|
|
resreq.minTasks = 1;
|
|
resreq.maxTasks = 1;
|
|
|
|
bind_plan_iter(iter);
|
|
TEST_ASSERT_EQUAL_INT(0, iter->init(iter, &resreq, NULL));
|
|
|
|
plan = iter->next(iter);
|
|
assert_plan_eq(plan, expect_k1, 1);
|
|
|
|
TEST_ASSERT_NULL(iter->next(iter));
|
|
|
|
iter->fini(iter);
|
|
}
|
|
|
|
/*
|
|
* Req 4 AC 1 ("never inspects live worker state" / "pure function of the
|
|
* task count alone"): the SAME maxTasks value must yield IDENTICAL plan
|
|
* sequences regardless of what (if anything) `core` points to --
|
|
* including a NULL core, and a garbage-but-non-NULL one whose fields are
|
|
* never touched. Since parjob_plan_iter_init()/_next() never dereference
|
|
* `core` at all, a non-NULL "poisoned" core pointer that would crash on
|
|
* any real field access is a stronger proof of purity than merely
|
|
* passing NULL.
|
|
*/
|
|
static void test_plan_generation_ignores_core_state(void)
|
|
{
|
|
xce_primary_resreq_t resreq;
|
|
xce_alloc_plan_iter_t iter_a_storage;
|
|
xce_alloc_plan_iter_t* iter_a = &iter_a_storage;
|
|
xce_alloc_plan_iter_t iter_b_storage;
|
|
xce_alloc_plan_iter_t* iter_b = &iter_b_storage;
|
|
xce_sched_core_t* poisoned_core = (xce_sched_core_t*)0x1;
|
|
int k;
|
|
|
|
memset(&resreq, 0, sizeof(resreq));
|
|
resreq.minTasks = 1;
|
|
resreq.maxTasks = 7;
|
|
|
|
bind_plan_iter(iter_a);
|
|
TEST_ASSERT_EQUAL_INT(0, iter_a->init(iter_a, &resreq, NULL));
|
|
|
|
bind_plan_iter(iter_b);
|
|
/* A deliberately invalid, non-dereferenceable pointer -- init()/
|
|
* next() must never read through it, or this test would crash
|
|
* instead of merely failing an assertion. */
|
|
TEST_ASSERT_EQUAL_INT(0, iter_b->init(iter_b, &resreq, poisoned_core));
|
|
|
|
for (k = 1; k <= 7; k++) {
|
|
xce_alloc_plan_t* plan_a = iter_a->next(iter_a);
|
|
xce_alloc_plan_t* plan_b = iter_b->next(iter_b);
|
|
int i;
|
|
|
|
TEST_ASSERT_NOT_NULL(plan_a);
|
|
TEST_ASSERT_NOT_NULL(plan_b);
|
|
TEST_ASSERT_EQUAL_INT(plan_a->nbuckets, plan_b->nbuckets);
|
|
for (i = 0; i < plan_a->nbuckets; i++) {
|
|
TEST_ASSERT_EQUAL_INT(plan_a->bucket_slots[i],
|
|
plan_b->bucket_slots[i]);
|
|
}
|
|
}
|
|
|
|
TEST_ASSERT_NULL(iter_a->next(iter_a));
|
|
TEST_ASSERT_NULL(iter_b->next(iter_b));
|
|
|
|
iter_a->fini(iter_a);
|
|
iter_b->fini(iter_b);
|
|
}
|
|
|
|
/*
|
|
* init() rejects invalid input rather than crashing: a NULL iter, a NULL
|
|
* resreq, and a non-positive maxTasks.
|
|
*/
|
|
static void test_init_rejects_invalid_input(void)
|
|
{
|
|
xce_primary_resreq_t resreq;
|
|
xce_alloc_plan_iter_t iter_storage;
|
|
xce_alloc_plan_iter_t* iter = &iter_storage;
|
|
|
|
memset(&resreq, 0, sizeof(resreq));
|
|
resreq.maxTasks = 3;
|
|
|
|
bind_plan_iter(iter);
|
|
TEST_ASSERT_TRUE(iter->init(NULL, &resreq, NULL) != 0);
|
|
TEST_ASSERT_TRUE(iter->init(iter, NULL, NULL) != 0);
|
|
|
|
resreq.maxTasks = 0;
|
|
TEST_ASSERT_TRUE(iter->init(iter, &resreq, NULL) != 0);
|
|
}
|
|
|
|
/*
|
|
* Req 2 AC 6: next() calls pthread_testcancel() (or an equivalent real
|
|
* cancellation point) at least once per invocation. Proved behaviorally,
|
|
* same technique as test_host_iter.c's test_next_has_a_real_cancellation_
|
|
* point(): a worker thread parked in a tight loop calling next()
|
|
* repeatedly (spinning past exhaustion, which still executes the
|
|
* cancellation point on every call) is reliably torn down by
|
|
* pthread_cancel() from the main thread. If parjob_plan_iter_next() had
|
|
* no real cancellation point, this tight loop would never yield to the
|
|
* pending cancellation and pthread_join() below would hang (timing out
|
|
* the whole test binary) rather than return.
|
|
*/
|
|
typedef struct {
|
|
xce_alloc_plan_iter_t* plan_iter;
|
|
} cancel_thread_arg_t;
|
|
|
|
static void* cancel_spin_thread(void* argp)
|
|
{
|
|
cancel_thread_arg_t* arg = (cancel_thread_arg_t*)argp;
|
|
|
|
for (;;) {
|
|
(void)arg->plan_iter->next(arg->plan_iter);
|
|
}
|
|
|
|
return NULL; /* unreachable */
|
|
}
|
|
|
|
static void test_next_has_a_real_cancellation_point(void)
|
|
{
|
|
xce_primary_resreq_t resreq;
|
|
xce_alloc_plan_iter_t iter_storage;
|
|
xce_alloc_plan_iter_t* iter = &iter_storage;
|
|
cancel_thread_arg_t arg;
|
|
pthread_t tid;
|
|
int rc;
|
|
|
|
memset(&resreq, 0, sizeof(resreq));
|
|
resreq.minTasks = 1;
|
|
resreq.maxTasks = 4;
|
|
|
|
bind_plan_iter(iter);
|
|
TEST_ASSERT_EQUAL_INT(0, iter->init(iter, &resreq, NULL));
|
|
|
|
arg.plan_iter = iter;
|
|
|
|
rc = pthread_create(&tid, NULL, cancel_spin_thread, &arg);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
/* Let the spin thread actually get running before cancelling it. */
|
|
usleep(20000);
|
|
|
|
rc = pthread_cancel(tid);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
rc = pthread_join(tid, NULL);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
/* Safe post-cancellation cleanup: pthread_testcancel() only ever
|
|
* fires between complete logical steps (function entry), never
|
|
* mid-mutation, so the iterator's private state is always
|
|
* well-formed here regardless of exactly where cancellation
|
|
* landed. */
|
|
iter->fini(iter);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Task 4.2 tests: the real per-bucket allocation loop */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
/*
|
|
* Req 4 AC 2/AC 3/AC 5/AC 6: a real multi-bucket plan, driven end to end
|
|
* through the real my_alloc() (not parjob_try_plan_() directly). Two
|
|
* hosts are engineered so the outcome is deterministic REGARDLESS of
|
|
* sched->core.workers' own hash-based iteration order: host-A has exactly
|
|
* 2 free slots (only it can satisfy a "need 2" bucket -- host-B's 1 free
|
|
* slot never qualifies) and host-B has exactly 1 free slot. maxTasks=3
|
|
* forces k=1 ([3]) to fail first (no single host has >=3), so k=2's
|
|
* [2,1] plan is the one that actually succeeds -- proving the bucket loop
|
|
* itself works, not just the trivial single-bucket/single-plan case.
|
|
* Also proves Req 4 AC 5/AC 6: parjob_prepare_allocation() must have
|
|
* staged a "slots" want of exactly 2 (not a fixed 1, and not maxTasks=3)
|
|
* for host-A's bucket, and exactly 1 for host-B's -- an unfixed stub or a
|
|
* fixed-value bug would either allocate nothing or debit the wrong
|
|
* amount, and assert_uncommitted_host_debit() below would catch either.
|
|
*/
|
|
static void test_multi_bucket_plan_succeeds_through_real_my_alloc(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
xce_resource_t* slots_res;
|
|
xce_sched_worker_t* host_a;
|
|
xce_sched_worker_t* host_b;
|
|
xce_sched_obj_t obj;
|
|
xce_primary_schedobj_t primary_obj;
|
|
xce_primary_resreq_t primary;
|
|
xce_sched_ctx_t ctx;
|
|
int rc;
|
|
int i;
|
|
int seen_a = 0;
|
|
int seen_b = 0;
|
|
|
|
memset(&sched, 0, sizeof(sched));
|
|
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core)));
|
|
|
|
slots_res = register_slots_resource(sched.core.resmgr);
|
|
TEST_ASSERT_NOT_NULL(slots_res);
|
|
|
|
host_a
|
|
= add_worker_with_slots(&(sched.core), "host-a", slots_res->idx, 2.0f);
|
|
host_b
|
|
= add_worker_with_slots(&(sched.core), "host-b", slots_res->idx, 1.0f);
|
|
|
|
memset(&primary, 0, sizeof(primary));
|
|
primary.minTasks = 1;
|
|
primary.maxTasks = 3;
|
|
primary.select = NULL;
|
|
|
|
init_primary_obj(&obj, &primary_obj, &primary);
|
|
|
|
rc = drive_my_alloc(&sched, &obj, &primary, &ctx);
|
|
|
|
TEST_ASSERT_EQUAL_INT(1, rc);
|
|
TEST_ASSERT_NOT_NULL(ctx.alloc);
|
|
TEST_ASSERT_EQUAL_INT(2, ctx.alloc->nhosts);
|
|
|
|
for (i = 0; i < ctx.alloc->nhosts; i++) {
|
|
xce_host_alloc_t* ha = ctx.alloc->allocs[i];
|
|
|
|
TEST_ASSERT_NOT_NULL(ha);
|
|
TEST_ASSERT_EQUAL_INT(1, ha->nallocs); /* "slots" only */
|
|
if (strcmp(ha->host, "host-a") == 0) {
|
|
seen_a = 1;
|
|
} else if (strcmp(ha->host, "host-b") == 0) {
|
|
seen_b = 1;
|
|
}
|
|
}
|
|
TEST_ASSERT_TRUE(seen_a);
|
|
TEST_ASSERT_TRUE(seen_b);
|
|
|
|
/* host-a's own bucket needed 2 slots, host-b's needed 1 -- the exact
|
|
* per-bucket want Req 4 AC 5/AC 6 requires, not a fixed 1 (seqjob's
|
|
* old hardcode) or maxTasks=3 (a different, equally wrong fixed
|
|
* value). */
|
|
assert_uncommitted_host_debit(&ctx, host_a, slots_res->idx, 2.0f);
|
|
assert_uncommitted_host_debit(&ctx, host_b, slots_res->idx, 1.0f);
|
|
|
|
sched_ctx_fini(&ctx);
|
|
}
|
|
|
|
/*
|
|
* bug-1882: a two-host plan whose allocation records cannot be built --
|
|
* first for host 0, then for host 1 after host 0's record already exists --
|
|
* must report -ENOMEM with ctx->alloc left NULL (the caller moves ctx->alloc
|
|
* onto the job whatever the rc). ASAN catches a double free or use after
|
|
* free on the cleanup path.
|
|
*/
|
|
static void test_my_alloc_reports_enomem_when_allocs_cannot_be_built(void)
|
|
{
|
|
int fail_at;
|
|
|
|
for (fail_at = 0; fail_at < 2; fail_at++) {
|
|
xce_scheduler_t sched;
|
|
xce_resource_t* slots_res;
|
|
xce_sched_obj_t obj;
|
|
xce_primary_schedobj_t primary_obj;
|
|
xce_primary_resreq_t primary;
|
|
xce_sched_ctx_t ctx;
|
|
int rc;
|
|
|
|
memset(&sched, 0, sizeof(sched));
|
|
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core)));
|
|
|
|
slots_res = register_slots_resource(sched.core.resmgr);
|
|
TEST_ASSERT_NOT_NULL(slots_res);
|
|
add_worker_with_slots(&(sched.core), "host-a", slots_res->idx, 2.0f);
|
|
add_worker_with_slots(&(sched.core), "host-b", slots_res->idx, 1.0f);
|
|
|
|
memset(&primary, 0, sizeof(primary));
|
|
primary.minTasks = 1;
|
|
primary.maxTasks = 3;
|
|
primary.select = NULL;
|
|
|
|
init_primary_obj(&obj, &primary_obj, &primary);
|
|
|
|
g_make_allocs_calls = 0;
|
|
g_make_allocs_fail_at = fail_at;
|
|
rc = drive_my_alloc(&sched, &obj, &primary, &ctx);
|
|
g_make_allocs_fail_at = -1;
|
|
|
|
TEST_ASSERT_EQUAL_INT(-ENOMEM, rc);
|
|
TEST_ASSERT_NULL(ctx.alloc);
|
|
|
|
sched_ctx_fini(&ctx);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Req 4 AC 2: the exact descending-vs-ascending worked example from
|
|
* requirements.md's Composition Algorithm. Two hosts, capacities 1 and 2;
|
|
* WHICH physical host holds which capacity is assigned by a real,
|
|
* independent iter_make()/iter_next() scan (the identical primitive
|
|
* host_iter.c itself uses over the same sched.core.workers) so the test
|
|
* does not depend on assuming insertion order matches hash-table
|
|
* iteration order -- the same technique
|
|
* test_seqjob_rewrap_equivalence.c's own iteration-order fixture uses.
|
|
* The 2-free-slot capacity goes to whichever host is visited FIRST,
|
|
* matching requirements.md's own worked example precisely (a fresh scan
|
|
* for a small bucket greedily claims whatever it encounters first that
|
|
* already satisfies it).
|
|
*
|
|
* Driven directly through parjob_try_plan_() (not the real plan
|
|
* iterator, which only ever generates descending order) with a
|
|
* hand-built xce_alloc_plan_t for each order, against the SAME
|
|
* unmodified host pool (the descending attempt's claims are rolled back
|
|
* before the ascending attempt runs) -- this is the seam task 4.2's own
|
|
* bucket-loop factoring (out of my_alloc()) exists to make testable
|
|
* without reimplementing the loop in the test.
|
|
*/
|
|
static void test_descending_bucket_order_succeeds_where_ascending_fails(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
xce_resource_t* slots_res;
|
|
xce_sched_worker_t* host_first;
|
|
xce_sched_worker_t* host_second;
|
|
iterator_t it;
|
|
xce_sched_obj_t obj;
|
|
xce_primary_schedobj_t primary_obj;
|
|
xce_primary_resreq_t primary;
|
|
xce_sched_ctx_t ctx;
|
|
xce_alloc_plan_t plan;
|
|
int bucket_slots_desc[2];
|
|
int bucket_slots_asc[2];
|
|
parjob_claim_t claimed[2];
|
|
int nclaimed;
|
|
int rc;
|
|
int idx[1];
|
|
float val[1];
|
|
|
|
memset(&sched, 0, sizeof(sched));
|
|
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core)));
|
|
|
|
slots_res = register_slots_resource(sched.core.resmgr);
|
|
TEST_ASSERT_NOT_NULL(slots_res);
|
|
|
|
/* Added with a placeholder capacity -- real capacities are assigned
|
|
* below, once the shared iteration order is known. */
|
|
add_worker_with_slots(&(sched.core), "host-1", slots_res->idx, 0.0f);
|
|
add_worker_with_slots(&(sched.core), "host-2", slots_res->idx, 0.0f);
|
|
|
|
rc = iter_make(&it, sched.core.workers);
|
|
TEST_ASSERT_TRUE(rc >= 0);
|
|
rc = iter_next(&it, (void**)&host_first);
|
|
TEST_ASSERT_TRUE(rc > 0);
|
|
rc = iter_next(&it, (void**)&host_second);
|
|
TEST_ASSERT_TRUE(rc > 0);
|
|
iter_destroy(&it);
|
|
|
|
idx[0] = slots_res->idx;
|
|
val[0] = 2.0f;
|
|
put_resvals(&(host_first->resvals), 1, idx, val);
|
|
val[0] = 1.0f;
|
|
put_resvals(&(host_second->resvals), 1, idx, val);
|
|
|
|
memset(&primary, 0, sizeof(primary));
|
|
primary.minTasks = 1;
|
|
primary.maxTasks = 3;
|
|
primary.select = NULL;
|
|
|
|
init_primary_obj(&obj, &primary_obj, &primary);
|
|
/* scheduler-sim-live-allocation spec, task 2.1: ctx->sched is now
|
|
* honestly typed xce_sched_core_t* -- pass &(sched.core), not &sched. */
|
|
TEST_ASSERT_EQUAL_INT(0, sched_ctx_init(&ctx, &(sched.core), &obj));
|
|
sched_ctx_set_primary(&ctx, &primary);
|
|
|
|
/* my_alloc() always lazily creates ctx->reason before ever reaching
|
|
* parjob_try_plan_() (see my_alloc()'s own top-of-function guard);
|
|
* this test calls parjob_try_plan_() directly, bypassing that step,
|
|
* so it must set one up itself -- sched_try_alloc_on_host_()
|
|
* (resource.c) unconditionally records a pend reason into
|
|
* ctx->reason->reasons on a resource-unmet candidate, which the
|
|
* ascending attempt below deliberately exercises (bucket1 failing
|
|
* "need 2" against host_second's 1 free slot). */
|
|
ctx.reason = make_pendreason(0);
|
|
TEST_ASSERT_NOT_NULL(ctx.reason);
|
|
|
|
/* Descending [2,1] -- the plan iterator's own v1 default order --
|
|
* succeeds: bucket0 ("need 2") can only be satisfied by host_first
|
|
* (2 free); bucket1 ("need 1") then claims host_second (1 free),
|
|
* the only host left. */
|
|
bucket_slots_desc[0] = 2;
|
|
bucket_slots_desc[1] = 1;
|
|
memset(&plan, 0, sizeof(plan));
|
|
plan.nbuckets = 2;
|
|
plan.bucket_slots = bucket_slots_desc;
|
|
|
|
/* scheduler-sim-live-allocation spec, task 4.3: parjob_try_plan_() is
|
|
* now honestly typed xce_sched_core_t* (mirrors task 2.1's identical
|
|
* ctx->sched ripple immediately above) -- pass &(sched.core), not
|
|
* &sched. */
|
|
rc = parjob_try_plan_(&(sched.core), &ctx, &plan, claimed, &nclaimed);
|
|
TEST_ASSERT_EQUAL_INT(1, rc);
|
|
TEST_ASSERT_EQUAL_INT(2, nclaimed);
|
|
TEST_ASSERT_TRUE(claimed[0].w == host_first);
|
|
TEST_ASSERT_TRUE(claimed[1].w == host_second);
|
|
|
|
/* Roll back before trying the ascending variant against the exact
|
|
* same, otherwise-untouched host pool. */
|
|
parjob_rollback_claims_(&ctx, claimed, nclaimed);
|
|
assert_uncommitted_host_debit(&ctx, host_first, slots_res->idx, 0.0f);
|
|
assert_uncommitted_host_debit(&ctx, host_second, slots_res->idx, 0.0f);
|
|
|
|
/* Ascending [1,2] -- the SAME host pool -- fails: bucket0 ("need 1")
|
|
* greedily claims host_first (2 free, visited first, trivially
|
|
* satisfies "need 1" even though it is overqualified), leaving only
|
|
* host_second (1 free) for bucket1 ("need 2"), which cannot satisfy
|
|
* it. */
|
|
bucket_slots_asc[0] = 1;
|
|
bucket_slots_asc[1] = 2;
|
|
memset(&plan, 0, sizeof(plan));
|
|
plan.nbuckets = 2;
|
|
plan.bucket_slots = bucket_slots_asc;
|
|
|
|
/* scheduler-sim-live-allocation spec, task 4.3: parjob_try_plan_() is
|
|
* now honestly typed xce_sched_core_t* (mirrors task 2.1's identical
|
|
* ctx->sched ripple immediately above) -- pass &(sched.core), not
|
|
* &sched. */
|
|
rc = parjob_try_plan_(&(sched.core), &ctx, &plan, claimed, &nclaimed);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
TEST_ASSERT_EQUAL_INT(0, nclaimed);
|
|
|
|
/* No phantom debit left behind by the failed ascending attempt. */
|
|
assert_uncommitted_host_debit(&ctx, host_first, slots_res->idx, 0.0f);
|
|
assert_uncommitted_host_debit(&ctx, host_second, slots_res->idx, 0.0f);
|
|
|
|
sched_ctx_fini(&ctx);
|
|
}
|
|
|
|
/*
|
|
* Req 4 AC 2 ("Rollback scope"): a mid-plan bucket failure must reverse
|
|
* every earlier bucket's provisional debit in that SAME plan attempt
|
|
* before the next candidate plan is tried. Proved the strong way -- not
|
|
* just "the debit reads back as zero" but "the SAME host is successfully
|
|
* reclaimed by a LATER plan attempt for a full, real allocation," which
|
|
* could only happen if the earlier phantom debit was genuinely reversed
|
|
* (sched_check_host_satisfy() would otherwise reject the later attempt's
|
|
* own fresh want against a host that still looked partially consumed).
|
|
*
|
|
* Fixture: h1 (2 free), h2/h3/h4 (1 free each), maxTasks=4.
|
|
* k=1 ([4]): fails (no single host has >=4).
|
|
* k=2 ([2,2]): bucket0 ("need 2") can only be satisfied by h1 (the only
|
|
* host with >=2 free) -- claims h1. bucket1 ("need 2"), excluding h1,
|
|
* has only h2/h3/h4 left, each with just 1 free -- ALL fail. The
|
|
* whole k=2 plan fails, and h1's provisional debit from bucket0 must
|
|
* be rolled back here.
|
|
* k=3 ([2,1,1]): a fresh, empty claimed[] (independent of k=2's
|
|
* rolled-back attempt) -- bucket0 ("need 2") again only h1 qualifies;
|
|
* if k=2's debit on h1 had NOT been reversed, h1 would appear to
|
|
* already carry a 2.0 phantom debit and sched_check_host_satisfy()
|
|
* would reject this fresh "need 2" want against it (2 total < 2
|
|
* phantom + 2 new) -- the whole allocation would fail outright
|
|
* instead of succeeding. bucket1/bucket2 ("need 1" each) then claim
|
|
* two of the three remaining 1-free-slot hosts. SUCCEEDS with exactly
|
|
* 3 claimed hosts.
|
|
*/
|
|
static void
|
|
test_mid_plan_bucket_failure_rolls_back_before_later_plan_succeeds(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
xce_resource_t* slots_res;
|
|
xce_sched_worker_t* h1;
|
|
xce_sched_worker_t* h2;
|
|
xce_sched_worker_t* h3;
|
|
xce_sched_worker_t* h4;
|
|
xce_sched_obj_t obj;
|
|
xce_primary_schedobj_t primary_obj;
|
|
xce_primary_resreq_t primary;
|
|
xce_sched_ctx_t ctx;
|
|
int rc;
|
|
int i;
|
|
int seen_h1 = 0;
|
|
int nother = 0;
|
|
xvec_t* u2 = NULL;
|
|
xvec_t* u3 = NULL;
|
|
xvec_t* u4 = NULL;
|
|
float other_sum;
|
|
|
|
memset(&sched, 0, sizeof(sched));
|
|
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core)));
|
|
|
|
slots_res = register_slots_resource(sched.core.resmgr);
|
|
TEST_ASSERT_NOT_NULL(slots_res);
|
|
|
|
h1 = add_worker_with_slots(&(sched.core), "h1", slots_res->idx, 2.0f);
|
|
h2 = add_worker_with_slots(&(sched.core), "h2", slots_res->idx, 1.0f);
|
|
h3 = add_worker_with_slots(&(sched.core), "h3", slots_res->idx, 1.0f);
|
|
h4 = add_worker_with_slots(&(sched.core), "h4", slots_res->idx, 1.0f);
|
|
|
|
memset(&primary, 0, sizeof(primary));
|
|
primary.minTasks = 1;
|
|
primary.maxTasks = 4;
|
|
primary.select = NULL;
|
|
|
|
init_primary_obj(&obj, &primary_obj, &primary);
|
|
|
|
rc = drive_my_alloc(&sched, &obj, &primary, &ctx);
|
|
|
|
TEST_ASSERT_EQUAL_INT(1, rc);
|
|
TEST_ASSERT_NOT_NULL(ctx.alloc);
|
|
TEST_ASSERT_EQUAL_INT(3, ctx.alloc->nhosts);
|
|
|
|
for (i = 0; i < ctx.alloc->nhosts; i++) {
|
|
xce_host_alloc_t* ha = ctx.alloc->allocs[i];
|
|
|
|
TEST_ASSERT_NOT_NULL(ha);
|
|
if (strcmp(ha->host, "h1") == 0) {
|
|
seen_h1++;
|
|
} else {
|
|
nother++;
|
|
}
|
|
}
|
|
TEST_ASSERT_EQUAL_INT(1, seen_h1);
|
|
TEST_ASSERT_EQUAL_INT(2, nother);
|
|
|
|
/* THE regression assertion: h1 carries exactly its own bucket's 2.0
|
|
* want in the final, committed state -- not 4.0 (which a missing
|
|
* rollback would leave: 2.0 phantom from the failed k=2 attempt plus
|
|
* 2.0 genuine from k=3's own successful claim). */
|
|
assert_uncommitted_host_debit(&ctx, h1, slots_res->idx, 2.0f);
|
|
|
|
/* Exactly two of h2/h3/h4 were claimed at 1.0 each (the third stayed
|
|
* at 0.0, host_iter's own internal candidate order decides which) --
|
|
* proves neither a double-debit nor a leaked phantom debit from the
|
|
* rolled-back k=2 attempt. */
|
|
TEST_ASSERT_EQUAL_INT(1, sched_ctx_get_uncomit_host(&u2, &ctx, h2->idx));
|
|
TEST_ASSERT_EQUAL_INT(1, sched_ctx_get_uncomit_host(&u3, &ctx, h3->idx));
|
|
TEST_ASSERT_EQUAL_INT(1, sched_ctx_get_uncomit_host(&u4, &ctx, h4->idx));
|
|
other_sum = ((float*)u2->data)[slots_res->idx]
|
|
+ ((float*)u3->data)[slots_res->idx]
|
|
+ ((float*)u4->data)[slots_res->idx];
|
|
TEST_ASSERT_EQUAL_FLOAT(2.0f, other_sum);
|
|
|
|
sched_ctx_fini(&ctx);
|
|
}
|
|
|
|
/*
|
|
* Req 4 AC 4: when every candidate plan is exhausted without a full
|
|
* success, my_alloc() returns the same not-satisfied signal (0) the old
|
|
* stub always returned, unconditionally -- pend-reason handling
|
|
* downstream is unaffected by this task's change.
|
|
*/
|
|
static void test_returns_zero_when_every_plan_is_exhausted(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
xce_resource_t* slots_res;
|
|
xce_sched_obj_t obj;
|
|
xce_primary_schedobj_t primary_obj;
|
|
xce_primary_resreq_t primary;
|
|
xce_sched_ctx_t ctx;
|
|
int rc;
|
|
|
|
memset(&sched, 0, sizeof(sched));
|
|
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core)));
|
|
|
|
slots_res = register_slots_resource(sched.core.resmgr);
|
|
TEST_ASSERT_NOT_NULL(slots_res);
|
|
|
|
/* A single host with only 1 free slot can never satisfy ANY plan for
|
|
* a 3-task job (k=1 needs 3 on one host; k=2/k=3 each need a SECOND
|
|
* distinct host that does not exist). */
|
|
add_worker_with_slots(&(sched.core), "lonely-host", slots_res->idx, 1.0f);
|
|
|
|
memset(&primary, 0, sizeof(primary));
|
|
primary.minTasks = 1;
|
|
primary.maxTasks = 3;
|
|
primary.select = NULL;
|
|
|
|
init_primary_obj(&obj, &primary_obj, &primary);
|
|
|
|
rc = drive_my_alloc(&sched, &obj, &primary, &ctx);
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
TEST_ASSERT_NULL(ctx.alloc);
|
|
|
|
sched_ctx_fini(&ctx);
|
|
}
|
|
|
|
/*
|
|
* Task 4.3, Req 4 AC 7's own previously-unverified clause: "leaving
|
|
* job-level shared-resource allocation computed once and unaffected by how
|
|
* many hosts were used." Task 4.2's fixtures only ever used a per-host
|
|
* ("slots") resource -- this test drives a job carrying BOTH a per-host
|
|
* resource AND a genuinely job-level/shared (XCE_RESF_GLOBAL) resource
|
|
* through a real multi-host my_alloc() success and asserts the shared
|
|
* resource is debited exactly ONCE at the job level, not once per claimed
|
|
* host/bucket.
|
|
*
|
|
* The shared rusage uses PER_JOB semantics deliberately, not PER_TASK:
|
|
* calc_wanted_global_resvals()'s PER_TASK case scales by ctx->nslots, and
|
|
* since a plan's bucket_slots[] always sum to maxTasks by construction, a
|
|
* (hypothetical, buggy) per-bucket-repeated PER_TASK computation would sum
|
|
* to the SAME total as a correct once-only computation -- masking exactly
|
|
* the defect this test exists to catch. PER_JOB's flat value has no such
|
|
* coincidence: calc_wanted_global_resvals()'s `case PER_JOB: val =
|
|
* v->value;` ignores nslots/nhosts entirely, so a mistaken per-bucket (or
|
|
* per-claimed-host) repeat of the job-level pass would total value*nbuckets
|
|
* instead of value.
|
|
*
|
|
* Fixture: 4 hosts, each with exactly 1 free "slots" -- forces k=1 ([4]),
|
|
* k=2 ([2,2]), and k=3 ([2,1,1]) to all fail (no single host, or any pair/
|
|
* triple, can supply a bucket needing >=2 slots) and k=4 ([1,1,1,1]) to be
|
|
* the only plan that succeeds, claiming ALL 4 hosts. This is a wider
|
|
* multi-host spread than task 4.2's existing 2-3 host tests, so this test
|
|
* also independently re-proves this task's OTHER Observable clause ("every
|
|
* claimed host with its own slot count") without relying on 4.2's own
|
|
* tests remaining unchanged.
|
|
*
|
|
* g_job_level_prepare_calls (the fan-out stub above) additionally proves,
|
|
* by direct dynamic count rather than code inspection alone, that the
|
|
* job-level (ctx->worker == NULL) prepare pass fires exactly once for this
|
|
* whole 4-host allocation attempt -- confirming sched_ctx_set_primary()'s
|
|
* single call site (schedobj.c), not my_alloc()'s plan/bucket loop, is what
|
|
* drives it.
|
|
*/
|
|
static void test_shared_resource_debited_once_regardless_of_host_count(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
xce_resource_t* slots_res;
|
|
xce_resource_t* license_res;
|
|
xce_sched_worker_t* hosts[4];
|
|
xce_sched_obj_t obj;
|
|
xce_primary_schedobj_t primary_obj;
|
|
xce_primary_resreq_t primary;
|
|
rusage_value_t license_rusage;
|
|
xce_sched_ctx_t ctx;
|
|
xvec_t* shared_uncomit = NULL;
|
|
int rc;
|
|
int i;
|
|
int idx[1];
|
|
float val[1];
|
|
char name[16];
|
|
|
|
memset(&sched, 0, sizeof(sched));
|
|
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core)));
|
|
|
|
slots_res = register_slots_resource(sched.core.resmgr);
|
|
TEST_ASSERT_NOT_NULL(slots_res);
|
|
|
|
license_res = register_global_resource(sched.core.resmgr, "licenses");
|
|
TEST_ASSERT_NOT_NULL(license_res);
|
|
|
|
/* Shared-pool total capacity for "licenses" -- far more than the 5.0
|
|
* PER_JOB want below, so the shared-satisfy check trivially passes and
|
|
* the only thing under test is HOW MANY TIMES / HOW MUCH gets debited,
|
|
* not whether capacity exists. sched->core.sharedInUse is left at its
|
|
* zarray_init() default (nothing put), i.e. 0 currently in use. */
|
|
idx[0] = license_res->idx;
|
|
val[0] = 100.0f;
|
|
put_resvals(&(sched.core.sharedInst), 1, idx, val);
|
|
|
|
for (i = 0; i < 4; i++) {
|
|
snprintf(name, sizeof(name), "host-%d", i);
|
|
hosts[i]
|
|
= add_worker_with_slots(&(sched.core), name, slots_res->idx, 1.0f);
|
|
}
|
|
|
|
memset(&primary, 0, sizeof(primary));
|
|
primary.minTasks = 1;
|
|
primary.maxTasks = 4;
|
|
primary.select = NULL;
|
|
|
|
memset(&license_rusage, 0, sizeof(license_rusage));
|
|
license_rusage.res = strdup("licenses");
|
|
license_rusage.value = 5.0f;
|
|
license_rusage.per_flag = PER_JOB;
|
|
|
|
xlist_init(&(primary.rusages), 4);
|
|
xlist_store(&(primary.rusages), &license_rusage);
|
|
|
|
init_primary_obj(&obj, &primary_obj, &primary);
|
|
|
|
g_job_level_prepare_calls = 0;
|
|
rc = drive_my_alloc(&sched, &obj, &primary, &ctx);
|
|
|
|
TEST_ASSERT_EQUAL_INT(1, rc);
|
|
TEST_ASSERT_NOT_NULL(ctx.alloc);
|
|
|
|
/* This task's OTHER Observable clause, re-proved independently of
|
|
* task 4.2's own tests: every claimed host with its own slot count. */
|
|
TEST_ASSERT_EQUAL_INT(4, ctx.alloc->nhosts);
|
|
for (i = 0; i < ctx.alloc->nhosts; i++) {
|
|
xce_host_alloc_t* ha = ctx.alloc->allocs[i];
|
|
|
|
TEST_ASSERT_NOT_NULL(ha);
|
|
TEST_ASSERT_EQUAL_INT(1, ha->nallocs); /* "slots" only */
|
|
}
|
|
for (i = 0; i < 4; i++) {
|
|
assert_uncommitted_host_debit(&ctx, hosts[i], slots_res->idx, 1.0f);
|
|
}
|
|
|
|
/* THE dispatch-count claim, confirmed dynamically: the job-level
|
|
* shared/global-resource prepare pass fired exactly once for this
|
|
* whole 4-host allocation attempt -- not once per claimed bucket/host. */
|
|
TEST_ASSERT_EQUAL_INT(1, g_job_level_prepare_calls);
|
|
|
|
/* THE property under test: the committed shared debit is the flat
|
|
* PER_JOB value exactly once (5.0) -- not multiplied by nhosts/
|
|
* nbuckets (which a per-bucket-repeated commit would make 20.0). */
|
|
rc = sched_ctx_get_uncomit_shared(&shared_uncomit, &ctx);
|
|
TEST_ASSERT_EQUAL_INT(1, rc);
|
|
TEST_ASSERT_NOT_NULL(shared_uncomit);
|
|
TEST_ASSERT_EQUAL_FLOAT(5.0f,
|
|
((float*)shared_uncomit->data)[license_res->idx]);
|
|
|
|
/* The final committed allocation record's own shared-resource fields
|
|
* mirror the same once-only amount, unaffected by nhosts. */
|
|
TEST_ASSERT_EQUAL_INT(1, ctx.alloc->nres_shared);
|
|
TEST_ASSERT_NOT_NULL(ctx.alloc->allocs_shared);
|
|
|
|
xlist_fini(&(primary.rusages));
|
|
FREEUP(license_rusage.res);
|
|
sched_ctx_fini(&ctx);
|
|
}
|
|
|
|
void setUp(void)
|
|
{
|
|
g_make_allocs_fail_at = -1;
|
|
g_make_allocs_calls = 0;
|
|
}
|
|
|
|
void tearDown(void) { return; }
|
|
|
|
int main(void)
|
|
{
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_plans_ordered_ascending_host_count_with_balanced_partitions);
|
|
RUN_TEST(test_plans_for_evenly_divisible_task_count);
|
|
RUN_TEST(test_single_task_yields_single_plan);
|
|
RUN_TEST(test_plan_generation_ignores_core_state);
|
|
RUN_TEST(test_init_rejects_invalid_input);
|
|
RUN_TEST(test_next_has_a_real_cancellation_point);
|
|
RUN_TEST(test_multi_bucket_plan_succeeds_through_real_my_alloc);
|
|
RUN_TEST(test_my_alloc_reports_enomem_when_allocs_cannot_be_built);
|
|
RUN_TEST(test_descending_bucket_order_succeeds_where_ascending_fails);
|
|
RUN_TEST(
|
|
test_mid_plan_bucket_failure_rolls_back_before_later_plan_succeeds);
|
|
RUN_TEST(test_returns_zero_when_every_plan_is_exhausted);
|
|
RUN_TEST(test_shared_resource_debited_once_regardless_of_host_count);
|
|
return UNITY_END();
|
|
}
|