#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) * * `snapshot_ts` below is ONE field beyond design.md's literal 8-field * struct sketch (scheduler-core spec, task 11.5, Req 5 AC 19) -- added, not * merely implied, because writing a `--sim-output` file's initial * SnapshotEntry needs the EXACT timestamp Step 2's snapshot scan * (sim_locate_and_load_snapshot(), sim.c) chose, and re-deriving it via * xce_core_now(&sim->core) from inside sim_loop.c's event loop is NOT * equivalent for any --sim-speed other than frozen (0): the virtual clock * keeps advancing with real wall-clock time between * sched_sim_driver_start() returning and sim_loop.c's first opportunity to * write the output header/snapshot, so a re-derived value would drift from * "the same SnapshotEntry that was loaded" (AC 19's own wording -- same ts, * not merely a same-shaped one taken moments later). * * `repl_state` below is a SECOND field beyond design.md's literal sketch * (scheduler-core spec, task 12.1, Req 5 AC 22) -- the interactive REPL's * own execution-control state (current step-N/continue mode, remaining step * budget; task 12.2/12.3 grow the same struct with breakpoints/param * overrides). Declared here only as an opaque forward-declared pointer * (xce_sched_repl_state_t, just below) -- the full struct body lives in * repl.c, never in this header, matching plugin.h's own * `xce_sched_core_t` forward-declare precedent for the identical "opaque * pointer, defined in the consuming file" reason. NULL until * xce_repl_start() lazily allocates it (repl.c) -- guaranteed NULL after a * successful sched_sim_driver_start() the same way every other field here * is, via that function's own `memset(sim, 0, sizeof(*sim))` (sim.c); no * sim.c change was needed for this task. */ typedef struct xce_sched_repl_state_s xce_sched_repl_state_t; 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; uint64_t snapshot_ts; xce_sched_repl_state_t* repl_state; } 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). Defined in * sim_loop.c (scheduler-core spec, task 11.2), not sim.c: calls * sched_sim_driver_start() above (task 11.1) then runs Req 5 AC 13 step * 5's event loop (Req 5 AC 14/18) -- decode each `.replay` message in file * order, jump the virtual clock to each ReplayEvent's own timestamp as it * is queued (AC 14), and on each ReplaySession boundary run one complete * push-events -> start-session -> drain-decisions -> end-session round. * * Returns 0 once the replay stream is exhausted (its ReplayEvent{endSession} * marker is reached, Req 5 AC 20), or a negative errno: whatever * sched_sim_driver_start() returned on a startup failure, or a pthread * error standing up the picker-driving thread the event loop needs (see * sim_loop.c's own file header comment for why one exists). */ extern int sched_sim_driver_run(xce_sched_sim_driver_t* sim, const sched_sim_args_t* args); #endif