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.
225 lines
8.9 KiB
C
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;
|
|
}
|