Task 2.4: outcome capture moves to sched_trace_outcome.c and the position/host/summary/rollback half of the model to sched_trace_scan.c; shared helpers go behind sched_trace_internal.h (sti_ prefix). Code is moved unchanged; every target that compiles the trace sources gets all four, and the core isolation scan covers the new files. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1088 lines
42 KiB
C
1088 lines
42 KiB
C
/*
|
|
* test_sim_resource_contention.c - resource-contention replay fixture
|
|
* (scheduler-sim-decision-parity spec, task 1.2, Req 2.1/2.2).
|
|
*
|
|
* Why this fixture exists: every existing `.replay` fixture in this
|
|
* directory (test_sim_equivalence.c/test_repl_equivalence.c/
|
|
* test_sim_verify.c/etc.) uses jobs with a trivially-satisfiable resreq
|
|
* ("type == any") against a core that never has a worker with limited
|
|
* capacity -- capacity is never actually scarce, so no existing fixture can
|
|
* exercise or prove capacity-check correctness (design.md's own File
|
|
* Structure Plan entry for this task: "required because existing fixtures
|
|
* evidently don't exercise this path"). This file builds ONE recorded
|
|
* normal-mode session where THREE jobs compete for a worker with real,
|
|
* insufficient total capacity, so at least one job is genuinely denied
|
|
* capacity a currently-running job is holding.
|
|
*
|
|
* Fixture-construction technique: the SAME dual-phase pattern
|
|
* test_sim_equivalence.c already establishes (real normal-mode recording via
|
|
* sched_record_start()/sched_record_stop() driving the REAL builtin_fcfs
|
|
* picker + a "stand-in allocator" thread standing in for scheduler.c's own
|
|
* sched_allocator() -- that file's own header comment explains why a
|
|
* stand-in is used instead of the real allocator: the real one pulls in
|
|
* resreq-DSL compilation/plugin machinery this whole test-file family
|
|
* deliberately avoids). This file's stand-in allocator differs from
|
|
* test_sim_equivalence.c's own (which unconditionally PENDs every
|
|
* candidate): it drives the REAL, unmodified resource.c capacity-check and
|
|
* accounting primitives directly -- sched_check_host_satisfy() (genuine
|
|
* total-vs-inuse-vs-wanted arithmetic against a real xce_sched_worker_t) and
|
|
* sched_charge_account()/sched_commit_job_alloc() (task 1.1's relocated
|
|
* accounting functions, now xce_sched_core_t*-based) -- against a real
|
|
* worker added via sched_add_worker() with a real, finite capacity (10.0
|
|
* units of a resource, residx set directly the same way
|
|
* test_resource_accounting.c's own make_test_account() does, bypassing
|
|
* resmgr name resolution -- orthogonal to what capacity contention proves).
|
|
* This means every DISPATCH/PEND decision this fixture records is a
|
|
* genuine, real-arithmetic outcome of real production capacity-check code,
|
|
* not fabricated -- only the resreq-DSL-to-`wanted`-vector compilation step
|
|
* is replaced by a fixed per-job lookup table (JOB A/B/C's "wants" below),
|
|
* exactly the same scope-reduction test_sim_equivalence.c's own header
|
|
* comment documents for its own simplification.
|
|
*
|
|
* The resulting `.replay` file R is dual-use, per this task's own
|
|
* requirement text ("usable both as a normal-mode recording source and as a
|
|
* simulation-mode replay input"): it IS a genuine normal-mode recording (produced by
|
|
* sched_record_start()/sched_record_stop() around a real
|
|
* xce_sched_driver_run_normal_mode_round() call), and this file's own
|
|
* test_contention_fixture_is_replayable_via_simulation_driver() below
|
|
* additionally proves it is directly consumable by the REAL, unmodified
|
|
* sched_sim_driver_run() as replay input. A later task (4.1, out of this
|
|
* task's boundary) reuses this same two-phase shape to assert resInUse
|
|
* parity between a normal-mode recording and its simulation-mode replay;
|
|
* this task's own job is only to build the fixture and prove its own shape
|
|
* (multiple jobs, insufficient combined capacity, at least one round where
|
|
* one job is denied capacity a running job holds) -- not to run that later
|
|
* comparison.
|
|
*
|
|
* Scenario: worker "worker-a" has 10.0 total units of one resource. Job A
|
|
* (submitted first, sub_time=1000) wants 8.0 -- satisfied, dispatched, and
|
|
* charged into worker-a's resInUse. Job B (sub_time=1001) wants 5.0 -- 8.0
|
|
* (A's charge, now-running) + 5.0 (B's want) = 13.0 > 10.0 total, so B is
|
|
* genuinely DENIED capacity that job A, now running, is consuming (this
|
|
* task's own Observable Completion, proven by decoding the recorded
|
|
* decision stream below, not merely asserted from this file's own input
|
|
* constants). Job C (sub_time=1002) wants 2.0 -- exactly the remaining
|
|
* 10.0-8.0=2.0 headroom, so it also dispatches, proving the fixture is a
|
|
* genuine multi-job contention scenario (8.0+5.0+2.0=15.0 > 10.0 combined,
|
|
* while the two jobs that DO dispatch, A+C, exactly saturate the 10.0
|
|
* total) rather than a degenerate single-denied-job case.
|
|
*
|
|
* White-box #include set: identical to test_sim_equivalence.c's own
|
|
* combined normal-mode-driver + full-simulation-stack set, PLUS
|
|
* ../src/resource.c (this file's own new dependency: the real
|
|
* sched_check_host_satisfy()/sched_charge_account()/sched_commit_job_alloc()
|
|
* capacity-and-accounting primitives under real exercise here). No existing
|
|
* test file in this directory white-box #includes resource.c together with
|
|
* the full sim/driver stack -- test_resource_accounting.c (task 1.1) only
|
|
* pairs resource.c with core.c, deliberately staying below the
|
|
* driver/picker/recording layer (see that file's own header comment for
|
|
* why); this file is the first to need both together, since a genuine
|
|
* recorded fixture needs the real recording pipeline AND real capacity
|
|
* arithmetic in the same run.
|
|
*
|
|
* Stub surface: the union of test_sim_equivalence.c's own stubs (kill_sched_*
|
|
* family, sched_account_jobs -- scheduler.c itself is still not white-box
|
|
* included, so this thin ~20-line wrapper stays a no-op exactly as it does
|
|
* there; harmless here since this fixture's single round never depends on
|
|
* round-boundary re-accounting of a PRIOR round's charges, only its own
|
|
* mid-round sequential charging), sched_publish_dispatch/
|
|
* sched_publish_pend_reason/xcecli_reply_schedadmin (driver.c link-time
|
|
* dependencies, never invoked), plus ONE new stub this file's addition of
|
|
* resource.c requires: sched_ctx_get_uncomit_host() (resource.c's
|
|
* sched_try_alloc_on_host_()'s own link-time dependency -- that function is
|
|
* compiled as part of resource.c but never called by this file, which only
|
|
* calls the public sched_check_host_satisfy()/sched_charge_account()/
|
|
* sched_commit_job_alloc() entry points directly). Two stubs
|
|
* test_resource_accounting.c's own resource.c-only inclusion needed
|
|
* (plugin_prepare_allocation, sched_fill_limit_requests) are deliberately
|
|
* NOT redefined here: this file's combined include set already provides
|
|
* REAL definitions for both (plugin_prepare_allocation from plugin.c,
|
|
* already part of test_sim_equivalence.c's own set; sched_fill_limit_requests
|
|
* from resource.c itself, this file's own new addition) -- redefining
|
|
* either here would be a duplicate-symbol link error, not a harmless
|
|
* no-op. sched_worker_release_host_ctx is likewise NOT stubbed here (every
|
|
* other file in this directory stubs it because none of them white-box
|
|
* #include resource.c, which defines it for real) -- this file gets the
|
|
* real one via resource.c and must not shadow it.
|
|
*/
|
|
#include <errno.h>
|
|
#include <limits.h>
|
|
#include <pthread.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
#include <unity/unity.h>
|
|
|
|
#include "controller.capnp.h"
|
|
#include "kvmgr.h"
|
|
#include "preason.h"
|
|
#include "scheduler.h"
|
|
#include "xstring.h"
|
|
|
|
/* Pull in the real functions under test into this TU (white-box #include,
|
|
* same convention every other file in this directory establishes -- see
|
|
* this file's own header comment for the exact combined set and why it is
|
|
* novel). */
|
|
#include "../src/core.c"
|
|
#include "../src/core_clock.c"
|
|
#include "../src/core_events.c"
|
|
#include "../src/core_session.c"
|
|
#include "../src/decision.c"
|
|
#include "../src/driver.c"
|
|
#include "../src/plugin.c"
|
|
#include "../src/plugins/builtin_fcfs.c"
|
|
#include "../src/recording.c"
|
|
#include "../src/recording_events.c"
|
|
#include "../src/recording_session.c"
|
|
#include "../src/repl.c"
|
|
#include "../src/repl_fmt.c"
|
|
#include "../src/repl_rollup.c"
|
|
#include "../src/repl_break.c"
|
|
#include "../src/repl_info.c"
|
|
#include "../src/repl_params.c"
|
|
#include "../src/resource.c"
|
|
/* Hooked sources compile SCHED_TRACE bodies (scheduler-sim-decision-trace). */
|
|
#include "../src/sched_trace.c"
|
|
#include "../src/sched_trace_scan.c"
|
|
#include "../src/sched_trace_capture.c"
|
|
#include "../src/sched_trace_outcome.c"
|
|
#include "../src/sim.c"
|
|
#include "../src/sim_loop.c"
|
|
#include "../src/sim_picker.c" /* bug-1567: sched_sim_driver_run() now calls sim_bootstrap_picker() (sim_loop.c-cap sibling) as an extern -- undefined reference at link time otherwise */
|
|
#include "../src/sim_limit.c" /* bug-1825: sched_sim_driver_run() now also calls sim_bootstrap_limit_plugin() (sim_loop.c-cap sibling, sim_picker.c's own bug-1567 precedent) as an extern -- undefined reference at link time otherwise */
|
|
#include "../src/sim_record.c"
|
|
#include "../src/sim_verify.c"
|
|
#include "../src/snapshot.c"
|
|
#include "../src/snapshot_load.c"
|
|
#include "../src/snapshot_wire.c"
|
|
|
|
void kill_sched_job(void* arg) { (void)arg; }
|
|
void kill_sched_queue(void* arg) { (void)arg; }
|
|
void kill_sched_worker(void* arg) { (void)arg; }
|
|
void kill_sched_user(void* arg) { (void)arg; }
|
|
void kill_sched_project(void* arg) { (void)arg; }
|
|
void kill_sched_resval(xce_val_t* val) { (void)val; }
|
|
|
|
/* repl_info.c (scheduler-sim-diagnostics spec, task 2.1) now calls
|
|
* limit_get_configured_limits() (plugins/limit.c, not part of this TU) to
|
|
* resolve `info limit`'s name/all argument -- link-satisfying stub only,
|
|
* same rationale as kill_sched_job()/kill_sched_resval() above; this
|
|
* file's own fixtures never register the limit plugin, so NULL
|
|
* (limit_get_configured_limits()'s own documented "plugin absent" return)
|
|
* is also the behaviorally correct answer here, not just a link
|
|
* placeholder. repl_info_limit() itself is not yet wired into
|
|
* repl_cmd_info() (task 4), so no test in this file can reach it anyway. */
|
|
var limit_get_configured_limits(xce_sched_core_t* core)
|
|
{
|
|
(void)core;
|
|
return NULL;
|
|
}
|
|
|
|
/* scheduler-sim-diagnostics spec, task 3.1: repl_info.c now also calls
|
|
* limit_get_usage_instances() (plugins/limit.c, not part of this TU) to
|
|
* read a limit's live usage instances -- identical link-satisfying stub,
|
|
* same rationale as limit_get_configured_limits() immediately above; this
|
|
* file's own fixtures never register the limit plugin, so NULL
|
|
* (limit_get_usage_instances()'s own documented "plugin absent" return) is
|
|
* also the behaviorally correct answer here, not just a link placeholder. */
|
|
var limit_get_usage_instances(xce_sched_core_t* core)
|
|
{
|
|
(void)core;
|
|
return NULL;
|
|
}
|
|
|
|
/* scheduler-sim-diagnostics spec, task 6 (bug-1825): sim_loop.c's
|
|
* sched_sim_driver_run() now also calls sim_bootstrap_limit_plugin()
|
|
* (sim_limit.c, pulled into this TU via the #include above), which in
|
|
* turn calls limit_new_plugin() (plugins/limit.c, not part of this TU) --
|
|
* identical link-satisfying stub rationale as limit_get_configured_
|
|
* limits()/limit_get_usage_instances() immediately above. Returning NULL
|
|
* here is also the behaviorally correct answer, not just a link
|
|
* placeholder: sim_bootstrap_limit_plugin() already handles a NULL
|
|
* limit_new_plugin() return gracefully (logs an ERROR, registers nothing,
|
|
* returns) -- the exact same "out of memory" path sim_bootstrap_picker()
|
|
* (sim_picker.c) takes for a NULL builtin_fcfs_new_plugin() -- so this
|
|
* file's fixtures simply end up with no limit plugin registered, same
|
|
* observable state as before this task. */
|
|
xce_sched_plugin_t* limit_new_plugin(void)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
/* limit_seed_config() (plugins/limit.c, not part of this TU) -- bug-1825
|
|
* remediation round 2's new honestly-typed config-seed entry point.
|
|
* sim_bootstrap_limit_plugin() (sim_limit.c) only calls it after
|
|
* plugin_register_core() has already succeeded against a real, non-NULL
|
|
* limit_new_plugin() plugin -- unreachable in this file since the
|
|
* limit_new_plugin() stub immediately above always returns NULL, but the
|
|
* symbol must still resolve at link time. Same link-satisfying-stub
|
|
* rationale as limit_new_plugin() immediately above. */
|
|
int limit_seed_config(xce_sched_plugin_t* plugin, xce_sched_core_t* core,
|
|
xce_conf4sched_t* config)
|
|
{
|
|
(void)plugin;
|
|
(void)core;
|
|
(void)config;
|
|
return 0;
|
|
}
|
|
|
|
/* xce_sched_driver_run_normal_mode_round()'s (driver.c) linker dependency --
|
|
* a no-op stand-in, same rationale test_sim_equivalence.c's own copy
|
|
* documents (this file drives its own resource accounting directly via
|
|
* this file's own capacity_aware_allocator_thread_fn(), calling the real
|
|
* sched_check_host_satisfy()/sched_charge_account() -- scheduler.c's own
|
|
* sched_account_jobs() thin wrapper is not white-box #included here). */
|
|
int sched_account_jobs(xce_sched_core_t* core)
|
|
{
|
|
(void)core;
|
|
return 0;
|
|
}
|
|
|
|
|
|
/* xce_sched_driver_on_decision()'s (driver.c) publish-side linker
|
|
* dependencies -- never actually invoked: this test never registers a
|
|
* decision callback, so every decision is simply recorded and dropped.
|
|
* Same rationale test_sim_equivalence.c's own copy documents. */
|
|
int sched_publish_dispatch(xce_scheduler_t* sched, xce_job_dispatch_t* d)
|
|
{
|
|
(void)sched;
|
|
(void)d;
|
|
return 0;
|
|
}
|
|
|
|
int sched_publish_pend_reason(xce_scheduler_t* sched, xce_preason_set_t* r)
|
|
{
|
|
(void)sched;
|
|
(void)r;
|
|
return 0;
|
|
}
|
|
|
|
/* handle_deferred_admin_record()'s (driver.c) linker dependency -- never
|
|
* invoked (this test never enqueues XCE_SCHED_OP_ADMIN_RECORD). */
|
|
int xcecli_reply_schedadmin(xcecli_ctx_t c, uint64_t seq,
|
|
sched_admin_reply_t* reply)
|
|
{
|
|
(void)c;
|
|
(void)seq;
|
|
(void)reply;
|
|
return 0;
|
|
}
|
|
|
|
/* resource.c's sched_try_alloc_on_host_()'s own link-time dependency --
|
|
* never actually invoked (this file calls sched_check_host_satisfy()/
|
|
* sched_charge_account()/sched_commit_job_alloc() directly, never
|
|
* sched_try_alloc_on_hosts()/sched_try_alloc_on_host_() itself). Same
|
|
* link-satisfying-only stub test_resource_accounting.c's own resource.c
|
|
* inclusion already establishes needing. */
|
|
int sched_ctx_get_uncomit_host(xvec_t** v, xce_sched_ctx_t* c, int i)
|
|
{
|
|
(void)v;
|
|
(void)c;
|
|
(void)i;
|
|
return 0;
|
|
}
|
|
|
|
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
|
|
* copy every white-box test in this directory that does not link
|
|
* schedobj.c already establishes. */
|
|
int sched_resolve_job_picker(xce_sched_core_t* core, xce_sched_plugin_t** out)
|
|
{
|
|
xce_sched_plugin_t* plugin = NULL;
|
|
int rc = 0;
|
|
|
|
if ((core == NULL) || (out == NULL)) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
*out = NULL;
|
|
|
|
if (core->job_pick_plugin == NULL) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
lock(core->plugins, RTTI_LOCK_READ);
|
|
rc = key_find(core->plugins, core->job_pick_plugin, (void**)&plugin);
|
|
unlock(core->plugins);
|
|
|
|
if (rc <= 0) {
|
|
return (rc < 0) ? rc : -ENOENT;
|
|
}
|
|
|
|
if ((plugin == NULL) || (plugin->picker_iface == NULL)
|
|
|| (plugin->picker_iface->next_job == NULL)
|
|
|| (plugin->picker_iface->begin_session == NULL)
|
|
|| (plugin->picker_iface->end_session == NULL)
|
|
|| (plugin->picker_iface->on_dispatch == NULL)
|
|
|| (plugin->picker_iface->on_pend == NULL)) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
*out = plugin;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* scheduler-sim-live-allocation spec, task 4.1: sim_loop.c's real
|
|
* sched_sim_driver_run() (white-box #include'd into this TU above) now
|
|
* spawns sched_job_picker()/sched_allocator() (sched_election.c) directly
|
|
* instead of its own, now-removed sim_picker_thread_fn() -- an undefined
|
|
* reference at link time in this white-box TU otherwise, since
|
|
* sched_election.c's own sched_allocator() calls the REAL
|
|
* sched_make_allocation()/sched_commit_decision() (scheduler.c), neither
|
|
* part of this file's own hand-picked white-box #include set (this file's
|
|
* own header comment lists that set). This file's own test(s) do not
|
|
* exercise genuine resource-matching allocation, so this link-satisfying
|
|
* pair faithfully reproduces the exact PEND-only contract the REMOVED
|
|
* sim_picker_thread_fn() itself used to provide -- split across the SAME
|
|
* two-thread jbuf hand-off the real production pair now uses (one
|
|
* producer, one consumer, matching the project's jbuf capacity-1
|
|
* rendezvous invariant) instead of the old single fused thread -- so this
|
|
* file's own existing assertions are unaffected by this task's production
|
|
* change. Genuine resource-based dispatch through the real, unmodified
|
|
* sched_job_picker()/sched_allocator() pair is proved separately by
|
|
* test_sim_live_allocation.c (task 4.1's own dedicated fixture, which
|
|
* links the real scheduler_srcs instead of white-boxing individual files,
|
|
* precisely so it can reach the real allocation chain this stub
|
|
* deliberately does not). */
|
|
void* sched_job_picker(void* arg)
|
|
{
|
|
xce_sched_core_t* core = (xce_sched_core_t*)arg;
|
|
xce_sched_plugin_t* picker = NULL;
|
|
|
|
for (;;) {
|
|
int go = 0;
|
|
uint64_t jobid = 0;
|
|
|
|
cbuf_get(&(core->wbuf), &go);
|
|
|
|
if (sched_resolve_job_picker(core, &picker) < 0) {
|
|
jobid = 0;
|
|
cbuf_put(&(core->jbuf), &jobid);
|
|
continue;
|
|
}
|
|
|
|
picker->picker_iface->begin_session(picker);
|
|
|
|
for (;;) {
|
|
xce_sched_job_t* job = picker->picker_iface->next_job(picker);
|
|
|
|
if (job == NULL) {
|
|
break;
|
|
}
|
|
|
|
jobid = job->jobid;
|
|
cbuf_put(&(core->jbuf), &jobid);
|
|
}
|
|
|
|
jobid = 0;
|
|
cbuf_put(&(core->jbuf), &jobid);
|
|
|
|
picker->picker_iface->end_session(picker);
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
void* sched_allocator(void* arg)
|
|
{
|
|
xce_sched_core_t* core = (xce_sched_core_t*)arg;
|
|
xce_sched_plugin_t* picker = NULL;
|
|
|
|
for (;;) {
|
|
uint64_t jobid = 0;
|
|
xce_decision_t* decision = NULL;
|
|
xce_sched_job_t* job;
|
|
|
|
cbuf_get(&(core->jbuf), &jobid);
|
|
if (jobid == 0) {
|
|
decision = NULL;
|
|
cbuf_put(&(core->decisions), &decision);
|
|
continue;
|
|
}
|
|
|
|
job = sched_get_job(core, jobid);
|
|
if (job == NULL) {
|
|
continue;
|
|
}
|
|
|
|
(void)sched_resolve_job_picker(core, &picker);
|
|
if (picker != NULL) {
|
|
picker->picker_iface->on_pend(picker, job->jobid);
|
|
}
|
|
|
|
decision = make_pend_decision(make_pendreason(job->jobid));
|
|
cbuf_put(&(core->decisions), &decision);
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* scheduler-sim-live-allocation spec, task 4.1 (bug-1829 fix): sim_loop.c's
|
|
* real sched_sim_driver_run() (white-box #include'd into this TU above) now
|
|
* also calls sim_bootstrap_seqjob_allocator()/sim_bootstrap_parjob_
|
|
* allocator() (sim_alloc.c) -- undefined references at link time in this
|
|
* white-box TU otherwise, since neither symbol is part of this file's own
|
|
* hand-picked white-box #include set. This file's own test(s) exercise the
|
|
* link-satisfying sched_job_picker()/sched_allocator() stub pair above
|
|
* (which never reaches real primary allocation), so a true no-op is the
|
|
* correct, behavior-preserving stand-in here -- the real bootstrap logic is
|
|
* exercised separately by test_sim_live_allocation.c (task 4.1's own
|
|
* dedicated fixture, which links the real scheduler_srcs). */
|
|
void sim_bootstrap_seqjob_allocator(xce_sched_sim_driver_t* sim)
|
|
{
|
|
(void)sim;
|
|
}
|
|
|
|
void sim_bootstrap_parjob_allocator(xce_sched_sim_driver_t* sim)
|
|
{
|
|
(void)sim;
|
|
}
|
|
|
|
void setUp(void) { return; }
|
|
void tearDown(void) { return; }
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Fixture constants */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
#define WORKER_NAME "worker-a"
|
|
#define SLOTS_RESIDX 0
|
|
#define WORKER_CAPACITY 10.0f
|
|
|
|
#define JOBA_ID 9101ULL
|
|
#define JOBB_ID 9102ULL
|
|
#define JOBC_ID 9103ULL
|
|
#define JOBA_WANT 8.0f
|
|
#define JOBB_WANT 5.0f
|
|
#define JOBC_WANT 2.0f
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Fixture helpers */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
static void make_tmp_dir(char* out, size_t out_len, const char* suffix)
|
|
{
|
|
char tmpl[PATH_MAX];
|
|
|
|
snprintf(tmpl, sizeof(tmpl), "/tmp/sched_simcontention%s_XXXXXX", suffix);
|
|
TEST_ASSERT_NOT_NULL(mkdtemp(tmpl));
|
|
TEST_ASSERT_TRUE(strlen(tmpl) < out_len);
|
|
strcpy(out, tmpl);
|
|
}
|
|
|
|
/* Real xce_sched_core_init() + real builtin_fcfs registration (same
|
|
* technique test_sim_equivalence.c's own fixture_init() establishes) PLUS a
|
|
* real xce_sched_worker_t added directly via sched_add_worker() with a
|
|
* finite total capacity -- this file's own contention source. Adding the
|
|
* worker directly (not through a WORKERS_PUSH event) matches
|
|
* test_resource_accounting.c's own documented convention: "sched_add_worker()
|
|
* itself does not [initialize resvals/resInUse] -- callers own it, same as
|
|
* every other white-box fixture in this directory that adds a worker
|
|
* directly."
|
|
*/
|
|
static void fixture_init(xce_scheduler_t* sched, char* record_dir)
|
|
{
|
|
xce_sched_plugin_t* fcfs;
|
|
xce_sched_worker_t* w;
|
|
int rc;
|
|
int idx[1] = { SLOTS_RESIDX };
|
|
float val[1] = { WORKER_CAPACITY };
|
|
|
|
memset(sched, 0, sizeof(*sched));
|
|
mylock_init(&(sched->record_lock));
|
|
|
|
rc = xce_sched_core_init(&(sched->core));
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
sched->core.pend_updates[0] = (dataq_t*)calloc(1, sizeof(dataq_t));
|
|
dataq_init(sched->core.pend_updates[0], NULL);
|
|
sched->core.pend_updates[1] = (dataq_t*)calloc(1, sizeof(dataq_t));
|
|
dataq_init(sched->core.pend_updates[1], NULL);
|
|
|
|
fcfs = builtin_fcfs_new_plugin();
|
|
TEST_ASSERT_NOT_NULL(fcfs);
|
|
rc = plugin_register_core(&(sched->core), fcfs);
|
|
TEST_ASSERT_TRUE(rc >= 0);
|
|
|
|
sched->core.job_pick_plugin = (char*)"builtin_fcfs";
|
|
sched->record_dir = record_dir;
|
|
|
|
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 = put_resvals(&(w->resvals), 1, idx, val);
|
|
TEST_ASSERT_TRUE(rc >= 0);
|
|
|
|
rc = sched_add_worker(&(sched->core), w);
|
|
TEST_ASSERT_TRUE(rc >= 0);
|
|
}
|
|
|
|
/* Builds one heap-owned, deep-freeable xce_queue_t -- same technique
|
|
* test_sim_equivalence.c's own make_queue() establishes. */
|
|
static xce_queue_t* make_queue(const char* name, int32_t priority)
|
|
{
|
|
xce_queue_t* q = (xce_queue_t*)calloc(1, sizeof(*q));
|
|
|
|
q->queue = strdup(name);
|
|
q->status = QUEUE_STAT_OPEN;
|
|
q->priority = priority;
|
|
|
|
return q;
|
|
}
|
|
|
|
/* Builds one heap-owned, deep-freeable xce_job_t -- same technique
|
|
* test_sim_equivalence.c's own make_job() establishes. */
|
|
static xce_job_t* make_job(uint64_t jobid, uint64_t sub_time, const char* queue,
|
|
const char* user)
|
|
{
|
|
xce_job_t* j = (xce_job_t*)calloc(1, sizeof(*j));
|
|
|
|
j->jobid = jobid;
|
|
j->state = JOB_STAT_PEND;
|
|
j->sub_time = sub_time;
|
|
j->queue_time = sub_time;
|
|
j->start_time = 0;
|
|
|
|
j->submit = (job_submit_data_t*)calloc(1, sizeof(*(j->submit)));
|
|
j->submit->queue = strdup(queue);
|
|
j->submit->user = strdup(user);
|
|
j->submit->project_name = strdup("contention_proj");
|
|
j->submit->resreq = strdup("type == any");
|
|
j->submit->mintasks = 1;
|
|
j->submit->maxtasks = 1;
|
|
|
|
return j;
|
|
}
|
|
|
|
static void enqueue_queue_update(xce_scheduler_t* sched, const char* name,
|
|
int32_t priority)
|
|
{
|
|
xce_sched_update_request_t* req
|
|
= (xce_sched_update_request_t*)calloc(1, sizeof(*req));
|
|
|
|
req->op = XCE_SCHED_OP_UPDATE_QUEUE;
|
|
req->u.queueupdate.queue = make_queue(name, priority);
|
|
|
|
dataq_enq(sched->core.pend_updates[0], &(req->link));
|
|
}
|
|
|
|
static void enqueue_jobs_push(xce_scheduler_t* sched, xce_job_t** jobs,
|
|
int njobs)
|
|
{
|
|
xce_sched_update_request_t* req
|
|
= (xce_sched_update_request_t*)calloc(1, sizeof(*req));
|
|
|
|
req->op = XCE_SCHED_OP_UPDATE_JOB;
|
|
req->u.jobupdate.njobs = njobs;
|
|
req->u.jobupdate.jobs = jobs;
|
|
|
|
dataq_enq(sched->core.pend_updates[0], &(req->link));
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Picker thread -- identical to test_sim_equivalence.c's own */
|
|
/* picker_thread_fn(): resolves and drives the REAL builtin_fcfs picker. */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
static void* picker_thread_fn(void* arg)
|
|
{
|
|
xce_sched_core_t* core = (xce_sched_core_t*)arg;
|
|
xce_sched_plugin_t* job_picker = NULL;
|
|
int rc;
|
|
int go = 0;
|
|
uint64_t jobid;
|
|
|
|
rc = sched_resolve_job_picker(core, &job_picker);
|
|
if (rc < 0) {
|
|
return NULL;
|
|
}
|
|
|
|
cbuf_get(&(core->wbuf), &go);
|
|
|
|
job_picker->picker_iface->begin_session(job_picker);
|
|
|
|
for (;;) {
|
|
xce_sched_job_t* job = job_picker->picker_iface->next_job(job_picker);
|
|
|
|
if (job == NULL) {
|
|
break;
|
|
}
|
|
|
|
jobid = job->jobid;
|
|
cbuf_put(&(core->jbuf), &jobid);
|
|
}
|
|
|
|
jobid = 0;
|
|
cbuf_put(&(core->jbuf), &jobid);
|
|
|
|
job_picker->picker_iface->end_session(job_picker);
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Capacity-aware stand-in allocator -- unlike test_sim_equivalence.c's own */
|
|
/* stand_in_allocator_thread_fn() (which unconditionally PENDs), this one */
|
|
/* drives the REAL sched_check_host_satisfy()/sched_charge_account()/ */
|
|
/* sched_commit_job_alloc() (resource.c) against the real worker capacity */
|
|
/* fixture_init() set up -- see this file's own header comment for why a */
|
|
/* per-job "want" lookup table stands in for full resreq-DSL compilation. */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
static float lookup_want(uint64_t jobid)
|
|
{
|
|
if (jobid == JOBA_ID) {
|
|
return JOBA_WANT;
|
|
}
|
|
if (jobid == JOBB_ID) {
|
|
return JOBB_WANT;
|
|
}
|
|
if (jobid == JOBC_ID) {
|
|
return JOBC_WANT;
|
|
}
|
|
return 0.0f;
|
|
}
|
|
|
|
/* SESSION_FROM - the `from` an allocation decided in a session that began at
|
|
* `begin_ns` (epoch nanoseconds) carries: the session start in whole
|
|
* seconds, floor division (libxce-alloc-preason-equality Decision 8;
|
|
* scheduler.c's sched_make_primary_alloc_() stamps it, and in simulation
|
|
* mode the session starts at the recorded beginRound). This file's
|
|
* capacity-aware stand-in allocator stamps its DISPATCH allocations the
|
|
* same way, so the recording it produces carries the value a live replay
|
|
* of that round reproduces. */
|
|
#define SESSION_FROM(begin_ns) ((uint64_t)(begin_ns) / 1000000000ULL)
|
|
|
|
/* Builds the wire-shaped xce_job_alloc_t a DISPATCH decision carries -- same
|
|
* field shape test_sim_verify.c's own make_test_alloc()/make_test_host_alloc()/
|
|
* make_test_job_alloc() establish. */
|
|
static xce_job_alloc_t* make_dispatch_alloc(uint64_t jobid, const char* host,
|
|
float value, uint64_t from)
|
|
{
|
|
xce_alloc_t* res = (xce_alloc_t*)calloc(1, sizeof(*res));
|
|
xce_host_alloc_t* h = (xce_host_alloc_t*)calloc(1, sizeof(*h));
|
|
xce_host_alloc_t** hosts = (xce_host_alloc_t**)calloc(1, sizeof(*hosts));
|
|
xce_alloc_t** allocs = (xce_alloc_t**)calloc(1, sizeof(*allocs));
|
|
xce_job_alloc_t* j = (xce_job_alloc_t*)calloc(1, sizeof(*j));
|
|
|
|
TEST_ASSERT_NOT_NULL(res);
|
|
TEST_ASSERT_NOT_NULL(h);
|
|
TEST_ASSERT_NOT_NULL(hosts);
|
|
TEST_ASSERT_NOT_NULL(allocs);
|
|
TEST_ASSERT_NOT_NULL(j);
|
|
|
|
res->resname = strdup("slots");
|
|
res->reserve = strdup("run");
|
|
res->instid = 1;
|
|
res->value = value;
|
|
res->from = from;
|
|
res->duration = 0;
|
|
|
|
allocs[0] = res;
|
|
|
|
h->host = strdup(host);
|
|
h->nallocs = 1;
|
|
h->allocs = allocs;
|
|
|
|
hosts[0] = h;
|
|
|
|
j->jobid = jobid;
|
|
j->nhosts = 1;
|
|
j->allocs = hosts;
|
|
j->nres_shared = 0;
|
|
j->allocs_shared = NULL;
|
|
|
|
return j;
|
|
}
|
|
|
|
/* Builds the accounting-charge shape sched_charge_account()/
|
|
* sched_commit_job_alloc() consume -- same field shape
|
|
* test_resource_accounting.c's own make_test_account() establishes,
|
|
* simplified to one host charge (no shared-resource charge needed here). */
|
|
static xce_job_account_t* make_charge_account(const char* host, float value)
|
|
{
|
|
xce_job_account_t* acct = (xce_job_account_t*)calloc(1, sizeof(*acct));
|
|
|
|
TEST_ASSERT_NOT_NULL(acct);
|
|
|
|
acct->nres_shared = 0;
|
|
acct->accounts_shared = NULL;
|
|
|
|
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(host);
|
|
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 = SLOTS_RESIDX;
|
|
acct->accounts[0]->accounts[0]->used = value;
|
|
acct->accounts[0]->accounts[0]->reserved = 0.0;
|
|
|
|
return acct;
|
|
}
|
|
|
|
static void* capacity_aware_allocator_thread_fn(void* arg)
|
|
{
|
|
xce_scheduler_t* sched = (xce_scheduler_t*)arg;
|
|
xce_sched_core_t* core = &(sched->core);
|
|
xce_sched_plugin_t* picker = NULL;
|
|
uint64_t jobid;
|
|
|
|
if (sched_resolve_job_picker(core, &picker) < 0) {
|
|
xce_decision_t* decision = NULL;
|
|
|
|
cbuf_get(&(core->jbuf), &jobid); /* drain the lone 0 sentinel */
|
|
cbuf_put(&(core->decisions), &decision);
|
|
return NULL;
|
|
}
|
|
|
|
for (;;) {
|
|
xce_decision_t* decision;
|
|
xce_sched_job_t* job;
|
|
xce_sched_worker_t* w;
|
|
xvec_t uncomitted;
|
|
xvec_idval_t wanted;
|
|
xvec_idval_t unmeet;
|
|
float want;
|
|
int rc;
|
|
|
|
cbuf_get(&(core->jbuf), &jobid);
|
|
if (jobid == 0) {
|
|
decision = NULL;
|
|
cbuf_put(&(core->decisions), &decision);
|
|
break;
|
|
}
|
|
|
|
job = sched_get_job(core, jobid);
|
|
w = sched_get_worker(core, (char*)WORKER_NAME);
|
|
TEST_ASSERT_NOT_NULL(job);
|
|
TEST_ASSERT_NOT_NULL(w);
|
|
|
|
want = lookup_want(jobid);
|
|
|
|
/* uncomitted -- no in-flight, not-yet-committed reservations across
|
|
* this fixture's strictly-sequential, single-threaded evaluation of
|
|
* one candidate at a time (each job's charge is fully committed via
|
|
* sched_commit_job_alloc() before the next candidate is checked), so
|
|
* a fresh, zero-initialized vector is correct here, matching
|
|
* sched_check_host_satisfy()'s own documented caller-owned
|
|
* contract. */
|
|
rc = xvec_init(&uncomitted, 1, sizeof(float));
|
|
TEST_ASSERT_TRUE(rc >= 0);
|
|
|
|
rc = xvec_idval_init(&wanted, 1);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
rc = xvec_idval_store(&wanted, SLOTS_RESIDX, want);
|
|
TEST_ASSERT_TRUE(rc >= 0);
|
|
|
|
/* scheduler-sim-live-allocation task 1.3: sched_check_host_
|
|
* satisfy() is now core-typed -- pass the already-derived `core`
|
|
* local. */
|
|
rc = sched_check_host_satisfy(core, w, &uncomitted, &wanted, &unmeet);
|
|
|
|
xvec_fini(&uncomitted);
|
|
xvec_idval_fini(&wanted);
|
|
xvec_idval_fini(&unmeet);
|
|
|
|
if (rc == 1) {
|
|
/* Satisfied -- charge the real accounting state (resource.c,
|
|
* task 1.1's relocated sched_charge_account()/
|
|
* sched_commit_job_alloc()) so the NEXT candidate in this same
|
|
* round sees updated resInUse, then report DISPATCH via the
|
|
* real make_dispatch_decision() constructor -- mirrors
|
|
* scheduler.c's real sched_allocator()'s own
|
|
* on_dispatch/commit/decision sequence. */
|
|
job->accounts = make_charge_account(WORKER_NAME, want);
|
|
sched_commit_job_alloc(core, job);
|
|
|
|
/* Stamp `from` exactly as sched_make_primary_alloc_()
|
|
* (scheduler.c) stamps a real allocation: the deciding
|
|
* session's start in whole seconds. */
|
|
job->allocs = make_dispatch_alloc(
|
|
jobid, WORKER_NAME, want,
|
|
SESSION_FROM(core->session_start));
|
|
|
|
picker->picker_iface->on_dispatch(picker, jobid);
|
|
decision = make_dispatch_decision(job->allocs);
|
|
} else {
|
|
/* Denied -- this job is genuinely blocked by capacity a
|
|
* currently-running job (already charged above, earlier this
|
|
* same round) is consuming. */
|
|
picker->picker_iface->on_pend(picker, jobid);
|
|
decision = make_pend_decision(make_pendreason(jobid));
|
|
}
|
|
|
|
if (decision != NULL) {
|
|
cbuf_put(&(core->decisions), &decision);
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* Runs one complete normal-mode round (real picker thread + this file's own
|
|
* capacity-aware allocator thread) and blocks until it completes -- same
|
|
* design as test_sim_equivalence.c's own run_one_real_session(). */
|
|
static void run_one_real_session(xce_scheduler_t* sched)
|
|
{
|
|
pthread_t picker_tid;
|
|
pthread_t allocator_tid;
|
|
int rc;
|
|
|
|
rc = pthread_create(&picker_tid, NULL, picker_thread_fn, &(sched->core));
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
rc = pthread_create(&allocator_tid, NULL, capacity_aware_allocator_thread_fn,
|
|
sched);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
xce_sched_driver_run_normal_mode_round(sched);
|
|
|
|
rc = pthread_join(picker_tid, NULL);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
rc = pthread_join(allocator_tid, NULL);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Decoding */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
/* This fixture pushes exactly 2 events in one batch (1 QUEUE_UPDATE + 1
|
|
* UPDATE_JOB carrying all 3 jobs) before the round runs -- both
|
|
* sched_record_start()'s own ReplayHeader+initial-SnapshotEntry pair and
|
|
* those 2 events come before the ReplaySession message, same convention
|
|
* test_sim_equivalence.c's own N_MSGS_BEFORE_SESSION documents (that file
|
|
* has 3 events instead of this file's 2, since it pushes 2 queues). */
|
|
#define N_MSGS_BEFORE_SESSION 4 /* ReplayHeader, SnapshotEntry, 2 events */
|
|
|
|
/* Opens `path`, skips exactly `n_skip` capnp messages structurally, then
|
|
* decodes the NEXT message as a ReplaySession -- same technique
|
|
* test_sim_equivalence.c's own decode_session_after_skip() establishes. */
|
|
static replay_session_t* decode_session_after_skip(const char* path, int n_skip)
|
|
{
|
|
FILE* f;
|
|
struct capn c;
|
|
ReplaySession_ptr p;
|
|
replay_session_t* dst = NULL;
|
|
int i;
|
|
|
|
f = fopen(path, "r");
|
|
TEST_ASSERT_NOT_NULL(f);
|
|
|
|
for (i = 0; i < n_skip; i++) {
|
|
TEST_ASSERT_EQUAL_INT(0, capn_init_fp(&c, f, 0));
|
|
capn_free(&c);
|
|
}
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, capn_init_fp(&c, f, 0));
|
|
p.p = capn_getp(capn_root(&c), 0, 1);
|
|
decode_ReplaySession_ptr(&dst, p);
|
|
capn_free(&c);
|
|
|
|
fclose(f);
|
|
|
|
return dst;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* The fixture's own shape proof (task 1.2's own Observable Completion). */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
/*
|
|
* test_contention_fixture_denies_capacity_to_a_job_a_running_job_holds -
|
|
* builds the fixture (real recorded normal-mode session, 3 jobs, 1
|
|
* capacity-limited worker), then decodes the recorded ReplaySession and
|
|
* proves -- from the ACTUAL decoded decision stream, not merely this file's
|
|
* own input constants -- that job A and job C dispatched (consuming the
|
|
* worker's full 10.0-unit capacity between them) while job B was denied,
|
|
* genuinely blocked by capacity job A (running) holds.
|
|
*/
|
|
static void
|
|
test_contention_fixture_denies_capacity_to_a_job_a_running_job_holds(void)
|
|
{
|
|
char record_dir[PATH_MAX];
|
|
xce_scheduler_t sched;
|
|
char r_path[PATH_MAX];
|
|
xce_job_t** jobs;
|
|
replay_session_t* r_session = NULL;
|
|
int rc;
|
|
int i;
|
|
int n_dispatch = 0;
|
|
int n_pend = 0;
|
|
int pend_jobid_ok = 0;
|
|
float dispatched_total = 0.0f;
|
|
|
|
make_tmp_dir(record_dir, sizeof(record_dir), "_r");
|
|
fixture_init(&sched, record_dir);
|
|
|
|
rc = sched_record_start(&sched, "contention.replay");
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
TEST_ASSERT_TRUE(strlen(sched.record_file_path) < sizeof(r_path));
|
|
strcpy(r_path, sched.record_file_path);
|
|
|
|
enqueue_queue_update(&sched, "normal", 1);
|
|
|
|
jobs = (xce_job_t**)calloc(3, sizeof(xce_job_t*));
|
|
jobs[0] = make_job(JOBA_ID, 1000ULL, "normal", "alice");
|
|
jobs[1] = make_job(JOBB_ID, 1001ULL, "normal", "bob");
|
|
jobs[2] = make_job(JOBC_ID, 1002ULL, "normal", "carol");
|
|
enqueue_jobs_push(&sched, jobs, 3);
|
|
|
|
run_one_real_session(&sched);
|
|
|
|
/* Sanity check on the recorded batch shape (1 queue update + 1 batched
|
|
* jobs-push = 2 events) -- pins N_MSGS_BEFORE_SESSION's own assumption. */
|
|
TEST_ASSERT_EQUAL_UINT64(2ULL, sched.record_event_count);
|
|
|
|
rc = sched_record_stop(&sched);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
r_session = decode_session_after_skip(r_path, N_MSGS_BEFORE_SESSION);
|
|
TEST_ASSERT_NOT_NULL(r_session);
|
|
|
|
/* Non-tautology guard: a fixture with fewer than 3 recorded decisions,
|
|
* or one that is all-DISPATCH/all-PEND, would not prove multi-job
|
|
* contention. */
|
|
TEST_ASSERT_EQUAL_INT(3, r_session->n_decisions);
|
|
|
|
for (i = 0; i < r_session->n_decisions; i++) {
|
|
replay_decision_t* d = r_session->decisions[i];
|
|
|
|
if (d->body_which == ReplayDecision_body_dispatch) {
|
|
uint64_t jobid;
|
|
float value;
|
|
|
|
TEST_ASSERT_NOT_NULL(d->body.dispatch);
|
|
TEST_ASSERT_NOT_NULL(d->body.dispatch->alloc);
|
|
jobid = d->body.dispatch->alloc->jobid;
|
|
TEST_ASSERT_TRUE((jobid == JOBA_ID) || (jobid == JOBC_ID));
|
|
|
|
TEST_ASSERT_EQUAL_INT(1, d->body.dispatch->alloc->nhosts);
|
|
TEST_ASSERT_EQUAL_STRING(
|
|
WORKER_NAME, d->body.dispatch->alloc->allocs[0]->host);
|
|
value = d->body.dispatch->alloc->allocs[0]->allocs[0]->value;
|
|
|
|
if (jobid == JOBA_ID) {
|
|
TEST_ASSERT_EQUAL_FLOAT(JOBA_WANT, value);
|
|
} else {
|
|
TEST_ASSERT_EQUAL_FLOAT(JOBC_WANT, value);
|
|
}
|
|
|
|
dispatched_total += value;
|
|
n_dispatch++;
|
|
} else if (d->body_which == ReplayDecision_body_pend) {
|
|
TEST_ASSERT_NOT_NULL(d->body.pend);
|
|
if (d->body.pend->jobid == JOBB_ID) {
|
|
pend_jobid_ok = 1;
|
|
}
|
|
n_pend++;
|
|
} else {
|
|
TEST_FAIL_MESSAGE("unexpected decision kind in contention "
|
|
"fixture");
|
|
}
|
|
}
|
|
|
|
/* This task's own Observable Completion: exactly one job (B) is denied
|
|
* capacity, exactly two (A, C) dispatch. */
|
|
TEST_ASSERT_EQUAL_INT(2, n_dispatch);
|
|
TEST_ASSERT_EQUAL_INT(1, n_pend);
|
|
TEST_ASSERT_TRUE_MESSAGE(pend_jobid_ok,
|
|
"the denied decision must genuinely be job B, "
|
|
"not merely any PEND");
|
|
|
|
/* The combined-capacity proof: all three jobs' wants exceed total
|
|
* capacity (insufficient for all of them at once), while the two jobs
|
|
* that DID dispatch fit exactly within it -- i.e. job B was denied
|
|
* capacity that jobs A and C, both running, are genuinely consuming. */
|
|
TEST_ASSERT_TRUE((JOBA_WANT + JOBB_WANT + JOBC_WANT) > WORKER_CAPACITY);
|
|
TEST_ASSERT_TRUE(dispatched_total <= WORKER_CAPACITY);
|
|
TEST_ASSERT_TRUE((dispatched_total + JOBB_WANT) > WORKER_CAPACITY);
|
|
|
|
free_ReplaySession_ptr(&r_session);
|
|
|
|
printf("test_contention_fixture_denies_capacity_to_a_job_a_running_job_"
|
|
"holds: PASS\n");
|
|
}
|
|
|
|
/*
|
|
* test_contention_fixture_is_replayable_via_simulation_driver - this task's
|
|
* own "dual-use" requirement: the SAME `.replay` file the normal-mode
|
|
* recording above produced is directly consumable by the REAL, unmodified
|
|
* sched_sim_driver_run() as simulation-mode replay input, with no
|
|
* conversion step. (Whether simulation mode's own resInUse/decision output
|
|
* correctly reflects this fixture's contention is the later comparison
|
|
* task 4.1 proves -- out of this task's own boundary; this test only proves
|
|
* the file is a valid, replayable simulation-mode input.)
|
|
*/
|
|
static void test_contention_fixture_is_replayable_via_simulation_driver(void)
|
|
{
|
|
char record_dir[PATH_MAX];
|
|
xce_scheduler_t sched;
|
|
char r_path[PATH_MAX];
|
|
xce_job_t** jobs;
|
|
sched_sim_args_t sim_args;
|
|
xce_sched_sim_driver_t sim;
|
|
int rc;
|
|
|
|
make_tmp_dir(record_dir, sizeof(record_dir), "_dualuse");
|
|
fixture_init(&sched, record_dir);
|
|
|
|
rc = sched_record_start(&sched, "contention_dualuse.replay");
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
TEST_ASSERT_TRUE(strlen(sched.record_file_path) < sizeof(r_path));
|
|
strcpy(r_path, sched.record_file_path);
|
|
|
|
enqueue_queue_update(&sched, "normal", 1);
|
|
|
|
jobs = (xce_job_t**)calloc(3, sizeof(xce_job_t*));
|
|
jobs[0] = make_job(JOBA_ID, 1000ULL, "normal", "alice");
|
|
jobs[1] = make_job(JOBB_ID, 1001ULL, "normal", "bob");
|
|
jobs[2] = make_job(JOBC_ID, 1002ULL, "normal", "carol");
|
|
enqueue_jobs_push(&sched, jobs, 3);
|
|
|
|
run_one_real_session(&sched);
|
|
|
|
rc = sched_record_stop(&sched);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
memset(&sim_args, 0, sizeof(sim_args));
|
|
sim_args.replay_path = r_path;
|
|
sim_args.sim_start_ts = SCHED_SIM_START_TS_LATEST;
|
|
|
|
memset(&sim, 0, sizeof(sim));
|
|
rc = sched_sim_driver_run(&sim, &sim_args);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
0, rc,
|
|
"the same file the normal-mode recording produced must be directly "
|
|
"replayable by the real, unmodified simulation driver");
|
|
|
|
printf("test_contention_fixture_is_replayable_via_simulation_driver: "
|
|
"PASS\n");
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_contention_fixture_denies_capacity_to_a_job_a_running_job_holds);
|
|
RUN_TEST(test_contention_fixture_is_replayable_via_simulation_driver);
|
|
return UNITY_END();
|
|
}
|