myxce/src/scheduler/include/sim.h
Rongsong Shen 02f7f48ff9
feat(scheduler-core): implement REPL execution control (task 12.1)
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
2026-08-07 08:17:35 +00:00

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