feat(scheduler-core): implement snapshot save and load (task 8.1)

Adds xce_sched_core_save_snapshot()/xce_sched_core_load_snapshot(),
split across snapshot.c/snapshot_wire.c/snapshot_load.c (800-line cap)
along real save/load/shared-helper boundaries. Serializes the full
core state (config, jobs, queues, workers, resources, resvals, users,
projects, prng_seed, internal_state) into/out of sched_snapshot_data_t
(task 7.3's committed C mirror of the capnp SnapshotData struct) as
plain in-memory structures -- no wire encoding here, that's a later
task. After load, rebuilds the picker's private state by re-invoking
its state-change hooks (queue updates, then job pushes) against the
restored core state, matching xce_core_push_events()'s own hook-firing
order; picker-private state itself is never stored in the snapshot.

Adds a new prng_seed field to xce_sched_core_t -- requirements.md/
design.md both presuppose it but no prior task added it. Seeded to 0,
no PRNG consumer wired yet (deliberately out of scope).

Cross-core behavioral-equivalence proven directly: two independent
cores, each with its own registered picker, save on one and load into
a freshly-initialized other, both driven through an identical real
session, decision sequences asserted identical in order.

Found and fixed two real bugs along the way: a missing prng_seed
restore in this task's own new code, and a genuine guard bug in
already-committed core.c (sched_get_user()/sched_get_project()
treating key_find()'s expected -ENOENT as fatal, silently breaking
get-or-create for any new name) that also affects the live production
path (make_sched_job(), JOBS_PUSH) -- a narrowly-scoped two-line fix,
independently traced to the primitive level and confirmed correct by
review.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019dw5WnN3DXStaEaPYfACU1
This commit is contained in:
Rongsong Shen 2026-08-05 03:57:10 +00:00
parent 7704710985
commit 1a32e63a86
Signed by: rshen
SSH key fingerprint: SHA256:8Vf2pZ3h6rJa7EhGK+WaAQ9Mta/apZvteW6g9U/WKKk
10 changed files with 2383 additions and 4 deletions

View file

@ -370,6 +370,29 @@ sched_driver_session_ordering_test = executable(
dependencies: [scheduler_test_deps],
install: false)
# Cross-core behavioral-equivalence proof for
# xce_sched_core_save_snapshot()/xce_sched_core_load_snapshot()
# (scheduler-core spec, task 8.1, Req 5 AC 21 / Req 6 AC 5): save immediately
# followed by load into a freshly-initialized, completely separate core
# produces a core whose observable scheduling behavior (next session's
# decisions, given the same events) is identical to the pre-save core, plus
# direct unit-level proof of both functions' -EBUSY (mid-session)/-EINVAL
# (NULL args) guards. Same white-box #include pattern as
# test_core_full_cycle.c (../src/core.c, ../src/core_clock.c,
# ../src/core_events.c, ../src/core_session.c, ../src/decision.c) PLUS the
# three files task 8.1's own 800-line-cap split produced:
# ../src/snapshot.c, ../src/snapshot_wire.c, ../src/snapshot_load.c. See
# that test's own header comment for the two-independent-picker-instance
# design (xce_sched_plugin_t->data-scoped state) that lets this test drive
# two fully independent xce_sched_core_t instances through the identical
# session protocol without cross-contaminating a shared global.
sched_snapshot_save_load_test = executable(
'sched_snapshot_save_load_test',
['test_snapshot_save_load.c'],
include_directories:[incdir],
dependencies: [scheduler_test_deps],
install: false)
# Register as meson tests so `meson test --suite scheduler` runs them all.
# detect_leaks=0 matches the suite convention (fixtures are not torn down).
test('scheduler test', scheduler_test, suite: 'scheduler',
@ -427,3 +450,6 @@ test('scheduler driver decision cb test',
test('scheduler driver session ordering test',
sched_driver_session_ordering_test, suite: 'scheduler',
env: ['ASAN_OPTIONS=detect_leaks=0'])
test('scheduler snapshot save load test',
sched_snapshot_save_load_test, suite: 'scheduler',
env: ['ASAN_OPTIONS=detect_leaks=0'])