myxce/src/scheduler/testsuite/test_repl_trace_e2e_fixture.c
Rongsong Shen 0317c7af98
fix(scheduler): cache hits keep the full pend reason; sim uses the cache (bug-1932)
The session-level unsatisfiable-resreq cache stored only the exhausted
attempt's main reason (an int). The allocators record shortfalls as
per-host/per-resource entries and leave main_reason at 0, so every cache
hit in the live scheduler pended its job with an empty reason. The
simulator registered no cache at all (its table lived on xce_scheduler_t),
so it searched every identical pending job again and its traces differed
from production.

- The cache table moves to xce_sched_core_t; cache_lookup/insert/clear and
  the hook adapters take the core, with no cast to a driver struct.
  sched_init() builds it, cache_clear() rebuilds it per session,
  xce_sched_core_fini() destroys it.
- A cache entry is a deep copy of the attempt's full xce_job_preason_t
  plus the job that proved the request unsatisfiable (core->alloc_jobid,
  set by sched_allocator()). A hit moves a copy into ctx->reason; a miss
  leaves ctx->reason untouched.
- main.c passes the same three adapters to the simulator through new
  sched_sim_args_t fields; sched_sim_driver_start() sets them on the sim
  core (plain field writes: white-box tests include sim.c alone).
- Decision trace: a hit renders "no plan generated: skipped, same request
  as <job> proved unsatisfiable earlier this session" (new
  XCE_TRACE_NOPLAN_KNOWN_UNSAT via sched_trace_known_unsat()); the outcome
  shows the restored reasons.
- sim_verify keeps its main-only PEND compare (old recordings carry
  main-only hits); comments and the scheduler-sim-live-allocation design
  note updated.

Tests: full-reason assertions in the cache dispatch tests (RED before:
"a cache hit must restore the original attempt's FULL pend reason"),
test_alloc_cache.c ported to the new API (deep copy, clear frees, -EEXIST
frees), a sim-registered clear test, a renderer test, and a sim e2e
scenario with two identical unsatisfiable jobs (204, 205) where 205 is
skipped with 204's full reasons (RED before: no skip line).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 08:02:18 +00:00

530 lines
17 KiB
C

/*
* test_repl_trace_e2e_fixture.c - the replays and the REPL runner of the
* end-to-end decision-trace test (test_repl_trace_e2e.c, which documents
* the scenarios; scheduler-sim-decision-trace task 6.1).
*
* write_extra_replay() generates the extra replay with the technique of
* test_repl_trace_baseline.c's write_contention_replay(): the header and
* snapshot through the real sched_record_start() (resources and both
* limits baked in), then workers, queue, the license value and jobs as
* wire events. run_repl() runs the built xce_scheduler on a replay with a
* script on stdin in a fresh work directory and returns the console
* transcript. Kept apart from the scenarios for the 800-line file cap.
*/
#include <errno.h>
#include <limits.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <unity/unity.h>
#include "config.h"
#include "controller.capnp.h"
#include "recording.h"
#include "resobj.h"
#include "scheduler.h"
#include "sim.h"
#include "utils.h"
#include "xcelimit.h"
#include "xstring.h"
#include "test_repl_trace_e2e.h"
#if !defined(XCE_SCHEDULER_BIN) || !defined(XCE_BASELINE_DIR)
#error "XCE_SCHEDULER_BIN and XCE_BASELINE_DIR must be defined by meson"
#endif
extern int write_capnp_message(FILE* f, struct capn* c);
#define SCRIPT_NAME "script.txt"
#define OUT_NAME "console.txt"
#define ERR_NAME "stderr.txt"
#define LOG_NAME "sched.log"
#define QUEUE_NAME "normal"
#define LICENSE_RES "license"
#define SNAPSHOT_TS 1000ULL
#define GLIMIT_NAME "cluster_slots"
#define HLIMIT_NAME "host_slots"
#define ARRAY_SEQ ((uint64_t)32768)
/* ---------------------------------------------------------------------- */
/* File helpers */
/* ---------------------------------------------------------------------- */
char* slurp(const char* path, long* len)
{
FILE* f = fopen(path, "rb");
char* buf;
long n;
*len = 0;
if (f == NULL) {
return NULL;
}
if ((fseek(f, 0, SEEK_END) != 0) || ((n = ftell(f)) < 0)) {
fclose(f);
return NULL;
}
rewind(f);
buf = (char*)malloc((size_t)n + 1);
if (buf == NULL) {
fclose(f);
return NULL;
}
if ((n > 0) && (fread(buf, 1, (size_t)n, f) != (size_t)n)) {
free(buf);
fclose(f);
return NULL;
}
buf[n] = '\0';
fclose(f);
*len = n;
return buf;
}
static void spit(const char* path, const char* buf, long len)
{
FILE* f = fopen(path, "wb");
TEST_ASSERT_NOT_NULL_MESSAGE(f, path);
if (len > 0) {
TEST_ASSERT_EQUAL_UINT((size_t)len, fwrite(buf, 1, (size_t)len, f));
}
TEST_ASSERT_EQUAL_INT(0, fclose(f));
}
static void copy_file(const char* from, const char* to)
{
long len;
char* buf = slurp(from, &len);
TEST_ASSERT_NOT_NULL_MESSAGE(buf, from);
spit(to, buf, len);
free(buf);
}
static void join_path(char* out, size_t outsz, const char* dir,
const char* name)
{
int n = snprintf(out, outsz, "%s/%s", dir, name);
TEST_ASSERT_TRUE((n > 0) && ((size_t)n < outsz));
}
/* ---------------------------------------------------------------------- */
/* Extra replay writer (test_repl_trace_baseline.c's technique) */
/* ---------------------------------------------------------------------- */
static void bake_resource(var resmgr, const char* name, int flags)
{
xce_resource_t res;
memset(&res, 0, sizeof(res));
res.name = strdup(name);
res.valtype = XCE_VAL_NUMERIC;
res.flags = flags;
TEST_ASSERT_NOT_NULL(makeResource(resmgr, &res));
}
static xce_name_val_t* make_nv(const char* name, float v)
{
xce_name_val_t* nv = (xce_name_val_t*)calloc(1, sizeof(*nv));
xce_val_t* val = (xce_val_t*)calloc(1, sizeof(*val));
TEST_ASSERT_NOT_NULL(nv);
TEST_ASSERT_NOT_NULL(val);
val->which = Value_u_fvalue;
val->u.fvalue = v;
nv->name = strdup(name);
nv->val = val;
return nv;
}
static xce_worker_t* make_wire_worker(const char* name, float slots,
float mem)
{
xce_worker_t* w = (xce_worker_t*)calloc(1, sizeof(*w));
xce_name_val_t** nv = (xce_name_val_t**)calloc(2, sizeof(*nv));
TEST_ASSERT_NOT_NULL(w);
TEST_ASSERT_NOT_NULL(nv);
nv[0] = make_nv("slots", slots);
nv[1] = make_nv("mem", mem);
w->name = strdup(name);
w->nresvals_to_sched = 2;
w->resvals_to_sched = nv;
return w;
}
static xce_job_t* make_push_job(uint64_t jobid, uint64_t sub_time,
const char* resreq, const char* user,
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 = sub_time;
j->queue_time = sub_time;
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*)user;
j->submit->project_name = (char*)"proj1";
j->submit->resreq = (char*)resreq;
j->submit->mintasks = mintasks;
j->submit->maxtasks = maxtasks;
return j;
}
static void write_event(FILE* f, replay_event_t* src)
{
struct capn c;
struct capn_segment* cs;
ReplayEvent_ptr p;
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_workers(FILE* f, uint64_t ts, xce_worker_t** w, int n)
{
xce_workers_data_t wrap;
replay_event_t src;
memset(&wrap, 0, sizeof(wrap));
wrap.n_total = n;
wrap.count = n;
wrap.workers = w;
memset(&src, 0, sizeof(src));
src.ts = ts;
src.body_which = ReplayEvent_body_workersPush;
src.body.workers_push = &wrap;
write_event(f, &src);
}
static void append_queue(FILE* f, uint64_t ts)
{
xce_queue_t q;
replay_event_t src;
memset(&q, 0, sizeof(q));
q.queue = (char*)QUEUE_NAME;
q.status = QUEUE_STAT_OPEN;
q.priority = 1;
memset(&src, 0, sizeof(src));
src.ts = ts;
src.body_which = ReplayEvent_body_queueUpdate;
src.body.queue_update = &q;
write_event(f, &src);
}
static void append_license_value(FILE* f, uint64_t ts, float total)
{
xce_val_t val;
xce_resval_t rv;
xce_resval_t* rvs[1];
xce_resval_list_t list;
replay_event_t src;
memset(&val, 0, sizeof(val));
val.which = Value_u_fvalue;
val.u.fvalue = total;
memset(&rv, 0, sizeof(rv));
rv.name = (char*)LICENSE_RES;
rv.val = &val;
rvs[0] = &rv;
memset(&list, 0, sizeof(list));
list.count = 1;
list.vals = rvs;
memset(&src, 0, sizeof(src));
src.ts = ts;
src.body_which = ReplayEvent_body_resvalsFull;
src.body.resvals_full = &list;
write_event(f, &src);
}
static void append_jobs(FILE* f, uint64_t ts, xce_job_t** jobs, int n)
{
xce_jobs_data_t wrap;
replay_event_t src;
memset(&wrap, 0, sizeof(wrap));
wrap.n_total = n;
wrap.count = n;
wrap.jobs = jobs;
memset(&src, 0, sizeof(src));
src.ts = ts;
src.body_which = ReplayEvent_body_jobsPush;
src.body.jobs_push = &wrap;
write_event(f, &src);
}
static void append_state_change(FILE* f, uint64_t ts, uint64_t jobid,
uint32_t old_state, uint32_t new_state)
{
job_state_change_event_t change;
replay_event_t src;
memset(&change, 0, sizeof(change));
change.jobid = jobid;
change.old_state = old_state;
change.new_state = new_state;
memset(&src, 0, sizeof(src));
src.ts = ts;
src.body_which = ReplayEvent_body_jobStateChange;
src.body.job_state_change = &change;
write_event(f, &src);
}
static void append_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(FILE* f)
{
replay_event_t src;
memset(&src, 0, sizeof(src));
src.body_which = ReplayEvent_body_endSession;
write_event(f, &src);
}
/* Header + snapshot via the real sched_record_start(), resources and both
* limits baked into the snapshot (the baseline fixture's configuration). */
static FILE* write_header_and_snapshot(char* out_path, size_t out_len,
const char* record_dir)
{
xce_scheduler_t sched;
xce_sched_plugin_t* fcfs;
xce_limit_val_t gval = { .resname = (char*)"slots",
.percent = 0,
.val = 6.0 };
xce_limit_config_t gcfg = { .name = (char*)GLIMIT_NAME, .val = &gval };
xce_limit_val_t hval = { .resname = (char*)"slots",
.percent = 1,
.val = 0.75 };
xce_limit_config_t hcfg = { .name = (char*)HLIMIT_NAME, .val = &hval };
xce_limit_config_t* limits[2] = { &gcfg, &hcfg };
xce_conf4sched_t config;
memset(&sched, 0, sizeof(sched));
mylock_init(&(sched.record_lock));
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core)));
fcfs = builtin_fcfs_new_plugin();
TEST_ASSERT_NOT_NULL(fcfs);
TEST_ASSERT_TRUE(plugin_register_core(&(sched.core), fcfs) >= 0);
bake_resource(sched.core.resmgr, "slots", XCE_RESF_CONSUMABLE);
bake_resource(sched.core.resmgr, "mem", XCE_RESF_CONSUMABLE);
bake_resource(sched.core.resmgr, LICENSE_RES,
XCE_RESF_GLOBAL | XCE_RESF_SHARED | XCE_RESF_CONSUMABLE);
memset(&config, 0, sizeof(config));
config.nlimits = 2;
config.limits = limits;
sched.core.config = &config;
atomic_store(&(sched.core.virtual_clock_base), SNAPSHOT_TS);
atomic_store(&(sched.core.speed_num), 0ULL);
atomic_store(&(sched.core.speed_den), 1ULL);
sched.core.job_pick_plugin = (char*)"builtin_fcfs";
sched.record_dir = (char*)record_dir;
TEST_ASSERT_EQUAL_INT(0, sched_record_start(&sched, EXTRA_REPLAY));
TEST_ASSERT_TRUE(strlen(sched.record_file_path) < out_len);
strcpy(out_path, sched.record_file_path);
return sched.record_file;
}
static uint64_t array_elem(uint32_t k)
{
return MAKE_JOBID(0, 0, 0, TAG_JOB_ARRAY, ARRAY_SEQ | (uint64_t)k);
}
/*
* write_extra_replay - w01 (4 slots, 16384 mem) and w02 (4 slots, 32768
* mem), the baseline's license and limits. Rounds:
* 2000 201 compound {license} + {2 x mem} (satisfied, 3 slots);
* 202 compound whose first member (4 tasks) breaks the cluster-wide
* limit (6 - 3 < 4), so its second member is not tried;
* array elements [1] and [2] (1 slot each, dispatched);
* 203 selects mem>20000 (w01 mismatches), pended by the
* cluster-wide limit (6 of 6 in use)
* 3000 after 201 and both elements finish: 202 and 203 are tried again
* 4000 after 202 and 203 finish: 204 and 205 (identical requests, more
* mem than any host has; bug-1932) pend in every session -- 204
* by a full search, 205 by the unsatisfiable-request cache
*/
void write_extra_replay(const char* dest)
{
char record_dir[PATH_MAX];
char path[PATH_MAX];
FILE* f;
xce_worker_t* workers[2];
xce_job_t* jobs[7];
snprintf(record_dir, sizeof(record_dir), "/tmp/sched_tre2e_gen_XXXXXX");
TEST_ASSERT_NOT_NULL(mkdtemp(record_dir));
f = write_header_and_snapshot(path, sizeof(path), record_dir);
workers[0] = make_wire_worker("w01", 4.0f, 16384.0f);
workers[1] = make_wire_worker("w02", 4.0f, 32768.0f);
append_workers(f, 1001ULL, workers, 2);
append_queue(f, 1002ULL);
append_license_value(f, 1002ULL, 2.0f);
jobs[0] = make_push_job(201, 1101,
"1*{rusage[license=1]} + "
"2*{rusage[mem=1024]}",
"alice", 1, 1);
jobs[1] = make_push_job(202, 1102,
"4*{rusage[mem=512]} + "
"1*{rusage[mem=512]}",
"bob", 1, 1);
jobs[2] = make_push_job(array_elem(1), 1103, "rusage[mem=256]", "bob",
1, 1);
jobs[3] = make_push_job(array_elem(2), 1103, "rusage[mem=256]", "bob",
1, 1);
jobs[4] = make_push_job(203, 1104, "select[mem>20000] rusage[mem=512]",
"alice", 1, 1);
jobs[5] = make_push_job(204, 1105, "rusage[mem=999999]", "carol", 1, 1);
jobs[6] = make_push_job(205, 1106, "rusage[mem=999999]", "carol", 1, 1);
append_jobs(f, 1003ULL, jobs, 7);
append_session(f, 2000ULL);
append_state_change(f, 2500ULL, 201, JOB_STAT_RUN, JOB_STAT_DONE);
append_state_change(f, 2600ULL, array_elem(1), JOB_STAT_RUN,
JOB_STAT_DONE);
append_state_change(f, 2700ULL, array_elem(2), JOB_STAT_RUN,
JOB_STAT_DONE);
append_session(f, 3000ULL);
append_state_change(f, 3500ULL, 202, JOB_STAT_RUN, JOB_STAT_DONE);
append_state_change(f, 3600ULL, 203, JOB_STAT_RUN, JOB_STAT_DONE);
append_session(f, 4000ULL);
append_end_of_stream(f);
TEST_ASSERT_EQUAL_INT(0, fclose(f));
copy_file(path, dest);
unlink(path);
rmdir(record_dir);
}
/* ---------------------------------------------------------------------- */
/* Running a scripted REPL session */
/* ---------------------------------------------------------------------- */
/* Runs the built scheduler on `replay` (BASE_REPLAY or EXTRA_REPLAY) in a
* fresh work directory with `script` on stdin and returns the console
* transcript. Paths are relative to the work directory. */
void run_repl(run_t* r, const char* replay, const char* script)
{
char path[PATH_MAX];
pid_t pid;
int status;
memset(r, 0, sizeof(*r));
snprintf(r->dir, sizeof(r->dir), "/tmp/sched_tre2e_XXXXXX");
TEST_ASSERT_NOT_NULL(mkdtemp(r->dir));
join_path(path, sizeof(path), r->dir, replay);
if (strcmp(replay, BASE_REPLAY) == 0) {
char src[PATH_MAX];
join_path(src, sizeof(src), XCE_BASELINE_DIR, BASE_REPLAY);
copy_file(src, path);
} else {
write_extra_replay(path);
}
join_path(path, sizeof(path), r->dir, SCRIPT_NAME);
spit(path, script, (long)strlen(script));
fflush(stdout);
fflush(stderr);
pid = fork();
TEST_ASSERT_TRUE_MESSAGE(pid >= 0, "fork must succeed");
if (pid == 0) {
if ((chdir(r->dir) != 0)
|| (freopen(SCRIPT_NAME, "r", stdin) == NULL)
|| (freopen(OUT_NAME, "w", stdout) == NULL)
|| (freopen(ERR_NAME, "w", stderr) == NULL)) {
_exit(120);
}
setenv("TERM", "dumb", 1);
execl(XCE_SCHEDULER_BIN, XCE_SCHEDULER_BIN, "--sim", replay,
"--interactive", "--logfile", LOG_NAME, (char*)NULL);
_exit(121);
}
TEST_ASSERT_EQUAL_INT(pid, waitpid(pid, &status, 0));
TEST_ASSERT_TRUE_MESSAGE(WIFEXITED(status),
"the scheduler must exit normally");
TEST_ASSERT_EQUAL_INT_MESSAGE(0, WEXITSTATUS(status),
"the session must end with quit's exit(0)");
join_path(path, sizeof(path), r->dir, OUT_NAME);
r->out = slurp(path, &(r->out_len));
TEST_ASSERT_NOT_NULL(r->out);
if (getenv("XCE_TRACE_E2E_DUMP") != NULL) {
fprintf(stderr, "==== %s (%s)\n%s", replay, r->dir, r->out);
}
}
void run_done(run_t* r)
{
static const char* const names[] = { BASE_REPLAY, EXTRA_REPLAY,
SCRIPT_NAME, OUT_NAME, ERR_NAME,
LOG_NAME, TRACE_FILE };
char path[PATH_MAX];
size_t i;
free(r->out);
r->out = NULL;
if (getenv("XCE_TRACE_E2E_DUMP") != NULL) {
return;
}
for (i = 0; i < sizeof(names) / sizeof(names[0]); i++) {
int n = snprintf(path, sizeof(path), "%s/%s", r->dir, names[i]);
if ((n > 0) && ((size_t)n < sizeof(path))) {
unlink(path);
}
}
rmdir(r->dir);
}