Adds the interactive simulation REPL's execution-control slice (Req 5 AC 22): a single-threaded input loop (readline-based) supporting step [N], continue, quit, and help, pausing before the first replay event and blocking the driver loop while active. Review round 1 raised two findings, both resolved here: - GNU readline (GPL) was linked directly into xce_scheduler, a shipped production daemon. Switched the whole project to libedit (BSD) for line editing -- scheduler and controller (xce_test) both now use editline/readline.h, API-compatible (readline()/add_history()). - Getting a clean regression run after the libedit swap required a full clean rebuild, which exposed a genuine pre-existing SIGSEGV (bug-1504): SnapshotData.internalState was a capnp Data field, and this codebase's capnpc-c backend silently no-ops Data-field encoding without zero-initializing the caller's stack struct, leaving an uninitialized capn_ptr written onto the wire. Fixed by switching internalState to List(UInt8) (with a new internalStateLen wire field), matching the working FetchFileReply.content/bytesRead precedent -- no hand-written accessor code needed. A second, unrelated instance of the same bug class was found in the controller's WAL-catchup path (WalCatchupEntry.payload) and deliberately left unfixed as bug-1505, out of scheduler-core's boundary; needs its own project-wide Data-field audit. Scheduler suite: 34/34 clean. RED-phase reproduction recorded in tasks.md (repl_should_pause() step-budget check disabled -> 2/4 tests fail as expected -> reverted -> 34/34 green). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPPXeKSUXUqcqvzNXY1rzJ
256 lines
14 KiB
C
256 lines
14 KiB
C
#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)
|
|
*
|
|
* `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
|