/* * test_alloc_equivalence_seqjob_changes.c - scheduler-allocator-runner task * 4.2: the sequential-job allocator's intentional changes and missing-input * guards, driven through the registered plugin->job_alloc_fn of * seqjob_allocator_new_plugin() (the rest of its characterization is in * test_alloc_equivalence_seqjob.c, whose stubs and fixture this file * mirrors). * * - every selection-matching short host gets its (host, resource) * NO_RESOURCE reasons; main reason, decision and debits unchanged * (Req 3.6, 3.7) * - with two shared resources requested and one short, exactly one * host-less reason, naming the short one (Req 2.2, bug-1893) * - seqjob's plan is one bucket of one slot whatever the task count * (brief.md Constraints item 3) * - a missing core, scheduler back-reference, primary or allocator * contract is -EINVAL with no pend-reason record (Req 5.1) */ #include #include #include #include #include "preason.h" #include "resobj.h" #include "resreq.h" #include "sched/alloc_iter.h" #include "scheduler.h" #include "xcealloc.h" #include "xstring.h" /* White-box sources under test. */ #include "../src/core.c" #include "../src/resource.c" #include "../src/schedobj.c" #include "../src/host_iter.c" #include "../src/alloc_runner.c" /* Hooked sources compile SCHED_TRACE bodies (scheduler-sim-decision-trace). */ #include "../src/sched_trace.c" #include "../src/sched_trace_capture.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(); }