feat(scheduler-core): implement record start/stop lifecycle (task 9.2)

Adds recording-state fields directly to xce_scheduler_t (no new
xce_sched_driver_t type, per task 9.1's precedent). sched_record_start()/
sched_record_stop() implement AC 10: validate via task 9.1's path
validator, open with a TOCTOU-hardened sequence (O_NOFOLLOW, no O_TRUNC,
fstat()+st_nlink==1 check before ftruncate()), snapshot the core, write
ReplayHeader+SnapshotEntry on start, ReplayEvent{endSession} on stop.

driver.c gains boolean-gated recording hooks in
xce_sched_driver_translate_pending_events() (per-event-batch ReplayEvent
writes, new recording_events.c) and xce_sched_driver_run_normal_mode_round()
(per-session ReplaySession writes, new recording_session.c) -- zero I/O/
allocation when inactive.

Two real bugs found and fixed during implementation: bug-1498 (O_TRUNC
would have truncated a hard-linked victim file before the TOCTOU check
could reject it) and bug-1499 (a transient edit dropped
xce_core_signal_go(), causing a 3-way thread deadlock).

test_record_lifecycle.c decodes the full start->events->session->stop
cycle with the real capnp decoders and asserts the exact message
sequence; TOCTOU regression tests use real symlink(2)/link(2) syscalls.

25/25 scheduler suite, clang-format clean.
This commit is contained in:
Rongsong Shen 2026-08-05 09:07:51 +00:00
parent 4e5547ee55
commit ff8d4f416e
Signed by: rshen
SSH key fingerprint: SHA256:8Vf2pZ3h6rJa7EhGK+WaAQ9Mta/apZvteW6g9U/WKKk
15 changed files with 2110 additions and 36 deletions

View file

@ -5,7 +5,9 @@
#include <pthread.h>
#include <rtti.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <time.h>
#include "client.h"
@ -117,6 +119,37 @@ typedef struct {
* xce_sched_driver_t (record_file_path/record_file/recording/
* record_event_count/...) is task 9.2's job, not this field's. */
char* record_dir;
/* Req 5 AC 10 (scheduler-core spec, task 9.2): the recording lifecycle
* state design.md sketches on a future xce_sched_driver_t -- per
* record_dir's own comment above, this codebase keeps a single combined
* driver-level struct (xce_scheduler_t), so these fields land here
* instead of a new type. Owned exclusively by recording.c's
* sched_record_start()/sched_record_stop() (recording.h) -- see those
* functions' doc comments for the full lifecycle contract (path
* validation + TOCTOU-hardened open on start, end-of-recording marker +
* flush/close on stop). driver.c's normal-mode loop (driver.h) reads
* `recording` to boolean-gate its per-event-batch/per-session recording
* writes -- see xce_sched_driver_translate_pending_events()'s and
* xce_sched_driver_run_normal_mode_round()'s own comments.
*
* Invariant: recording == true iff record_file != NULL. */
bool recording;
/* strdup'd copy of the resolved record file path; owned by the record
* file lifecycle (strdup on start, free+NULL on stop or start/write
* failure) -- lets a future status query (task 9.4, out of this task's
* scope) answer with the current path even after record_file closes. */
char* record_file_path;
/* replay file handle; NULL when not recording. */
FILE* record_file;
/* count of ReplayEvent messages written since recording started (used
* by task 9.3's periodic-checkpoint trigger; incremented by
* recording.c's event-batch writer, task 9.2). */
uint64_t record_event_count;
/* N events between periodic snapshot checkpoints (0 = disabled).
* Reserved for task 9.3 -- inert (never read) as of task 9.2. */
uint64_t record_snapshot_interval;
/* logical time (xce_core_now()) recording started; 0 = not recording. */
uint64_t record_start_ts;
} xce_scheduler_t;
extern int sched_init(xce_scheduler_t*, xce_client_config_t*,