From 5d587ecf32d4be9a9c4dba1bf024f737f61f27bb Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Thu, 6 Aug 2026 09:22:04 +0000 Subject: [PATCH] feat(scheduler-core): implement simulation-mode startup (task 11.1) Add sim.h/sim.c implementing AC 13 steps 1-4: open+validate ReplayHeader, locate the most recent SnapshotEntry at/before --sim-start-ts, load it via xce_sched_core_load_snapshot(), and set the virtual clock anchors. Wire all seven simulation CLI flags into main.c's task-5.2 mode-selection branch. Event loop itself (step 5) deferred to task 11.2. Fixes two real bugs found during TDD: a SnapshotEntry_get_ts() use-after-free and a pend_updates NULL-deref in xce_sched_core_fini() for bare-core callers. 27/27 scheduler suite green, full project build clean. --- .kiro/specs/scheduler-core/tasks.md | 3 +- src/scheduler/include/sim.h | 228 +++++++++ src/scheduler/meson.build | 1 + src/scheduler/src/main.c | 153 +++++- src/scheduler/src/sim.c | 450 ++++++++++++++++++ src/scheduler/testsuite/meson.build | 24 + src/scheduler/testsuite/test_sim_startup.c | 528 +++++++++++++++++++++ 7 files changed, 1366 insertions(+), 21 deletions(-) create mode 100644 src/scheduler/include/sim.h create mode 100644 src/scheduler/src/sim.c create mode 100644 src/scheduler/testsuite/test_sim_startup.c diff --git a/.kiro/specs/scheduler-core/tasks.md b/.kiro/specs/scheduler-core/tasks.md index 04f75416..21a5f0fa 100644 --- a/.kiro/specs/scheduler-core/tasks.md +++ b/.kiro/specs/scheduler-core/tasks.md @@ -305,7 +305,7 @@ - [ ] 11. Core: Simulation Driver -- [ ] 11.1 Implement simulation startup +- [x] 11.1 Implement simulation startup - Add the simulation-mode CLI flags (replay file path, start timestamp, speed, verify, alt-events file, output file, interactive) that the process entry point's mode-selection branch (built in 5.2) routes into the simulation driver - Open and validate the replay file (must begin with the header message), locate the most recent snapshot entry at or before the requested start time, load it, and rebuild picker state from it - Set the virtual clock's anchors from the loaded snapshot's timestamp and the requested speed @@ -313,6 +313,7 @@ - Observable: starting simulation against a valid fixture file reaches a ready-to-run state with the virtual clock reporting the snapshot's timestamp; starting against a file missing its initial snapshot entry fails with a clear error before any event is processed - _Requirements: 5.13_ - _Depends: 8.1, 7.5_ + - _Verified: 2026-08-06 — new `sim.h`/`sim.c` implement AC 13 steps 1-4 (`sched_sim_driver_start()`): open+validate `ReplayHeader`, scan for `SnapshotEntry` messages tracking the most recent with `ts <= --sim-start-ts`, call `xce_sched_core_load_snapshot()`, set virtual clock anchors from `core_clock.c`'s real field names. Step 5 (event loop) deliberately deferred to task 11.2 via an `-ENOSYS` placeholder in `sched_sim_driver_run()`. All 7 CLI flags (`--sim`, `--sim-start-ts`, `--sim-speed`, `--sim-verify`, `--sim-alt-events`, `--sim-output`, `--interactive`/`-i`) wired into `main.c`'s existing task-5.2 mode-selection branch, replacing its `-ENOSYS` stub with real startup logic. Two real bugs found+fixed during TDD, both independently reproduced by review: a `SnapshotEntry_get_ts()` use-after-free (read through a `capn_ptr` after `capn_free()` — reviewer reverted the fix and reproduced a genuine ASan heap-UAF, then restored it) and a `pend_updates[0]/[1]` NULL-deref in `xce_sched_core_fini()` for any bare-core caller, worked around by allocating+`dataq_init()`ing them right after `xce_sched_core_init()`, matching `scheduler.c`'s existing `sched_init()` pattern. Deliberately does not register a picker plugin (no `xce_scheduler_t*` available under AC 17 isolation) — relies on `load_snapshot()`'s own documented graceful degradation; a working picker becomes load-bearing only once 11.2 drives real sessions. Filenames (`sim.h`/`sim.c`, not design.md's literal `sim_driver.h`/`sim_driver.c`) and a concrete `sched_sim_args_t*` (not design.md's literal `const void*`) are documented, justified deviations preserving the mandated struct/function contract. 27/27 scheduler suite (26 baseline + 6 new `sched_sim_startup_test` sub-tests, individually ASan-clean), full project build clean, RED phase independently re-reproduced by review (moved `sim.c` aside, confirmed the same missing-file build failure). Review APPROVED with one non-blocking coverage gap noted (multi-snapshot `ts`-selection logic — correct by code trace and by the reviewer's own bug-reversion experiment, but not yet covered by a dedicated 2+-snapshot fixture test) and two FYI-only naming/signature deviations, both already justified in `sim.h`'s header comment. Follow-up for a later task: `--sim-start-ts`/`--sim-speed` parse via `strtoull()`, which silently wraps a leading `-` into a huge unsigned value instead of a clear parse error._ - [ ] 11.2 Implement the simulation event loop and virtual clock advancement - Advance the virtual clock by jumping directly to each queued event's timestamp — no fixed tick, strictly monotonic, same-timestamp events processed in file order diff --git a/src/scheduler/include/sim.h b/src/scheduler/include/sim.h new file mode 100644 index 00000000..c06c81fb --- /dev/null +++ b/src/scheduler/include/sim.h @@ -0,0 +1,228 @@ +#if !defined(_SIM_H_) + +#define _SIM_H_ 1 + +#include +#include +#include + +#include "core.h" + +/* + * sim.h - simulation-mode driver (scheduler-core spec, task 11.1, Req 5 AC + * 13/14/17). NEW file per design.md's File Structure Plan ("sim_driver.h -- + * NEW (Phase 3): xce_sched_sim_driver_t"; named `sim.h`/`sim.c` here, + * matching this codebase's established one-focused-file-per-subsystem + * pattern -- driver.h/driver.c for normal mode, recording.h/recording.c for + * recording, snapshot.c for snapshot serialization). + * + * Boundary (Req 5 AC 17, "Simulation isolation"): `xce_sched_sim_driver_t` + * below contains no `xcecli_ctx_t`, no `xce_cluster_config_t`, no capnp + * socket, and no controller RPC of any kind -- confirmed by the struct + * definition itself (every field is either the pure-core `xce_sched_core_t`, + * a `FILE*`, or a scalar). Task 11.3 (not this task) builds the formal + * build-time isolation audit; this task's own obligation is simply to never + * introduce such a dependency here. + * + * This header only depends on core.h (+ stdbool/stdint/stdio) -- no + * scheduler.h, no client.h -- matching driver.h's own documented boundary + * comment for the same reason. + */ + +/* + * xce_sched_sim_driver_t - design.md's SimDriver Service Interface struct, + * verbatim field-for-field: + * + * xce_sched_core_t core; -- the restored scheduling state + * FILE *replay_file; -- input .replay stream (task 11.1) + * FILE *alt_events_file; -- optional --sim-alt-events input + * (task 11.5 consumes its content; + * this task only opens it) + * FILE *record_file; -- optional --sim-output stream + * (task 11.5 writes to it; this + * task only opens it) + * uint64_t sim_speed_num/sim_speed_den -- the SAME two numbers this task + * also stamps onto + * `core.speed_num`/`core.speed_den` + * (kept here too per design.md's + * literal struct, e.g. for the + * REPL/diagnostics to read without + * reaching into `core`) + * bool verify; -- --sim-verify (task 11.4 acts on + * it; this task only stores it) + * bool interactive; -- --interactive/-i (task 12 acts on + * it; this task only stores it) + */ +typedef struct { + xce_sched_core_t core; + FILE* replay_file; + FILE* alt_events_file; + FILE* record_file; + uint64_t sim_speed_num; + uint64_t sim_speed_den; + bool verify; + bool interactive; +} xce_sched_sim_driver_t; + +/* + * SCHED_SIM_START_TS_LATEST - default value for `sched_sim_args_t.sim_start_ts` + * when the caller does not pass --sim-start-ts (main.c). AC 13's own + * pseudocode ("find the most recent SnapshotEntry with ts <= sim_start_ts") + * has no documented default -- 0 would perversely mean "the earliest + * possible snapshot" and reject nearly every real (non-zero, wall-clock- + * derived) recording unless the user always passes an explicit value. + * UINT64_MAX instead means "no upper bound" -- the most recent SnapshotEntry + * in the whole file, i.e. resume simulation from wherever the recording (or + * a prior simulation's own --sim-output) left off. A deliberate, documented + * engineering decision (not specified verbatim in requirements.md/design.md), + * consistent with the AC's own filtering formula. + */ +#define SCHED_SIM_START_TS_LATEST UINT64_MAX + +/* + * sched_sim_args_t - CLI arguments for simulation mode (task 11.1, Req 5 AC + * 13's "The scheduler shall support simulation mode activated by --sim FILE + * and --sim-start-ts TIMESTAMP", plus this task's own "Add the simulation- + * mode CLI flags (replay file path, start timestamp, speed, verify, + * alt-events file, output file, interactive)" bullet -- all seven listed + * there are fields below). Populated by main.c's argp parsing, passed by + * const pointer into sched_sim_driver_start()/sched_sim_driver_run(). + * + * Deliberately a plain, self-contained struct -- only primitive/string + * fields, zero dependency on client.h/xce/cluster.h/scheduler.h -- so this + * header stays inside core.h's own dependency boundary (Req 5 AC 17). + * design.md's Service Interface literally writes `sched_sim_driver_run(..., + * const void* args)`, but a concrete, named struct type costs nothing extra + * here (no circular-include risk the way driver.h's `void* driver_ctx` + * idiom is solving for) and is safer -- both for this task's own test suite + * and for main.c's call site -- than an opaque void* that every caller must + * blindly cast. + */ +typedef struct { + const char* replay_path; /* --sim FILE (required) */ + uint64_t sim_start_ts; /* --sim-start-ts TIMESTAMP (optional; + * default SCHED_SIM_START_TS_LATEST) */ + uint64_t sim_speed; /* --sim-speed N: 0=as-fast-as-possible, + * 1=real-time, N>1=N x real-time + * (optional; default 1) */ + bool verify; /* --sim-verify (task 11.4) */ + const char* + alt_events_path; /* --sim-alt-events FILE (optional, task 11.5) */ + const char* output_path; /* --sim-output FILE (optional, task 11.5) */ + bool interactive; /* --interactive / -i (task 12) */ +} sched_sim_args_t; + +/* + * sched_sim_driver_start - Req 5 AC 13's five-step startup sequence, steps + * 1-4 ONLY (step 5, "enter event loop", is task 11.2's job -- see + * sched_sim_driver_run() below for the full entry point design.md + * documents). Independently exported (unlike design.md's single + * `sched_sim_driver_run()`) so this task's own Observable -- "starting + * simulation against a valid fixture file reaches a ready-to-run state" -- + * is directly, narrowly unit-testable without needing task 11.2's event + * loop to exist first. Matches this codebase's established pattern of + * splitting a driver's lifecycle into independently-testable pieces (e.g. + * driver.h's xce_sched_driver_translate_pending_events()/ + * xce_sched_driver_on_decision()/xce_sched_driver_run_normal_mode_round()). + * + * Step 1 (Open and validate replay file): opens `args->replay_path`, reads + * the first capnp message, and validates it is a ReplayHeader BEFORE + * touching anything else. Also threads `ReplayHeader.schedPolicy` (if + * present) onto the freshly-initialized `sim->core.job_pick_plugin` -- the + * ONLY place in the whole `.replay` stream that carries the picker-policy + * name (SnapshotData has no such field); this is simply completing the + * decode this step already performs, not separate new work. Registering a + * picker PLUGIN under that name is explicitly NOT this function's job (see + * "Known scope boundary" below). + * + * Step 2 (Locate starting snapshot): the file's SECOND message (the first + * data message after ReplayHeader) MUST itself be a SnapshotEntry -- Req 5 + * AC 20's stream-ordering rule and this task's own "Reject a replay file + * whose first data message isn't a snapshot entry" requirement. If it is + * not (including the case where the file simply ends right after the + * header), this function fails immediately, before scanning any further and + * before any ReplayEvent is ever decoded. Given that check passes, this + * function then scans every remaining message to EOF, decoding every + * SnapshotEntry's timestamp (cheaply, via SnapshotEntry_get_ts(), no + * allocation) and keeping the most recent one with `ts <= + * args->sim_start_ts` as the running best candidate (materialized fully + * only when it becomes the new best). If no SnapshotEntry anywhere in the + * file qualifies, the file is rejected (AC 13: "Without initial state, + * simulation has no starting point"). `sim->replay_file`'s read position is + * left immediately after the CHOSEN SnapshotEntry's message -- ready for + * task 11.2's event loop to resume sequential reads from exactly that point + * (every message before it, if any, is already reflected in the loaded + * snapshot's state and must never be replayed a second time). + * + * Step 3 (Load snapshot and rebuild picker): xce_sched_core_load_snapshot() + * (core.h, task 8.1) is called with the chosen candidate's decoded + * SnapshotData. That function ALREADY performs the picker-state rebuild + * walk (on_queue_update per queue, then on_job_push per pending job) + * internally as documented on its own core.h doc comment -- this function + * does not duplicate that walk. + * + * Step 4 (Set virtual clock): stamps `sim->core.virtual_clock_base` to the + * chosen snapshot's own timestamp, `sim->core.real_clock_base` to a fresh + * `clock_gettime(CLOCK_MONOTONIC)` wall-clock reading taken right here (Req + * 5 AC 13's own pseudocode explicitly performs this read from the DRIVER, + * not from core.c -- core.c's "zero wall-clock calls" isolation boundary, + * test_core_isolation.c, is scoped to core.c specifically, not to driver- + * level code that legitimately seeds the core's clock anchors), and + * `sim->core.speed_num`/`speed_den` to `args->sim_speed`/1. From this point, + * `xce_core_now(&sim->core)` returns virtual time computed from the + * snapshot's own logical timestamp -- this function's own Observable. + * + * Known scope boundary: this function does NOT register any picker plugin + * into `sim->core.plugins` (builtin_fcfs and friends are only registrable + * via plugin_register(), which requires a genuine `xce_scheduler_t*` -- + * `xce_sched_sim_driver_t` deliberately has no such field, Req 5 AC 17 -- + * and this codebase has no core.h-scoped plugin registration entry point). + * xce_sched_core_load_snapshot()'s own documented graceful degradation + * (WARN + skip the picker-rebuild hooks when no picker resolves) keeps this + * a non-fatal gap for THIS task's Observable, which only requires reaching + * a ready-to-run state with the correct virtual clock -- not a working + * picker (task 11.2, which actually drives scheduling rounds, is where a + * resolvable picker becomes load-bearing). + * + * Returns 0 on success (`*sim` is fully populated and ready). Returns a + * negative errno on failure; `*sim` is left in an indeterminate state, and + * IN THAT CASE ONLY, all resources this function itself opened (files, + * `sim->core`) have already been released before it returns -- the caller + * must not call anything else on `*sim` and must not fclose()/finalize it + * again itself. -EINVAL for NULL `sim`/`args`/`args->replay_path`; -ENOENT/ + * -EACCES/etc. (errno passthrough) for an unopenable replay/alt-events/ + * output file; -EIO for an empty/unreadable/malformed first capnp message; + * -EINVAL for a first message that is not ReplayHeader-shaped, or a first + * data message that is not SnapshotEntry-shaped, or a file with no + * SnapshotEntry anywhere satisfying `ts <= args->sim_start_ts`. + */ +extern int sched_sim_driver_start(xce_sched_sim_driver_t* sim, + const sched_sim_args_t* args); + +/* + * sched_sim_driver_run - design.md's literal SimDriver Service Interface + * entry point (`int sched_sim_driver_run(xce_sched_sim_driver_t*, const + * void* args)`, adapted to take the concrete sched_sim_args_t* this header + * defines rather than an opaque void* -- see sched_sim_args_t's own doc + * comment for why). This is what main.c's mode-selection branch calls when + * `--sim` is present (design.md's own Integration note). + * + * Calls sched_sim_driver_start() above (task 11.1's own work, this task's + * Observable) then -- since task 11.2's event loop (Req 5 AC 13 step 5 / + * AC 14/18) does not exist yet -- fails loudly with -ENOSYS rather than + * silently returning success having processed zero events. This is the + * same "honest, temporary placeholder" precedent main.c's own pre-task-11.1 + * `--sim` stub already established (task 5.2): startup is now genuinely + * real, working behavior; only the event loop remains a documented gap for + * task 11.2 to fill in, without requiring another main.c edit when it does + * (task 11.2 only needs to extend THIS function's body, after the + * sched_sim_driver_start() call, with the real event loop). + * + * Returns 0 (once task 11.2 lands) or a negative errno: whatever + * sched_sim_driver_start() returned on a startup failure, or -ENOSYS if + * startup succeeded but the event loop is not yet implemented. + */ +extern int sched_sim_driver_run(xce_sched_sim_driver_t* sim, + const sched_sim_args_t* args); + +#endif diff --git a/src/scheduler/meson.build b/src/scheduler/meson.build index a93c3d96..f1c3591e 100644 --- a/src/scheduler/meson.build +++ b/src/scheduler/meson.build @@ -14,6 +14,7 @@ scheduler_srcs = [ 'src/recording.c', 'src/recording_events.c', 'src/recording_session.c', + 'src/sim.c', 'src/schedobj.c', 'src/plugin.c', 'src/resource.c', diff --git a/src/scheduler/src/main.c b/src/scheduler/src/main.c index 74acf85b..ee85ebaf 100644 --- a/src/scheduler/src/main.c +++ b/src/scheduler/src/main.c @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -12,6 +13,7 @@ #include "driver.h" #include "scheduler.h" +#include "sim.h" #include "resreq.h" #include "utils.h" @@ -28,27 +30,59 @@ const char* argp_program_bug_address = BUG_EMAIL; * short-option letters above -- same convention as the controller's * main.c, e.g. its OPT_PEERS). */ #define OPT_RECORD_DIR 256 +/* Req 5 AC 13 (scheduler-core spec, task 11.1): the remaining simulation- + * mode CLI flags task 5.2's own --sim option left unparsed ("Phase 3 -- not + * yet implemented"). --sim-verify and --interactive are boolean flags (no + * argument); --interactive also gets the short letter -i (unused until + * now). */ +#define OPT_SIM_START_TS 257 +#define OPT_SIM_SPEED 258 +#define OPT_SIM_VERIFY 259 +#define OPT_SIM_ALT_EVENTS 260 +#define OPT_SIM_OUTPUT 261 struct arguments { char* logfile; int verbose; char* peers; /* --peers value (optional) */ - char* sim_file; /* --sim value (optional; Phase 3, not yet implemented) */ + char* sim_file; /* --sim value (optional; selects simulation mode) */ char* record_dir; /* --record-dir value (optional; Req 5 AC 10, task 9.1) */ + /* Req 5 AC 13 (scheduler-core spec, task 11.1): the rest of simulation + * mode's CLI surface, all optional except --sim itself. Raw strings + * here, same as sim_file/record_dir above -- numeric/bool + * interpretation happens in main() after argp_parse() returns (see + * sched_sim_args_t's own field comments, sim.h, for each flag's + * meaning/default). */ + char* sim_start_ts; /* --sim-start-ts TIMESTAMP */ + char* sim_speed; /* --sim-speed N */ + int sim_verify; /* --sim-verify */ + char* sim_alt_events; /* --sim-alt-events FILE */ + char* sim_output; /* --sim-output FILE */ + int interactive; /* --interactive / -i */ }; static struct argp_option options[] = { { "logfile", 'l', "FILE", 0, "The full path of log file" }, { "verbose", 'v', 0, 0, "Produce verbose log" }, { "peers", 'P', "LIST", 0, "HA candidate list: [name=]host:port,..." }, - /* Req 5 AC 9 "Mode selection" / "CLI flags" (scheduler-core spec, task - * 5.2): --sim FILE selects the simulation driver. Parsing only, as of - * this task -- SimDriver itself is Phase 3 (design.md's File Structure - * Plan marks sim_driver.c/sim_driver.h "NEW (Phase 3)"); see main()'s - * mode-selection branch below for the loud not-yet-implemented failure. */ - { "sim", 's', "FILE", 0, - "Replay FILE for simulation mode " - "(Phase 3 -- not yet implemented)" }, + /* Req 5 AC 9 "Mode selection" / Req 5 AC 13 "Simulation mode startup" + * (scheduler-core spec, tasks 5.2/11.1): --sim FILE selects the + * simulation driver; the flags below configure it. */ + { "sim", 's', "FILE", 0, "Replay FILE for simulation mode" }, + { "sim-start-ts", OPT_SIM_START_TS, "TIMESTAMP", 0, + "Simulation start timestamp (ns since epoch); default: the replay " + "file's most recent snapshot" }, + { "sim-speed", OPT_SIM_SPEED, "N", 0, + "Simulation speed: 0=as-fast-as-possible, 1=real-time (default), " + "N>1=N x real-time" }, + { "sim-verify", OPT_SIM_VERIFY, 0, 0, + "Compare simulated decisions against the replay file's recorded ones" }, + { "sim-alt-events", OPT_SIM_ALT_EVENTS, "FILE", 0, + "Replay events from FILE instead of the base --sim file (what-if " + "branching)" }, + { "sim-output", OPT_SIM_OUTPUT, "FILE", 0, + "Write simulated decisions to FILE as a .replay stream" }, + { "interactive", 'i', 0, 0, "Open the simulation REPL" }, /* Req 5 AC 10 "Record file path validation" (scheduler-core spec, task * 9.1): the ONLY way record_dir is ever provisioned -- never settable * via any request-path field (SchedAdminRequest included). Parsed @@ -76,6 +110,24 @@ static error_t parse_opt(int key, char* arg, struct argp_state* state) case 's': arguments->sim_file = arg; break; + case OPT_SIM_START_TS: + arguments->sim_start_ts = arg; + break; + case OPT_SIM_SPEED: + arguments->sim_speed = arg; + break; + case OPT_SIM_VERIFY: + arguments->sim_verify = 1; + break; + case OPT_SIM_ALT_EVENTS: + arguments->sim_alt_events = arg; + break; + case OPT_SIM_OUTPUT: + arguments->sim_output = arg; + break; + case 'i': + arguments->interactive = 1; + break; case OPT_RECORD_DIR: arguments->record_dir = arg; break; @@ -158,18 +210,79 @@ int main(int argc, char* argv[]) mylog_init(logfile, loglevel, logsize, logcount); - /* Req 5 AC 9 "Mode selection" (scheduler-core spec, task 5.2): --sim - * selects the simulation driver (SimDriver). Not yet implemented as of - * this task (Phase 3 -- design.md's File Structure Plan explicitly - * marks sim_driver.c/sim_driver.h "NEW (Phase 3)"; sched_sim_driver_run() - * does not exist). Fail LOUDLY and immediately rather than silently - * no-op'ing or crashing -- an honest, temporary placeholder for Phase 3 - * to fill in. Checked before any network/config setup below: there is - * no reason to pay for a controller connection attempt on a path that - * is about to fail. */ + /* Req 5 AC 9 "Mode selection" / Req 5 AC 13 "Simulation mode startup" + * (scheduler-core spec, task 11.1): --sim selects the simulation driver + * (SimDriver) instead of the normal, controller-attached one. Checked + * before any network/config setup below -- Req 5 AC 17 "Simulation + * isolation" requires the scheduler process establish NO connection to + * the controller in simulation mode, so this branch must both come + * before, and always `return` out ahead of, every sched_init()/ + * xce_client_config_t call further down (never falls through). */ if (arguments.sim_file != NULL) { - fprintf(stderr, "xce_scheduler: --sim mode not yet implemented\n"); - return 1; + sched_sim_args_t sim_args; + xce_sched_sim_driver_t sim; + uint64_t sim_start_ts = SCHED_SIM_START_TS_LATEST; + uint64_t sim_speed = 1; /* default: real-time */ + int rc; + + if (arguments.sim_start_ts != NULL) { + char* endp = NULL; + + sim_start_ts = strtoull(arguments.sim_start_ts, &endp, 10); + if ((endp == NULL) || (*endp != '\0') + || (endp == arguments.sim_start_ts)) { + fprintf(stderr, + "xce_scheduler: --sim-start-ts expects a numeric " + "timestamp, got \"%s\"\n", + arguments.sim_start_ts); + return 1; + } + } + + if (arguments.sim_speed != NULL) { + char* endp = NULL; + + sim_speed = strtoull(arguments.sim_speed, &endp, 10); + if ((endp == NULL) || (*endp != '\0') + || (endp == arguments.sim_speed)) { + fprintf(stderr, + "xce_scheduler: --sim-speed expects a numeric " + "value, got \"%s\"\n", + arguments.sim_speed); + return 1; + } + } + + memset(&sim_args, 0, sizeof(sim_args)); + sim_args.replay_path = arguments.sim_file; + sim_args.sim_start_ts = sim_start_ts; + sim_args.sim_speed = sim_speed; + sim_args.verify = (arguments.sim_verify != 0); + sim_args.alt_events_path = arguments.sim_alt_events; + sim_args.output_path = arguments.sim_output; + sim_args.interactive = (arguments.interactive != 0); + + rc = sched_sim_driver_run(&sim, &sim_args); + if (rc == -ENOSYS) { + /* Task 11.2 (scheduler-core spec, Req 5 AC 14/18) owns the + * event loop -- not yet implemented. Startup itself (this + * task's own Observable) genuinely succeeded: sim.core holds + * the fully restored, ready-to-run state and virtual clock. + * Same "honest, temporary placeholder" precedent this stub + * previously used for the whole --sim flag. */ + fprintf(stderr, + "xce_scheduler: --sim startup complete (virtual clock " + "at ts=%llu); event loop not yet implemented\n", + (unsigned long long)xce_core_now(&(sim.core))); + return 1; + } + if (rc < 0) { + fprintf(stderr, "xce_scheduler: --sim startup failed: %s\n", + strerror(-rc)); + return 1; + } + + return 0; } /* P2.5-6: populate and validate xce_cluster_config_t when --peers given */ diff --git a/src/scheduler/src/sim.c b/src/scheduler/src/sim.c new file mode 100644 index 00000000..2c2c489a --- /dev/null +++ b/src/scheduler/src/sim.c @@ -0,0 +1,450 @@ +#include +#include +#include +#include +#include + +#include "controller.capnp.h" +#include "sim.h" +#include "utils.h" +#include "xstring.h" + +/* + * sim.c - simulation-mode driver startup (scheduler-core spec, task 11.1, + * Req 5 AC 13/17/20). See sim.h for the full per-function contract. This + * file implements Req 5 AC 13's five-step startup sequence, steps 1-4 only + * (step 5, the event loop, is task 11.2's job -- sched_sim_driver_run() + * below fails loudly with -ENOSYS after a successful startup, rather than + * silently no-op'ing). + * + * Message-type discrimination (Req 5 AC 20): a `.replay` stream has no + * self-describing type tag for any message -- position alone pins down the + * first two messages (ReplayHeader, then SnapshotEntry), and c-capnproto's + * decode functions do not themselves distinguish "this struct is the wrong + * type" (decoding a SnapshotEntry's bytes as a ReplayHeader would silently + * "succeed" with garbage field values). What DOES reliably work -- verified + * empirically by the xceadmin `scheduler dump` command (task 10.4, + * xceadmin/scheduler.py's own module-section docstring) against the SAME + * schema -- is that each of the four top-level `.replay` message types has + * a distinct (dataWordCount, pointerCount) struct shape, encoded directly + * in the message's own root struct pointer, independent of whatever schema + * later interprets its fields: ReplayHeader=(1,5), SnapshotEntry=(1,1), + * ReplayEvent=(2,1), ReplaySession=(1,2) (controller.capnp.h's own + * generated `_word_count`/`_pointer_count` constants). This + * file uses the exact same shape-check technique, at the c-capnproto C API + * level (capn_ptr.datasz/.ptrs after capn_getp(capn_root(&c), 0, 1) -- + * every message in this codebase's convention stores its real payload at + * pointer slot 0 of the implicit root struct, matching recording.c's + * write_capnp_message() callers, which all `capn_setp(capn_root(&c), 0, + * ptr.p)`) rather than raw byte-peeking (unnecessary in C: capn_init_fp() + * already parses the stream framing for us). + */ + +/* + * msg_root_has_shape - true iff `c`'s decoded root payload (pointer slot 0 + * of the implicit root struct) is a CAPN_STRUCT with exactly `want_words` + * data words and `want_ptrs` pointers. On a match, `*out` receives the + * resolved capn_ptr (ready to wrap in the caller's own `_ptr`). Never + * allocates; safe to call speculatively before deciding whether to fully + * decode a message. + */ +static int msg_root_has_shape(struct capn* c, capn_ptr* out, size_t want_words, + size_t want_ptrs) +{ + capn_ptr p = capn_getp(capn_root(c), 0, 1); + + if (p.type != CAPN_STRUCT) { + return 0; + } + if (((size_t)(p.datasz / 8) != want_words) + || ((size_t)p.ptrs != want_ptrs)) { + return 0; + } + + if (out != NULL) { + *out = p; + } + + return 1; +} + +/* + * sim_open_and_validate_header - Req 5 AC 13 Step 1. Opens `path`, decodes + * the first capnp message, and validates it is ReplayHeader-shaped. On + * success, `sim->replay_file` is set (left open, positioned right after the + * header, ready for the Step-2 scan below) and `*out_sched_policy` receives + * a fresh heap copy of `ReplayHeader.schedPolicy` (NULL if the field is + * empty/absent) -- see sim.h's own doc comment on sched_sim_driver_start() + * for why this is threaded through here rather than left for a later step. + */ +static int sim_open_and_validate_header(xce_sched_sim_driver_t* sim, + const char* path, + char** out_sched_policy) +{ + FILE* f; + struct capn c; + capn_ptr hdr_root; + ReplayHeader_ptr hdr; + struct ReplayHeader wire; + replay_header_t decoded; + + *out_sched_policy = NULL; + + f = fopen(path, "r"); + if (f == NULL) { + int rc = (errno > 0) ? -errno : -EIO; + + ERROR("%s cannot open replay file '%s': %s", __func__, path, + strerror(errno)); + return rc; + } + + if (capn_init_fp(&c, f, 0) != 0) { + ERROR("%s replay file '%s' is empty, unreadable, or its first " + "message is malformed", + __func__, path); + fclose(f); + return -EIO; + } + + if (!msg_root_has_shape(&c, &hdr_root, ReplayHeader_word_count, + ReplayHeader_pointer_count)) { + ERROR("%s replay file '%s': first message is not a valid " + "ReplayHeader (wrong wire shape)", + __func__, path); + capn_free(&c); + fclose(f); + return -EINVAL; + } + + hdr.p = hdr_root; + read_ReplayHeader(&wire, hdr); + memset(&decoded, 0, sizeof(decoded)); + decode_ReplayHeader(&decoded, &wire); + + if ((decoded.sched_policy != NULL) && (decoded.sched_policy[0] != '\0')) { + *out_sched_policy = makeString(decoded.sched_policy); + } + + free_ReplayHeader(&decoded); + capn_free(&c); + + sim->replay_file = f; + + return 0; +} + +/* + * sim_locate_and_load_snapshot - Req 5 AC 13 Step 2 + Step 3. Requires + * `sim->replay_file` to already be open and positioned right after the + * ReplayHeader (sim_open_and_validate_header() above). See sim.h's own doc + * comment on sched_sim_driver_start() for the full algorithm and the + * ownership reasoning behind never freeing the CHOSEN candidate. + */ +static int sim_locate_and_load_snapshot(xce_sched_sim_driver_t* sim, + uint64_t sim_start_ts, + uint64_t* out_snapshot_ts) +{ + FILE* f = sim->replay_file; + struct capn c; + capn_ptr snap_root; + SnapshotEntry_ptr sp; + replay_snapshot_entry_t best; + int have_best = 0; + uint64_t best_ts = 0; + long resume_at = -1; + int n_seen = 0; + int first = 1; + int rc; + + memset(&best, 0, sizeof(best)); + + for (;;) { + uint64_t ts; + + rc = capn_init_fp(&c, f, 0); + if (rc != 0) { + /* EOF (or malformed trailing data, treated identically -- same + * "nonzero means no more messages" convention + * test_record_lifecycle.c's own capn_init_fp() end-of-stream + * assertions already establish). */ + if (first) { + ERROR("%s replay file has no data messages after " + "ReplayHeader (missing initial SnapshotEntry, Req 5 " + "AC 20 violation)", + __func__); + return -EINVAL; + } + break; + } + + if (!msg_root_has_shape(&c, &snap_root, SnapshotEntry_word_count, + SnapshotEntry_pointer_count)) { + if (first) { + /* The file's SECOND message (the first data message after + * ReplayHeader) MUST itself be a SnapshotEntry -- Req 5 AC + * 20's stream-ordering rule and this task's own "reject a + * replay file whose first data message isn't a snapshot + * entry" requirement. Fail immediately here, before ANY + * ReplayEvent is ever decoded -- do not fall through to the + * general scan below. */ + ERROR("%s first data message after ReplayHeader is not a " + "SnapshotEntry (Req 5 AC 20 violation) -- rejecting " + "replay file", + __func__); + capn_free(&c); + return -EINVAL; + } + + /* Not the first message, and not SnapshotEntry-shaped -- + * ReplayEvent/ReplaySession/unrecognized. Task 11.1's own scan + * only cares about locating a starting snapshot; ignore and + * keep scanning. */ + capn_free(&c); + first = 0; + continue; + } + + first = 0; + n_seen++; + + sp.p = snap_root; + ts = SnapshotEntry_get_ts(sp); + + if (ts <= sim_start_ts) { + struct SnapshotEntry entry_wire; + replay_snapshot_entry_t decoded; + + read_SnapshotEntry(&entry_wire, sp); + memset(&decoded, 0, sizeof(decoded)); + decode_SnapshotEntry(&decoded, &entry_wire); + capn_free(&c); + + /* SnapshotEntry timestamps are monotonically non-decreasing + * across a well-formed `.replay` stream (both normal-mode + * recording and simulation --sim-output derive ts from + * xce_core_now(), which never runs backwards) -- so any + * qualifying candidate found later in the scan is always at + * least as recent as the current best. Freeing the OLD best + * here is always safe: xce_sched_core_load_snapshot() is only + * ever called once, below, on whichever candidate survives to + * the end of this loop -- an intermediate candidate that gets + * replaced was never passed to it, so nothing aliases its + * memory yet. */ + if (have_best) { + free_SnapshotEntry(&best); + } + best = decoded; + have_best = 1; + best_ts = ts; + resume_at = ftell(f); + } else { + capn_free(&c); + } + } + + if (!have_best) { + ERROR("%s no SnapshotEntry with ts <= %llu found (%d snapshot " + "entries seen in file) -- replay file has no usable starting " + "point", + __func__, (unsigned long long)sim_start_ts, n_seen); + return -EINVAL; + } + + if (fseek(f, resume_at, SEEK_SET) != 0) { + rc = (errno > 0) ? -errno : -EIO; + /* best.config is about to be handed to xce_sched_core_load_snapshot() + * in the success path -- but we are failing before that call ever + * happens, so best's contents are still wholly ours to free here + * (nothing aliases them yet). */ + free_SnapshotEntry(&best); + return rc; + } + + /* Step 3: xce_sched_core_load_snapshot() (core.h, task 8.1) already + * performs the picker-rebuild walk (on_queue_update per queue, then + * on_job_push per pending job) internally -- see sim.h's own doc + * comment on sched_sim_driver_start() Step 3. Not duplicated here. */ + rc = xce_sched_core_load_snapshot(&(sim->core), best.data); + if (rc < 0) { + ERROR("%s xce_sched_core_load_snapshot() failed: rc=%d", __func__, rc); + free_SnapshotEntry(&best); + return rc; + } + + /* `best` (specifically best.data->config) is NOT freed on success: + * xce_sched_core_load_snapshot() aliases `core->config = in->config` + * (a borrowed-reference assignment, not a deep copy -- core.h/ + * snapshot_load.c's own documented "config -- borrowed reference" + * choice) -- core->config now points at the SAME memory as + * best.data->config. Freeing `best` here would immediately dangle + * `sim->core.config`. Same "no free-snapshot function, deliberate + * accepted leak of the non-aliased remainder" precedent + * recording.c's write_snapshot_entry() already documents for the + * identical save-side ownership shape (detect_leaks=0 across this + * whole test suite, by design). */ + + /* Read from `best_ts` (captured while the message's own capn arena was + * still live), never by re-reading `sp` here -- the capn_free(&c) call + * above (for whichever scan iteration produced `best`) already freed + * the arena `sp` points into; re-reading through it here would be a + * use-after-free. */ + *out_snapshot_ts = best_ts; + + return 0; +} + +/* + * sim_set_virtual_clock - Req 5 AC 13 Step 4. See sim.h's own doc comment + * on sched_sim_driver_start() for why this driver-level clock_gettime() + * call does not violate core.c's own "zero wall-clock calls" isolation + * boundary (test_core_isolation.c) -- that boundary is scoped to core.c + * itself, not to driver-level code legitimately seeding the core's clock + * anchors, exactly as AC 13's own pseudocode specifies. + */ +static void sim_set_virtual_clock(xce_sched_sim_driver_t* sim, + uint64_t snapshot_ts, uint64_t sim_speed) +{ + struct timespec ts; + uint64_t real_clock_base_ns; + + clock_gettime(CLOCK_MONOTONIC, &ts); + real_clock_base_ns + = ((uint64_t)ts.tv_sec * 1000000000ULL) + (uint64_t)ts.tv_nsec; + + atomic_store_explicit(&(sim->core.virtual_clock_base), snapshot_ts, + memory_order_release); + atomic_store_explicit(&(sim->core.real_clock_base), real_clock_base_ns, + memory_order_release); + atomic_store_explicit(&(sim->core.speed_num), sim_speed, + memory_order_release); + atomic_store_explicit(&(sim->core.speed_den), 1, memory_order_release); + + sim->sim_speed_num = sim_speed; + sim->sim_speed_den = 1; +} + +int sched_sim_driver_start(xce_sched_sim_driver_t* sim, + const sched_sim_args_t* args) +{ + char* sched_policy = NULL; + uint64_t snapshot_ts = 0; + int rc; + + if ((sim == NULL) || (args == NULL) || (args->replay_path == NULL)) { + return -EINVAL; + } + + memset(sim, 0, sizeof(*sim)); + + rc = xce_sched_core_init(&(sim->core)); + if (rc < 0) { + return rc; + } + + /* xce_sched_core_init() deliberately leaves core->pend_updates[0]/[1] + * NULL (core.c's own file header comment: their dataq_init() killer + * callback stays driver-owned) -- but xce_sched_core_fini() + * unconditionally dataq_fini()s both, so ANY caller of + * xce_sched_core_init() must allocate+dataq_init() them before the core + * can safely be torn down, matching the exact pattern scheduler.c's + * sched_init() (normal mode) and every core.c-including test fixture in + * this directory already establish. Simulation mode never actually + * enqueues anything into these queues (task 11.2's event loop + * translates .replay ReplayEvent messages directly into + * xce_core_event_t batches and calls xce_core_push_events() itself, Req + * 5 AC 18 -- it bypasses pend_updates entirely, unlike the normal-mode + * driver's xce_sched_driver_translate_pending_events()) -- a NULL + * killer is therefore correct here, same choice the test fixtures make + * for the same reason. */ + sim->core.pend_updates[0] = (dataq_t*)calloc(1, sizeof(dataq_t)); + dataq_init(sim->core.pend_updates[0], NULL); + sim->core.pend_updates[1] = (dataq_t*)calloc(1, sizeof(dataq_t)); + dataq_init(sim->core.pend_updates[1], NULL); + + rc = sim_open_and_validate_header(sim, args->replay_path, &sched_policy); + if (rc < 0) { + xce_sched_core_fini(&(sim->core)); + return rc; + } + + /* See sim.h's doc comment on sched_sim_driver_start() Step 1 / "Known + * scope boundary": this only threads the picker NAME through -- no + * plugin is registered under it, so + * xce_sched_core_load_snapshot()'s own internal + * sched_resolve_job_picker() call may still legitimately WARN and skip + * the picker rebuild below. Ownership transfers to sim->core (freed by + * xce_sched_core_fini(), same as job_pick_plugin's normal-mode + * lifecycle). */ + if (sched_policy != NULL) { + sim->core.job_pick_plugin = sched_policy; + } + + rc = sim_locate_and_load_snapshot(sim, args->sim_start_ts, &snapshot_ts); + if (rc < 0) { + fclose(sim->replay_file); + sim->replay_file = NULL; + xce_sched_core_fini(&(sim->core)); + return rc; + } + + sim_set_virtual_clock(sim, snapshot_ts, args->sim_speed); + + sim->verify = args->verify; + sim->interactive = args->interactive; + + if (args->alt_events_path != NULL) { + sim->alt_events_file = fopen(args->alt_events_path, "r"); + if (sim->alt_events_file == NULL) { + rc = (errno > 0) ? -errno : -EIO; + ERROR("%s cannot open --sim-alt-events file '%s': %s", __func__, + args->alt_events_path, strerror(errno)); + fclose(sim->replay_file); + sim->replay_file = NULL; + xce_sched_core_fini(&(sim->core)); + return rc; + } + } + + if (args->output_path != NULL) { + sim->record_file = fopen(args->output_path, "w"); + if (sim->record_file == NULL) { + rc = (errno > 0) ? -errno : -EIO; + ERROR("%s cannot open --sim-output file '%s': %s", __func__, + args->output_path, strerror(errno)); + if (sim->alt_events_file != NULL) { + fclose(sim->alt_events_file); + sim->alt_events_file = NULL; + } + fclose(sim->replay_file); + sim->replay_file = NULL; + xce_sched_core_fini(&(sim->core)); + return rc; + } + } + + return 0; +} + +int sched_sim_driver_run(xce_sched_sim_driver_t* sim, + const sched_sim_args_t* args) +{ + int rc = sched_sim_driver_start(sim, args); + + if (rc < 0) { + return rc; + } + + /* Task 11.2 (scheduler-core spec, Req 5 AC 14/18) owns the event loop -- + * not yet implemented. Startup (task 11.1, this file's own work) is + * genuinely complete and correct up to this point: sim->core holds the + * fully restored, ready-to-run state and virtual clock. Fail loudly + * rather than silently returning success having processed zero events + * -- same "honest, temporary placeholder" precedent main.c's own + * pre-task-11.1 --sim stub already established (task 5.2). */ + ERROR("%s simulation startup complete but the event loop is not yet " + "implemented (scheduler-core spec task 11.2)", + __func__); + + return -ENOSYS; +} diff --git a/src/scheduler/testsuite/meson.build b/src/scheduler/testsuite/meson.build index 617fa2ce..8583fd7d 100644 --- a/src/scheduler/testsuite/meson.build +++ b/src/scheduler/testsuite/meson.build @@ -458,6 +458,27 @@ sched_admin_request_test = executable( dependencies: [scheduler_test_deps], install: false) +# Contract test for simulation-mode startup (scheduler-core spec, task 11.1, +# Req 5 AC 13): opening and validating the replay file (first message must +# be ReplayHeader, first data message must be SnapshotEntry), locating the +# most recent SnapshotEntry at or before --sim-start-ts, loading it via the +# real xce_sched_core_load_snapshot(), and setting the virtual clock's +# anchors from the loaded snapshot's timestamp and requested speed. Same +# white-box #include pattern as sched_snapshot_save_load_test (../src/core.c, +# ../src/core_clock.c, ../src/snapshot.c, ../src/snapshot_wire.c, +# ../src/snapshot_load.c) PLUS ../src/recording.c (used only to build real +# `.replay` fixtures via sched_record_start()/sched_record_stop(), not under +# test here) PLUS the new ../src/sim.c under test. No driver.c/plugin.c/ +# core_events.c/core_session.c/decision.c: this task stops at "ready-to-run" +# (task 11.2 owns the event loop), so no real scheduling session is ever +# driven. +sched_sim_startup_test = executable( + 'sched_sim_startup_test', + ['test_sim_startup.c'], + include_directories:[incdir], + dependencies: [scheduler_test_deps], + install: false) + # Register as meson tests so `meson test --suite scheduler` runs them all. # detect_leaks=0 matches the suite convention (fixtures are not torn down). test('scheduler test', scheduler_test, suite: 'scheduler', @@ -527,3 +548,6 @@ test('scheduler record lifecycle test', test('scheduler admin request test', sched_admin_request_test, suite: 'scheduler', env: ['ASAN_OPTIONS=detect_leaks=0']) +test('scheduler sim startup test', + sched_sim_startup_test, suite: 'scheduler', + env: ['ASAN_OPTIONS=detect_leaks=0']) diff --git a/src/scheduler/testsuite/test_sim_startup.c b/src/scheduler/testsuite/test_sim_startup.c new file mode 100644 index 00000000..efa3b206 --- /dev/null +++ b/src/scheduler/testsuite/test_sim_startup.c @@ -0,0 +1,528 @@ +/* + * test_sim_startup.c - contract test for simulation-mode startup + * (scheduler-core spec, task 11.1, Req 5 AC 13). + * + * Observable under test (task 11.1's own text): + * - starting simulation against a valid fixture file reaches a + * ready-to-run state with the virtual clock reporting the snapshot's + * timestamp. + * - starting against a file missing its initial snapshot entry fails with + * a clear error before any event is processed. + * + * Fixture strategy: rather than hand-constructing capnp bytes, this file + * builds REAL, valid `.replay` fixtures via the SAME production code path + * normal-mode recording already uses (sched_record_start()/ + * sched_record_stop(), task 9.2) against a small real xce_scheduler_t + * fixture -- same technique test_record_lifecycle.c already establishes. + * The "missing initial snapshot entry" fixture is then derived from a valid + * one by truncating it to exactly its first message (the ReplayHeader), + * discovered by decoding that one message and reading the resulting file + * offset -- no hand-rolled capnp byte construction needed for that case + * either. The "first message is not a valid ReplayHeader" and "first data + * message is not a SnapshotEntry" fixtures DO hand-construct one wrong- + * shaped message directly (same technique test_e2e_capnp.c/ + * test_record_lifecycle.c already use for building capnp messages by hand), + * since there is no production code path that ever writes a malformed + * stream to reuse. + * + * White-box #include of ../src/core.c, ../src/core_clock.c (task 11.1's own + * code under test, sim.c, needs both -- xce_sched_core_init/fini and + * xce_core_now()) PLUS ../src/snapshot.c, ../src/snapshot_wire.c, + * ../src/snapshot_load.c (task 8.1's real save/load, needed both to BUILD + * the fixture via sched_record_start() and to exercise sim.c's own + * xce_sched_core_load_snapshot() call) PLUS ../src/recording.c (real + * sched_record_start()/sched_record_stop(), used ONLY to build fixtures -- + * this file never asserts anything about recording.c's own behavior, that + * is test_record_lifecycle.c's job) PLUS ../src/sim.c (the code under + * test). No driver.c/plugin.c/core_events.c/core_session.c/decision.c: this + * file never drives a real scheduling session (task 11.1 stops at + * "ready-to-run", not the event loop), so none of those are needed. + * + * Stubs below mirror the exact same link-satisfying set + * test_record_lifecycle.c/test_driver_session_ordering.c already establish + * for this core.c/core_clock.c/snapshot.c/snapshot_wire.c/snapshot_load.c/ + * recording.c combination (kill_sched_*, sched_resolve_job_picker's real + * reimplementation -- schedobj.c is not linked). + */ +#include +#include +#include +#include +#include +#include + +#include + +#include "controller.capnp.h" +#include "kvmgr.h" +#include "scheduler.h" +#include "xstring.h" + +/* Pull in the real functions under test into this TU (white-box #include, + * same convention as test_record_lifecycle.c). */ +#include "../src/core.c" +#include "../src/core_clock.c" +#include "../src/recording.c" +#include "../src/sim.c" +#include "../src/snapshot.c" +#include "../src/snapshot_load.c" +#include "../src/snapshot_wire.c" + +void kill_sched_job(void* arg) { (void)arg; } +void kill_sched_queue(void* arg) { (void)arg; } +void kill_sched_worker(void* arg) { (void)arg; } +void kill_sched_user(void* arg) { (void)arg; } +void kill_sched_project(void* arg) { (void)arg; } +void kill_sched_resval(xce_val_t* val) { (void)val; } + +/* plugin.c is not linked into this TU (no session is ever driven, so no + * real plugin unregistration ever happens) -- same stub every other + * core.c-including test file in this directory establishes for + * xce_sched_core_init()'s kvmgr datakiller. */ +void plugin_unregister(void* arg) { (void)arg; } + +/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same + * rationale/algorithm as test_record_lifecycle.c's own copy (schedobj.c is + * not linked). Both the FIXTURE core (built via a real xce_scheduler_t, + * needs a resolvable picker for sched_record_start()'s own snapshot to be + * well-formed) and sim.c's own core (which never registers a picker, per + * sim.h's documented scope boundary -- this will legitimately return + * -EINVAL/-ENOENT for that core, which xce_sched_core_load_snapshot() + * gracefully WARNs and skips) share this one implementation. */ +int sched_resolve_job_picker(xce_sched_core_t* core, xce_sched_plugin_t** out) +{ + xce_sched_plugin_t* plugin = NULL; + int rc = 0; + + if ((core == NULL) || (out == NULL)) { + return -EINVAL; + } + + *out = NULL; + + if (core->job_pick_plugin == NULL) { + return -EINVAL; + } + + lock(core->plugins, RTTI_LOCK_READ); + rc = key_find(core->plugins, core->job_pick_plugin, (void**)&plugin); + unlock(core->plugins); + + if (rc <= 0) { + return (rc < 0) ? rc : -ENOENT; + } + + if ((plugin == NULL) || (plugin->picker_iface == NULL) + || (plugin->picker_iface->next_job == NULL) + || (plugin->picker_iface->begin_session == NULL) + || (plugin->picker_iface->end_session == NULL) + || (plugin->picker_iface->on_dispatch == NULL) + || (plugin->picker_iface->on_pend == NULL)) { + return -EINVAL; + } + + *out = plugin; + + return 0; +} + +void setUp(void) { return; } +void tearDown(void) { return; } + +/* ---------------------------------------------------------------------- */ +/* Fixture helpers */ +/* ---------------------------------------------------------------------- */ + +static void make_tmp_dir(char* out, size_t out_len, const char* suffix) +{ + char tmpl[PATH_MAX]; + + snprintf(tmpl, sizeof(tmpl), "/tmp/sched_simstart%s_XXXXXX", suffix); + TEST_ASSERT_NOT_NULL(mkdtemp(tmpl)); + TEST_ASSERT_TRUE(strlen(tmpl) < out_len); + strcpy(out, tmpl); +} + +static int stub_picker_begin_session(void* picker) +{ + (void)picker; + return 0; +} + +static xce_sched_job_t* stub_picker_next_job(void* picker) +{ + (void)picker; + return NULL; +} + +static int stub_picker_end_session(void* picker) +{ + (void)picker; + return 0; +} + +static int stub_picker_jobid_noop(void* picker, uint64_t jobid) +{ + (void)picker; + (void)jobid; + return 0; +} + +static int stub_picker_queue_update_noop(void* picker, xce_sched_queue_t* q) +{ + (void)picker; + (void)q; + return 0; +} + +static int stub_picker_job_noop(void* picker, xce_sched_job_t* job) +{ + (void)picker; + (void)job; + return 0; +} + +static int stub_picker_job_state_change_noop(void* picker, xce_sched_job_t* job, + uint32_t state) +{ + (void)picker; + (void)job; + (void)state; + return 0; +} + +static xce_job_picker_iface_t g_stub_picker_iface + = { .init = NULL, + .fini = NULL, + .begin_session = stub_picker_begin_session, + .next_job = stub_picker_next_job, + .end_session = stub_picker_end_session, + .on_dispatch = stub_picker_jobid_noop, + .on_pend = stub_picker_jobid_noop, + .on_queue_update = stub_picker_queue_update_noop, + .on_job_push = stub_picker_job_noop, + .on_job_state_change = stub_picker_job_state_change_noop, + .on_job_update = stub_picker_job_noop, + .on_config = NULL, + .on_job_rusage = NULL }; + +static xce_sched_plugin_t g_stub_picker_plugin + = { .name = "test_sim_startup_picker", + .picker_iface = &g_stub_picker_iface }; + +/* Builds a small, real xce_scheduler_t fixture (same pattern + * test_record_lifecycle.c's fixture_init() establishes) purely to drive + * sched_record_start()/sched_record_stop() and produce a REAL, valid + * `.replay` file this test's OWN code under test (sim.c) then consumes. */ +static void build_fixture_scheduler(xce_scheduler_t* sched, char* record_dir) +{ + int rc; + + memset(sched, 0, sizeof(*sched)); + mylock_init(&(sched->record_lock)); + + rc = xce_sched_core_init(&(sched->core)); + TEST_ASSERT_EQUAL_INT(0, rc); + + lock(sched->core.plugins, RTTI_LOCK_WRITE); + rc = key_add(sched->core.plugins, (void*)g_stub_picker_plugin.name, + (void*)&g_stub_picker_plugin); + TEST_ASSERT_GREATER_OR_EQUAL(0, rc); + unlock(sched->core.plugins); + + sched->core.job_pick_plugin = g_stub_picker_plugin.name; + sched->record_dir = record_dir; +} + +/* Produces a valid `.replay` fixture (ReplayHeader + initial SnapshotEntry) + * at `out_path` via the real sched_record_start()/sched_record_stop(); the + * chosen snapshot timestamp is returned via `out_ts`. */ +static void write_valid_fixture(char* out_path, size_t out_path_len, + uint64_t* out_ts, const char* suffix) +{ + char record_dir[PATH_MAX]; + xce_scheduler_t sched; + int rc; + + make_tmp_dir(record_dir, sizeof(record_dir), suffix); + build_fixture_scheduler(&sched, record_dir); + + rc = sched_record_start(&sched, "fixture.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); + *out_ts = sched.record_start_ts; + + rc = sched_record_stop(&sched); + TEST_ASSERT_EQUAL_INT(0, rc); +} + +/* Truncates `src_path` down to just its first capnp message (the + * ReplayHeader) and writes the result to `dst_path` -- simulates a replay + * file whose stream ends immediately after the header, i.e. "missing + * initial snapshot entry". */ +static void truncate_to_header_only(const char* src_path, const char* dst_path) +{ + FILE* src; + FILE* dst; + struct capn c; + long header_end; + char* buf; + size_t n; + + src = fopen(src_path, "r"); + TEST_ASSERT_NOT_NULL(src); + TEST_ASSERT_EQUAL_INT(0, capn_init_fp(&c, src, 0)); + capn_free(&c); + header_end = ftell(src); + TEST_ASSERT_TRUE(header_end > 0); + rewind(src); + + buf = (char*)malloc((size_t)header_end); + TEST_ASSERT_NOT_NULL(buf); + n = fread(buf, 1, (size_t)header_end, src); + TEST_ASSERT_EQUAL_UINT((size_t)header_end, n); + fclose(src); + + dst = fopen(dst_path, "w"); + TEST_ASSERT_NOT_NULL(dst); + TEST_ASSERT_EQUAL_UINT((size_t)header_end, + fwrite(buf, 1, (size_t)header_end, dst)); + fclose(dst); + free(buf); +} + +/* Hand-builds a single capnp message shaped like `SnapshotEntry` + * (word_count=1, pointer_count=1) but with all-default/empty field + * contents, and writes it standalone to `path` -- used to simulate a + * malformed "first message is not ReplayHeader" fixture (ReplayHeader is + * shaped (1,5); a lone (1,1)-shaped message can never be mistaken for one + * by sim.c's shape check). */ +static void write_lone_snapshotentry_message(const char* path) +{ + struct capn c; + struct capn_segment* cs; + SnapshotData_ptr sdata; + SnapshotEntry_ptr snap; + FILE* f; + int rc; + + capn_init_malloc(&c); + cs = capn_root(&c).seg; + + sdata = new_SnapshotData(cs); + snap = new_SnapshotEntry(cs); + SnapshotEntry_set_ts(snap, 42ULL); + SnapshotEntry_set_data(snap, sdata); + capn_setp(capn_root(&c), 0, snap.p); + + f = fopen(path, "w"); + TEST_ASSERT_NOT_NULL(f); + rc = write_capnp_message(f, &c); + TEST_ASSERT_EQUAL_INT(0, rc); + fclose(f); + capn_free(&c); +} + +/* ---------------------------------------------------------------------- */ +/* Test 1: starting simulation against a valid fixture file reaches a */ +/* ready-to-run state with the virtual clock reporting the snapshot's */ +/* timestamp -- this task's own primary Observable. */ +/* ---------------------------------------------------------------------- */ + +static void +test_valid_fixture_reaches_ready_state_with_frozen_virtual_clock(void) +{ + char path[PATH_MAX]; + uint64_t snapshot_ts = 0; + sched_sim_args_t args; + xce_sched_sim_driver_t sim; + int rc; + + write_valid_fixture(path, sizeof(path), &snapshot_ts, "_valid"); + TEST_ASSERT_TRUE(snapshot_ts > 0); + + memset(&args, 0, sizeof(args)); + args.replay_path = path; + args.sim_start_ts = SCHED_SIM_START_TS_LATEST; + args.sim_speed = 0; /* fast/frozen -- xce_core_now() stays pinned to + * virtual_clock_base regardless of real elapsed + * time, giving this test a deterministic, + * non-flaky assertion. */ + + rc = sched_sim_driver_start(&sim, &args); + TEST_ASSERT_EQUAL_INT(0, rc); + + /* Step 4's Observable: the virtual clock reports the snapshot's own + * timestamp, exactly (frozen -- speed_num=0). */ + TEST_ASSERT_EQUAL_UINT64(snapshot_ts, xce_core_now(&(sim.core))); + + /* Direct inspection of the anchors Step 4 documents setting. */ + TEST_ASSERT_EQUAL_UINT64( + snapshot_ts, atomic_load_explicit(&(sim.core.virtual_clock_base), + memory_order_acquire)); + TEST_ASSERT_EQUAL_UINT64(0ULL, atomic_load_explicit(&(sim.core.speed_num), + memory_order_acquire)); + TEST_ASSERT_EQUAL_UINT64(1ULL, atomic_load_explicit(&(sim.core.speed_den), + memory_order_acquire)); + TEST_ASSERT_EQUAL_UINT64(0ULL, sim.sim_speed_num); + TEST_ASSERT_EQUAL_UINT64(1ULL, sim.sim_speed_den); + + /* "Ready-to-run" -- no session is active, load_snapshot()'s own + * precondition would otherwise have been violated. */ + TEST_ASSERT_EQUAL_INT(0, atomic_load(&(sim.core.session_active))); + + TEST_ASSERT_NOT_NULL(sim.replay_file); + + printf("test_valid_fixture_reaches_ready_state_with_frozen_virtual_clock:" + " PASS\n"); +} + +/* ---------------------------------------------------------------------- */ +/* Test 2: starting against a file missing its initial snapshot entry */ +/* fails with a clear error before any event is processed -- this task's */ +/* own second Observable. */ +/* ---------------------------------------------------------------------- */ + +static void test_missing_initial_snapshot_entry_fails_cleanly(void) +{ + char valid_path[PATH_MAX]; + char header_only_path[PATH_MAX + 32]; + uint64_t snapshot_ts = 0; + sched_sim_args_t args; + xce_sched_sim_driver_t sim; + int rc; + + write_valid_fixture(valid_path, sizeof(valid_path), &snapshot_ts, + "_hdronly_src"); + snprintf(header_only_path, sizeof(header_only_path), + "%s.header_only.replay", valid_path); + truncate_to_header_only(valid_path, header_only_path); + + memset(&args, 0, sizeof(args)); + args.replay_path = header_only_path; + args.sim_start_ts = SCHED_SIM_START_TS_LATEST; + args.sim_speed = 1; + + rc = sched_sim_driver_start(&sim, &args); + TEST_ASSERT_TRUE(rc < 0); + + printf("test_missing_initial_snapshot_entry_fails_cleanly: PASS\n"); +} + +/* ---------------------------------------------------------------------- */ +/* Test 3: a replay file whose FIRST message is not a valid ReplayHeader */ +/* is rejected (Req 5 AC 13 step 1) -- distinct code path from test 2. */ +/* ---------------------------------------------------------------------- */ + +static void test_first_message_not_replayheader_fails_cleanly(void) +{ + char dir[PATH_MAX]; + char path[PATH_MAX + 32]; + sched_sim_args_t args; + xce_sched_sim_driver_t sim; + int rc; + + make_tmp_dir(dir, sizeof(dir), "_badhdr"); + snprintf(path, sizeof(path), "%s/bad_header.replay", dir); + write_lone_snapshotentry_message(path); + + memset(&args, 0, sizeof(args)); + args.replay_path = path; + args.sim_start_ts = SCHED_SIM_START_TS_LATEST; + args.sim_speed = 1; + + rc = sched_sim_driver_start(&sim, &args); + TEST_ASSERT_TRUE(rc < 0); + + printf("test_first_message_not_replayheader_fails_cleanly: PASS\n"); +} + +/* ---------------------------------------------------------------------- */ +/* Test 4: a missing replay file fails with a clear error (Req 5 AC 13 */ +/* step 1, "the file is missing, unreadable"). */ +/* ---------------------------------------------------------------------- */ + +static void test_missing_replay_file_fails_cleanly(void) +{ + sched_sim_args_t args; + xce_sched_sim_driver_t sim; + int rc; + + memset(&args, 0, sizeof(args)); + args.replay_path = "/nonexistent/path/does_not_exist.replay"; + args.sim_start_ts = SCHED_SIM_START_TS_LATEST; + args.sim_speed = 1; + + rc = sched_sim_driver_start(&sim, &args); + TEST_ASSERT_TRUE(rc < 0); + + printf("test_missing_replay_file_fails_cleanly: PASS\n"); +} + +/* ---------------------------------------------------------------------- */ +/* Test 5: NULL argument guards. */ +/* ---------------------------------------------------------------------- */ + +static void test_null_arguments_return_einval(void) +{ + xce_sched_sim_driver_t sim; + sched_sim_args_t args; + + memset(&args, 0, sizeof(args)); + args.replay_path = "/tmp/does-not-matter.replay"; + + TEST_ASSERT_EQUAL_INT(-EINVAL, sched_sim_driver_start(NULL, &args)); + TEST_ASSERT_EQUAL_INT(-EINVAL, sched_sim_driver_start(&sim, NULL)); + + memset(&args, 0, sizeof(args)); + args.replay_path = NULL; + TEST_ASSERT_EQUAL_INT(-EINVAL, sched_sim_driver_start(&sim, &args)); + + printf("test_null_arguments_return_einval: PASS\n"); +} + +/* ---------------------------------------------------------------------- */ +/* Test 6: sched_sim_driver_run() -- design.md's literal entry point -- */ +/* calls start() then fails loudly with -ENOSYS (task 11.2's event loop */ +/* does not exist yet), never silently returning success. */ +/* ---------------------------------------------------------------------- */ + +static void test_run_starts_then_fails_enosys_pending_event_loop(void) +{ + char path[PATH_MAX]; + uint64_t snapshot_ts = 0; + sched_sim_args_t args; + xce_sched_sim_driver_t sim; + int rc; + + write_valid_fixture(path, sizeof(path), &snapshot_ts, "_run"); + + 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(&sim, &args); + TEST_ASSERT_EQUAL_INT(-ENOSYS, rc); + + /* Startup itself still genuinely succeeded -- state is real and + * inspectable, matching sched_sim_driver_start()'s own contract. */ + TEST_ASSERT_EQUAL_UINT64(snapshot_ts, xce_core_now(&(sim.core))); + + printf("test_run_starts_then_fails_enosys_pending_event_loop: PASS\n"); +} + +int main(void) +{ + UNITY_BEGIN(); + + RUN_TEST(test_valid_fixture_reaches_ready_state_with_frozen_virtual_clock); + RUN_TEST(test_missing_initial_snapshot_entry_fails_cleanly); + RUN_TEST(test_first_message_not_replayheader_fails_cleanly); + RUN_TEST(test_missing_replay_file_fails_cleanly); + RUN_TEST(test_null_arguments_return_einval); + RUN_TEST(test_run_starts_then_fails_enosys_pending_event_loop); + + return UNITY_END(); +}