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.
This commit is contained in:
parent
16d756aca3
commit
5d587ecf32
7 changed files with 1366 additions and 21 deletions
228
src/scheduler/include/sim.h
Normal file
228
src/scheduler/include/sim.h
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
#if !defined(_SIM_H_)
|
||||
|
||||
#define _SIM_H_ 1
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#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
|
||||
Loading…
Add table
Add a link
Reference in a new issue