test(scheduler-sim-live-allocation): prove replay-driven limit visibility (task 5.2, closes bug-1826)
Adds the feature's concrete original acceptance test: a genuine capnp-encoded .replay (real ReplayHeader/SnapshotEntry/Conf4Sched with one global and one host-scoped limit), driven end-to-end through the real, unmodified sched_sim_driver_run(). The dispatched job's usage registers against both limits via limit_get_configured_limits()/ limit_get_usage_instances() -- the same accessors info limit reads -- proving real, non-zero usage where today's REPL previously showed none. RED-phase evidence: reverting task 4.3's 4 files to their pre-fix commit reproduces the exact predicted failure (host-scoped usage missing, global usage intact); restoring to HEAD reproduces GREEN. Closes bug-1826 (the bug this whole feature was built to fix) -- buglog entry updated from NOT FIXED to FULLY FIXED, citing this test. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLSNJhNoULvVcUhXEFafQC
This commit is contained in:
parent
ab88848f91
commit
a70c7a2c61
4 changed files with 5006 additions and 2198 deletions
|
|
@ -2191,3 +2191,33 @@ sched_sim_contention_limit_driver_parity_test = executable(
|
|||
test('scheduler sim contention limit driver parity test',
|
||||
sched_sim_contention_limit_driver_parity_test, suite: 'scheduler',
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
|
||||
# scheduler-sim-live-allocation spec, task 5.2 (Req 2.1, 2.2, 2.3, 2.4): the
|
||||
# feature's concrete, original acceptance test (bug-1826) -- a real global
|
||||
# AND host-scoped limit configured in a real SnapshotEntry's Conf4Sched,
|
||||
# carried through a real `.replay` file, driven end to end via the real,
|
||||
# unmodified sched_sim_driver_run() -- confirming `info limit`'s own
|
||||
# backing accessors (limit_get_configured_limits()/limit_get_usage_
|
||||
# instances()) show real, non-zero usage for both, where before this
|
||||
# feature they showed none for either. Same real-scheduler_srcs link
|
||||
# technique as sched_sim_live_allocation_test/sched_sim_alloc_limit_fanout_
|
||||
# test/sched_sim_contention_limit_driver_parity_test above.
|
||||
sched_sim_e2e_limit_visibility_test_srcs = [
|
||||
'test_sim_e2e_limit_visibility.c'
|
||||
]
|
||||
foreach f : scheduler_srcs
|
||||
if f != 'src/main.c'
|
||||
sched_sim_e2e_limit_visibility_test_srcs += ['../' + f]
|
||||
endif
|
||||
endforeach
|
||||
|
||||
sched_sim_e2e_limit_visibility_test = executable(
|
||||
'sched_sim_e2e_limit_visibility_test',
|
||||
sched_sim_e2e_limit_visibility_test_srcs,
|
||||
include_directories:[incdir],
|
||||
dependencies: [scheduler_test_deps],
|
||||
install: false)
|
||||
|
||||
test('scheduler sim e2e limit visibility test',
|
||||
sched_sim_e2e_limit_visibility_test, suite: 'scheduler',
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
|
|
|
|||
721
src/scheduler/testsuite/test_sim_e2e_limit_visibility.c
Normal file
721
src/scheduler/testsuite/test_sim_e2e_limit_visibility.c
Normal file
|
|
@ -0,0 +1,721 @@
|
|||
/*
|
||||
* test_sim_e2e_limit_visibility.c - scheduler-sim-live-allocation spec,
|
||||
* task 5.2 (Req 2.1, 2.2, 2.3, 2.4, Depends: 4.1, 4.2, 4.3): the feature's
|
||||
* concrete, original acceptance test -- bug-1826 itself. "if the `info
|
||||
* limit` can not show the usage information of limit resources, then the
|
||||
* power of this command will get limited."
|
||||
*
|
||||
* ── What this proves that no existing test does ───────────────────────────
|
||||
* test_sim_alloc_limit_fanout.c (task 4.3) proves a HOST-scoped limit's
|
||||
* usage instance is created via a live seqjob dispatch, but builds its
|
||||
* fixture by hand (xce_sched_core_init() + the same four sim_bootstrap_*()
|
||||
* calls sched_sim_driver_run() itself makes, called directly) -- it never
|
||||
* drives an actual `.replay` FILE through the real sched_sim_driver_run()
|
||||
* entry point, and it configures ONLY a host-scoped limit, never a global
|
||||
* one. test_sim_contention_limit_driver_parity.c (task 5.1) is narrower
|
||||
* still: a bare-core contention fixture proving normal/sim parity, again
|
||||
* with only a host-scoped limit and again never touching a real `.replay`
|
||||
* file. test_repl_limit_table.c's own sim-mode-registration tests (task
|
||||
* 2.3/6) go even further from a real replay: they reproduce
|
||||
* sim->core.config's end state "via direct construction... no capnp/.replay
|
||||
* file needed" (that file's own header comment), explicitly bypassing the
|
||||
* wire/snapshot round trip.
|
||||
*
|
||||
* THIS file is the first in this feature (and, so far as a full-codebase
|
||||
* grep for `sched.core.config =` before `sched_record_start()` confirms,
|
||||
* the first anywhere) to seed a limit config into a REAL SnapshotEntry
|
||||
* (Conf4Sched, capnp-encoded, containing BOTH a global and a host-scoped
|
||||
* xce_limit_config_t), write it to a real `.replay` file, and drive that
|
||||
* file through the unmodified, real `sched_sim_driver_run()` end-to-end --
|
||||
* exactly what an operator's real `xce_scheduler -s <replay>` invocation
|
||||
* does. It is the concrete Integration Test #1 design.md's own "Testing
|
||||
* Strategy" section names: "A real xce_sched_sim_driver_t driven through
|
||||
* sched_sim_driver_run() against a replay whose original session had global
|
||||
* AND host-scoped limits configured and actively enforced -- info limit
|
||||
* shows real usage for both".
|
||||
*
|
||||
* ── Why config seeded via the snapshot, not a configPush replay event ─────
|
||||
* A tempting alternative is a `ReplayEvent.body.configPush` wire event
|
||||
* (schema-legal, `XCE_CORE_EVENT_CONFIG_PUSH`, core_events.c) -- but reading
|
||||
* xce_core_push_events()'s CONFIG_PUSH case (core_events.c) before writing
|
||||
* this fixture shows it only decomposes queues/resources/resvals and fires
|
||||
* the PICKER's on_config hook; it never calls plugin_config_update() or any
|
||||
* plugin's own config_hook. plugin_config_update() (plugin.c) has exactly
|
||||
* ONE call site in this whole codebase: scheduler.c's own one-time
|
||||
* sched_init_config() (normal mode's startup config load, never re-invoked
|
||||
* from a live CONFIG_PUSH). A configPush replay event would therefore
|
||||
* silently never reach the limit plugin in EITHER mode -- not a
|
||||
* sim-mode-specific gap, just not how limit config ever reaches a plugin
|
||||
* post-startup in this codebase today. The REAL mechanism (confirmed by
|
||||
* reading sim_loop.c's sched_sim_driver_run(), sim.c's snapshot-load path,
|
||||
* and sim_limit.c's own header comment) is: a snapshot's `config` field
|
||||
* (SnapshotData.config, a real Conf4Sched capnp field, schema-verified
|
||||
* against src/schema/controller.capnp) round-trips through
|
||||
* xce_sched_core_load_snapshot() into sim->core.config (snapshot_load.c:200,
|
||||
* a borrowed-reference assignment), which sim_bootstrap_limit_plugin()
|
||||
* (called by sched_sim_driver_run() immediately after driver_start(), per
|
||||
* sim_limit.c's own header comment) then seeds via limit_seed_config() --
|
||||
* exactly what would happen for a genuine recorded production session whose
|
||||
* limits.yaml was active when the recording snapshot was taken. This
|
||||
* fixture reproduces that shape as literally as a hand-built `.replay` file
|
||||
* can: the two limit resources ("e2e_global_res"/"e2e_host_res") are baked
|
||||
* into the snapshot's own resmgr (required -- sim_bootstrap_limit_plugin()
|
||||
* runs strictly before any replay event is processed, and
|
||||
* is_host_based_limit()/build_limit() key_find() these resources by name),
|
||||
* while "slots" and the worker/queue/job all arrive afterward as ordinary
|
||||
* wire ReplayEvents, the same real JOBS_PUSH/WORKERS_PUSH/QUEUE_UPDATE
|
||||
* ingest path test_sim_live_allocation.c's own proven fixture already
|
||||
* establishes.
|
||||
*
|
||||
* ── Link style: real scheduler_srcs, not a white-box #include ────────────
|
||||
* Same technique as test_sim_live_allocation.c/test_sim_alloc_limit_
|
||||
* fanout.c/test_sim_contention_limit_driver_parity.c: `info limit`'s own
|
||||
* backing functions (repl_info_limit(), repl_dispatch()) are `static` to
|
||||
* repl.c/repl_info.c and unreachable from a real-scheduler_srcs-linked TU
|
||||
* (only a white-box #include of those files, as test_repl_limit_table.c
|
||||
* uses, can call them directly) -- but this fixture NEEDS the real,
|
||||
* unmodified resource.c/scheduler.c/sched_election.c/plugins/
|
||||
* seqjob_allocator.c chain for a genuine dispatch, which a white-box
|
||||
* #include of repl_info.c's own narrower dependency set does not link.
|
||||
* This file therefore reads the exact same accessors repl_info_limit()
|
||||
* itself reads -- limit_get_configured_limits()/limit_get_usage_
|
||||
* instances() (plugin.h, unchanged since scheduler-sim-diagnostics) -- the
|
||||
* "existing limit-inspection accessor" task 5.2's own text calls for,
|
||||
* and additionally reads back each usage instance's actual numeric value
|
||||
* via get_resvals() (scheduler.h) against the exact array
|
||||
* update_limit_res_value() (plugins/limit.c) wrote it into (core->
|
||||
* sharedInst for the global limit, the worker's own resvals for the
|
||||
* host-scoped one), to prove "real, non-zero usage" literally, not merely
|
||||
* "an instance object exists".
|
||||
*
|
||||
* ── bug-1598/1632/1763 fixture-safety requirement ─────────────────────────
|
||||
* g_sim below is `static` (file/process lifetime), not stack-local, for the
|
||||
* identical reason test_sim_live_allocation.c's own header comment
|
||||
* documents in full: sched_sim_driver_run() spawns two real, never-joined
|
||||
* pthreads that read &(sim->core) for the rest of the process's life.
|
||||
*
|
||||
* ── RED-phase note (see this task's own status report for the full
|
||||
* account) ───────────────────────────────────────────────────────────
|
||||
* This is a pure validation task (no production code changes expected).
|
||||
* Task 4.3's commit (f6a28621) is the specific, narrowly-scoped prior
|
||||
* change that closes the exact gap this test's host-scoped half exercises
|
||||
* (before it, plugin_prepare_allocation()'s fan-out during a live seqjob/
|
||||
* parjob dispatch attempt reached a throwaway wrapper's own separate plugin
|
||||
* registry, never sim->core.plugins, so a host-scoped limit registered
|
||||
* directly against sim->core could never be reached). The status report
|
||||
* documents a targeted revert of exactly that commit's production files,
|
||||
* confirming this test's host-scoped assertion genuinely fails without it.
|
||||
*/
|
||||
#include <errno.h>
|
||||
#include <limits.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 "recording.h"
|
||||
#include "resobj.h"
|
||||
#include "scheduler.h"
|
||||
#include "sim.h"
|
||||
#include "xcelimit.h"
|
||||
#include "xstring.h"
|
||||
|
||||
/* write_capnp_message (recording.c) has no public header declaration --
|
||||
* see test_sim_live_allocation.c's own identical comment for the
|
||||
* established "extern declared where consumed" rationale. */
|
||||
extern int write_capnp_message(FILE* f, struct capn* c);
|
||||
|
||||
#define WORKER_NAME "e2e_limit_worker"
|
||||
#define QUEUE_NAME "normal"
|
||||
#define TEST_RESREQ ((char*)"select[type==any] rusage[mem=1]")
|
||||
#define TEST_JOBID ((uint64_t)940301ULL)
|
||||
#define WORKER_SLOTS 4.0f
|
||||
|
||||
#define GLOBAL_LIMIT_NAME "e2e_global_limit"
|
||||
#define GLOBAL_LIMIT_RESNAME "e2e_global_res"
|
||||
#define GLOBAL_LIMIT_VAL 10.0f
|
||||
|
||||
#define HOST_LIMIT_NAME "e2e_host_limit"
|
||||
#define HOST_LIMIT_RESNAME "e2e_host_res"
|
||||
#define HOST_LIMIT_PERCENT_VAL 50.0f
|
||||
#define HOST_RES_CAPACITY 1000.0f
|
||||
|
||||
void setUp(void) { return; }
|
||||
void tearDown(void) { return; }
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Fixture helpers -- capnp event writers duplicated from test_sim_live_ */
|
||||
/* allocation.c's own proven shape (this directory's established */
|
||||
/* "duplicate, don't share" testsuite convention). */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void make_tmp_dir(char* out, size_t out_len, const char* suffix)
|
||||
{
|
||||
char tmpl[PATH_MAX];
|
||||
|
||||
snprintf(tmpl, sizeof(tmpl), "/tmp/sched_e2elimit%s_XXXXXX", suffix);
|
||||
TEST_ASSERT_NOT_NULL(mkdtemp(tmpl));
|
||||
TEST_ASSERT_TRUE(strlen(tmpl) < out_len);
|
||||
strcpy(out, tmpl);
|
||||
}
|
||||
|
||||
/* register_plain_resource - same shape test_sim_contention_limit_driver_
|
||||
* parity.c's/test_sim_alloc_limit_fanout.c's own identically-named helper
|
||||
* establishes: a bare resource with no LIMIT/CONSUMABLE/host flags --
|
||||
* is_host_based_limit() (xcelimit.c) resolves host-vs-global scope purely
|
||||
* from limit->val.percent (plus, for a flagged resource, XCE_RESF_GLOBAL/
|
||||
* XCE_RESF_SHARED), never from any flag on the resource itself. */
|
||||
static void register_plain_resource(var resmgr, const char* name)
|
||||
{
|
||||
xce_resource_t res;
|
||||
|
||||
memset(&res, 0, sizeof(res));
|
||||
res.name = strdup(name);
|
||||
|
||||
TEST_ASSERT_NOT_NULL(makeResource(resmgr, &res));
|
||||
}
|
||||
|
||||
/* make_wire_worker_two_resvals - test_sim_live_allocation.c's own
|
||||
* make_wire_worker(), extended from one resval to two: this fixture's
|
||||
* worker needs both "slots" (the job's auto-injected per-task want) and
|
||||
* the host-scoped limit's own underlying resource (a real, non-zero
|
||||
* capacity -- without it, update_limit_res_value()'s get_resvals() call
|
||||
* against the worker's own resvals would fail before ever recording a
|
||||
* usage value, per plugins/limit.c's own body, read in full before writing
|
||||
* this fixture). */
|
||||
static xce_worker_t* make_wire_worker_two_resvals(const char* name,
|
||||
const char* res1name,
|
||||
float res1val,
|
||||
const char* res2name,
|
||||
float res2val)
|
||||
{
|
||||
xce_worker_t* w = (xce_worker_t*)calloc(1, sizeof(*w));
|
||||
xce_name_val_t** nv_ptrs = (xce_name_val_t**)calloc(2, sizeof(*nv_ptrs));
|
||||
xce_val_t* val1 = (xce_val_t*)calloc(1, sizeof(*val1));
|
||||
xce_val_t* val2 = (xce_val_t*)calloc(1, sizeof(*val2));
|
||||
xce_name_val_t* nv1 = (xce_name_val_t*)calloc(1, sizeof(*nv1));
|
||||
xce_name_val_t* nv2 = (xce_name_val_t*)calloc(1, sizeof(*nv2));
|
||||
|
||||
TEST_ASSERT_NOT_NULL(w);
|
||||
TEST_ASSERT_NOT_NULL(nv_ptrs);
|
||||
TEST_ASSERT_NOT_NULL(val1);
|
||||
TEST_ASSERT_NOT_NULL(val2);
|
||||
TEST_ASSERT_NOT_NULL(nv1);
|
||||
TEST_ASSERT_NOT_NULL(nv2);
|
||||
|
||||
val1->which = Value_u_fvalue;
|
||||
val1->u.fvalue = res1val;
|
||||
nv1->name = strdup(res1name);
|
||||
nv1->val = val1;
|
||||
|
||||
val2->which = Value_u_fvalue;
|
||||
val2->u.fvalue = res2val;
|
||||
nv2->name = strdup(res2name);
|
||||
nv2->val = val2;
|
||||
|
||||
nv_ptrs[0] = nv1;
|
||||
nv_ptrs[1] = nv2;
|
||||
|
||||
w->name = strdup(name);
|
||||
w->status = 0;
|
||||
w->nresvals_to_sched = 2;
|
||||
w->resvals_to_sched = nv_ptrs;
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static void append_resourcespush_event(FILE* f, uint64_t ts,
|
||||
xce_resource_t** resources, int n)
|
||||
{
|
||||
xce_resource_data_t wrap;
|
||||
replay_event_t src;
|
||||
struct capn c;
|
||||
struct capn_segment* cs;
|
||||
ReplayEvent_ptr p;
|
||||
|
||||
memset(&wrap, 0, sizeof(wrap));
|
||||
wrap.n_total = n;
|
||||
wrap.count = n;
|
||||
wrap.resources = resources;
|
||||
|
||||
memset(&src, 0, sizeof(src));
|
||||
src.ts = ts;
|
||||
src.body_which = ReplayEvent_body_resourcesPush;
|
||||
src.body.resources_push = &wrap;
|
||||
|
||||
capn_init_malloc(&c);
|
||||
cs = capn_root(&c).seg;
|
||||
encode_ReplayEvent_ptr(cs, &p, &src);
|
||||
capn_setp(capn_root(&c), 0, p.p);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c));
|
||||
capn_free(&c);
|
||||
}
|
||||
|
||||
static void append_workerspush_event(FILE* f, uint64_t ts,
|
||||
xce_worker_t** workers, int n)
|
||||
{
|
||||
xce_workers_data_t wrap;
|
||||
replay_event_t src;
|
||||
struct capn c;
|
||||
struct capn_segment* cs;
|
||||
ReplayEvent_ptr p;
|
||||
|
||||
memset(&wrap, 0, sizeof(wrap));
|
||||
wrap.n_total = n;
|
||||
wrap.count = n;
|
||||
wrap.workers = workers;
|
||||
|
||||
memset(&src, 0, sizeof(src));
|
||||
src.ts = ts;
|
||||
src.body_which = ReplayEvent_body_workersPush;
|
||||
src.body.workers_push = &wrap;
|
||||
|
||||
capn_init_malloc(&c);
|
||||
cs = capn_root(&c).seg;
|
||||
encode_ReplayEvent_ptr(cs, &p, &src);
|
||||
capn_setp(capn_root(&c), 0, p.p);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c));
|
||||
capn_free(&c);
|
||||
}
|
||||
|
||||
static void append_queueupdate_event(FILE* f, uint64_t ts, xce_queue_t* q)
|
||||
{
|
||||
replay_event_t src;
|
||||
struct capn c;
|
||||
struct capn_segment* cs;
|
||||
ReplayEvent_ptr p;
|
||||
|
||||
memset(&src, 0, sizeof(src));
|
||||
src.ts = ts;
|
||||
src.body_which = ReplayEvent_body_queueUpdate;
|
||||
src.body.queue_update = q;
|
||||
|
||||
capn_init_malloc(&c);
|
||||
cs = capn_root(&c).seg;
|
||||
encode_ReplayEvent_ptr(cs, &p, &src);
|
||||
capn_setp(capn_root(&c), 0, p.p);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c));
|
||||
capn_free(&c);
|
||||
}
|
||||
|
||||
static void append_jobspush_event(FILE* f, uint64_t ts, xce_job_t** jobs,
|
||||
int njobs)
|
||||
{
|
||||
xce_jobs_data_t wrap;
|
||||
replay_event_t src;
|
||||
struct capn c;
|
||||
struct capn_segment* cs;
|
||||
ReplayEvent_ptr p;
|
||||
|
||||
memset(&wrap, 0, sizeof(wrap));
|
||||
wrap.n_total = njobs;
|
||||
wrap.count = njobs;
|
||||
wrap.jobs = jobs;
|
||||
|
||||
memset(&src, 0, sizeof(src));
|
||||
src.ts = ts;
|
||||
src.body_which = ReplayEvent_body_jobsPush;
|
||||
src.body.jobs_push = &wrap;
|
||||
|
||||
capn_init_malloc(&c);
|
||||
cs = capn_root(&c).seg;
|
||||
encode_ReplayEvent_ptr(cs, &p, &src);
|
||||
capn_setp(capn_root(&c), 0, p.p);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c));
|
||||
capn_free(&c);
|
||||
}
|
||||
|
||||
/* append_replay_session - EMPTY decisions list is load-bearing here, not
|
||||
* incidental -- see test_sim_live_allocation.c's own identical rationale
|
||||
* ("Why the fixture cannot simply replay a normal-mode recording"): an
|
||||
* empty decisions list guarantees sim_reconcile_unaccounted_jobs() finds
|
||||
* nothing to reconstruct, so this fixture's job reaches the live
|
||||
* sched_job_picker()/sched_allocator() pair genuinely, not via
|
||||
* reconciliation. */
|
||||
static void append_replay_session(FILE* f, uint64_t begin_round)
|
||||
{
|
||||
struct capn c;
|
||||
struct capn_segment* cs;
|
||||
ReplaySession_ptr sp;
|
||||
|
||||
capn_init_malloc(&c);
|
||||
cs = capn_root(&c).seg;
|
||||
sp = new_ReplaySession(cs);
|
||||
ReplaySession_set_beginRound(sp, begin_round);
|
||||
capn_setp(capn_root(&c), 0, sp.p);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c));
|
||||
capn_free(&c);
|
||||
}
|
||||
|
||||
static void append_end_of_stream_marker(FILE* f)
|
||||
{
|
||||
replay_event_t src;
|
||||
struct capn c;
|
||||
struct capn_segment* cs;
|
||||
ReplayEvent_ptr p;
|
||||
|
||||
memset(&src, 0, sizeof(src));
|
||||
src.body_which = ReplayEvent_body_endSession;
|
||||
|
||||
capn_init_malloc(&c);
|
||||
cs = capn_root(&c).seg;
|
||||
encode_ReplayEvent_ptr(cs, &p, &src);
|
||||
capn_setp(capn_root(&c), 0, p.p);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c));
|
||||
capn_free(&c);
|
||||
}
|
||||
|
||||
/* write_header_and_snapshot - produces a valid ReplayHeader+SnapshotEntry
|
||||
* prefix at a fresh temp path via the real sched_record_start(), same
|
||||
* throwaway-xce_scheduler_t technique test_sim_live_allocation.c's own
|
||||
* write_header_and_snapshot() establishes -- extended here to additionally
|
||||
* bake BOTH limit resources ("e2e_global_res"/"e2e_host_res") into the
|
||||
* throwaway core's own resmgr and set core.config to a real, 2-entry
|
||||
* xce_conf4sched_t (one global, one host-scoped limit) BEFORE
|
||||
* sched_record_start() takes the snapshot. This is what makes the snapshot
|
||||
* genuinely carry a Conf4Sched with both limits on the wire -- see this
|
||||
* file's own header comment ("Why config seeded via the snapshot") for the
|
||||
* full mechanism trace.
|
||||
*
|
||||
* The `config`/`limit_val`/`limit_cfg`/`limits` locals below need only
|
||||
* survive through sched_record_start()'s own synchronous call (which
|
||||
* capnp-encodes their contents into the wire buffer before returning) --
|
||||
* exactly like every other throwaway construction this function already
|
||||
* makes (`sched` itself, `fcfs`), all discarded the moment this function
|
||||
* returns. */
|
||||
static FILE* write_header_and_snapshot(char* out_path, size_t out_path_len)
|
||||
{
|
||||
char record_dir[PATH_MAX];
|
||||
xce_scheduler_t sched;
|
||||
xce_sched_plugin_t* fcfs;
|
||||
xce_limit_val_t global_val = { .resname = (char*)GLOBAL_LIMIT_RESNAME,
|
||||
.percent = 0,
|
||||
.val = GLOBAL_LIMIT_VAL };
|
||||
xce_limit_config_t global_cfg = { .name = (char*)GLOBAL_LIMIT_NAME,
|
||||
.val = &global_val };
|
||||
xce_limit_val_t host_val = { .resname = (char*)HOST_LIMIT_RESNAME,
|
||||
.percent = 1,
|
||||
.val = HOST_LIMIT_PERCENT_VAL };
|
||||
xce_limit_config_t host_cfg = { .name = (char*)HOST_LIMIT_NAME,
|
||||
.val = &host_val };
|
||||
xce_limit_config_t* limits[2] = { &global_cfg, &host_cfg };
|
||||
xce_conf4sched_t config;
|
||||
int rc;
|
||||
|
||||
make_tmp_dir(record_dir, sizeof(record_dir), "_r");
|
||||
|
||||
memset(&sched, 0, sizeof(sched));
|
||||
mylock_init(&(sched.record_lock));
|
||||
|
||||
rc = xce_sched_core_init(&(sched.core));
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
fcfs = builtin_fcfs_new_plugin();
|
||||
TEST_ASSERT_NOT_NULL(fcfs);
|
||||
rc = plugin_register_core(&(sched.core), fcfs);
|
||||
TEST_ASSERT_TRUE(rc >= 0);
|
||||
|
||||
/* Both limit resources must exist in resmgr BEFORE sim_bootstrap_
|
||||
* limit_plugin() runs (sched_sim_driver_run() calls it immediately
|
||||
* after driver_start()/snapshot-load, strictly before any replay event
|
||||
* -- including a RESOURCES_PUSH for "slots" -- is ever processed), so
|
||||
* they are baked into the snapshot itself rather than pushed as a
|
||||
* later wire event. */
|
||||
register_plain_resource(sched.core.resmgr, GLOBAL_LIMIT_RESNAME);
|
||||
register_plain_resource(sched.core.resmgr, HOST_LIMIT_RESNAME);
|
||||
|
||||
memset(&config, 0, sizeof(config));
|
||||
config.nlimits = 2;
|
||||
config.limits = limits;
|
||||
sched.core.config = &config;
|
||||
|
||||
sched.core.job_pick_plugin = (char*)"builtin_fcfs";
|
||||
sched.record_dir = record_dir;
|
||||
|
||||
rc = sched_record_start(&sched, "e2e_limit.replay");
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_TRUE(strlen(sched.record_file_path) < out_path_len);
|
||||
strcpy(out_path, sched.record_file_path);
|
||||
|
||||
return sched.record_file;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* The sim driver under test -- static storage, see this file's own header */
|
||||
/* comment ("bug-1598/1632/1763 fixture-safety requirement") for why. */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static xce_sched_sim_driver_t g_sim;
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* The test */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* Observable (task 5.2's own text): "the existing limit-inspection command
|
||||
* displays real, non-zero usage for both limits against this replay, where
|
||||
* today it reports no active usage instance for either". This is bug-1826
|
||||
* itself, proven end to end: a real `.replay` file, a real
|
||||
* sched_sim_driver_run() invocation, and both a global and a host-scoped
|
||||
* limit that genuinely match the one dispatched job -- not two separate
|
||||
* fixtures, not a bare-core shortcut.
|
||||
*/
|
||||
static void
|
||||
test_sim_replay_shows_real_global_and_host_scoped_limit_usage(void)
|
||||
{
|
||||
char path[PATH_MAX];
|
||||
FILE* f;
|
||||
xce_resource_t* slots_res;
|
||||
xce_resource_t* res_ptrs[1];
|
||||
xce_worker_t* worker_ptrs[1];
|
||||
xce_queue_t* queue;
|
||||
xce_job_t* job_ptrs[1];
|
||||
sched_sim_args_t args;
|
||||
xce_sched_job_t* sjob;
|
||||
xce_sched_worker_t* worker;
|
||||
var limits_handle;
|
||||
var instances_handle;
|
||||
iterator_t it;
|
||||
int n_configured = 0;
|
||||
int n_instances = 0;
|
||||
int saw_global = 0;
|
||||
int saw_host = 0;
|
||||
int rc;
|
||||
|
||||
f = write_header_and_snapshot(path, sizeof(path));
|
||||
|
||||
/* "slots" -- the job's auto-injected per-task want -- arrives as an
|
||||
* ordinary post-snapshot wire event, the same proven shape
|
||||
* test_sim_live_allocation.c's own fixture already establishes. */
|
||||
slots_res = (xce_resource_t*)calloc(1, sizeof(*slots_res));
|
||||
TEST_ASSERT_NOT_NULL(slots_res);
|
||||
slots_res->name = strdup("slots");
|
||||
res_ptrs[0] = slots_res;
|
||||
append_resourcespush_event(f, 1001ULL, res_ptrs, 1);
|
||||
|
||||
/* The worker carries real, sufficient capacity for BOTH the job's
|
||||
* "slots" want (so the job genuinely DISPATCHES -- the strongest
|
||||
* possible proof, matching test_sim_live_allocation.c's own precedent)
|
||||
* and the host-scoped limit's own underlying resource (a generous,
|
||||
* non-zero capacity, so the limit itself never denies anything -- the
|
||||
* same "generous cap" discipline test_sim_contention_limit_driver_
|
||||
* parity.c's own fixture documents). */
|
||||
worker_ptrs[0] = make_wire_worker_two_resvals(
|
||||
WORKER_NAME, "slots", WORKER_SLOTS, HOST_LIMIT_RESNAME,
|
||||
HOST_RES_CAPACITY);
|
||||
append_workerspush_event(f, 1002ULL, worker_ptrs, 1);
|
||||
|
||||
queue = make_wire_queue(QUEUE_NAME, 1);
|
||||
append_queueupdate_event(f, 1003ULL, queue);
|
||||
|
||||
/* A single job is enough to exercise BOTH limits: plugin_job_election()
|
||||
* (global-limit match) fires once per job before any allocation
|
||||
* attempt, unconditionally; plugin_prepare_allocation() (host-scoped
|
||||
* match) fires once per candidate host considered during the
|
||||
* allocation search that follows -- both permissive-scoped limits
|
||||
* (no per-user/queue/project/host restriction configured) match any
|
||||
* job/host, per this codebase's own build_consumer() "all"-group
|
||||
* fallback, the same default every sibling limit fixture in this
|
||||
* feature already relies on. */
|
||||
job_ptrs[0] = make_push_job(TEST_JOBID, TEST_RESREQ, (char*)"alice",
|
||||
(char*)QUEUE_NAME, (char*)"proj1", 1, 1);
|
||||
append_jobspush_event(f, 1004ULL, job_ptrs, 1);
|
||||
|
||||
/* Empty decisions -- see this file's own header comment ("Why config
|
||||
* seeded via the snapshot") and test_sim_live_allocation.c's identical
|
||||
* rationale for why this is load-bearing, not incidental. */
|
||||
append_replay_session(f, 2000ULL);
|
||||
append_end_of_stream_marker(f);
|
||||
fclose(f);
|
||||
|
||||
memset(&args, 0, sizeof(args));
|
||||
args.replay_path = path;
|
||||
args.sim_start_ts = SCHED_SIM_START_TS_LATEST;
|
||||
args.sim_speed = 0;
|
||||
|
||||
rc = sched_sim_driver_run(&g_sim, &args);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
0, rc, "sched_sim_driver_run() must complete this fixture's single "
|
||||
"round and reach its own end-of-stream marker");
|
||||
|
||||
sjob = sched_get_job(&(g_sim.core), TEST_JOBID);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
sjob, "the job pushed via the real JOBS_PUSH ingest path must be "
|
||||
"present in the simulation driver's own core state");
|
||||
TEST_ASSERT_EQUAL_UINT_MESSAGE(
|
||||
JOB_STAT_RUN, sjob->state,
|
||||
"the job must genuinely DISPATCH -- a real worker with sufficient "
|
||||
"'slots' capacity is provided specifically so both limit hooks "
|
||||
"(job election AND host-prepare-allocation) are reached along the "
|
||||
"real live dispatch path, not merely a PEND that happens to touch "
|
||||
"one of them");
|
||||
|
||||
worker = sched_get_worker(&(g_sim.core), (char*)WORKER_NAME);
|
||||
TEST_ASSERT_NOT_NULL(worker);
|
||||
|
||||
/* ---- `info limit all` today reports "no limits configured" for a
|
||||
* replay -- confirm both limits are genuinely configured, seeded from
|
||||
* this replay's own snapshot config (sim_bootstrap_limit_plugin(),
|
||||
* bug-1825's mechanism, extended by this feature to actually MATCH,
|
||||
* not merely display). ---- */
|
||||
limits_handle = limit_get_configured_limits(&(g_sim.core));
|
||||
TEST_ASSERT_NOT_NULL(limits_handle);
|
||||
|
||||
rc = iter_make(&it, limits_handle);
|
||||
TEST_ASSERT_TRUE(rc >= 0);
|
||||
for (;;) {
|
||||
void* citem = NULL;
|
||||
|
||||
rc = iter_next(&it, &citem);
|
||||
if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) {
|
||||
break;
|
||||
}
|
||||
n_configured++;
|
||||
}
|
||||
iter_destroy(&it);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
2, n_configured,
|
||||
"both the global and host-scoped limit must be configured, seeded "
|
||||
"from the replay's own snapshot config -- Requirement 2.3's own "
|
||||
"'info limit all' resolution depends on this");
|
||||
|
||||
/* ---- THE acceptance test: `info limit` must show REAL, non-zero
|
||||
* usage for BOTH limits -- where today (before this feature) it
|
||||
* reports no active usage instance for either (Requirement 2.1, 2.2,
|
||||
* 2.3). ---- */
|
||||
instances_handle = limit_get_usage_instances(&(g_sim.core));
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
instances_handle,
|
||||
"before this feature, plugin->sched was NULL for every "
|
||||
"sim-registered plugin and no usage instance could ever be "
|
||||
"created against a replay -- info limit reported none active for "
|
||||
"either limit");
|
||||
|
||||
rc = iter_make(&it, instances_handle);
|
||||
TEST_ASSERT_TRUE(rc >= 0);
|
||||
for (;;) {
|
||||
xce_vres_t* vres = NULL;
|
||||
float value = -1.0f;
|
||||
int grc;
|
||||
|
||||
rc = iter_next(&it, (void**)&vres);
|
||||
if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) {
|
||||
break;
|
||||
}
|
||||
TEST_ASSERT_NOT_NULL(vres);
|
||||
TEST_ASSERT_NOT_NULL(vres->limit);
|
||||
TEST_ASSERT_NOT_NULL(vres->vres);
|
||||
|
||||
n_instances++;
|
||||
|
||||
if (vres->limit->val.percent == 0) {
|
||||
saw_global = 1;
|
||||
grc = get_resvals(&(g_sim.core.sharedInst), 1,
|
||||
&(vres->vres->idx), &value);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
1, grc,
|
||||
"the global limit's usage value must be readable back from "
|
||||
"core->sharedInst -- exactly what plugin_job_election() -> "
|
||||
"match_job_global_limit_() -> update_limit_res_value() "
|
||||
"wrote into, the same array 'info limit' reads to display "
|
||||
"global usage");
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
value > 0.0f,
|
||||
"Requirement 2.1: the global limit's usage must be real "
|
||||
"and non-zero, not merely an empty/zeroed instance");
|
||||
} else {
|
||||
saw_host = 1;
|
||||
grc = get_resvals(&(worker->resvals), 1, &(vres->vres->idx),
|
||||
&value);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
1, grc,
|
||||
"the host-scoped limit's usage value must be readable back "
|
||||
"from the dispatched-onto worker's own resvals -- exactly "
|
||||
"what plugin_prepare_allocation() -> match_job_host_limit_"
|
||||
"() -> update_limit_res_value() wrote into");
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
value > 0.0f,
|
||||
"Requirement 2.2: the host-scoped limit's usage must be "
|
||||
"real and non-zero, not merely an empty/zeroed instance");
|
||||
}
|
||||
}
|
||||
iter_destroy(&it);
|
||||
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
saw_global,
|
||||
"Requirement 2.1/2.3: the global limit must have a real usage "
|
||||
"instance with non-zero usage, visible via the same accessor "
|
||||
"'info limit' reads -- this is bug-1826's own global-limit half");
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
saw_host,
|
||||
"Requirement 2.2/2.3: the host-scoped limit must have a real "
|
||||
"usage instance with non-zero usage, visible via the same "
|
||||
"accessor 'info limit' reads -- this is bug-1826's own "
|
||||
"host-scoped-limit half");
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
2, n_instances,
|
||||
"exactly one usage instance per configured limit -- no spurious "
|
||||
"extra instance, no limit silently missing one (Requirement 2.4's "
|
||||
"own 'a never-matched limit reports none' guarantee, checked here "
|
||||
"from the opposite direction: a limit that DID match reports "
|
||||
"exactly one)");
|
||||
|
||||
printf("test_sim_replay_shows_real_global_and_host_scoped_limit_usage: "
|
||||
"PASS\n");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_sim_replay_shows_real_global_and_host_scoped_limit_usage);
|
||||
return UNITY_END();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue