myxce/src/scheduler/src/recording_events.c
Rongsong Shen 7494542a60
feat(scheduler-core): implement periodic snapshot checkpointing (task 9.3)
Periodic checkpointing hooks into sched_record_write_events(): once per
call, when record_snapshot_interval > 0 and record_event_count crosses a
multiple of it, writes a fresh SnapshotEntry via the same
write_snapshot_entry() helper sched_record_start() already uses (made
non-static, zero duplication). Safe from the session_active precondition
because this call site is only ever reached from
xce_sched_driver_translate_pending_events(), which always runs strictly
between sessions.

Confirmed the write-failure error-handling half (AC 10) was already
complete from task 9.2's sched_record_abort_on_error() -- added the
missing end-to-end test using a genuine closed-fd write failure rather
than a mocked return code, proving the driver keeps processing sessions
afterward.

25/25 scheduler suite, clang-format clean. Two non-blocking follow-ups
logged in tasks.md: --record-snapshot-interval has no CLI wiring yet
(AC 11's own struct-field mechanism is fully tested, but nothing sets it
in production), and test_record_lifecycle.c has grown past this suite's
prior size precedent with a natural future split point identified.
2026-08-05 10:02:16 +00:00

225 lines
8.9 KiB
C

#include <errno.h>
#include <stdio.h>
#include <string.h>
#include "controller.capnp.h"
#include "core.h"
#include "recording.h"
#include "utils.h"
/*
* recording_events.c - scheduler-core spec, tasks 9.2/9.3 (Req 5 AC 10, "On
* each core_push_events() call ... For each event in the batch, serialize
* and write the corresponding ReplayEvent message"; Req 5 AC 11, "the
* driver shall periodically append fresh snapshots to the replay file --
* default: every N events"). Split out of recording.c (this project's
* 800-line-per-file convention, CLAUDE.md/coding-style.md) -- see
* recording.c's own file header comment, "Split across three files", for
* the full rationale. This file owns the per-event-batch recording-write
* hook driver.c's xce_sched_driver_translate_pending_events() calls into,
* plus (task 9.3) the periodic-checkpoint trigger layered onto that same
* hook.
*
* write_capnp_message()/sched_record_abort_on_error()/write_snapshot_entry()
* are defined (non-`static`) in recording.c and redeclared here via a
* plain `extern`, matching this codebase's existing cross-.c-file idiom for
* a narrow shared dependency (snapshot_wire.c's own documented precedent,
* itself citing core_session.c's `extern void
* sched_worker_release_host_ctx(...)`).
*/
extern int write_capnp_message(FILE* f, struct capn* c);
extern void sched_record_abort_on_error(xce_scheduler_t* sched, int rc);
extern int write_snapshot_entry(FILE* f, xce_sched_core_t* core, uint64_t ts);
/*
* write_one_replay_event - builds and writes one ReplayEvent message from
* a single translated `xce_core_event_t` (the SAME op-type table
* driver.c's translate_one_pending_update() already established -- see
* this function's per-case mapping below). Every wrapper local is declared
* unconditionally (cheap stack space) so its address stays valid across
* the single encode+write call after the switch, regardless of which case
* populated it.
*/
static int write_one_replay_event(FILE* f, xce_core_event_t* ev,
uint64_t* event_count)
{
struct capn c;
struct capn_segment* cs;
replay_event_t wire;
ReplayEvent_ptr ptr;
int rc;
xce_jobs_data_t jobs_wrap;
xce_workers_data_t workers_wrap;
xce_resource_data_t resources_wrap;
xce_resval_list_t resvals_wrap;
job_state_change_event_t jsc_wrap;
job_update_event_t ju_wrap;
memset(&wire, 0, sizeof(wire));
memset(&jobs_wrap, 0, sizeof(jobs_wrap));
memset(&workers_wrap, 0, sizeof(workers_wrap));
memset(&resources_wrap, 0, sizeof(resources_wrap));
memset(&resvals_wrap, 0, sizeof(resvals_wrap));
memset(&jsc_wrap, 0, sizeof(jsc_wrap));
memset(&ju_wrap, 0, sizeof(ju_wrap));
wire.ts = ev->ts;
switch (ev->type) {
case XCE_CORE_EVENT_CONFIG_PUSH:
wire.body_which = ReplayEvent_body_configPush;
wire.body.config_push = ev->u.config_push.config;
break;
case XCE_CORE_EVENT_JOBS_PUSH:
jobs_wrap.n_total = ev->u.jobs_push.njobs;
jobs_wrap.count = ev->u.jobs_push.njobs;
jobs_wrap.jobs = ev->u.jobs_push.jobs;
wire.body_which = ReplayEvent_body_jobsPush;
wire.body.jobs_push = &jobs_wrap;
break;
case XCE_CORE_EVENT_WORKERS_PUSH:
workers_wrap.n_total = ev->u.workers_push.nworkers;
workers_wrap.count = ev->u.workers_push.nworkers;
workers_wrap.workers = ev->u.workers_push.workers;
wire.body_which = ReplayEvent_body_workersPush;
wire.body.workers_push = &workers_wrap;
break;
case XCE_CORE_EVENT_RESOURCES_PUSH:
resources_wrap.n_total = ev->u.resources_push.n;
resources_wrap.count = ev->u.resources_push.n;
resources_wrap.resources = ev->u.resources_push.resources;
wire.body_which = ReplayEvent_body_resourcesPush;
wire.body.resources_push = &resources_wrap;
break;
case XCE_CORE_EVENT_RESVALS_FULL:
resvals_wrap.count = ev->u.resvals_full.n;
resvals_wrap.vals = ev->u.resvals_full.resvals;
wire.body_which = ReplayEvent_body_resvalsFull;
wire.body.resvals_full = &resvals_wrap;
break;
case XCE_CORE_EVENT_RESVALS_DELTA:
wire.body_which = ReplayEvent_body_resvalsDelta;
wire.body.resvals_delta = ev->u.resvals_delta.deltas;
break;
case XCE_CORE_EVENT_JOB_STATE_CHANGE:
jsc_wrap.jobid = ev->u.job_state_change.jobid;
jsc_wrap.old_state = ev->u.job_state_change.old_state;
jsc_wrap.new_state = ev->u.job_state_change.new_state;
jsc_wrap.exit_code = ev->u.job_state_change.exit_code;
wire.body_which = ReplayEvent_body_jobStateChange;
wire.body.job_state_change = &jsc_wrap;
break;
case XCE_CORE_EVENT_JOB_UPDATE:
ju_wrap.jobid = ev->u.job_update.jobid;
ju_wrap.changed_fields = ev->u.job_update.changed_fields;
ju_wrap.job = ev->u.job_update.job;
wire.body_which = ReplayEvent_body_jobUpdate;
wire.body.job_update = &ju_wrap;
break;
case XCE_CORE_EVENT_QUEUE_UPDATE:
wire.body_which = ReplayEvent_body_queueUpdate;
wire.body.queue_update = ev->u.queue_update.queue;
break;
case XCE_CORE_EVENT_JOB_RUSAGE:
wire.body_which = ReplayEvent_body_jobRusage;
wire.body.job_rusage = ev->u.job_rusage.rusages;
break;
default:
WARN("%s unrecognized core event type %d, skipping recording write",
__func__, (int)ev->type);
return 0;
}
capn_init_malloc(&c);
cs = capn_root(&c).seg;
encode_ReplayEvent_ptr(cs, &ptr, &wire);
capn_setp(capn_root(&c), 0, ptr.p);
rc = write_capnp_message(f, &c);
capn_free(&c);
if (rc == 0) {
(*event_count)++;
}
return rc;
}
/*
* sched_record_write_events - see recording.h's declaration comment for the
* full contract. This function's tail implements Req 5 AC 11's periodic
* checkpoint: task 9.3.
*
* Boundary semantics ("every N events", documented choice): after writing
* this call's whole batch, the check is `record_snapshot_interval > 0 &&
* record_event_count > 0 && record_event_count % record_snapshot_interval
* == 0` against the POST-batch, cumulative `record_event_count` -- i.e.
* "has the running total, since recording started, just landed exactly on
* a multiple of N" -- evaluated ONCE PER CALL, not once per boundary
* crossed within the call. Matches requirements.md's own annotated
* pseudocode for this step verbatim (a single `if`, not a loop over
* crossed multiples). Consequence, deliberate and documented: if a single
* batch is large enough to cross more than one multiple of N (e.g. N=3 and
* one call's batch pushes record_event_count from 0 straight to 6), only
* ONE checkpoint is written for that call, not one per crossed boundary --
* periodic checkpointing is a diagnostic/replay-optimization aid, not a
* strict per-event guarantee, so this is a proportionate simplification
* (same discipline this file's sibling driver.c documents for its own
* batch-level simplifications), not a correctness gap: the checkpoint
* cadence measured in wall-clock/event-arrival terms is unaffected for the
* common case of one-or-few-events-per-call this driver's normal-mode loop
* actually produces, and simulation replay never depends on periodic
* checkpoints existing at every possible offset -- Req 5 AC 13's "most
* recent SnapshotEntry with ts <= start_ts" scan tolerates any subset.
*
* On a periodic-checkpoint write failure, recording is disabled via
* sched_record_abort_on_error() -- the exact same shared path every other
* recording write failure in this codebase uses, never special-cased
* (Req 5 AC 10's "Error handling" clause applies uniformly).
*/
int sched_record_write_events(xce_scheduler_t* sched, xce_core_event_t* events,
int n)
{
int i;
if ((sched == NULL) || (events == NULL) || (n <= 0)) {
return 0;
}
if (!sched->recording || (sched->record_file == NULL)) {
return 0;
}
for (i = 0; i < n; i++) {
int rc = write_one_replay_event(sched->record_file, &events[i],
&(sched->record_event_count));
if (rc < 0) {
sched_record_abort_on_error(sched, rc);
return rc;
}
}
if ((sched->record_snapshot_interval > 0) && (sched->record_event_count > 0)
&& ((sched->record_event_count % sched->record_snapshot_interval)
== 0)) {
int rc = write_snapshot_entry(sched->record_file, &(sched->core),
xce_core_now(&(sched->core)));
if (rc < 0) {
sched_record_abort_on_error(sched, rc);
return rc;
}
}
return 0;
}