Extends test_maxtasks_wiring_ingest.c with two tests: (1) a resource- request update never changes a job's already-dispatched task count, because core_events.c's live JOB_UPDATE handler never rebuilds job->schedobj (Requirement 1.4); (2) two otherwise-identical jobs differing only in requested task count don't collide in the unsatisfiable-resource-request cache on the fresh-ingest path (Requirement 5.4). Adds a new file, test_maxtasks_wiring_sim_driver_parity.c, proving the simulation driver's real static translator chain (sim_process_replay_event_message()/replay_event_to_core_event()) applies this feature's wiring identically to the real driver -- a genuine white-box test of the real sim_loop.c code (following test_sim_event_loop.c's established #include technique), not a mirror of its logic. This was split into its own file because test_maxtasks_wiring_ingest.c already links the full scheduler_srcs object list for its own task-3.1 test, which collides at link time with white-box #include-ing core_events.c/sim_loop.c in one translation unit; see tasks.md's Implementation Notes. No production code changed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
1153 lines
55 KiB
C
1153 lines
55 KiB
C
/*
|
|
* test_maxtasks_wiring_ingest.c - resreq-maxtasks-wiring spec, task 3.1
|
|
* (Req 1.1, 1.3, 3.1, 3.2, 5.6): the dedicated integration test file for
|
|
* this feature's two live ingest call sites (`core_build_job()`,
|
|
* core_events.c; the snapshot-restore path, snapshot_load.c), proving three
|
|
* things NEITHER task 2.1's own test_core_events.c additions
|
|
* (test_core_build_job_wires_real_task_count_into_resreq_primary/
|
|
* test_core_build_job_default_task_count_still_one) NOR task 2.2's own
|
|
* test_snapshot_save_load.c addition
|
|
* (test_snapshot_load_wires_real_task_count_into_resreq_primary) attempted:
|
|
*
|
|
* 1. Fresh ingest reflects a real multi-task job's requested task count
|
|
* on the standalone primary the allocator dispatch actually reads
|
|
* (Req 1.1) -- a fresh, independent proof point alongside task 2.1's
|
|
* own, not a duplicate of it (different task-count values, and this
|
|
* file additionally asserts job->resreq->u.primary and
|
|
* job->schedobj->u.primary->resreq are the SAME pointer, i.e. that
|
|
* what dispatch reads (scheduler.c:315,
|
|
* job->schedobj->u.primary->resreq) really is the object this
|
|
* feature's helper mutated).
|
|
* 2. Fresh ingest and snapshot-restore ingest, given an EQUIVALENT job
|
|
* (same mintasks/maxtasks/resreq), derive the IDENTICAL standalone
|
|
* primary task count -- Req 5.6's own comparative claim ("shown to
|
|
* derive the same ... task count as it would through fresh
|
|
* submission"), asserted directly (fresh value == restored value), not
|
|
* just each independently checked against the expected constant. This
|
|
* is genuinely new: task 2.1's and task 2.2's own tests each check
|
|
* their own call site in isolation and never compare the two against
|
|
* each other.
|
|
* 3. A single-task job (maxTasks==1, the common case, indistinguishable
|
|
* at this layer from "no explicit task count" -- see note on
|
|
* test_single_task_job_dispatch_and_host_selection_unchanged_via_real_
|
|
* allocator() below) reaches a real host-selection/allocation
|
|
* decision through the REAL, live allocator dispatch chain
|
|
* (sched_init()'s real sched_allocator() thread ->
|
|
* sched_make_primary_alloc_() -> seqjob_allocator) with the SAME
|
|
* single-host, non-split outcome production always produced before
|
|
* this feature existed -- Req 3.1/3.2's no-regression guarantee,
|
|
* proven at the BEHAVIORAL (host-selection/dispatch) level, not merely
|
|
* by inspecting the resreq struct's own minTasks/maxTasks fields (task
|
|
* 2.1's/task 2.2's own default-case tests only ever did the latter).
|
|
*
|
|
* ── Link technique ──────────────────────────────────────────────────────
|
|
* All three tests in this file link the REAL, whole `scheduler_srcs` object
|
|
* list (meson.build), the same established precedent as
|
|
* test_parjob_e2e_multislot.c / test_snapshot_load_allocator_regression.c
|
|
* (see either file's own header comment for the full rationale: reaching
|
|
* scheduler.c's `static` sched_make_allocation()/sched_make_primary_alloc_()
|
|
* dispatch chain requires either white-box #include-ing scheduler.c
|
|
* directly -- ruled out project-wide for scheduler.c specifically, its own
|
|
* dependency surface being too large -- or linking the real, separately-
|
|
* compiled scheduler_srcs and driving them through sched_init()'s real,
|
|
* public entry point). Using ONE link technique for the whole file (rather
|
|
* than mixing this with test_core_events.c's/test_snapshot_save_load.c's
|
|
* own white-box #include-of-core.c-family technique) avoids a duplicate-
|
|
* symbol link error that would result from #include-ing core.c/
|
|
* core_events.c/snapshot_load.c directly INTO this TU while meson ALSO
|
|
* compiles those same files as their own separate translation units for
|
|
* the executable below.
|
|
*
|
|
* A useful, incidental consequence: since every function this file calls
|
|
* (xce_core_push_events(), xce_sched_core_init(), xce_sched_core_load_
|
|
* snapshot(), sched_init()) is the REAL, separately-linked production
|
|
* object -- not a white-box #include with this directory's usual hand-
|
|
* written stub surface (kill_sched_*, plugin_prepare_allocation,
|
|
* sched_ctx_get_uncomit_host, etc.) -- this file needs NONE of those stubs;
|
|
* only the two `-Wl,--wrap=` doubles for xcecli_start()/xcecli_join() any
|
|
* real-scheduler_srcs test in this directory needs, so sched_init() (test 3
|
|
* only) succeeds with no live controller.
|
|
*
|
|
* Tests 1 and 2 use a bare `xce_sched_core_init()`'d core (no picker/
|
|
* allocator threads started -- same technique test_snapshot_load_allocator_
|
|
* regression.c's own build_source_core_with_pending_job() helper already
|
|
* establishes for a throwaway core), since they only need to inspect the
|
|
* resreq the ingest call site itself builds, never dispatch. Test 3 alone
|
|
* uses the heavier real `sched_init()` (starts the real picker/allocator
|
|
* threads) since it is the one test in this file that must observe a real
|
|
* allocator dispatch decision. Mixing a bare xce_sched_core_init()'d core
|
|
* with a separate, fully sched_init()'d scheduler in the same file/process
|
|
* is itself an established, safe combination -- test_snapshot_load_
|
|
* allocator_regression.c's own single test function already does exactly
|
|
* this (its throwaway `core_src` alongside its real `sched`).
|
|
*
|
|
* ── RED-phase verification performed while writing this file (not
|
|
* committed here) ──────────────────────────────────────────────────────
|
|
* Since the production wiring (task 2.1/2.2, commits 0aadb494/6577e041) is
|
|
* already merged, this file's own tests pass against current code by
|
|
* construction -- there is no new production bug for them to catch going
|
|
* forward. To confirm each is still a MEANINGFUL, non-tautological
|
|
* assertion (rather than one that would pass regardless of whether the fix
|
|
* exists), both call sites' `apply_job_task_count(...)` lines were
|
|
* temporarily commented out (core_events.c, snapshot_load.c) and this
|
|
* file's binary rebuilt and re-run in isolation:
|
|
* - test_fresh_ingest_reflects_real_multi_task_job_task_count: FAILED
|
|
* (primary->minTasks/maxTasks read back 1/1, parse_resreq_primary()'s
|
|
* bare hardcode, instead of the requested 3/5) -- confirms this
|
|
* assertion is a real, meaningful proof of Req 1.1, not a tautology.
|
|
* - test_fresh_ingest_and_snapshot_restore_derive_identical_task_count_
|
|
* for_equivalent_job: FAILED on its expected-constant assertions (both
|
|
* paths read back 1/1 instead of the requested 2/9) -- note the
|
|
* fresh==restored CROSS-comparison alone would NOT have caught this
|
|
* regression (both paths degrade to the SAME wrong 1/1 default without
|
|
* the fix, so they'd still equal each other); this is exactly why this
|
|
* test asserts both the expected-constant form AND the cross-path
|
|
* equality form together, not the cross-comparison alone.
|
|
* - test_single_task_job_dispatch_and_host_selection_unchanged_via_real_
|
|
* allocator: PASSED unchanged either way -- confirms by direct
|
|
* empirical measurement (not merely by code-reading argument) that a
|
|
* maxTasks==1 job's real dispatch outcome is genuinely a no-op change
|
|
* for this feature, since minTasks=maxTasks=1 is exactly what parse_
|
|
* resreq_primary()'s own pre-existing hardcode already produced.
|
|
* All three lines were restored immediately afterward (git-verified clean
|
|
* diff against core_events.c/snapshot_load.c) before this file's own
|
|
* implementation was finalized.
|
|
*
|
|
* ── Task 3.2 addendum -- two more tests, same file/link technique ────────
|
|
* Task 3.2 (Req 1.4, 5.4, and design.md's "Project Invariant Cross-Check")
|
|
* appends two MORE tests to this same file, reusing every fixture helper
|
|
* above. (A third task-3.2 test, proving driver/simulation ingest parity
|
|
* via the REAL, `static` simulation-driver translator chain, lives in its
|
|
* own separate file, test_maxtasks_wiring_sim_driver_parity.c -- see that
|
|
* file's own header comment for why: an independent review rejected this
|
|
* file's original mirrored version of that test and required white-box
|
|
* #include-ing sim_loop.c's real translator instead, which cannot share a
|
|
* translation unit with this file's own test 3, below, since that test
|
|
* links the whole real scheduler_srcs object list.)
|
|
*
|
|
* 4. test_resource_request_update_never_changes_dispatched_task_count:
|
|
* Req 1.4's structural regression test. A job is ingested fresh, then
|
|
* a real XCE_CORE_EVENT_JOB_UPDATE is driven for the SAME jobid
|
|
* carrying BOTH a different resreq string AND a different requested
|
|
* task count. Confirms job->resreq WAS genuinely replaced (so the
|
|
* immunity assertion below isn't vacuous), then confirms
|
|
* job->schedobj->u.primary->resreq -- what the real allocator dispatch
|
|
* reads -- is the EXACT SAME object with the EXACT SAME ingest-time
|
|
* task count, because core_events.c's JOB_UPDATE case never reassigns
|
|
* job->schedobj (grepped exhaustively: no such assignment exists in
|
|
* that case block). This is a reasoned, not empirically red/green
|
|
* -tested, proof -- see design.md's Existing Architecture Analysis:
|
|
* the guarantee holds today for this structural reason (bug-1773),
|
|
* independent of any new code this feature adds; there is no existing
|
|
* production bug for a RED phase to catch here, only a future
|
|
* regression (job->schedobj being reassigned on update) this test now
|
|
* guards against.
|
|
*
|
|
* 5. test_differing_task_counts_do_not_collide_in_unsatisfiable_cache:
|
|
* Req 5.4, fresh-ingest path only (snapshot-restore is the documented,
|
|
* pre-existing bug-1772 exception, out of this test's scope). Two
|
|
* otherwise-identical jobs, differing ONLY in requested task count,
|
|
* are ingested fresh into the SAME core (so their resreq_fp's other
|
|
* components -- select_node_id/rusage_hash -- are guaranteed
|
|
* identical). Reuses scheduler-allocation's own test_alloc_cache.c
|
|
* conventions (cache_insert()/cache_lookup() driven directly against
|
|
* real primary schedobj pointers) combined with this file's own
|
|
* fresh-ingest technique. Empirically verified meaningful: with
|
|
* core_events.c's apply_job_task_count() call temporarily commented
|
|
* out, both jobs collapse to the IDENTICAL resreq_fp and this test
|
|
* then FAILS one assertion earlier than cache_lookup() itself -- at
|
|
* its own fingerprint sanity check (job A's and job B's
|
|
* resreq_fp.min_tasks, expected to differ, are instead both 1) --
|
|
* confirmed by temporarily disabling the wiring, rebuilding this
|
|
* binary in isolation, observing that failure, then reverting to a
|
|
* git-clean state before finalizing this test.
|
|
*/
|
|
#include <errno.h>
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#include <unity/unity.h>
|
|
|
|
#include "client.h"
|
|
#include "controller.capnp.h"
|
|
#include "core.h"
|
|
#include "decision.h"
|
|
#include "preason.h"
|
|
#include "resobj.h"
|
|
#include "resreq.h"
|
|
#include "sched/alloc_cache.h"
|
|
#include "scheduler.h"
|
|
#include "xce/cluster.h"
|
|
#include "xstring.h"
|
|
|
|
extern int scheduler_req_handler(xce_request_t*, void*);
|
|
|
|
#define WORKER_C_NAME "maxtasks-wiring-host-c"
|
|
#define WORKER_D_NAME "maxtasks-wiring-host-d"
|
|
#define WORKER_SLOTS 1.0f
|
|
#define SINGLE_TASK_RESREQ_STR ((char*)"rusage[mem=1]")
|
|
|
|
#define JOBID_FRESH_MULTI ((uint64_t)910101ULL)
|
|
#define JOBID_PARITY ((uint64_t)910201ULL)
|
|
#define JOBID_SINGLE_TASK ((uint64_t)910301ULL)
|
|
|
|
/* task 3.2 additions (Req 1.4, 5.4) -- see this file's own top-of-file
|
|
* header comment's task-3.2 addendum, below the task-3.1 narrative, for
|
|
* what each of these two tests proves. (The driver/simulation parity test,
|
|
* design.md's "Project Invariant Cross-Check" commitment, now lives in its
|
|
* own separate file, test_maxtasks_wiring_sim_driver_parity.c.) */
|
|
#define JOBID_UPDATE_IMMUNE ((uint64_t)910501ULL)
|
|
#define JOBID_CACHE_A ((uint64_t)910601ULL)
|
|
#define JOBID_CACHE_B ((uint64_t)910602ULL)
|
|
|
|
/* ════════════════════════════════════════════════════════════════════════
|
|
* __wrap_xcecli_start / __wrap_xcecli_join -- trivial always-succeed
|
|
* doubles, identical rationale/shape to every other real-scheduler_srcs
|
|
* test in this directory (test_parjob_e2e_multislot.c/test_snapshot_load_
|
|
* allocator_regression.c): the one real sched_init() call in this file
|
|
* (test 3 only) must succeed with no live controller so the real picker/
|
|
* allocator threads actually start.
|
|
* ════════════════════════════════════════════════════════════════════════ */
|
|
|
|
int __wrap_xcecli_start(xcecli_ctx_t c)
|
|
{
|
|
(void)c;
|
|
return 0;
|
|
}
|
|
|
|
int __wrap_xcecli_join(xcecli_ctx_t c, int role, hostattrs_t* attrs)
|
|
{
|
|
(void)c;
|
|
(void)role;
|
|
(void)attrs;
|
|
return 0;
|
|
}
|
|
|
|
void setUp(void) { return; }
|
|
void tearDown(void) { return; }
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Fixture helpers */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
/* make_wire_queue -- a real xce_queue_t wire payload, reused both for a
|
|
* QUEUE_UPDATE ingest event (tests 1/2's fresh-ingest side) and for a
|
|
* snapshot wire payload's `queues` array (test 2's restore side). Same
|
|
* shape as test_parjob_e2e_multislot.c's own helper of the same name --
|
|
* see that file's header comment (bug-1771) for why a real queue
|
|
* registration matters once a DISPATCH decision is ever produced; not
|
|
* strictly required for tests 1/2 (which never reach dispatch), but kept
|
|
* for parity/hygiene with the rest of this directory's real-ingest
|
|
* fixtures. Never torn down, same "fixtures aren't torn down" convention
|
|
* this whole directory follows. */
|
|
static xce_queue_t* make_wire_queue(const char* name, int32_t priority)
|
|
{
|
|
xce_queue_t* q = (xce_queue_t*)calloc(1, sizeof(*q));
|
|
|
|
TEST_ASSERT_NOT_NULL(q);
|
|
|
|
q->queue = strdup(name);
|
|
q->status = QUEUE_STAT_OPEN;
|
|
q->priority = priority;
|
|
|
|
return q;
|
|
}
|
|
|
|
/* make_push_job -- a heap-allocated xce_job_t/job_submit_data_t pair, ready
|
|
* for EITHER a real XCE_CORE_EVENT_JOBS_PUSH ingest event (core_build_job())
|
|
* OR a snapshot wire payload's `jobs` array (xce_sched_core_load_snapshot(),
|
|
* which reads the identical job_submit_data_t fields). Same field shapes
|
|
* test_parjob_e2e_multislot.c's own make_push_job()/test_snapshot_save_
|
|
* load.c's own make_job_with_resreq() each independently establish -- this
|
|
* is the one place this file needs BOTH shapes to be literally the same
|
|
* struct, since test 2's whole point is driving an equivalent job through
|
|
* both ingest mechanisms. Never torn down, same convention. */
|
|
static xce_job_t* make_push_job(uint64_t jobid, char* resreq_str, char* user,
|
|
char* queue, char* project, uint32_t mintasks,
|
|
uint32_t maxtasks)
|
|
{
|
|
xce_job_t* j = (xce_job_t*)calloc(1, sizeof(*j));
|
|
|
|
TEST_ASSERT_NOT_NULL(j);
|
|
|
|
j->jobid = jobid;
|
|
j->state = JOB_STAT_PEND;
|
|
j->sub_time = 1000;
|
|
j->queue_time = 1000;
|
|
|
|
j->submit = (job_submit_data_t*)calloc(1, sizeof(*(j->submit)));
|
|
TEST_ASSERT_NOT_NULL(j->submit);
|
|
|
|
j->submit->queue = queue;
|
|
j->submit->user = user;
|
|
j->submit->project_name = project;
|
|
j->submit->resreq = resreq_str;
|
|
j->submit->mintasks = mintasks;
|
|
j->submit->maxtasks = maxtasks;
|
|
|
|
return j;
|
|
}
|
|
|
|
/* make_wire_worker -- a real xce_worker_t wire payload carrying one
|
|
* "slots" resval, ready for a real XCE_CORE_EVENT_WORKERS_PUSH ingest.
|
|
* Identical shape to test_parjob_e2e_multislot.c's own helper of the same
|
|
* name. Only needed by test 3 (the only test in this file that dispatches
|
|
* against real workers). Never torn down, same convention. */
|
|
static xce_worker_t* make_wire_worker(const char* name, float slots)
|
|
{
|
|
xce_worker_t* w = (xce_worker_t*)calloc(1, sizeof(*w));
|
|
xce_val_t* val = (xce_val_t*)calloc(1, sizeof(*val));
|
|
xce_name_val_t* nv = (xce_name_val_t*)calloc(1, sizeof(*nv));
|
|
xce_name_val_t** nv_ptrs = (xce_name_val_t**)calloc(1, sizeof(*nv_ptrs));
|
|
|
|
TEST_ASSERT_NOT_NULL(w);
|
|
TEST_ASSERT_NOT_NULL(val);
|
|
TEST_ASSERT_NOT_NULL(nv);
|
|
TEST_ASSERT_NOT_NULL(nv_ptrs);
|
|
|
|
val->which = Value_u_fvalue;
|
|
val->u.fvalue = slots;
|
|
|
|
nv->name = strdup("slots");
|
|
nv->val = val;
|
|
nv_ptrs[0] = nv;
|
|
|
|
w->name = strdup(name);
|
|
w->status = 0;
|
|
w->nresvals_to_sched = 1;
|
|
w->resvals_to_sched = nv_ptrs;
|
|
|
|
return w;
|
|
}
|
|
|
|
/* register_slots_resource -- identical helper/rationale to
|
|
* test_parjob_e2e_multislot.c's/test_seqjob_allocator.c's own: seqjob_
|
|
* prepare_allocation()'s real key_find(sched->core.resmgr, "slots", ...)
|
|
* call needs "slots" to be a genuinely registered resource. Only needed by
|
|
* test 3. */
|
|
static xce_resource_t* register_slots_resource(var resmgr)
|
|
{
|
|
xce_resource_t res;
|
|
|
|
memset(&res, 0, sizeof(res));
|
|
res.name = strdup("slots");
|
|
|
|
return makeResource(resmgr, &res);
|
|
}
|
|
|
|
/* start_sched -- real sched_init() (registers the real builtin_fcfs/
|
|
* seqjob_allocator/parjob_allocator plugins and starts the real
|
|
* sched_job_picker()/sched_allocator() threads), plus the real per-session
|
|
* cache-clear hook registration main.c itself performs -- identical
|
|
* rationale/shape to test_parjob_e2e_multislot.c's own helper of the same
|
|
* name. Only needed by test 3. Port 20101 is not otherwise used by any
|
|
* other file in this testsuite directory (checked: 19997/19998/19999 are
|
|
* the only ones already claimed) -- moot regardless, since xcecli_start()/
|
|
* xcecli_join() are wrapped to no-op, so no real socket is ever bound to
|
|
* this port by this fixture. */
|
|
static xce_scheduler_t* start_sched(void)
|
|
{
|
|
static xce_client_config_t config;
|
|
static xce_cluster_config_t cluster_cfg;
|
|
xce_scheduler_t* sched
|
|
= (xce_scheduler_t*)calloc(1, sizeof(*sched));
|
|
xce_resource_t* slots_res;
|
|
int rc;
|
|
|
|
TEST_ASSERT_NOT_NULL(sched);
|
|
|
|
memset(&config, 0, sizeof(config));
|
|
config.nmasters = 1;
|
|
config.masters[0] = "127.0.0.1";
|
|
config.master_ports[0] = 20101;
|
|
config.from = "sched_maxtasks_wiring_ingest_test";
|
|
config.key = "keepsecret$";
|
|
|
|
memset(&cluster_cfg, 0, sizeof(cluster_cfg));
|
|
cluster_cfg.candidates_count = 1;
|
|
strncpy(cluster_cfg.candidates[0].hostname, "127.0.0.1",
|
|
XCE_MAXHOSTNAME_LEN - 1);
|
|
cluster_cfg.candidates[0].port = 20101;
|
|
strncpy(cluster_cfg.identity, "sched_maxtasks_wiring_ingest_test",
|
|
sizeof(cluster_cfg.identity) - 1);
|
|
|
|
rc = sched_init(sched, &config, &cluster_cfg, scheduler_req_handler);
|
|
TEST_ASSERT_TRUE_MESSAGE(
|
|
rc >= 0, "fixture: sched_init() must succeed (xcecli_start()/"
|
|
"xcecli_join() wrapped to succeed with no I/O) so the "
|
|
"real picker/allocator threads actually start");
|
|
|
|
slots_res = register_slots_resource(sched->core.resmgr);
|
|
TEST_ASSERT_NOT_NULL(slots_res);
|
|
|
|
xce_core_set_cache_clear_cb(&(sched->core), xce_alloc_cache_clear_cb);
|
|
|
|
return sched;
|
|
}
|
|
|
|
/* dispatch_and_wait -- bypass the dormant picker thread and feed jobid
|
|
* directly into the live allocator thread's input queue, then wait
|
|
* (bounded, non-blocking poll) for the resulting decision. Identical
|
|
* technique to test_parjob_e2e_multislot.c's/test_snapshot_load_allocator_
|
|
* regression.c's own helper of the same name. Only needed by test 3. */
|
|
static xce_decision_t* dispatch_and_wait(xce_scheduler_t* sched,
|
|
uint64_t jobid)
|
|
{
|
|
xce_decision_t* decision = NULL;
|
|
int rc;
|
|
int waited_ms;
|
|
|
|
rc = cbuf_tryput(&(sched->core.jbuf), &jobid);
|
|
TEST_ASSERT_TRUE_MESSAGE(rc >= 0, "fixture: jbuf must have room");
|
|
|
|
for (waited_ms = 0; waited_ms < 2000; waited_ms += 5) {
|
|
struct timespec ts = { 0, 5 * 1000 * 1000 };
|
|
|
|
if (cbuf_tryget(&(sched->core.decisions), &decision) >= 0) {
|
|
return decision;
|
|
}
|
|
nanosleep(&ts, NULL);
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Test 1 -- Req 1.1: fresh ingest reflects the real multi-task count */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
/*
|
|
* A real job submitted with mintasks=3/maxtasks=6 (deliberately different
|
|
* from task 2.1's own 4/6 fixture, so this is a genuinely independent data
|
|
* point, not a copy) is pushed through the REAL, live core_build_job()
|
|
* ingest path (XCE_CORE_EVENT_JOBS_PUSH). Confirms the standalone primary's
|
|
* minTasks/maxTasks reflect the real requested count, AND confirms
|
|
* job->schedobj->u.primary->resreq (what scheduler.c's real dispatch reads,
|
|
* scheduler.c:315) is the SAME object as job->resreq->u.primary (what task
|
|
* 2.1's own test checked) -- i.e. that this feature's fix genuinely reaches
|
|
* the dispatch-visible resreq, not merely a copy of it that dispatch never
|
|
* sees.
|
|
*/
|
|
static void test_fresh_ingest_reflects_real_multi_task_job_task_count(void)
|
|
{
|
|
xce_sched_core_t core;
|
|
xce_queue_t* queue;
|
|
xce_core_event_t queue_event;
|
|
xce_job_t* job;
|
|
xce_job_t* job_ptrs[1];
|
|
xce_core_event_t job_event;
|
|
xce_sched_job_t* sjob;
|
|
xce_primary_resreq_t* primary;
|
|
int rc;
|
|
|
|
rc = xce_sched_core_init(&core);
|
|
TEST_ASSERT_TRUE_MESSAGE(rc >= 0, "fixture: xce_sched_core_init() must "
|
|
"succeed");
|
|
|
|
queue = make_wire_queue("normal", 1);
|
|
memset(&queue_event, 0, sizeof(queue_event));
|
|
queue_event.type = XCE_CORE_EVENT_QUEUE_UPDATE;
|
|
queue_event.u.queue_update.queue = queue;
|
|
rc = xce_core_push_events(&core, &queue_event, 1);
|
|
TEST_ASSERT_TRUE_MESSAGE(rc >= 0, "fixture: real QUEUE_UPDATE ingest "
|
|
"must succeed");
|
|
|
|
job = make_push_job(JOBID_FRESH_MULTI, "type == any", "alice", "normal",
|
|
"proj1", 3, 6);
|
|
job_ptrs[0] = job;
|
|
|
|
memset(&job_event, 0, sizeof(job_event));
|
|
job_event.type = XCE_CORE_EVENT_JOBS_PUSH;
|
|
job_event.u.jobs_push.njobs = 1;
|
|
job_event.u.jobs_push.jobs = job_ptrs;
|
|
|
|
rc = xce_core_push_events(&core, &job_event, 1);
|
|
TEST_ASSERT_TRUE_MESSAGE(rc >= 0, "real JOBS_PUSH ingest must succeed");
|
|
|
|
sjob = sched_get_job(&core, JOBID_FRESH_MULTI);
|
|
TEST_ASSERT_NOT_NULL(sjob);
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(sjob->resreq, "core_build_job() must have "
|
|
"parsed a resreq");
|
|
TEST_ASSERT_EQUAL_INT(XCE_RESREQ_PRIMARY, sjob->resreq->tag);
|
|
TEST_ASSERT_NOT_NULL(sjob->resreq->u.primary);
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(
|
|
sjob->schedobj, "core_build_job() must have built job->schedobj");
|
|
TEST_ASSERT_EQUAL_INT(XCE_RESREQ_PRIMARY, sjob->schedobj->tag);
|
|
TEST_ASSERT_NOT_NULL(sjob->schedobj->u.primary);
|
|
|
|
/* The Req 1.1 proof: the real submitted task count reached the
|
|
* standalone primary's minTasks/maxTasks, not parse_resreq_primary()'s
|
|
* own always-1 default. */
|
|
primary = sjob->resreq->u.primary;
|
|
TEST_ASSERT_EQUAL_INT(3, primary->minTasks);
|
|
TEST_ASSERT_EQUAL_INT(6, primary->maxTasks);
|
|
|
|
/* The dispatch-reachability proof: what scheduler.c's real dispatch
|
|
* actually reads (job->schedobj->u.primary->resreq) is the SAME
|
|
* xce_primary_resreq_t object this feature's helper mutated (job->
|
|
* resreq->u.primary) -- not an independent copy that would leave
|
|
* dispatch still seeing the stale 1/1 default. */
|
|
TEST_ASSERT_TRUE_MESSAGE(
|
|
sjob->schedobj->u.primary->resreq == primary,
|
|
"job->schedobj->u.primary->resreq (what the real allocator dispatch "
|
|
"reads, scheduler.c:315) must be the exact same object as "
|
|
"job->resreq->u.primary (what apply_job_task_count() mutated) -- "
|
|
"otherwise the fix would be invisible to dispatch");
|
|
TEST_ASSERT_EQUAL_INT(3, sjob->schedobj->u.primary->resreq->minTasks);
|
|
TEST_ASSERT_EQUAL_INT(6, sjob->schedobj->u.primary->resreq->maxTasks);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Test 2 -- Req 1.3 / 5.6: fresh-ingest vs snapshot-restore parity */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
/*
|
|
* The SAME job spec (mintasks=2/maxtasks=9, resreq "type == any") is driven
|
|
* through BOTH live ingest call sites independently: fresh ingest
|
|
* (core_build_job(), via a real XCE_CORE_EVENT_JOBS_PUSH) into one bare
|
|
* core, and snapshot restore (xce_sched_core_load_snapshot(), the
|
|
* simulation/replay driver's own load path) into a SEPARATE, independently
|
|
* xce_sched_core_init()'d core. Req 5.6's comparative claim -- snapshot
|
|
* restore "shown to derive the same standalone primary resource request
|
|
* task count as it would through fresh submission" -- is asserted directly
|
|
* (fresh == restored), in addition to each path's own value being checked
|
|
* against the expected constant (the constant checks are what make the
|
|
* comparison meaningful rather than two independently-wrong values
|
|
* happening to agree -- see this file's own header comment's RED-phase
|
|
* note).
|
|
*/
|
|
static void
|
|
test_fresh_ingest_and_snapshot_restore_derive_identical_task_count_for_equivalent_job(void)
|
|
{
|
|
xce_sched_core_t core_fresh;
|
|
xce_sched_core_t core_restored;
|
|
xce_queue_t* queue_fresh;
|
|
xce_core_event_t queue_event;
|
|
xce_job_t* fresh_job;
|
|
xce_job_t* fresh_job_ptrs[1];
|
|
xce_core_event_t job_event;
|
|
xce_sched_job_t* fresh_sjob;
|
|
xce_primary_resreq_t* fresh_primary;
|
|
|
|
xce_queue_t* restore_queue;
|
|
xce_queue_t* restore_queues[1];
|
|
xce_job_t* restore_job;
|
|
xce_job_t* restore_jobs[1];
|
|
xce_jobs_data_t jobs_data;
|
|
sched_snapshot_data_t wire_in;
|
|
xce_sched_job_t* restored_sjob;
|
|
xce_primary_resreq_t* restored_primary;
|
|
|
|
int rc;
|
|
|
|
/* ── Path A: fresh ingest ────────────────────────────────────────── */
|
|
rc = xce_sched_core_init(&core_fresh);
|
|
TEST_ASSERT_TRUE_MESSAGE(rc >= 0, "fixture: xce_sched_core_init() must "
|
|
"succeed for the fresh-ingest core");
|
|
|
|
queue_fresh = make_wire_queue("normal", 1);
|
|
memset(&queue_event, 0, sizeof(queue_event));
|
|
queue_event.type = XCE_CORE_EVENT_QUEUE_UPDATE;
|
|
queue_event.u.queue_update.queue = queue_fresh;
|
|
rc = xce_core_push_events(&core_fresh, &queue_event, 1);
|
|
TEST_ASSERT_TRUE_MESSAGE(rc >= 0, "fixture: real QUEUE_UPDATE ingest "
|
|
"must succeed on the fresh core");
|
|
|
|
fresh_job = make_push_job(JOBID_PARITY, "type == any", "alice", "normal",
|
|
"proj1", 2, 9);
|
|
fresh_job_ptrs[0] = fresh_job;
|
|
|
|
memset(&job_event, 0, sizeof(job_event));
|
|
job_event.type = XCE_CORE_EVENT_JOBS_PUSH;
|
|
job_event.u.jobs_push.njobs = 1;
|
|
job_event.u.jobs_push.jobs = fresh_job_ptrs;
|
|
|
|
rc = xce_core_push_events(&core_fresh, &job_event, 1);
|
|
TEST_ASSERT_TRUE_MESSAGE(rc >= 0, "fixture: real JOBS_PUSH ingest must "
|
|
"succeed on the fresh core");
|
|
|
|
fresh_sjob = sched_get_job(&core_fresh, JOBID_PARITY);
|
|
TEST_ASSERT_NOT_NULL(fresh_sjob);
|
|
TEST_ASSERT_NOT_NULL(fresh_sjob->resreq);
|
|
TEST_ASSERT_EQUAL_INT(XCE_RESREQ_PRIMARY, fresh_sjob->resreq->tag);
|
|
fresh_primary = fresh_sjob->resreq->u.primary;
|
|
TEST_ASSERT_NOT_NULL(fresh_primary);
|
|
|
|
/* ── Path B: snapshot restore (the simulation/replay driver's own
|
|
* load path) -- an EQUIVALENT job (same jobid/mintasks/maxtasks/
|
|
* resreq/queue), built directly as a wire payload (same technique
|
|
* test_snapshot_save_load.c's own test_snapshot_load_wires_real_task_
|
|
* count_into_resreq_primary() uses: no real xce_sched_core_save_
|
|
* snapshot() round-trip needed, since the wire shape IS the input
|
|
* xce_sched_core_load_snapshot() itself consumes), loaded into a
|
|
* SEPARATE, independently-initialized core. ────────────────────── */
|
|
rc = xce_sched_core_init(&core_restored);
|
|
TEST_ASSERT_TRUE_MESSAGE(rc >= 0, "fixture: xce_sched_core_init() must "
|
|
"succeed for the restore-target core");
|
|
|
|
restore_queue = make_wire_queue("normal", 1);
|
|
restore_queues[0] = restore_queue;
|
|
|
|
restore_job = make_push_job(JOBID_PARITY, "type == any", "alice",
|
|
"normal", "proj1", 2, 9);
|
|
restore_jobs[0] = restore_job;
|
|
|
|
memset(&jobs_data, 0, sizeof(jobs_data));
|
|
jobs_data.count = 1;
|
|
jobs_data.jobs = restore_jobs;
|
|
|
|
memset(&wire_in, 0, sizeof(wire_in));
|
|
wire_in.jobs = &jobs_data;
|
|
wire_in.queues = restore_queues;
|
|
wire_in.n_queues = 1;
|
|
|
|
rc = xce_sched_core_load_snapshot(&core_restored, &wire_in);
|
|
TEST_ASSERT_TRUE_MESSAGE(rc >= 0, "xce_sched_core_load_snapshot() must "
|
|
"succeed");
|
|
|
|
restored_sjob = sched_get_job(&core_restored, JOBID_PARITY);
|
|
TEST_ASSERT_NOT_NULL(restored_sjob);
|
|
TEST_ASSERT_NOT_NULL(restored_sjob->resreq);
|
|
TEST_ASSERT_EQUAL_INT(XCE_RESREQ_PRIMARY, restored_sjob->resreq->tag);
|
|
restored_primary = restored_sjob->resreq->u.primary;
|
|
TEST_ASSERT_NOT_NULL(restored_primary);
|
|
|
|
/* ── Expected-constant checks (what make the comparison below
|
|
* meaningful, not vacuous) ──────────────────────────────────────── */
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
2, fresh_primary->minTasks,
|
|
"fresh ingest must derive the job's real minTasks");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
9, fresh_primary->maxTasks,
|
|
"fresh ingest must derive the job's real maxTasks");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
2, restored_primary->minTasks,
|
|
"snapshot restore must derive the job's real minTasks");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
9, restored_primary->maxTasks,
|
|
"snapshot restore must derive the job's real maxTasks");
|
|
|
|
/* ── Req 5.6's own comparative claim, asserted directly ──────────── */
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
fresh_primary->minTasks, restored_primary->minTasks,
|
|
"Req 5.6: snapshot-restore ingest must derive the IDENTICAL "
|
|
"standalone primary minTasks an equivalent fresh submission would");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
fresh_primary->maxTasks, restored_primary->maxTasks,
|
|
"Req 5.6: snapshot-restore ingest must derive the IDENTICAL "
|
|
"standalone primary maxTasks an equivalent fresh submission would");
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Test 3 -- Req 3.1 / 3.2: single-task job, no dispatch regression */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
/*
|
|
* A job requesting exactly one task (mintasks=maxtasks=1) is driven through
|
|
* the REAL, live allocator dispatch chain (sched_init()'s real
|
|
* sched_allocator() thread -> the real, static sched_make_primary_alloc_()
|
|
* choke point -> seqjob_allocator, since maxTasks==1) against two real
|
|
* 1.0-slot workers. Note: at the C layer, "no explicit task count" and "an
|
|
* explicit count of one" are indistinguishable -- job_submit_data_t only
|
|
* ever carries mintasks/maxtasks, and the apiserver's own validation
|
|
* (checkNumProcessors()/convert.go's clamp, outside this spec's boundary)
|
|
* already normalizes an absent count to 1 before a job ever reaches this
|
|
* layer, the same way task 2.1's own test_core_build_job_default_task_
|
|
* count_still_one() (test_core_events.c) established for the field-level
|
|
* check -- so this one dispatch-level scenario faithfully covers both
|
|
* phrasings of Req 3.1/3.2 (a bare "1" is what "no explicit count" looks
|
|
* like once it has already reached the C layer).
|
|
*
|
|
* Since apply_job_task_count(resreq, 1, 1) writes back exactly the same
|
|
* (1, 1) pair parse_resreq_primary()'s own pre-existing hardcode already
|
|
* produced, this is a no-op change by construction -- the point of this
|
|
* test is to catch an ACCIDENTAL behavioral regression (e.g. a bug in the
|
|
* helper, or an ordering mistake at the call site corrupting resreq_fp or
|
|
* something else downstream reads), not to discover an intentional one. A
|
|
* clean pass -- exactly one of the two real workers claimed, with exactly
|
|
* 1.0 slots allocated, never a 2-host split -- is the correct, expected
|
|
* outcome (confirmed empirically to hold identically with the fix's own
|
|
* apply_job_task_count() call temporarily removed -- see this file's own
|
|
* header comment's RED-phase note).
|
|
*/
|
|
static void
|
|
test_single_task_job_dispatch_and_host_selection_unchanged_via_real_allocator(void)
|
|
{
|
|
xce_scheduler_t* sched = start_sched();
|
|
xce_queue_t* queue;
|
|
xce_core_event_t queue_event;
|
|
xce_worker_t* worker_ptrs[2];
|
|
xce_core_event_t worker_event;
|
|
xce_job_t* job;
|
|
xce_job_t* job_ptrs[1];
|
|
xce_core_event_t job_event;
|
|
xce_sched_job_t* sjob;
|
|
xce_primary_resreq_t* primary;
|
|
xce_decision_t* decision;
|
|
int rc;
|
|
int claimed_host_count = 0;
|
|
int saw_c = 0;
|
|
int saw_d = 0;
|
|
double total_slots = 0.0;
|
|
|
|
/* ── Real queue + worker ingest ───────────────────────────────────── */
|
|
queue = make_wire_queue("normal", 1);
|
|
memset(&queue_event, 0, sizeof(queue_event));
|
|
queue_event.type = XCE_CORE_EVENT_QUEUE_UPDATE;
|
|
queue_event.u.queue_update.queue = queue;
|
|
rc = xce_core_push_events(&(sched->core), &queue_event, 1);
|
|
TEST_ASSERT_TRUE_MESSAGE(rc >= 0, "fixture: real QUEUE_UPDATE ingest "
|
|
"must succeed");
|
|
|
|
worker_ptrs[0] = make_wire_worker(WORKER_C_NAME, WORKER_SLOTS);
|
|
worker_ptrs[1] = make_wire_worker(WORKER_D_NAME, WORKER_SLOTS);
|
|
|
|
memset(&worker_event, 0, sizeof(worker_event));
|
|
worker_event.type = XCE_CORE_EVENT_WORKERS_PUSH;
|
|
worker_event.u.workers_push.nworkers = 2;
|
|
worker_event.u.workers_push.workers = worker_ptrs;
|
|
|
|
rc = xce_core_push_events(&(sched->core), &worker_event, 1);
|
|
TEST_ASSERT_TRUE_MESSAGE(rc >= 0, "fixture: real WORKERS_PUSH ingest "
|
|
"must succeed");
|
|
TEST_ASSERT_NOT_NULL(sched_get_worker(&(sched->core), WORKER_C_NAME));
|
|
TEST_ASSERT_NOT_NULL(sched_get_worker(&(sched->core), WORKER_D_NAME));
|
|
|
|
/* ── Real job ingest (single-task) ───────────────────────────────── */
|
|
job = make_push_job(JOBID_SINGLE_TASK, SINGLE_TASK_RESREQ_STR,
|
|
"single_task_user", "normal", "single_task_proj", 1,
|
|
1);
|
|
job_ptrs[0] = job;
|
|
|
|
memset(&job_event, 0, sizeof(job_event));
|
|
job_event.type = XCE_CORE_EVENT_JOBS_PUSH;
|
|
job_event.u.jobs_push.njobs = 1;
|
|
job_event.u.jobs_push.jobs = job_ptrs;
|
|
|
|
rc = xce_core_push_events(&(sched->core), &job_event, 1);
|
|
TEST_ASSERT_TRUE_MESSAGE(rc >= 0, "fixture: real JOBS_PUSH ingest must "
|
|
"succeed");
|
|
|
|
sjob = sched_get_job(&(sched->core), JOBID_SINGLE_TASK);
|
|
TEST_ASSERT_NOT_NULL(sjob);
|
|
TEST_ASSERT_NOT_NULL(sjob->schedobj);
|
|
TEST_ASSERT_EQUAL_INT(XCE_RESREQ_PRIMARY, sjob->schedobj->tag);
|
|
|
|
primary = sjob->schedobj->u.primary->resreq;
|
|
TEST_ASSERT_NOT_NULL(primary);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
1, primary->minTasks,
|
|
"a single-task job's standalone primary minTasks must still "
|
|
"resolve to 1 -- this feature must be a no-op for this case");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
1, primary->maxTasks,
|
|
"a single-task job's standalone primary maxTasks must still "
|
|
"resolve to 1 -- this feature must be a no-op for this case, and "
|
|
"this is also the dispatch gate that routes to seqjob_allocator "
|
|
"rather than parjob_allocator (scheduler.c)");
|
|
|
|
/* ── Real per-session begin + drive through the REAL, live allocator */
|
|
rc = xce_core_begin_session(&(sched->core));
|
|
TEST_ASSERT_TRUE_MESSAGE(rc >= 0, "fixture: xce_core_begin_session() "
|
|
"must succeed");
|
|
|
|
decision = dispatch_and_wait(sched, JOBID_SINGLE_TASK);
|
|
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(
|
|
decision, "the real allocator must produce a decision for the "
|
|
"single-task job within 2s");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
XCE_DECISION_DISPATCH, decision->op,
|
|
"a single-task job must DISPATCH -- two real 1.0-slot workers each "
|
|
"trivially satisfy a 1-slot want");
|
|
|
|
TEST_ASSERT_NOT_NULL(decision->u.alloc);
|
|
TEST_ASSERT_EQUAL_UINT64(JOBID_SINGLE_TASK, decision->u.alloc->jobid);
|
|
|
|
/* The Req 3.1/3.2 proof: exactly ONE host claimed, never a split
|
|
* across both -- the observable that would change if maxTasks had
|
|
* been accidentally corrupted to something other than 1 (which would
|
|
* route to parjob_allocator instead and could span both workers). */
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
1, decision->u.alloc->nhosts,
|
|
"a single-task job's committed allocation must span EXACTLY one "
|
|
"host -- a value other than 1 here would mean this job was "
|
|
"accidentally routed to the multi-host parjob_allocator path "
|
|
"instead of the single-host seqjob_allocator path this feature "
|
|
"must leave unaffected");
|
|
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < decision->u.alloc->nhosts; i++) {
|
|
xce_host_alloc_t* h = decision->u.alloc->allocs[i];
|
|
int j;
|
|
int found_slots = 0;
|
|
|
|
TEST_ASSERT_NOT_NULL(h);
|
|
TEST_ASSERT_NOT_NULL(h->host);
|
|
claimed_host_count++;
|
|
|
|
if (strcmp(h->host, WORKER_C_NAME) == 0) {
|
|
saw_c = 1;
|
|
} else if (strcmp(h->host, WORKER_D_NAME) == 0) {
|
|
saw_d = 1;
|
|
} else {
|
|
TEST_FAIL_MESSAGE(
|
|
"claimed host is neither of the two real ingested "
|
|
"workers");
|
|
}
|
|
|
|
for (j = 0; j < h->nallocs; j++) {
|
|
xce_alloc_t* a = h->allocs[j];
|
|
|
|
TEST_ASSERT_NOT_NULL(a);
|
|
if ((a->resname != NULL)
|
|
&& (strcmp(a->resname, "slots") == 0)) {
|
|
found_slots = 1;
|
|
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
|
|
1.0f, (float)a->value,
|
|
"the single claimed host's own slots allocation "
|
|
"must be exactly 1.0 -- the job's full (and only) "
|
|
"requested task, not some other value");
|
|
total_slots += a->value;
|
|
}
|
|
}
|
|
|
|
TEST_ASSERT_TRUE_MESSAGE(
|
|
found_slots, "the claimed host must carry a real 'slots' "
|
|
"allocation entry");
|
|
}
|
|
}
|
|
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
1, claimed_host_count,
|
|
"exactly one host must appear in the committed allocation");
|
|
TEST_ASSERT_TRUE_MESSAGE(
|
|
saw_c || saw_d,
|
|
"the single claimed host must be one of the two real ingested "
|
|
"workers");
|
|
TEST_ASSERT_FALSE_MESSAGE(
|
|
saw_c && saw_d,
|
|
"a single-task job must never claim BOTH workers -- that would be "
|
|
"the multi-host split this feature must not introduce for "
|
|
"maxTasks==1");
|
|
TEST_ASSERT_EQUAL_FLOAT_MESSAGE(
|
|
1.0f, (float)total_slots,
|
|
"total allocated slots across the (single) claimed host must be "
|
|
"exactly 1.0");
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Test 4 -- Req 1.4: an update never changes the already-ingested task */
|
|
/* count (job->schedobj is never rebuilt by JOB_UPDATE) */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
/*
|
|
* Design.md's Existing Architecture Analysis: core_events.c's JOB_UPDATE
|
|
* handler (live) replaces job->resreq with a freshly parse_resreq()'d
|
|
* object when updated->submit->resreq is non-NULL, but never reassigns
|
|
* job->schedobj (grepped exhaustively: no `job->schedobj =` assignment
|
|
* anywhere in the JOB_UPDATE case, core_events.c:1174-1257) -- so
|
|
* scheduler.c's real dispatch (job->schedobj->u.primary->resreq,
|
|
* scheduler.c:315) never sees an update at all, for ANY field, not only
|
|
* task count (bug-1773). Requirement 1.4's guarantee already holds today
|
|
* for this structural reason, not because this feature actively re-wires
|
|
* anything on update (design.md, Non-Goals) -- this test's job is to PROVE
|
|
* the guarantee holds, not to add new production wiring.
|
|
*
|
|
* A real multi-task job (mintasks=4/maxtasks=7, deliberately distinct from
|
|
* every other test's own fixture values in this file) is ingested fresh,
|
|
* then a JOB_UPDATE event for the SAME jobid is driven with BOTH a
|
|
* different resreq string ("rusage[mem=2]" instead of "type == any") AND a
|
|
* different requested task count (mintasks=1/maxtasks=1) in its own
|
|
* job_submit_data_t -- exactly the scenario Req 1.4 describes ("a job's
|
|
* resource request is updated"). Confirms job->resreq WAS genuinely
|
|
* replaced (proving the update was not silently a no-op, which would make
|
|
* the immunity assertion below vacuous), and that job->schedobj->u.primary
|
|
* ->resreq -- what the real allocator dispatch actually reads -- is the
|
|
* EXACT SAME object, with the SAME minTasks/maxTasks, ingest originally
|
|
* produced, completely unaffected by the update's own different task
|
|
* count.
|
|
*
|
|
* Meaningfulness (reasoned, per this task's own brief, rather than
|
|
* empirically red/green-tested -- the failure mode guarded against is a
|
|
* future production change, not a bug present today): this test would fail
|
|
* if a future change to the JOB_UPDATE case started reassigning
|
|
* job->schedobj (e.g. rebuilding it via core_build_schedobj() the way
|
|
* core_build_job() does), since apply_job_task_count() applied there would
|
|
* then pick up the UPDATE's own (differing) mintasks/maxtasks rather than
|
|
* ingest's -- the post-update assertions below would read 1/1 instead of
|
|
* the expected 4/7.
|
|
*/
|
|
static void
|
|
test_resource_request_update_never_changes_dispatched_task_count(void)
|
|
{
|
|
xce_sched_core_t core;
|
|
xce_queue_t* queue;
|
|
xce_core_event_t queue_event;
|
|
xce_job_t* job;
|
|
xce_job_t* job_ptrs[1];
|
|
xce_core_event_t job_event;
|
|
xce_sched_job_t* sjob;
|
|
xce_primary_resreq_t* ingest_time_primary;
|
|
xce_job_t* update_job;
|
|
xce_core_event_t update_event;
|
|
int rc;
|
|
|
|
rc = xce_sched_core_init(&core);
|
|
TEST_ASSERT_TRUE_MESSAGE(rc >= 0, "fixture: xce_sched_core_init() must "
|
|
"succeed");
|
|
|
|
queue = make_wire_queue("normal", 1);
|
|
memset(&queue_event, 0, sizeof(queue_event));
|
|
queue_event.type = XCE_CORE_EVENT_QUEUE_UPDATE;
|
|
queue_event.u.queue_update.queue = queue;
|
|
rc = xce_core_push_events(&core, &queue_event, 1);
|
|
TEST_ASSERT_TRUE_MESSAGE(rc >= 0, "fixture: real QUEUE_UPDATE ingest "
|
|
"must succeed");
|
|
|
|
job = make_push_job(JOBID_UPDATE_IMMUNE, "type == any", "alice", "normal",
|
|
"proj1", 4, 7);
|
|
job_ptrs[0] = job;
|
|
|
|
memset(&job_event, 0, sizeof(job_event));
|
|
job_event.type = XCE_CORE_EVENT_JOBS_PUSH;
|
|
job_event.u.jobs_push.njobs = 1;
|
|
job_event.u.jobs_push.jobs = job_ptrs;
|
|
|
|
rc = xce_core_push_events(&core, &job_event, 1);
|
|
TEST_ASSERT_TRUE_MESSAGE(rc >= 0, "fixture: real JOBS_PUSH ingest must "
|
|
"succeed");
|
|
|
|
sjob = sched_get_job(&core, JOBID_UPDATE_IMMUNE);
|
|
TEST_ASSERT_NOT_NULL(sjob);
|
|
TEST_ASSERT_NOT_NULL(sjob->schedobj);
|
|
TEST_ASSERT_EQUAL_INT(XCE_RESREQ_PRIMARY, sjob->schedobj->tag);
|
|
ingest_time_primary = sjob->schedobj->u.primary->resreq;
|
|
TEST_ASSERT_NOT_NULL(ingest_time_primary);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
4, ingest_time_primary->minTasks,
|
|
"fixture: ingest must derive the job's real minTasks before the "
|
|
"update is driven");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
7, ingest_time_primary->maxTasks,
|
|
"fixture: ingest must derive the job's real maxTasks before the "
|
|
"update is driven");
|
|
|
|
/* ── Drive a real JOB_UPDATE: a DIFFERENT resreq string AND a
|
|
* DIFFERENT requested task count for the SAME job ─────────────────── */
|
|
update_job = make_push_job(JOBID_UPDATE_IMMUNE, "rusage[mem=2]", "alice",
|
|
"normal", "proj1", 1, 1);
|
|
|
|
memset(&update_event, 0, sizeof(update_event));
|
|
update_event.type = XCE_CORE_EVENT_JOB_UPDATE;
|
|
update_event.u.job_update.jobid = JOBID_UPDATE_IMMUNE;
|
|
update_event.u.job_update.job = update_job;
|
|
|
|
rc = xce_core_push_events(&core, &update_event, 1);
|
|
TEST_ASSERT_TRUE_MESSAGE(rc >= 0, "real JOB_UPDATE ingest must succeed");
|
|
|
|
sjob = sched_get_job(&core, JOBID_UPDATE_IMMUNE);
|
|
TEST_ASSERT_NOT_NULL(sjob);
|
|
|
|
/* Sanity: the update DID genuinely replace job->resreq (not a silent
|
|
* no-op) -- job->resreq is now a fresh object showing parse_resreq_
|
|
* primary()'s own bare 1/1 default (JOB_UPDATE never calls apply_job_
|
|
* task_count(), design.md), confirming the assertions below are a
|
|
* meaningful immunity proof, not a vacuous one from an update that
|
|
* never took effect. */
|
|
TEST_ASSERT_NOT_NULL(sjob->resreq);
|
|
TEST_ASSERT_EQUAL_INT(XCE_RESREQ_PRIMARY, sjob->resreq->tag);
|
|
TEST_ASSERT_FALSE_MESSAGE(
|
|
sjob->resreq->u.primary == ingest_time_primary,
|
|
"fixture: JOB_UPDATE must have replaced job->resreq with a "
|
|
"genuinely NEW object, not left the old one in place, for this "
|
|
"test's immunity assertion to be meaningful");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
1, sjob->resreq->u.primary->minTasks,
|
|
"fixture: job->resreq (freshly reparsed by JOB_UPDATE) must show "
|
|
"parse_resreq_primary()'s own bare default, confirming JOB_UPDATE "
|
|
"never calls apply_job_task_count()");
|
|
|
|
/* ── The Req 1.4 proof: job->schedobj->u.primary->resreq (what the
|
|
* real allocator dispatch reads, scheduler.c:315) is the EXACT SAME
|
|
* object, with the EXACT SAME ingest-time task count, unaffected by
|
|
* the update -- because JOB_UPDATE never reassigns job->schedobj. ──── */
|
|
TEST_ASSERT_NOT_NULL(sjob->schedobj);
|
|
TEST_ASSERT_TRUE_MESSAGE(
|
|
sjob->schedobj->u.primary->resreq == ingest_time_primary,
|
|
"job->schedobj->u.primary->resreq must remain the EXACT SAME "
|
|
"object ingest originally built -- JOB_UPDATE must never rebuild "
|
|
"job->schedobj");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
4, sjob->schedobj->u.primary->resreq->minTasks,
|
|
"Req 1.4: the job's dispatched minTasks must remain the "
|
|
"ingest-time value (4), unaffected by the update's own different "
|
|
"mintasks (1)");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
7, sjob->schedobj->u.primary->resreq->maxTasks,
|
|
"Req 1.4: the job's dispatched maxTasks must remain the "
|
|
"ingest-time value (7), unaffected by the update's own different "
|
|
"maxtasks (1)");
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Test 5 -- Req 5.4: differing task counts never collide in the */
|
|
/* unsatisfiable-resreq cache, on the fresh-ingest path */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
/*
|
|
* Req 5.4 (fresh-ingest path only; snapshot-restore is the documented,
|
|
* pre-existing bug-1772 exception -- design.md's Testing Strategy). Two
|
|
* otherwise-identical jobs (same resreq string, same user/queue/project)
|
|
* that differ ONLY in requested task count must never collide in
|
|
* scheduler-allocation's unsatisfiable-resreq cache (sched/alloc_cache.h).
|
|
* core_build_primary_schedobj() (core_events.c) computes primary->
|
|
* resreq_fp.min_tasks/max_tasks directly from resreq->minTasks/maxTasks --
|
|
* the exact field this feature's apply_job_task_count() call
|
|
* (core_events.c, immediately before core_build_schedobj()) sets from the
|
|
* job's real requested count. Two such jobs, ingested fresh into the SAME
|
|
* core/resmgr (so their resreq_fp's OTHER components -- select_node_id,
|
|
* rusage_hash -- are guaranteed identical, isolating task count as the
|
|
* only possible source of a difference), must therefore produce DIFFERENT
|
|
* cache keys: an unsatisfiable outcome cached for one must not hit for the
|
|
* other.
|
|
*
|
|
* This reuses scheduler-allocation's own alloc_cache test conventions
|
|
* (test_alloc_cache.c: cache_insert()/cache_lookup() driven directly
|
|
* against real primary schedobj pointers, a bare xce_scheduler_t with only
|
|
* unsatisfiable_cache constructed) combined with REAL job ingest via
|
|
* core_build_job() (this file's own established fresh-ingest technique)
|
|
* rather than test_alloc_cache.c's own hand-built xce_primary_schedobj_t
|
|
* fixtures -- this is the genuinely new combination this task adds.
|
|
*
|
|
* Empirically verified meaningful (not merely reasoned): with
|
|
* core_events.c's apply_job_task_count() call temporarily commented out
|
|
* (the pre-fix always-1/1 behavior bug-1770 describes), both jobs below
|
|
* collapse to the IDENTICAL resreq_fp (min_tasks=max_tasks=1 for both,
|
|
* regardless of their different requested counts), and this test's own
|
|
* cache_lookup() assertion for job B FAILS (a real collision: rc==1, a
|
|
* false hit) -- confirmed by temporarily commenting out that one line,
|
|
* rebuilding this binary in isolation, observing the failure, then
|
|
* reverting the file to a git-clean state before finalizing this test.
|
|
*/
|
|
static void
|
|
test_differing_task_counts_do_not_collide_in_unsatisfiable_cache(void)
|
|
{
|
|
xce_scheduler_t sched;
|
|
xce_queue_t* queue;
|
|
xce_core_event_t queue_event;
|
|
xce_job_t* job_a;
|
|
xce_job_t* job_b;
|
|
xce_job_t* job_ptrs[2];
|
|
xce_core_event_t job_event;
|
|
xce_sched_job_t* sjob_a;
|
|
xce_sched_job_t* sjob_b;
|
|
int out_reason = -1;
|
|
int rc;
|
|
|
|
memset(&sched, 0, sizeof(sched));
|
|
rc = xce_sched_core_init(&(sched.core));
|
|
TEST_ASSERT_TRUE_MESSAGE(rc >= 0, "fixture: xce_sched_core_init() must "
|
|
"succeed");
|
|
|
|
cache_clear(&sched);
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(
|
|
sched.unsatisfiable_cache,
|
|
"fixture: cache_clear() must construct a real unsatisfiable_cache "
|
|
"table (mirrors the real per-session construction, main.c's "
|
|
"xce_core_set_cache_clear_cb() registration)");
|
|
|
|
queue = make_wire_queue("normal", 1);
|
|
memset(&queue_event, 0, sizeof(queue_event));
|
|
queue_event.type = XCE_CORE_EVENT_QUEUE_UPDATE;
|
|
queue_event.u.queue_update.queue = queue;
|
|
rc = xce_core_push_events(&(sched.core), &queue_event, 1);
|
|
TEST_ASSERT_TRUE_MESSAGE(rc >= 0, "fixture: real QUEUE_UPDATE ingest "
|
|
"must succeed");
|
|
|
|
/* Same resreq string/user/queue/project -- differ ONLY in requested
|
|
* task count. Both ingested into the SAME core/resmgr, so their
|
|
* resreq_fp's select_node_id/rusage_hash components are guaranteed
|
|
* identical -- isolating task count as the only possible source of a
|
|
* fingerprint difference. */
|
|
job_a = make_push_job(JOBID_CACHE_A, "rusage[mem=1]", "cache_user",
|
|
"normal", "cache_proj", 2, 2);
|
|
job_b = make_push_job(JOBID_CACHE_B, "rusage[mem=1]", "cache_user",
|
|
"normal", "cache_proj", 3, 3);
|
|
job_ptrs[0] = job_a;
|
|
job_ptrs[1] = job_b;
|
|
|
|
memset(&job_event, 0, sizeof(job_event));
|
|
job_event.type = XCE_CORE_EVENT_JOBS_PUSH;
|
|
job_event.u.jobs_push.njobs = 2;
|
|
job_event.u.jobs_push.jobs = job_ptrs;
|
|
|
|
rc = xce_core_push_events(&(sched.core), &job_event, 1);
|
|
TEST_ASSERT_TRUE_MESSAGE(rc >= 0, "real JOBS_PUSH ingest must succeed "
|
|
"for both jobs");
|
|
|
|
sjob_a = sched_get_job(&(sched.core), JOBID_CACHE_A);
|
|
sjob_b = sched_get_job(&(sched.core), JOBID_CACHE_B);
|
|
TEST_ASSERT_NOT_NULL(sjob_a);
|
|
TEST_ASSERT_NOT_NULL(sjob_b);
|
|
TEST_ASSERT_NOT_NULL(sjob_a->schedobj);
|
|
TEST_ASSERT_NOT_NULL(sjob_b->schedobj);
|
|
TEST_ASSERT_EQUAL_INT(XCE_RESREQ_PRIMARY, sjob_a->schedobj->tag);
|
|
TEST_ASSERT_EQUAL_INT(XCE_RESREQ_PRIMARY, sjob_b->schedobj->tag);
|
|
|
|
/* Sanity: the two jobs' real, ingest-derived fingerprints DO differ in
|
|
* exactly (and only) their task-count component -- confirms this
|
|
* test's premise before exercising the cache itself. */
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
2, sjob_a->schedobj->u.primary->resreq_fp.min_tasks,
|
|
"job A's real ingest-derived resreq_fp.min_tasks must reflect its "
|
|
"own requested task count");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
3, sjob_b->schedobj->u.primary->resreq_fp.min_tasks,
|
|
"job B's real ingest-derived resreq_fp.min_tasks must reflect its "
|
|
"own (different) requested task count");
|
|
TEST_ASSERT_EQUAL_UINT64_MESSAGE(
|
|
sjob_a->schedobj->u.primary->resreq_fp.rusage_hash,
|
|
sjob_b->schedobj->u.primary->resreq_fp.rusage_hash,
|
|
"fixture: both jobs share the identical resreq string, so their "
|
|
"rusage_hash fingerprint component must be identical -- isolating "
|
|
"task count as the only differing component");
|
|
|
|
/* ── The Req 5.4 proof: an unsatisfiable outcome cached for job A
|
|
* must NOT produce a cache hit for job B. ──────────────────────────── */
|
|
rc = cache_insert(&sched, sjob_a->schedobj, 7 /* arbitrary category */);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
0, rc, "fixture: cache_insert() for job A must succeed");
|
|
|
|
rc = cache_lookup(&sched, sjob_b->schedobj, &out_reason);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
0, rc,
|
|
"Req 5.4: job B (a different requested task count, otherwise "
|
|
"identical) must NOT hit job A's cached unsatisfiable outcome");
|
|
|
|
/* Sanity: the cache mechanism itself genuinely works -- job A's OWN
|
|
* key IS a hit, with the SAME reason category given at insert time.
|
|
* Without this, the miss above could just as easily mean the cache is
|
|
* dead altogether, not that it correctly distinguishes task counts. */
|
|
rc = cache_lookup(&sched, sjob_a->schedobj, &out_reason);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
|
1, rc, "sanity: job A's own key must hit its own cached entry");
|
|
TEST_ASSERT_EQUAL_INT(7, out_reason);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_fresh_ingest_reflects_real_multi_task_job_task_count);
|
|
RUN_TEST(
|
|
test_fresh_ingest_and_snapshot_restore_derive_identical_task_count_for_equivalent_job);
|
|
RUN_TEST(
|
|
test_single_task_job_dispatch_and_host_selection_unchanged_via_real_allocator);
|
|
RUN_TEST(test_resource_request_update_never_changes_dispatched_task_count);
|
|
RUN_TEST(test_differing_task_counts_do_not_collide_in_unsatisfiable_cache);
|
|
return UNITY_END();
|
|
}
|