Task 1.8: the core gains trace_ctl/trace_rec (NULL unless the sim REPL sets them) and SCHED_TRACE evaluates its call only while a record is being built. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
409 lines
15 KiB
C
409 lines
15 KiB
C
/*
|
|
* test_trace_hooks_gate.c - the core trace gate and the SCHED_TRACE hook
|
|
* macro (scheduler-sim-decision-trace spec, task 1.8; design.md
|
|
* "TraceModel", "Testing Strategy" 3; Requirements 11.1, 11.2, 11.3).
|
|
*
|
|
* - xce_sched_core_init() leaves trace_ctl and trace_rec NULL, and
|
|
* xce_sched_core_fini() clears them without freeing what they point to
|
|
* (the REPL owns the settings, the decision owns a handed-off record);
|
|
* - SCHED_TRACE does not evaluate its call, side effects included, while
|
|
* no record is being built, also when settings are published;
|
|
* - SCHED_TRACE evaluates its call exactly once while a record is being
|
|
* built, and is a single statement (safe in an unbraced if/else);
|
|
* - a full allocation through the real live-mode scheduler (sched_init(),
|
|
* real picker and allocator threads, real ingest) with tracing never
|
|
* enabled creates no record: no sched_trace_rec_new() call is made,
|
|
* both core pointers stay NULL and no decision carries a trace.
|
|
*
|
|
* Record-creation proof: the binary links every scheduler source as its
|
|
* own translation unit (the real-scheduler_srcs technique), and
|
|
* -Wl,--wrap=sched_trace_rec_new routes every cross-unit call to the
|
|
* constructor through __wrap_sched_trace_rec_new() below, which counts it.
|
|
* The gate tests call the constructor through the same seam, which shows
|
|
* the counter sees real calls. The only intra-unit callers are inside
|
|
* sched_trace.c itself, which the allocation path reaches only through a
|
|
* hook that has already passed the gate.
|
|
*/
|
|
#include <errno.h>
|
|
#include <stdatomic.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 "resobj.h"
|
|
#include "sched/alloc_cache.h"
|
|
#include "sched/trace.h"
|
|
#include "scheduler.h"
|
|
#include "xce/cluster.h"
|
|
|
|
extern int scheduler_req_handler(xce_request_t*, void*);
|
|
|
|
#define WORKER_A_NAME "gate-host-a"
|
|
#define WORKER_B_NAME "gate-host-b"
|
|
#define WORKER_SLOTS 3.0f
|
|
#define QUEUE_NAME "gate_queue"
|
|
#define JOB_ALT ((uint64_t)611001ULL)
|
|
#define JOB_PAR ((uint64_t)611002ULL)
|
|
#define JOB_PEND ((uint64_t)611003ULL)
|
|
|
|
/* ── link seams ─────────────────────────────────────────────────────── */
|
|
|
|
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;
|
|
}
|
|
|
|
extern xce_sched_trace_rec_t*
|
|
__real_sched_trace_rec_new(const xce_sched_trace_ctl_t* ctl, uint64_t jobid,
|
|
xce_sched_trace_rec_kind_t kind);
|
|
|
|
/* Written by the allocator thread once hooks exist, read by the test. */
|
|
static atomic_int g_rec_new_calls;
|
|
|
|
xce_sched_trace_rec_t*
|
|
__wrap_sched_trace_rec_new(const xce_sched_trace_ctl_t* ctl, uint64_t jobid,
|
|
xce_sched_trace_rec_kind_t kind)
|
|
{
|
|
atomic_fetch_add(&g_rec_new_calls, 1);
|
|
return __real_sched_trace_rec_new(ctl, jobid, kind);
|
|
}
|
|
|
|
void setUp(void) { return; }
|
|
void tearDown(void) { return; }
|
|
|
|
/* ── helpers ────────────────────────────────────────────────────────── */
|
|
|
|
static int g_bumps;
|
|
|
|
static int bump(int* n)
|
|
{
|
|
(*n)++;
|
|
g_bumps++;
|
|
return *n;
|
|
}
|
|
|
|
static xce_sched_trace_ctl_t make_ctl(void)
|
|
{
|
|
xce_sched_trace_ctl_t ctl;
|
|
|
|
memset(&ctl, 0, sizeof(ctl));
|
|
ctl.all = true;
|
|
ctl.level = XCE_TRACE_LEVEL_SUMMARY;
|
|
ctl.line_bound = XCE_TRACE_LINE_BOUND_DEFAULT;
|
|
ctl.session_no = 1;
|
|
ctl.policy_name = "fcfs";
|
|
return ctl;
|
|
}
|
|
|
|
/* ── core fields ────────────────────────────────────────────────────── */
|
|
|
|
static void test_core_init_leaves_trace_pointers_null(void)
|
|
{
|
|
xce_sched_core_t* core = (xce_sched_core_t*)malloc(sizeof(*core));
|
|
|
|
TEST_ASSERT_NOT_NULL(core);
|
|
memset(core, 0xA5, sizeof(*core));
|
|
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(core));
|
|
TEST_ASSERT_NULL(core->trace_ctl);
|
|
TEST_ASSERT_NULL(core->trace_rec);
|
|
|
|
/* xce_sched_core_fini() finalizes these; both drivers calloc them */
|
|
for (int i = 0; i < 2; i++) {
|
|
core->pend_updates[i] = (dataq_t*)calloc(1, sizeof(dataq_t));
|
|
TEST_ASSERT_NOT_NULL(core->pend_updates[i]);
|
|
dataq_init(core->pend_updates[i], NULL);
|
|
}
|
|
|
|
/* fini clears both and frees neither: these point at storage the core
|
|
* does not own (a stack ctl, a stack byte), which ASAN would flag as a
|
|
* bad free. */
|
|
{
|
|
xce_sched_trace_ctl_t ctl = make_ctl();
|
|
char not_a_record;
|
|
|
|
core->trace_ctl = &ctl;
|
|
core->trace_rec = (xce_sched_trace_rec_t*)¬_a_record;
|
|
xce_sched_core_fini(core);
|
|
TEST_ASSERT_NULL(core->trace_ctl);
|
|
TEST_ASSERT_NULL(core->trace_rec);
|
|
}
|
|
free(core);
|
|
}
|
|
|
|
/* ── SCHED_TRACE gate ───────────────────────────────────────────────── */
|
|
|
|
static void test_gate_skips_call_and_side_effects_without_record(void)
|
|
{
|
|
xce_sched_core_t core;
|
|
xce_sched_core_t* cp = &core;
|
|
xce_sched_trace_ctl_t ctl = make_ctl();
|
|
int n = 0;
|
|
|
|
memset(&core, 0, sizeof(core));
|
|
g_bumps = 0;
|
|
|
|
SCHED_TRACE(cp, n++);
|
|
SCHED_TRACE(cp, bump(&n));
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(0, n, "hook argument evaluated with "
|
|
"no record being built");
|
|
TEST_ASSERT_EQUAL_INT(0, g_bumps);
|
|
|
|
/* published settings alone do not open the gate: only a record does */
|
|
core.trace_ctl = &ctl;
|
|
SCHED_TRACE(cp, n++);
|
|
SCHED_TRACE(cp, bump(&n));
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(0, n, "hook argument evaluated with "
|
|
"settings published but no record");
|
|
TEST_ASSERT_EQUAL_INT(0, g_bumps);
|
|
}
|
|
|
|
static void test_gate_evaluates_call_once_with_record(void)
|
|
{
|
|
xce_sched_core_t core;
|
|
xce_sched_core_t* cp = &core;
|
|
xce_sched_trace_ctl_t ctl = make_ctl();
|
|
xce_sched_trace_rec_t* rec;
|
|
int before = atomic_load(&g_rec_new_calls);
|
|
int n = 0;
|
|
|
|
memset(&core, 0, sizeof(core));
|
|
rec = sched_trace_rec_new(&ctl, 42, XCE_TRACE_REC_DECISION);
|
|
TEST_ASSERT_NOT_NULL(rec);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(before + 1, atomic_load(&g_rec_new_calls),
|
|
"the counting seam must see a real "
|
|
"constructor call");
|
|
|
|
core.trace_ctl = &ctl;
|
|
core.trace_rec = rec;
|
|
SCHED_TRACE(cp, n++);
|
|
TEST_ASSERT_EQUAL_INT(1, n);
|
|
SCHED_TRACE(cp, sched_trace_path_push(core.trace_rec,
|
|
XCE_TRACE_PATH_SINGLE, 1, 1));
|
|
SCHED_TRACE(cp, sched_trace_path_pop(core.trace_rec));
|
|
|
|
/* one statement: an unbraced if/else around it still parses as such */
|
|
if (n == 1)
|
|
SCHED_TRACE(cp, n += 10);
|
|
else
|
|
n = -1;
|
|
TEST_ASSERT_EQUAL_INT(11, n);
|
|
|
|
core.trace_rec = NULL;
|
|
SCHED_TRACE(cp, n++);
|
|
TEST_ASSERT_EQUAL_INT(11, n);
|
|
|
|
sched_trace_rec_free(rec);
|
|
}
|
|
|
|
/* ── full allocation with tracing never enabled ─────────────────────── */
|
|
|
|
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;
|
|
}
|
|
|
|
static xce_job_t* make_push_job(uint64_t jobid, const char* resreq,
|
|
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 = (char*)QUEUE_NAME;
|
|
j->submit->user = (char*)"gate_user";
|
|
j->submit->project_name = (char*)"gate_proj";
|
|
j->submit->resreq = (char*)resreq;
|
|
j->submit->mintasks = 1;
|
|
j->submit->maxtasks = maxtasks;
|
|
return j;
|
|
}
|
|
|
|
/* Real sched_init(): registers the real plugins and starts the real picker
|
|
* and allocator threads, as the live scheduler does. Never torn down, the
|
|
* same convention as the other real-scheduler_srcs tests. */
|
|
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 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] = 19997;
|
|
config.from = "sched_trace_hooks_gate_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 = 19997;
|
|
strncpy(cluster_cfg.identity, "sched_trace_hooks_gate_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");
|
|
|
|
memset(&res, 0, sizeof(res));
|
|
res.name = strdup("slots");
|
|
TEST_ASSERT_NOT_NULL(makeResource(sched->core.resmgr, &res));
|
|
|
|
xce_core_set_cache_clear_cb(&(sched->core), xce_alloc_cache_clear_cb);
|
|
return sched;
|
|
}
|
|
|
|
/* Feed jobid to the live allocator thread and wait (bounded poll) for its
|
|
* decision, the technique test_parjob_e2e_multislot.c uses. */
|
|
static xce_decision_t* allocate_and_wait(xce_scheduler_t* sched,
|
|
uint64_t jobid)
|
|
{
|
|
xce_decision_t* decision = NULL;
|
|
int waited_ms;
|
|
|
|
TEST_ASSERT_TRUE(cbuf_tryput(&(sched->core.jbuf), &jobid) >= 0);
|
|
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;
|
|
}
|
|
|
|
static void assert_untraced(xce_scheduler_t* sched, xce_decision_t* d,
|
|
int op, uint64_t jobid)
|
|
{
|
|
TEST_ASSERT_NOT_NULL(d);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(op, d->op, "unexpected decision op");
|
|
if (op == XCE_DECISION_DISPATCH) {
|
|
TEST_ASSERT_EQUAL_UINT64(jobid, d->u.alloc->jobid);
|
|
} else {
|
|
TEST_ASSERT_EQUAL_UINT64(jobid, d->u.reason->jobid);
|
|
}
|
|
TEST_ASSERT_NULL_MESSAGE(d->trace, "untraced decision carries a trace");
|
|
TEST_ASSERT_NULL(sched->core.trace_ctl);
|
|
TEST_ASSERT_NULL(sched->core.trace_rec);
|
|
}
|
|
|
|
static void test_full_allocation_without_tracing_creates_no_record(void)
|
|
{
|
|
xce_scheduler_t* sched = start_sched();
|
|
xce_core_event_t ev;
|
|
xce_worker_t* workers[2];
|
|
xce_job_t* jobs[3];
|
|
xce_queue_t* q = (xce_queue_t*)calloc(1, sizeof(*q));
|
|
|
|
TEST_ASSERT_NULL(sched->core.trace_ctl);
|
|
TEST_ASSERT_NULL(sched->core.trace_rec);
|
|
|
|
TEST_ASSERT_NOT_NULL(q);
|
|
q->queue = strdup(QUEUE_NAME);
|
|
q->status = QUEUE_STAT_OPEN;
|
|
q->priority = 1;
|
|
memset(&ev, 0, sizeof(ev));
|
|
ev.type = XCE_CORE_EVENT_QUEUE_UPDATE;
|
|
ev.u.queue_update.queue = q;
|
|
TEST_ASSERT_TRUE(xce_core_push_events(&(sched->core), &ev, 1) >= 0);
|
|
|
|
workers[0] = make_wire_worker(WORKER_A_NAME, WORKER_SLOTS);
|
|
workers[1] = make_wire_worker(WORKER_B_NAME, WORKER_SLOTS);
|
|
memset(&ev, 0, sizeof(ev));
|
|
ev.type = XCE_CORE_EVENT_WORKERS_PUSH;
|
|
ev.u.workers_push.nworkers = 2;
|
|
ev.u.workers_push.workers = workers;
|
|
TEST_ASSERT_TRUE(xce_core_push_events(&(sched->core), &ev, 1) >= 0);
|
|
|
|
/* On 2 x 3 slots, in allocation order: a 4-task parallel job spanning
|
|
* both hosts, an ALTER whose 5-task first alternative cannot fit in the
|
|
* 2 slots left (falls back to the 1-task second), and a 5-task compound
|
|
* that cannot fit in the last slot: the single, alternative, compound,
|
|
* plan, position and per-host paths all run. */
|
|
jobs[0] = make_push_job(JOB_ALT,
|
|
"{5*{rusage[mem=1]}} || {1*{rusage[mem=1]}}", 1);
|
|
jobs[1] = make_push_job(JOB_PAR, "rusage[mem=1]", 4);
|
|
jobs[2] = make_push_job(JOB_PEND, "5*{rusage[mem=1]}", 1);
|
|
memset(&ev, 0, sizeof(ev));
|
|
ev.type = XCE_CORE_EVENT_JOBS_PUSH;
|
|
ev.u.jobs_push.njobs = 3;
|
|
ev.u.jobs_push.jobs = jobs;
|
|
TEST_ASSERT_TRUE(xce_core_push_events(&(sched->core), &ev, 1) >= 0);
|
|
{
|
|
xce_sched_job_t* par = sched_get_job(&(sched->core), JOB_PAR);
|
|
|
|
TEST_ASSERT_NOT_NULL(par);
|
|
TEST_ASSERT_NOT_NULL(par->schedobj);
|
|
}
|
|
|
|
atomic_store(&g_rec_new_calls, 0);
|
|
TEST_ASSERT_TRUE(xce_core_begin_session(&(sched->core)) >= 0);
|
|
|
|
assert_untraced(sched, allocate_and_wait(sched, JOB_PAR),
|
|
XCE_DECISION_DISPATCH, JOB_PAR);
|
|
assert_untraced(sched, allocate_and_wait(sched, JOB_ALT),
|
|
XCE_DECISION_DISPATCH, JOB_ALT);
|
|
assert_untraced(sched, allocate_and_wait(sched, JOB_PEND),
|
|
XCE_DECISION_PEND, JOB_PEND);
|
|
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(0, atomic_load(&g_rec_new_calls),
|
|
"a trace record was created with tracing "
|
|
"never enabled");
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_core_init_leaves_trace_pointers_null);
|
|
RUN_TEST(test_gate_skips_call_and_side_effects_without_record);
|
|
RUN_TEST(test_gate_evaluates_call_once_with_record);
|
|
/* last: it leaves the real scheduler threads running */
|
|
RUN_TEST(test_full_allocation_without_tracing_creates_no_record);
|
|
return UNITY_END();
|
|
}
|