/* * test_resource_accounting.c - Requirement 1.4 proof for the Core-Level * Resource Accounting component (scheduler-sim-decision-parity spec, task * 1.1; design.md's "Core-Level Resource Accounting" Unit Tests bullet): * "produces identical resInUse/sharedInUse output whether called with a * core obtained from xce_scheduler_t or from xce_sched_sim_driver_t." * * Task 1.1 itself is a pure signature relocation (xce_scheduler_t* -> * xce_sched_core_t*) of sched_account_jobs()/sched_charge_account()/ * sched_uncharge_account()/sched_update_account()/ * sched_update_res_accounts()/sched_commit_job_alloc() -- body unchanged, * every reference already `sched->core.*` before the move (confirmed via * `grep "sched->" resource.c | grep -v "sched->core"` returning zero * matches, design.md's Existing Architecture Analysis). Requirement 1.4 is * nonetheless a genuinely NEW observable property worth its own test (not * just "nothing broke"): now that these functions take xce_sched_core_t* * directly, they are usable identically regardless of which driver-level * container struct (xce_scheduler_t, the normal-mode driver's; or * xce_sched_sim_driver_t, the simulation driver's, sim.h) embeds the core. * This file drives the REAL sched_charge_account()/sched_uncharge_account()/ * sched_commit_job_alloc() (resource.c) against two independently * initialized xce_sched_core_t instances -- one embedded in an * xce_scheduler_t, one embedded in an xce_sched_sim_driver_t -- with * identical worker/account state, and asserts byte-identical * resInUse/sharedInUse results. * * sched_account_jobs() itself (scheduler.c) is NOT white-box #included here * -- unlike this file's own sched_charge_account()/sched_commit_job_alloc() * targets (resource.c, a lightweight, already-proven-includable file per * test.c's own #include "../src/resource.c"), scheduler.c is a ~1400-line * file with a much larger link-time dependency surface (controller-join, * capnp decision publishing, HA candidate handling, picker/allocator thread * lifecycle) that no existing test in this directory white-box #includes for * exactly that reason (confirmed via grep across testsuite/ before writing * this file). sched_account_jobs() itself is a thin, ~20-line wrapper * (iterate core->jobs, call sched_charge_account() for each RUN-state job * with a known allocation) with no accounting logic of its own -- its * driver-ordering discipline (called at the right point in the round * boundary, on both drivers) is already proven by * test_driver_session_ordering.c (a spy, by design -- see that file's own * header comment) and this feature's later tasks (2.x/3.x, sim_loop.c's own * round-boundary integration). This file instead proves the actual * accounting *body* -- the part design.md's Testing Strategy bullet is * about -- via its real callees, exercised directly. * * White-box #include of ../src/core.c (for the real * xce_sched_core_init()/sched_add_worker()/sched_get_worker() the fixture * needs), ../src/resource.c (the functions under test), and * ../src/schedobj.c (for the real sched_ctx_get_uncomit_host() that the * new sched_ctx_uncommit_host() under test itself calls -- white-box * #included here rather than reimplemented, the same convention * test_picker_config_fields.c's own #include "../src/core.c" + * #include "../src/schedobj.c" pairing already established in this * directory "so it exercises the real functions, not a re-implementation * of any of them"). None of the three needs core_clock.c/core_events.c/ * core_session.c/decision.c/plugin.c -- this test never calls * xce_core_push_events()/xce_core_begin_session()/anything decision- or * plugin-registration-related, only the plain add-worker/charge-account/ * uncommit-host entry points core.c/resource.c/schedobj.c themselves * define. The kill_sched_* family (the kvmgr datakillers * xce_sched_core_init() references) is now the REAL implementation * pulled in from schedobj.c, not a stub -- only plugin_unregister remains * a link-satisfying no-op (plugin.c is still not white-box #included). * sched_worker_release_host_ctx() is a real (non-stub) symbol this pulls * from resource.c itself, not from core_session.c -- unused here (this * test never calls it), listed only because resource.c defines it. * plugin_job_update()/plugin_queue_update()/plugin_job_rusage_update() * below are new link-satisfying stubs for schedobj.c's OWN unrelated * functions (sched_update_existing_queue()/sched_update_existing_job()/ * sched_apply_job_rusage()) that reference them (plugin.c, still not * included) -- never actually invoked, since this file never calls any * of those three schedobj.c functions. */ #include #include #include #include #include "resobj.h" #include "scheduler.h" #include "sim.h" /* Pull in the real xce_sched_core_init()/sched_add_worker()/ * sched_get_worker() (core.c), sched_charge_account()/ * sched_uncharge_account()/sched_commit_job_alloc()/get_resvals()/ * sched_ctx_uncommit_host() (resource.c, the functions under test), and * sched_ctx_get_uncomit_host() (schedobj.c) into this TU (white-box * #include, same convention as every other file in this directory -- * see this file's own header comment for why schedobj.c specifically). */ #include "../src/core.c" #include "../src/resource.c" #include "../src/schedobj.c" void plugin_unregister(void* arg) { (void)arg; } /* Link-satisfying stub for resource.c's sched_try_alloc_on_host_() (its * OWN callee plugin_prepare_allocation() lives in plugin.c, not white-box * #included here; its other callee, sched_ctx_get_uncomit_host(), is now * the real function pulled in from schedobj.c above). Never actually * invoked: this file never calls sched_try_alloc_on_hosts()/ * sched_try_alloc_on_host_(), only sched_charge_account()/ * sched_uncharge_account()/sched_commit_job_alloc(). Same stub test.c's * own #include "../src/resource.c" already establishes needing. */ int plugin_prepare_allocation(xce_sched_core_t* s, xce_sched_ctx_t* c) { (void)s; (void)c; return 0; } /* Link-satisfying stubs for schedobj.c's OWN unrelated functions * (sched_update_existing_queue()/sched_update_existing_job()/ * sched_apply_job_rusage()) that reference plugin_job_update()/ * plugin_queue_update()/plugin_job_rusage_update() (plugin.c, not * white-box #included here). Never actually invoked: this file never * calls any of those three schedobj.c functions. */ 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; } void setUp(void) { return; } void tearDown(void) { return; } /* ---------------------------------------------------------------------- */ /* Fixture helpers */ /* ---------------------------------------------------------------------- */ /* * setup_core_with_worker - real xce_sched_core_init() (core.c) plus one * real worker (sched_add_worker(), core.c), its resvals/resInUse zarrays * freshly zarray_init()'d exactly the way a real worker join would leave * them (sched_add_worker() itself does not do this -- callers own it, same * as every other white-box fixture in this directory that adds a worker * directly rather than through the JOBS_PUSH/WORKERS_PUSH event path). */ static void setup_core_with_worker(xce_sched_core_t* core, const char* worker_name) { xce_sched_worker_t* w; int rc; rc = xce_sched_core_init(core); TEST_ASSERT_EQUAL_INT(0, rc); w = (xce_sched_worker_t*)calloc(1, sizeof(*w)); TEST_ASSERT_NOT_NULL(w); w->name = strdup(worker_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); rc = sched_add_worker(core, w); TEST_ASSERT_TRUE(rc >= 0); } /* * make_test_account - one shared-resource charge (residx `shared_residx`) * plus one per-host charge (residx `host_residx`, against `worker_name`) -- * enough to exercise both of sched_update_account()'s two branches * (core->sharedInUse and a specific worker's resInUse). residx is set * non-negative directly (bypassing sched_update_res_accounts()'s * resmgr-lookup branch entirely) -- this test targets the charge/uncharge * arithmetic itself, not resource-name resolution, which is unrelated, * already-existing logic this task's relocation does not touch. */ static xce_job_account_t* make_test_account(const char* worker_name, int shared_residx, double shared_used, double shared_reserved, int host_residx, double host_used, double host_reserved) { xce_job_account_t* acct = (xce_job_account_t*)calloc(1, sizeof(*acct)); TEST_ASSERT_NOT_NULL(acct); acct->nres_shared = 1; acct->accounts_shared = (xce_res_account_t**)calloc( 1, sizeof(*acct->accounts_shared)); TEST_ASSERT_NOT_NULL(acct->accounts_shared); acct->accounts_shared[0] = (xce_res_account_t*)calloc(1, sizeof(xce_res_account_t)); TEST_ASSERT_NOT_NULL(acct->accounts_shared[0]); acct->accounts_shared[0]->residx = shared_residx; acct->accounts_shared[0]->used = shared_used; acct->accounts_shared[0]->reserved = shared_reserved; acct->nhosts = 1; acct->accounts = (xce_host_account_t**)calloc(1, sizeof(*acct->accounts)); TEST_ASSERT_NOT_NULL(acct->accounts); acct->accounts[0] = (xce_host_account_t*)calloc(1, sizeof(xce_host_account_t)); TEST_ASSERT_NOT_NULL(acct->accounts[0]); acct->accounts[0]->host = strdup(worker_name); acct->accounts[0]->naccount = 1; acct->accounts[0]->accounts = (xce_res_account_t**)calloc(1, sizeof(*acct->accounts[0]->accounts)); TEST_ASSERT_NOT_NULL(acct->accounts[0]->accounts); acct->accounts[0]->accounts[0] = (xce_res_account_t*)calloc(1, sizeof(xce_res_account_t)); TEST_ASSERT_NOT_NULL(acct->accounts[0]->accounts[0]); acct->accounts[0]->accounts[0]->residx = host_residx; acct->accounts[0]->accounts[0]->used = host_used; acct->accounts[0]->accounts[0]->reserved = host_reserved; return acct; } /* ---------------------------------------------------------------------- */ /* Tests */ /* ---------------------------------------------------------------------- */ #define WORKER_NAME "worker-a" #define SHARED_RESIDX 0 #define HOST_RESIDX 1 #define SHARED_USED 3.0 #define SHARED_RESERVED 1.5 #define HOST_USED 2.0 #define HOST_RESERVED 0.5 /* SHARED_USED + SHARED_RESERVED, HOST_USED + HOST_RESERVED */ #define EXPECTED_SHARED_CHARGE 4.5f #define EXPECTED_HOST_CHARGE 2.5f /* * test_charge_account_identical_across_core_containers - Requirement 1.4: * sched_charge_account()/sched_uncharge_account() (resource.c) produce * byte-identical resInUse/sharedInUse results whether `core` came from an * xce_scheduler_t (sched.core, the normal-mode driver's container) or an * xce_sched_sim_driver_t (sim.core, the simulation driver's container) -- * the exact property that makes the simulation driver's build now able to * call the same accounting service the normal-mode driver already used. */ static void test_charge_account_identical_across_core_containers(void) { xce_scheduler_t sched; xce_sched_sim_driver_t sim; xce_job_account_t* acct; xce_sched_worker_t* w_sched; xce_sched_worker_t* w_sim; float shared_sched[1]; float shared_sim[1]; float host_sched[1]; float host_sim[1]; int shared_idx[1] = { SHARED_RESIDX }; int host_idx[1] = { HOST_RESIDX }; int rc; memset(&sched, 0, sizeof(sched)); memset(&sim, 0, sizeof(sim)); setup_core_with_worker(&(sched.core), WORKER_NAME); setup_core_with_worker(&(sim.core), WORKER_NAME); /* Same account struct fed to both cores -- sched_charge_account() never * mutates it when residx is already non-negative (see * make_test_account()'s own comment), so reuse is safe and keeps the * two calls genuinely identical, not merely equal-by-construction. */ acct = make_test_account(WORKER_NAME, SHARED_RESIDX, SHARED_USED, SHARED_RESERVED, HOST_RESIDX, HOST_USED, HOST_RESERVED); rc = sched_charge_account(&(sched.core), acct); TEST_ASSERT_EQUAL_INT(0, rc); rc = sched_charge_account(&(sim.core), acct); TEST_ASSERT_EQUAL_INT(0, rc); /* --- sharedInUse: identical across both containers, and correct --- */ get_resvals(&(sched.core.sharedInUse), 1, shared_idx, shared_sched); get_resvals(&(sim.core.sharedInUse), 1, shared_idx, shared_sim); TEST_ASSERT_EQUAL_FLOAT(EXPECTED_SHARED_CHARGE, shared_sched[0]); TEST_ASSERT_EQUAL_FLOAT(shared_sched[0], shared_sim[0]); /* --- per-worker resInUse: identical across both containers --- */ w_sched = sched_get_worker(&(sched.core), WORKER_NAME); w_sim = sched_get_worker(&(sim.core), WORKER_NAME); TEST_ASSERT_NOT_NULL(w_sched); TEST_ASSERT_NOT_NULL(w_sim); get_resvals(&(w_sched->resInUse), 1, host_idx, host_sched); get_resvals(&(w_sim->resInUse), 1, host_idx, host_sim); TEST_ASSERT_EQUAL_FLOAT(EXPECTED_HOST_CHARGE, host_sched[0]); TEST_ASSERT_EQUAL_FLOAT(host_sched[0], host_sim[0]); /* --- sched_uncharge_account() mirrors this identically too --- */ rc = sched_uncharge_account(&(sched.core), acct); TEST_ASSERT_EQUAL_INT(0, rc); rc = sched_uncharge_account(&(sim.core), acct); TEST_ASSERT_EQUAL_INT(0, rc); get_resvals(&(sched.core.sharedInUse), 1, shared_idx, shared_sched); get_resvals(&(sim.core.sharedInUse), 1, shared_idx, shared_sim); TEST_ASSERT_EQUAL_FLOAT(0.0f, shared_sched[0]); TEST_ASSERT_EQUAL_FLOAT(shared_sched[0], shared_sim[0]); get_resvals(&(w_sched->resInUse), 1, host_idx, host_sched); get_resvals(&(w_sim->resInUse), 1, host_idx, host_sim); TEST_ASSERT_EQUAL_FLOAT(0.0f, host_sched[0]); TEST_ASSERT_EQUAL_FLOAT(host_sched[0], host_sim[0]); } /* * test_commit_job_alloc_identical_across_core_containers - Requirement 1.4 * for sched_commit_job_alloc() specifically (the third relocated group * member scheduler.c's sched_commit_decision() calls, design.md's own File * Structure Plan entry for scheduler.c): both its no-op (job->accounts == * NULL) and its charging path behave identically regardless of which * driver-level struct the core came from. */ static void test_commit_job_alloc_identical_across_core_containers(void) { xce_scheduler_t sched; xce_sched_sim_driver_t sim; xce_sched_job_t job_sched; xce_sched_job_t job_sim; xce_job_account_t* acct; float shared_sched[1]; float shared_sim[1]; int shared_idx[1] = { SHARED_RESIDX }; int rc; memset(&sched, 0, sizeof(sched)); memset(&sim, 0, sizeof(sim)); memset(&job_sched, 0, sizeof(job_sched)); memset(&job_sim, 0, sizeof(job_sim)); setup_core_with_worker(&(sched.core), WORKER_NAME); setup_core_with_worker(&(sim.core), WORKER_NAME); /* --- job->accounts == NULL: documented no-op, identical on both --- */ job_sched.accounts = NULL; job_sim.accounts = NULL; rc = sched_commit_job_alloc(&(sched.core), &job_sched); TEST_ASSERT_EQUAL_INT(0, rc); rc = sched_commit_job_alloc(&(sim.core), &job_sim); TEST_ASSERT_EQUAL_INT(0, rc); get_resvals(&(sched.core.sharedInUse), 1, shared_idx, shared_sched); get_resvals(&(sim.core.sharedInUse), 1, shared_idx, shared_sim); TEST_ASSERT_EQUAL_FLOAT(0.0f, shared_sched[0]); TEST_ASSERT_EQUAL_FLOAT(shared_sched[0], shared_sim[0]); /* --- job->accounts set: charges through sched_charge_account(), * identical on both --- */ acct = make_test_account(WORKER_NAME, SHARED_RESIDX, SHARED_USED, SHARED_RESERVED, HOST_RESIDX, HOST_USED, HOST_RESERVED); job_sched.accounts = acct; job_sim.accounts = acct; rc = sched_commit_job_alloc(&(sched.core), &job_sched); TEST_ASSERT_EQUAL_INT(0, rc); rc = sched_commit_job_alloc(&(sim.core), &job_sim); TEST_ASSERT_EQUAL_INT(0, rc); get_resvals(&(sched.core.sharedInUse), 1, shared_idx, shared_sched); get_resvals(&(sim.core.sharedInUse), 1, shared_idx, shared_sim); TEST_ASSERT_EQUAL_FLOAT(EXPECTED_SHARED_CHARGE, shared_sched[0]); TEST_ASSERT_EQUAL_FLOAT(shared_sched[0], shared_sim[0]); } /* * test_charge_account_skips_reconciling_or_unavail_worker_host_charge - * bug-1799 fix: sched_charge_account()'s per-host resInUse charge must be * skipped for a worker currently IS_WORKER_RECONCILING or IS_WORKER_UNAVAIL. * core_reset_workers() (core_session.c, bug-1751) already skips RESETTING * such a worker's resInUse every round; sched_account_jobs()'s own * per-round re-derive pass reaches resInUse only through this same * sched_charge_account()->sched_update_account() path, so without a * matching skip here it re-added a full charge ON TOP of the not-reset * total every round the worker stayed RECONCILING/UNAVAIL -- resInUse grew * unboundedly and could exceed the resource's real capacity (live-observed: * a 22-slot worker showing used=26.00). core->sharedInUse has no * per-worker gating at all (its own reset, core_reset_shared_resval(), is * unconditional) and must keep charging normally regardless of any one * host's status. */ static void test_charge_account_skips_reconciling_or_unavail_worker_host_charge(void) { xce_scheduler_t sched; xce_job_account_t* acct; xce_sched_worker_t* w; float shared_val[1]; float host_val[1]; int shared_idx[1] = { SHARED_RESIDX }; int host_idx[1] = { HOST_RESIDX }; int rc; memset(&sched, 0, sizeof(sched)); setup_core_with_worker(&(sched.core), WORKER_NAME); w = sched_get_worker(&(sched.core), WORKER_NAME); TEST_ASSERT_NOT_NULL(w); acct = make_test_account(WORKER_NAME, SHARED_RESIDX, SHARED_USED, SHARED_RESERVED, HOST_RESIDX, HOST_USED, HOST_RESERVED); /* --- RECONCILING: shared charges normally, per-host charge skipped --- */ w->status = XCE_WORKER_RECONCILING; rc = sched_charge_account(&(sched.core), acct); TEST_ASSERT_EQUAL_INT(0, rc); get_resvals(&(sched.core.sharedInUse), 1, shared_idx, shared_val); TEST_ASSERT_EQUAL_FLOAT_MESSAGE( EXPECTED_SHARED_CHARGE, shared_val[0], "shared resources have no per-worker gating and must still charge " "while the host is RECONCILING"); get_resvals(&(w->resInUse), 1, host_idx, host_val); TEST_ASSERT_EQUAL_FLOAT_MESSAGE( 0.0f, host_val[0], "bug-1799: a RECONCILING worker's per-host resInUse must NOT be " "charged -- core_reset_workers() already skips resetting it, so " "charging on top would grow it every round it stays RECONCILING"); /* --- UNAVAIL: same per-host skip --- */ w->status = XCE_WORKER_UNAVAIL; rc = sched_charge_account(&(sched.core), acct); TEST_ASSERT_EQUAL_INT(0, rc); get_resvals(&(w->resInUse), 1, host_idx, host_val); TEST_ASSERT_EQUAL_FLOAT_MESSAGE( 0.0f, host_val[0], "bug-1799: an UNAVAIL worker's per-host resInUse must NOT be " "charged either -- the same reset-skip condition covers both bits"); /* --- status clears: normal charging resumes on the very next call --- */ w->status = 0; rc = sched_charge_account(&(sched.core), acct); TEST_ASSERT_EQUAL_INT(0, rc); get_resvals(&(w->resInUse), 1, host_idx, host_val); TEST_ASSERT_EQUAL_FLOAT_MESSAGE( EXPECTED_HOST_CHARGE, host_val[0], "once the worker's status bit clears, charging must resume " "normally on the very next call -- this is a per-call skip, not a " "permanent block"); } /* * resource-accounting.md's reservation-vs-actual-usage reconciliation for * a REAL (gauge-style) resource, e.g. "mem" -- sched_reconcile_host_ * resource_usage() (the single-account-entry primitive) and * sched_reconcile_job_resource_usage() (the rusages-report wrapper it's * built for). Both are defined in resource.c, already white-box #included * above. */ #define MEM_RESIDX 2 #define MEM_RESERVED_MB 4.0 /* make_mem_account - a single-host xce_job_account_t with one "mem" * xce_res_account_t entry (residx MEM_RESIDX, reserved MEM_RESERVED_MB, * used 0.0 -- a job that has just been dispatched and never had a rusage * tick applied yet). resname is real (strdup'd), unlike make_test_account's * residx-only entries above, since sched_reconcile_job_resource_usage() * looks accounts up BY NAME. */ static xce_job_account_t* make_mem_account(const char* worker_name) { xce_job_account_t* acct = (xce_job_account_t*)calloc(1, sizeof(*acct)); TEST_ASSERT_NOT_NULL(acct); acct->nhosts = 1; acct->accounts = (xce_host_account_t**)calloc(1, sizeof(*acct->accounts)); TEST_ASSERT_NOT_NULL(acct->accounts); acct->accounts[0] = (xce_host_account_t*)calloc(1, sizeof(xce_host_account_t)); TEST_ASSERT_NOT_NULL(acct->accounts[0]); acct->accounts[0]->host = strdup(worker_name); acct->accounts[0]->naccount = 1; acct->accounts[0]->accounts = (xce_res_account_t**)calloc(1, sizeof(*acct->accounts[0]->accounts)); TEST_ASSERT_NOT_NULL(acct->accounts[0]->accounts); acct->accounts[0]->accounts[0] = (xce_res_account_t*)calloc(1, sizeof(xce_res_account_t)); TEST_ASSERT_NOT_NULL(acct->accounts[0]->accounts[0]); acct->accounts[0]->accounts[0]->resname = strdup("mem"); acct->accounts[0]->accounts[0]->residx = MEM_RESIDX; acct->accounts[0]->accounts[0]->used = 0.0; acct->accounts[0]->accounts[0]->reserved = MEM_RESERVED_MB; acct->accounts[0]->accounts[0]->allocated = MEM_RESERVED_MB; return acct; } static xce_job_rusages_t* make_mem_rusages(float memory_rss_mb) { xce_job_rusages_t* r = (xce_job_rusages_t*)calloc(1, sizeof(*r)); TEST_ASSERT_NOT_NULL(r); r->count = 1; r->usages = (xce_rusage_t**)calloc(1, sizeof(*r->usages)); TEST_ASSERT_NOT_NULL(r->usages); r->usages[0] = (xce_rusage_t*)calloc(1, sizeof(xce_rusage_t)); TEST_ASSERT_NOT_NULL(r->usages[0]); r->usages[0]->resname = strdup("memory_rss"); r->usages[0]->value = memory_rss_mb; return r; } static float get_worker_resinuse(xce_sched_worker_t* w, int residx) { int idx[1] = { residx }; float val[1]; get_resvals(&(w->resInUse), 1, idx, val); return val[0]; } /* * test_reconcile_host_resource_usage_below_reservation_keeps_reservation - * resource-accounting.md case (a): actual usage below the reservation must * still charge the FULL reservation (used stays 0, since overage is 0). */ static void test_reconcile_host_resource_usage_below_reservation_keeps_reservation(void) { xce_scheduler_t sched; xce_sched_worker_t* w; xce_res_account_t acct_entry = { 0 }; int rc; memset(&sched, 0, sizeof(sched)); setup_core_with_worker(&(sched.core), WORKER_NAME); w = sched_get_worker(&(sched.core), WORKER_NAME); TEST_ASSERT_NOT_NULL(w); acct_entry.residx = MEM_RESIDX; acct_entry.reserved = MEM_RESERVED_MB; acct_entry.used = 0.0; /* First charge: reservation only (mirrors sched_charge_account() at * dispatch time -- used=0, reserved=4 -> pool charge 4). */ rc = sched_reconcile_host_resource_usage(&(sched.core), w, &acct_entry, 0.0); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_FLOAT(0.0, (float)acct_entry.used); /* actual usage (1MB) is below the 4MB reservation -- used must stay 0, * the pool must still show the full 4MB reservation charged, not the * 1MB actually in use. */ rc = sched_reconcile_host_resource_usage(&(sched.core), w, &acct_entry, 1.0); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_FLOAT(0.0, (float)acct_entry.used); } /* * test_reconcile_host_resource_usage_above_reservation_charges_actual - * resource-accounting.md case (b): actual usage at/above the reservation * must charge the REAL usage instead of the stale reservation. */ static void test_reconcile_host_resource_usage_above_reservation_charges_actual(void) { xce_scheduler_t sched; xce_sched_worker_t* w; xce_res_account_t acct_entry = { 0 }; int rc; int idx[1] = { MEM_RESIDX }; float val[1]; memset(&sched, 0, sizeof(sched)); setup_core_with_worker(&(sched.core), WORKER_NAME); w = sched_get_worker(&(sched.core), WORKER_NAME); TEST_ASSERT_NOT_NULL(w); acct_entry.residx = MEM_RESIDX; acct_entry.reserved = MEM_RESERVED_MB; acct_entry.used = 0.0; /* Seed the pool with the initial reservation charge, exactly as * sched_charge_account() would at dispatch time. */ val[0] = (float)(acct_entry.used + acct_entry.reserved); put_resvals(&(w->resInUse), 1, idx, val); /* actual usage (6MB) EXCEEDS the 4MB reservation -- overage = 2MB, so * used must become 2 and the pool must show 6 (reserved 4 + overage 2 = * actual 6), not the stale 4MB reservation. */ rc = sched_reconcile_host_resource_usage(&(sched.core), w, &acct_entry, 6.0); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_FLOAT(2.0, (float)acct_entry.used); TEST_ASSERT_EQUAL_FLOAT(6.0, get_worker_resinuse(w, MEM_RESIDX)); /* usage drops back to 3MB (still >= reserved? no: 3 < 4) -- back to * charging the flat reservation, pool returns to 4. */ rc = sched_reconcile_host_resource_usage(&(sched.core), w, &acct_entry, 3.0); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_FLOAT(0.0, (float)acct_entry.used); TEST_ASSERT_EQUAL_FLOAT(4.0, get_worker_resinuse(w, MEM_RESIDX)); } /* * test_reconcile_host_resource_usage_repeated_growth_applies_only_delta - * confirms the pool is updated by DELTA each call, not a full recompute * that would double-count a sibling resource's own charge in the same * pool array (resource.c's own doc comment on this function). */ static void test_reconcile_host_resource_usage_repeated_growth_applies_only_delta(void) { xce_scheduler_t sched; xce_sched_worker_t* w; xce_res_account_t acct_entry = { 0 }; int slots_idx[1] = { 0 }; /* a sibling resource */ float slots_val[1] = { 7.0f }; int rc; memset(&sched, 0, sizeof(sched)); setup_core_with_worker(&(sched.core), WORKER_NAME); w = sched_get_worker(&(sched.core), WORKER_NAME); TEST_ASSERT_NOT_NULL(w); /* A sibling resource ("slots", a different residx) already charged in * the same pool array -- must be untouched by every call below. */ put_resvals(&(w->resInUse), 1, slots_idx, slots_val); acct_entry.residx = MEM_RESIDX; acct_entry.reserved = MEM_RESERVED_MB; acct_entry.used = 0.0; rc = sched_reconcile_host_resource_usage(&(sched.core), w, &acct_entry, 5.0); /* overage 1 */ TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_FLOAT(1.0, get_worker_resinuse(w, MEM_RESIDX)); rc = sched_reconcile_host_resource_usage(&(sched.core), w, &acct_entry, 9.0); /* overage 5 -- delta +4 */ TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_FLOAT(5.0, get_worker_resinuse(w, MEM_RESIDX)); /* Sibling resource never touched by any of the above. */ TEST_ASSERT_EQUAL_FLOAT(7.0, get_worker_resinuse(w, 0)); } static void test_reconcile_host_resource_usage_null_args_return_einval(void) { xce_scheduler_t sched; xce_res_account_t acct_entry = { 0 }; memset(&sched, 0, sizeof(sched)); setup_core_with_worker(&(sched.core), WORKER_NAME); TEST_ASSERT_EQUAL_INT( -EINVAL, sched_reconcile_host_resource_usage(NULL, NULL, NULL, 0.0)); TEST_ASSERT_EQUAL_INT(-EINVAL, sched_reconcile_host_resource_usage( &(sched.core), NULL, &acct_entry, 0.0)); } /* * test_reconcile_host_resource_usage_skips_reconciling_or_unavail_worker - * bug-1814 follow-up: sched_reconcile_host_resource_usage() is a THIRD * w->resInUse-mutating path (alongside sched_charge_account()/sched_ * uncharge_account(), both already gated by bug-1801) that must respect the * same "frozen while RECONCILING/UNAVAIL" invariant core_reset_workers() * (bug-1751) and sched_update_account() (bug-1801) already share -- a live * replay showed a job's "mem used" growing and never self-correcting until * the job finished, root-caused to this exact gap: a rusage tick landing * mid-resync kept mutating resInUse (and advancing acct_entry->used) on top * of a pool a concurrent per-round rebuild was deliberately leaving alone. */ static void test_reconcile_host_resource_usage_skips_reconciling_or_unavail_worker(void) { xce_scheduler_t sched; xce_sched_worker_t* w; xce_res_account_t acct_entry = { 0 }; int rc; memset(&sched, 0, sizeof(sched)); setup_core_with_worker(&(sched.core), WORKER_NAME); w = sched_get_worker(&(sched.core), WORKER_NAME); TEST_ASSERT_NOT_NULL(w); acct_entry.residx = MEM_RESIDX; acct_entry.reserved = MEM_RESERVED_MB; acct_entry.used = 0.0; /* --- RECONCILING: no mutation at all, not to the pool, not to * acct_entry->used --- */ w->status = XCE_WORKER_RECONCILING; rc = sched_reconcile_host_resource_usage(&(sched.core), w, &acct_entry, 9.0); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_FLOAT_MESSAGE( 0.0f, get_worker_resinuse(w, MEM_RESIDX), "bug-1814: a RECONCILING worker's resInUse must not be touched by a " "rusage tick -- it is deliberately frozen until a clean per-round " "rebuild can reconcile it, same invariant bug-1801 already " "restores for the regular charge path"); TEST_ASSERT_EQUAL_FLOAT_MESSAGE( 0.0, (float)acct_entry.used, "the account's own used baseline must also stay untouched while " "skipped, so the eventual real reconcile call sees a clean delta"); /* --- UNAVAIL: same skip --- */ w->status = XCE_WORKER_UNAVAIL; rc = sched_reconcile_host_resource_usage(&(sched.core), w, &acct_entry, 9.0); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_FLOAT_MESSAGE( 0.0f, get_worker_resinuse(w, MEM_RESIDX), "bug-1814: an UNAVAIL worker's resInUse must not be touched either"); /* --- status clears: reconciliation resumes normally on the very next * call, not a permanent block --- */ w->status = 0; rc = sched_reconcile_host_resource_usage(&(sched.core), w, &acct_entry, 9.0); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_FLOAT_MESSAGE( 5.0, (float)acct_entry.used, "overage = 9 - 4 reserved = 5, applied on the first call once the " "worker is no longer RECONCILING/UNAVAIL"); TEST_ASSERT_EQUAL_FLOAT(5.0, get_worker_resinuse(w, MEM_RESIDX)); } /* * test_reconcile_job_resource_usage_* - the rusages-report wrapper: resname * mapping ("memory_rss" on the wire -> "mem" in the account), the single- * host scope, and the no-request no-op. */ static void test_reconcile_job_resource_usage_maps_memory_rss_to_mem_account(void) { xce_scheduler_t sched; xce_sched_worker_t* w; xce_job_account_t* jacct; xce_job_rusages_t* rusages; int rc; memset(&sched, 0, sizeof(sched)); setup_core_with_worker(&(sched.core), WORKER_NAME); w = sched_get_worker(&(sched.core), WORKER_NAME); TEST_ASSERT_NOT_NULL(w); jacct = make_mem_account(WORKER_NAME); rusages = make_mem_rusages(7.0f); /* > 4MB reserved -> overage 3 */ /* Seed the pool with the initial dispatch-time reservation charge * (mirrors what sched_charge_account()/sched_commit_job_alloc() would * already have done before any rusage tick ever arrives -- the * function under test only ever applies a DELTA on top of this, per * its own doc comment, never a from-scratch total). */ { int idx[1] = { MEM_RESIDX }; float val[1] = { (float)MEM_RESERVED_MB }; put_resvals(&(w->resInUse), 1, idx, val); } rc = sched_reconcile_job_resource_usage(&(sched.core), jacct, rusages); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_FLOAT(3.0, (float)jacct->accounts[0]->accounts[0]->used); TEST_ASSERT_EQUAL_FLOAT(7.0, get_worker_resinuse(w, MEM_RESIDX)); } static void test_reconcile_job_resource_usage_skips_multihost_jobs(void) { xce_scheduler_t sched; xce_job_account_t jacct = { 0 }; xce_job_rusages_t* rusages = make_mem_rusages(99.0f); int rc; memset(&sched, 0, sizeof(sched)); setup_core_with_worker(&(sched.core), WORKER_NAME); jacct.nhosts = 2; /* multi-host: no per-report host attribution possible * (see sched_reconcile_job_resource_usage()'s own * doc comment) -- must be a documented no-op. */ rc = sched_reconcile_job_resource_usage(&(sched.core), &jacct, rusages); TEST_ASSERT_EQUAL_INT(0, rc); } /* * test_reconcile_job_resource_usage_charges_unreserved_job_full_usage - * resource-accounting.md's correction: a job with NO rusage[mem=...] at * all is reserved=0, but that must NOT mean "nothing to charge". This * codebase's admission check has no live OS read at decision time (see * the steering doc) -- w->resInUse is the ONLY place a job's real memory * footprint is ever reflected, so an unreserved job's actual usage must * still be charged in full (max(0, actual) == actual), or the scheduler's * own capacity tracking silently under-counts real consumption and can * over-admit new jobs onto memory a live, unreserved job is genuinely * sitting on. */ static void test_reconcile_job_resource_usage_charges_unreserved_job_full_usage(void) { xce_scheduler_t sched; xce_sched_worker_t* w; xce_job_account_t* jacct; xce_job_rusages_t* rusages = make_mem_rusages(5.0f); xce_resource_t mem_res = { 0 }; xce_resource_t* registered; int rc; memset(&sched, 0, sizeof(sched)); setup_core_with_worker(&(sched.core), WORKER_NAME); w = sched_get_worker(&(sched.core), WORKER_NAME); TEST_ASSERT_NOT_NULL(w); /* "mem" must be a real, registered resource for ensure_host_account_ * by_resname()'s key_find() lookup to resolve a residx -- mirrors how * a real cluster config would already have it registered long before * any job ever runs. */ mem_res.name = strdup(SCHED_MEM_RESOURCE_NAME); registered = makeResource(sched.core.resmgr, &mem_res); TEST_ASSERT_NOT_NULL(registered); /* A single-host account with NO "mem" entry at all -- the job never * requested rusage[mem=...]. */ jacct = (xce_job_account_t*)calloc(1, sizeof(*jacct)); jacct->nhosts = 1; jacct->accounts = (xce_host_account_t**)calloc(1, sizeof(*jacct->accounts)); jacct->accounts[0] = (xce_host_account_t*)calloc(1, sizeof(xce_host_account_t)); jacct->accounts[0]->host = strdup(WORKER_NAME); jacct->accounts[0]->naccount = 0; rc = sched_reconcile_job_resource_usage(&(sched.core), jacct, rusages); TEST_ASSERT_EQUAL_INT(0, rc); /* An account entry now exists (created on first use), reserved=0, * used == the full actual usage, and the pool reflects it. */ TEST_ASSERT_EQUAL_INT(1, jacct->accounts[0]->naccount); TEST_ASSERT_EQUAL_STRING(SCHED_MEM_RESOURCE_NAME, jacct->accounts[0]->accounts[0]->resname); TEST_ASSERT_EQUAL_FLOAT(0.0, (float)jacct->accounts[0]->accounts[0]->reserved); TEST_ASSERT_EQUAL_FLOAT(5.0, (float)jacct->accounts[0]->accounts[0]->used); TEST_ASSERT_EQUAL_FLOAT( 5.0, get_worker_resinuse(w, jacct->accounts[0]->accounts[0]->residx)); } static void test_reconcile_job_resource_usage_noop_when_mem_not_a_registered_resource(void) { xce_scheduler_t sched; xce_job_account_t* jacct; xce_job_rusages_t* rusages = make_mem_rusages(99.0f); int rc; memset(&sched, 0, sizeof(sched)); setup_core_with_worker(&(sched.core), WORKER_NAME); /* Deliberately do NOT register "mem" into sched.core.resmgr -- a * misconfigured cluster with no such resource defined at all. */ jacct = (xce_job_account_t*)calloc(1, sizeof(*jacct)); jacct->nhosts = 1; jacct->accounts = (xce_host_account_t**)calloc(1, sizeof(*jacct->accounts)); jacct->accounts[0] = (xce_host_account_t*)calloc(1, sizeof(xce_host_account_t)); jacct->accounts[0]->host = strdup(WORKER_NAME); jacct->accounts[0]->naccount = 0; rc = sched_reconcile_job_resource_usage(&(sched.core), jacct, rusages); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_INT(0, jacct->accounts[0]->naccount); } static void test_reconcile_job_resource_usage_null_args_return_einval(void) { xce_scheduler_t sched; memset(&sched, 0, sizeof(sched)); setup_core_with_worker(&(sched.core), WORKER_NAME); TEST_ASSERT_EQUAL_INT(-EINVAL, sched_reconcile_job_resource_usage(NULL, NULL, NULL)); } /* * test_ctx_uncommit_host_reverses_provisional_debit - Requirement 4.2 * (scheduler-allocation spec, task 1.4): sched_ctx_uncommit_host() * (resource.c, new) reverses exactly the provisional per-host resource * debit an earlier successful host-claim made. * * Debits ctx->uncomitted.hosts[idx] directly via xvec_idval(..., * XVEC_ADD_FLOAT) -- the exact call sched_try_alloc_on_host_() makes on a * successful host claim (resource.c line ~544) -- rather than driving the * full plugin_prepare_allocation()/sched_check_host_satisfy() chain, which * needs a real worker/select-expression/resmgr fixture unrelated to what * this rollback primitive itself does (this file's own convention: see * make_test_account()'s residx-only shortcut above for the same rationale). * Then calls the new sched_ctx_uncommit_host() and asserts the per-host * uncommitted vector is back to its pre-debit (zero) baseline -- proving * debit-then-reverse leaves provisional usage unchanged, this task's * Observable. * * sched_ctx_get_uncomit_host() (the baseline/re-read get-or-create * accessor called below) and host_uncommit_killer() (the zarray item * killer passed to zarray_init() below) are both the REAL functions * white-box #included from schedobj.c above, not reimplementations -- * host_uncommit_killer() is `static` in schedobj.c, but the #include * pastes it into this same translation unit, so it resolves normally. * This is the same "exercise the real functions, not a re-implementation * of any of them" precedent test_picker_config_fields.c already * established for combining core.c + schedobj.c in one white-box test. */ #define UNCOMMIT_HOST_IDX 3 #define UNCOMMIT_RESIDX 0 #define UNCOMMIT_DEBIT 2.5f static void test_ctx_uncommit_host_reverses_provisional_debit(void) { xce_sched_ctx_t ctx; xvec_t* uncomit = NULL; xvec_idval_t committed; float* data; int rc; memset(&ctx, 0, sizeof(ctx)); rc = zarray_init(&(ctx.uncomitted.hosts), 8, (zarray_item_killer_t)host_uncommit_killer); TEST_ASSERT_EQUAL_INT(0, rc); rc = xvec_idval_init(&committed, 4); TEST_ASSERT_EQUAL_INT(0, rc); rc = xvec_idval_store(&committed, UNCOMMIT_RESIDX, UNCOMMIT_DEBIT); TEST_ASSERT_TRUE(rc >= 0); /* Baseline: a freshly created per-host uncommitted vector reads 0. */ rc = sched_ctx_get_uncomit_host(&uncomit, &ctx, UNCOMMIT_HOST_IDX); TEST_ASSERT_EQUAL_INT(1, rc); TEST_ASSERT_NOT_NULL(uncomit); data = (float*)uncomit->data; TEST_ASSERT_EQUAL_FLOAT(0.0f, data[UNCOMMIT_RESIDX]); /* Debit: the exact commit call sched_try_alloc_on_host_() makes on a * successful host claim. */ rc = xvec_idval(uncomit, &committed, XVEC_ADD_FLOAT); TEST_ASSERT_EQUAL_INT(committed.count, rc); TEST_ASSERT_EQUAL_FLOAT(UNCOMMIT_DEBIT, data[UNCOMMIT_RESIDX]); /* Reverse: the new primitive under test. */ rc = sched_ctx_uncommit_host(&ctx, UNCOMMIT_HOST_IDX, &committed); TEST_ASSERT_EQUAL_INT(committed.count, rc); /* Back to the pre-debit baseline -- same vector instance, re-read. */ TEST_ASSERT_EQUAL_FLOAT(0.0f, data[UNCOMMIT_RESIDX]); /* Same per-host vector instance is returned again on a second lookup * (get-or-create semantics unchanged by the reversal). */ uncomit = NULL; rc = sched_ctx_get_uncomit_host(&uncomit, &ctx, UNCOMMIT_HOST_IDX); TEST_ASSERT_EQUAL_INT(1, rc); TEST_ASSERT_EQUAL_FLOAT(0.0f, ((float*)uncomit->data)[UNCOMMIT_RESIDX]); } /* * bug-1789/1790, limit-plugin-ownership refactor: sched_fill_limit_requests() * (resource.c) computes, for each matched limit, how many units of the * limit's own virtual resource (vres->vres) a job wants -- by reading how * many units of the limit's UNDERLYING resource (vres->res) the job already * requested. The write destination is one of alloc_req->shared/.host, * chosen by vres->vres's own XCE_RESF_GLOBAL flag (set by instantiate_ * limit()->add_vres() from the same is_host_based_limit() call that put * this limit in obj->glimits vs obj->hlimits in the first place). * * The read source depends on BOTH flags together: three of the four * (scope, underlying-resource) combinations read straight out of alloc_req * (already populated by whichever allocator plugin ran). This test proves * the fourth, different-in-kind combination (bug-1789's actual reported * case, generalized in bug-1790): a GLOBAL limit (write target * alloc_req->shared) on a per-host underlying resource that IS a normal * declared rusage -- e.g. "mem" (a future global mem cap, bug-1790's own * regression coverage) -- alloc_req->host never holds a job-WIDE total for * this (only per-host/per-bucket-specific data for the real per-host * capacity check), so the want is computed directly from the job's own * rusages instead, via calc_wanted_job_total_resval() (resreq.c) -- * PER_TASK: value * nslots, using nslots=3 here specifically to prove the * job's WHOLE total is used, not any one host's/bucket's share (this is * the limit-evaluation layer, allocator-agnostic -- no seqjob/parjob- * specific test needed for this any more). "slots" is deliberately NOT * used as the underlying resource here -- see the sibling test below for * why it needs its own, separate coverage. */ static void test_fill_limit_requests_global_limit_on_host_resource_reads_host(void) { xce_scheduler_t sched; xce_resource_t mem_res_in = { 0 }; xce_resource_t limit_res_in = { 0 }; xce_resource_t* mem_res; xce_resource_t* limit_res; xce_sched_alloc_req_t alloc_req; xlist_t limits; xce_vres_t vres; xlist_t rusages; rusage_value_t rv; int rc; memset(&sched, 0, sizeof(sched)); TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core))); /* "mem" -- deliberately NOT XCE_RESF_GLOBAL, matching the real * makeResource() call every host-capacity resource in this codebase * goes through, AND (unlike "slots") a resource that genuinely * appears as a normal declared rusage entry when requested via * -R "rusage[mem=...]" -- the real production shape this test's * on-demand rusages lookup needs to match. */ mem_res_in.name = strdup(SCHED_MEM_RESOURCE_NAME); mem_res = makeResource(sched.core.resmgr, &mem_res_in); TEST_ASSERT_NOT_NULL(mem_res); /* the LIMIT's own virtual resource -- GLOBAL, exactly as * instantiate_limit()->add_vres() would flag it for a limit like * ug1_global_mem (is_host_based_limit() == 0). */ limit_res_in.name = strdup("limit=ug1_global_mem"); limit_res_in.flags = XCE_RESF_GLOBAL; limit_res = makeResource(sched.core.resmgr, &limit_res_in); TEST_ASSERT_NOT_NULL(limit_res); memset(&vres, 0, sizeof(vres)); vres.res = mem_res; vres.vres = limit_res; rc = xlist_init(&limits, 4); TEST_ASSERT_EQUAL_INT(0, rc); rc = xlist_store(&limits, &vres); TEST_ASSERT_TRUE(rc >= 0); /* the job's own declared "mem" rusage -- 2.0/task, 3 tasks total * (nslots below), for an expected job-wide total of 6.0. */ rc = xlist_init(&rusages, 4); TEST_ASSERT_EQUAL_INT(0, rc); memset(&rv, 0, sizeof(rv)); rv.res = strdup(SCHED_MEM_RESOURCE_NAME); rv.value = 2.0f; rv.per_flag = PER_TASK; rc = xlist_store(&rusages, &rv); TEST_ASSERT_TRUE(rc >= 0); rc = xvec_idval_init(&(alloc_req.shared), 8); TEST_ASSERT_EQUAL_INT(0, rc); rc = xvec_idval_init(&(alloc_req.host), 8); TEST_ASSERT_EQUAL_INT(0, rc); rc = sched_fill_limit_requests(&alloc_req, &limits, &rusages, 1, 3); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_INT(1, alloc_req.shared.count); TEST_ASSERT_EQUAL_INT(limit_res->idx, ((int*)alloc_req.shared.idxs.data)[0]); TEST_ASSERT_EQUAL_FLOAT(6.0f, ((float*)alloc_req.shared.vals.data)[0]); /* .host is untouched -- the limit's own vres entry went to .shared, * and this combination never reads/writes .host at all. */ TEST_ASSERT_EQUAL_INT(0, alloc_req.host.count); xlist_fini(&rusages); FREEUP(rv.res); xlist_fini(&limits); } /* * bug-1790 regression (found live, gitea#46): "slots" is NEVER a declared * rusage entry -- seqjob_prepare_allocation()/parjob_prepare_allocation() * derive its want directly from the job's own task count (1.0 for seqjob, * always maxTasks==1; (float)ctx->nslots for parjob), via their own * unconditional key_find("slots", ...) + store into alloc_req->host, * regardless of phase, entirely outside the normal rusages loop. The * general on-demand path (calc_wanted_job_total_resval(), keyed by * scanning rusages for a matching entry) always misses for "slots" as a * result -- confirmed live against a fresh deploy of limits.yaml's actual * ug1_slots/ug1_normal_slots/limit1 (all global, slots-resname'd): * matching worked, but every job's "wanted" silently stayed 0, so the caps * never bound at all, reintroducing bug-1789's exact original symptom. * * sched_fill_limit_requests() (resource.c) now reads alloc_req->host FIRST * for this combination -- since the allocator's own unconditional "add * slots" step already puts exactly the job-wide total there, no name-based * special case is needed, just reading the data that's already staged -- * and only falls back to the rusages-based computation when nothing is * staged (any OTHER per-host resource). This test proves the "slots" path * by pre-staging alloc_req->host exactly as the allocator plugins already * do (value == the job's whole nslots, matching parjob's own (float)ctx-> * nslots store) and leaving rusages EMPTY -- the real production shape, * unlike this test's own first version, which artificially added a * "slots" rusages entry and masked this exact gap. */ static void test_fill_limit_requests_global_limit_on_slots_reads_host(void) { xce_scheduler_t sched; xce_resource_t slots_res_in = { 0 }; xce_resource_t limit_res_in = { 0 }; xce_resource_t* slots_res; xce_resource_t* limit_res; xce_sched_alloc_req_t alloc_req; xlist_t limits; xce_vres_t vres; xlist_t rusages; int rc; memset(&sched, 0, sizeof(sched)); TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core))); slots_res_in.name = strdup("slots"); slots_res = makeResource(sched.core.resmgr, &slots_res_in); TEST_ASSERT_NOT_NULL(slots_res); limit_res_in.name = strdup("limit=ug1_normal_slots"); limit_res_in.flags = XCE_RESF_GLOBAL; limit_res = makeResource(sched.core.resmgr, &limit_res_in); TEST_ASSERT_NOT_NULL(limit_res); memset(&vres, 0, sizeof(vres)); vres.res = slots_res; vres.vres = limit_res; rc = xlist_init(&limits, 4); TEST_ASSERT_EQUAL_INT(0, rc); rc = xlist_store(&limits, &vres); TEST_ASSERT_TRUE(rc >= 0); /* deliberately EMPTY -- "slots" never appears here in real production * either. */ rc = xlist_init(&rusages, 4); TEST_ASSERT_EQUAL_INT(0, rc); rc = xvec_idval_init(&(alloc_req.shared), 8); TEST_ASSERT_EQUAL_INT(0, rc); rc = xvec_idval_init(&(alloc_req.host), 8); TEST_ASSERT_EQUAL_INT(0, rc); /* pre-staged into .host, exactly as seqjob/parjob_prepare_allocation() * always do for "slots" regardless of phase. */ rc = xvec_idval_store(&(alloc_req.host), slots_res->idx, 4.0f); TEST_ASSERT_TRUE(rc >= 0); rc = sched_fill_limit_requests(&alloc_req, &limits, &rusages, 1, 4); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_INT(1, alloc_req.shared.count); TEST_ASSERT_EQUAL_INT(limit_res->idx, ((int*)alloc_req.shared.idxs.data)[0]); TEST_ASSERT_EQUAL_FLOAT(4.0f, ((float*)alloc_req.shared.vals.data)[0]); /* .host stays exactly as pre-staged -- the limit's own vres entry went * to .shared, nothing new was written to .host. */ TEST_ASSERT_EQUAL_INT(1, alloc_req.host.count); xlist_fini(&rusages); xlist_fini(&limits); } /* * bug-1789 (already-working sibling case, unaffected by the fix): a * HOST-scoped limit (its own vres NOT GLOBAL-flagged, so its write * target is alloc_req->host) on a per-host resource (e.g. "mem", also * not XCE_RESF_GLOBAL) reads from alloc_req->host too -- same vector as * its own write target, exactly as before this fix. */ static void test_fill_limit_requests_host_limit_on_host_resource_reads_host(void) { xce_scheduler_t sched; xce_resource_t mem_res_in = { 0 }; xce_resource_t limit_res_in = { 0 }; xce_resource_t* mem_res; xce_resource_t* limit_res; xce_sched_alloc_req_t alloc_req; xlist_t limits; xce_vres_t vres; int rc; memset(&sched, 0, sizeof(sched)); TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core))); mem_res_in.name = strdup(SCHED_MEM_RESOURCE_NAME); mem_res = makeResource(sched.core.resmgr, &mem_res_in); TEST_ASSERT_NOT_NULL(mem_res); /* the LIMIT's own virtual resource -- NOT GLOBAL (host-scoped limit, * is_host_based_limit() == 1); flags left at the struct's zero * default is sufficient, mirroring add_vres()'s XCE_RESF_DEFAULT_HOST * (not GLOBAL) branch. */ limit_res_in.name = strdup("limit=mem_cap"); limit_res = makeResource(sched.core.resmgr, &limit_res_in); TEST_ASSERT_NOT_NULL(limit_res); memset(&vres, 0, sizeof(vres)); vres.res = mem_res; vres.vres = limit_res; rc = xlist_init(&limits, 4); TEST_ASSERT_EQUAL_INT(0, rc); rc = xlist_store(&limits, &vres); TEST_ASSERT_TRUE(rc >= 0); rc = xvec_idval_init(&(alloc_req.shared), 8); TEST_ASSERT_EQUAL_INT(0, rc); rc = xvec_idval_init(&(alloc_req.host), 8); TEST_ASSERT_EQUAL_INT(0, rc); /* pre-staged, as calc_wanted_host_resvals() would have already done * for a real "mem" rusage want. */ rc = xvec_idval_store(&(alloc_req.host), mem_res->idx, 3.0f); TEST_ASSERT_TRUE(rc >= 0); rc = sched_fill_limit_requests(&alloc_req, &limits, NULL, 0, 0); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_INT(2, alloc_req.host.count); TEST_ASSERT_EQUAL_INT(limit_res->idx, ((int*)alloc_req.host.idxs.data)[1]); TEST_ASSERT_EQUAL_FLOAT(3.0f, ((float*)alloc_req.host.vals.data)[1]); xlist_fini(&limits); } /* * bug-1789 (symmetric case, proves the dispatch is a genuine flag-based * rule and not a "slots" special case): a HOST-scoped limit (its own * vres NOT GLOBAL-flagged, so its write target is alloc_req->host) on a * resource that IS XCE_RESF_GLOBAL (e.g. a pooled license count) must * read from alloc_req->shared instead -- the opposite vector from its * write target, mirroring exactly how calc_wanted_global_resvals() is * the only one of the pair that ever populates a GLOBAL-flagged * resource's want. */ static void test_fill_limit_requests_host_limit_on_global_resource_reads_shared(void) { xce_scheduler_t sched; xce_resource_t license_res_in = { 0 }; xce_resource_t limit_res_in = { 0 }; xce_resource_t* license_res; xce_resource_t* limit_res; xce_sched_alloc_req_t alloc_req; xlist_t limits; xce_vres_t vres; int rc; memset(&sched, 0, sizeof(sched)); TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core))); license_res_in.name = strdup("licenses"); license_res_in.flags = XCE_RESF_GLOBAL; license_res = makeResource(sched.core.resmgr, &license_res_in); TEST_ASSERT_NOT_NULL(license_res); /* the LIMIT's own virtual resource -- NOT GLOBAL (host-scoped limit), * same rationale as the sibling test above. */ limit_res_in.name = strdup("limit=license_cap"); limit_res = makeResource(sched.core.resmgr, &limit_res_in); TEST_ASSERT_NOT_NULL(limit_res); memset(&vres, 0, sizeof(vres)); vres.res = license_res; vres.vres = limit_res; rc = xlist_init(&limits, 4); TEST_ASSERT_EQUAL_INT(0, rc); rc = xlist_store(&limits, &vres); TEST_ASSERT_TRUE(rc >= 0); rc = xvec_idval_init(&(alloc_req.shared), 8); TEST_ASSERT_EQUAL_INT(0, rc); rc = xvec_idval_init(&(alloc_req.host), 8); TEST_ASSERT_EQUAL_INT(0, rc); /* pre-staged into .shared (this job's earlier global-phase pass), NOT * .host -- .host (this limit's own write target, derived from * limit_res's non-GLOBAL flag) starts empty. */ rc = xvec_idval_store(&(alloc_req.shared), license_res->idx, 5.0f); TEST_ASSERT_TRUE(rc >= 0); rc = sched_fill_limit_requests(&alloc_req, &limits, NULL, 0, 0); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_INT(1, alloc_req.host.count); TEST_ASSERT_EQUAL_INT(limit_res->idx, ((int*)alloc_req.host.idxs.data)[0]); TEST_ASSERT_EQUAL_FLOAT(5.0f, ((float*)alloc_req.host.vals.data)[0]); xlist_fini(&limits); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_charge_account_identical_across_core_containers); RUN_TEST(test_commit_job_alloc_identical_across_core_containers); RUN_TEST(test_charge_account_skips_reconciling_or_unavail_worker_host_charge); RUN_TEST( test_reconcile_host_resource_usage_below_reservation_keeps_reservation); RUN_TEST( test_reconcile_host_resource_usage_above_reservation_charges_actual); RUN_TEST( test_reconcile_host_resource_usage_repeated_growth_applies_only_delta); RUN_TEST(test_reconcile_host_resource_usage_null_args_return_einval); RUN_TEST( test_reconcile_host_resource_usage_skips_reconciling_or_unavail_worker); RUN_TEST(test_reconcile_job_resource_usage_maps_memory_rss_to_mem_account); RUN_TEST(test_reconcile_job_resource_usage_skips_multihost_jobs); RUN_TEST(test_reconcile_job_resource_usage_charges_unreserved_job_full_usage); RUN_TEST(test_reconcile_job_resource_usage_noop_when_mem_not_a_registered_resource); RUN_TEST(test_reconcile_job_resource_usage_null_args_return_einval); RUN_TEST(test_ctx_uncommit_host_reverses_provisional_debit); RUN_TEST(test_fill_limit_requests_global_limit_on_host_resource_reads_host); RUN_TEST(test_fill_limit_requests_global_limit_on_slots_reads_host); RUN_TEST(test_fill_limit_requests_host_limit_on_host_resource_reads_host); RUN_TEST(test_fill_limit_requests_host_limit_on_global_resource_reads_shared); return UNITY_END(); }