feat(scheduler-core): implement simulation event loop and virtual clock advancement (task 11.2)

Add sim_loop.c implementing sched_sim_driver_run(): decode .replay
messages in file order, jump the virtual clock to each ReplayEvent's
exact timestamp as it's queued, and run a real push-events/go/drain/
end-session cycle on each ReplaySession boundary (AC 14, AC 18).

Task 11.1 left no picker registered against the isolated core and no
thread backing its cbufs, which deadlocks the decision drain the moment
a real event loop exists. Add plugin_register_core() (registers a
picker against a bare xce_sched_core_t*, no xce_scheduler_t* required)
and extract builtin_fcfs_new_plugin() from register_builtin_fcfs() as a
behavior-preserving factory, plus a picker-driving thread for the sim
driver. Since real resource allocation is unreachable without a live
xce_scheduler_t* (scheduler-allocation spec's job), the sim picker
thread conservatively PENDs every candidate rather than dispatching.

28/28 scheduler suite, 32/32 whole-project tests green.
This commit is contained in:
Rongsong Shen 2026-08-06 10:27:40 +00:00
parent 5d587ecf32
commit 7e1e861e4f
Signed by: rshen
SSH key fingerprint: SHA256:8Vf2pZ3h6rJa7EhGK+WaAQ9Mta/apZvteW6g9U/WKKk
11 changed files with 1248 additions and 76 deletions

View file

@ -315,12 +315,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
- [x] 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
- Treat each session-boundary message as the trigger for one complete scheduling round: push accumulated events, start the session, drain decisions, end the session
- Observable: replaying a fixture file with events at non-uniform timestamps processes them in file order with the virtual clock reporting each event's exact timestamp at the moment it's pushed
- _Requirements: 5.14, 5.18_
- _Depends: 11.1_
- _Verified: 2026-08-06 — new `sim_loop.c` (562 lines) implements `sched_sim_driver_run()` (replacing task 11.1's `-ENOSYS` placeholder): decodes `.replay` messages in file order, jumps the virtual clock to each `ReplayEvent`'s exact timestamp as it's queued, and on each `ReplaySession` boundary runs a real `core_push_events→core_go→drain decisions→core_end_session` cycle before resetting the batch; mid-stream `SnapshotEntry` messages correctly skipped per AC 20. Discovered task 11.1 left no picker registered and no thread backing the core's cbufs, which would deadlock the decision-drain loop unconditionally the moment a real event loop existed — necessarily in-scope, not a convenience shortcut (independently confirmed by review). Fixed via new `plugin_register_core()` (`plugin.c`/`plugin.h`, registers a picker against a bare `xce_sched_core_t*`, no `xce_scheduler_t*` needed, precedent-matched to task 4.6's `test_core_full_cycle.c`) + `builtin_fcfs_new_plugin()` factory extracted from the existing `register_builtin_fcfs()` (`plugins/builtin_fcfs.c`) + a persistent picker-driving thread (`sim_picker_thread_fn`). Review independently verified the extraction is genuinely additive and behavior-preserving on the LIVE normal-mode path (32/32 whole-project tests including controller/libxce, not just the scheduler suite) — the only finding was a non-blocking doc-accuracy nit (`plugin_register_core()`'s doc comment overclaims parity with the legacy `job_elect_fn` adapter step, which it correctly omits since its only caller never uses that path). Since real resource-matching allocation is unreachable without a genuine `xce_scheduler_t*` (a pre-existing gap belonging to the not-yet-designed scheduler-allocation spec), `sim_picker_thread_fn()` conservatively PENDs every candidate rather than fabricating a DISPATCH — judged by review as honestly-scoped and sufficient for this task's narrow ordering/clock-timing Observable (AC 18's pseudocode drains "decisions" generically, not specifically dispatches), but flagged as a real gap once `--sim`'s decisions need to be meaningful (task 11.4/scheduler-allocation's problem, not this task's). RED phase independently re-reproduced by review (reverted to `-ENOSYS`, confirmed real test failure, restored). 28/28 scheduler suite, 32/32 whole-project, clang-format clean. Non-blocking follow-up noted: the PEND-candidate loop itself has no direct test yet (both new tests exercise the "no resolvable picker" branch only) — worth covering when task 11.4 needs that path exercised for real._
- [ ] 11.3 Enforce simulation isolation
- The simulation driver's state holds no live-controller client context, no cluster configuration, and no capnp socket of any kind — confirmed by the struct definition itself, not runtime checks alone