feat(scheduler-core): add scheduler admin opcode schema (task 7.4)

Adds SchedAdminAction (recordStart/recordStop/recordStatus),
SchedAdminRequest (action/recordFile), SchedAdminReply (ok/error/
recording/file/eventCount/startedAt) to controller.capnp, matching
requirements.md Req 5 AC 10 verbatim. Adds schedAdmin@42 to OPCode,
schedadminmsg@31 to Request.body, schedadminreplymsg@19 to
RequestReply.data -- the exact next-available ordinals, verified live
before implementation and re-verified byte-identical against every
existing value/member.

Deliberately schema-only, per this task's own scope -- the controller
dispatch logic (cluster.c's case OPCode_schedAdmin:) is task 10.1's
job. cluster.c, src/frontend/, src/xceadmin/, src/scheduler/ all
untouched.

Round-trip tests cover all 3 SchedAdminAction values and both
ok=true/ok=false SchedAdminReply cases, including an asymmetric
ok=false/recording=true combination that rules out a Bool-field
bit-packing mixup at the wire level.

Closes out Group 7 (Foundation: Replay Wire Format) entirely.

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-04 14:00:16 +00:00
parent bbb3972ae6
commit 5cd8687b32
Signed by: rshen
SSH key fingerprint: SHA256:8Vf2pZ3h6rJa7EhGK+WaAQ9Mta/apZvteW6g9U/WKKk
4 changed files with 217 additions and 1 deletions

View file

@ -1344,6 +1344,157 @@ static void test_snapshot_data_round_trip(void)
printf("test_snapshot_data_round_trip: PASS\n");
}
/* ------------------------------------------------------------------ */
/* Test 16: SchedAdminRequest round-trip (scheduler-core task 7.4, */
/* Req 5 AC 10) -- encodes and decodes a SchedAdminRequest for each of */
/* the three SchedAdminAction enum values (recordStart/recordStop/ */
/* recordStatus), proving the plain enum-typed `action` field (mapped */
/* to `int` in sched_admin_req_t, same convention as HostRole/ */
/* JoinData.role) and the `recordFile` Text field round-trip field-for- */
/* field. recordStart uses a non-empty recordFile; recordStop/ */
/* recordStatus use an empty one (auto-generate / pure query), covering */
/* both the populated and empty-string Text cases. Same encode_X_ptr/ */
/* decode_X codec-bridge pattern as Test 9 (job_state_change_event_t) */
/* since sched_admin_req_t is a fully-scalar struct plus one Text */
/* field, no Data fields. */
/* ------------------------------------------------------------------ */
static void sched_admin_request_round_trip_case(int action, const char *record_file)
{
sched_admin_req_t src;
memset(&src, 0, sizeof(src));
src.action = action;
src.recordFile = (char *)record_file;
struct capn c;
capn_init_malloc(&c);
struct capn_segment *cs = capn_root(&c).seg;
SchedAdminRequest_ptr p;
encode_SchedAdminRequest_ptr(cs, &p, &src);
capn_setp(capn_root(&c), 0, p.p);
int64_t sz = capn_size(&c);
TEST_ASSERT_TRUE(sz > 0);
uint8_t *buf = (uint8_t *)malloc((size_t)sz);
TEST_ASSERT_NOT_NULL(buf);
capn_write_mem(&c, buf, (size_t)sz, 0);
capn_free(&c);
struct capn d;
TEST_ASSERT_EQUAL_INT(0, capn_init_mem(&d, buf, (size_t)sz, 0));
SchedAdminRequest_ptr dp;
dp.p = capn_getp(capn_root(&d), 0, 1);
struct SchedAdminRequest wire;
read_SchedAdminRequest(&wire, dp);
sched_admin_req_t dst;
memset(&dst, 0, sizeof(dst));
decode_SchedAdminRequest(&dst, &wire);
TEST_ASSERT_EQUAL_INT(action, (int)dst.action);
TEST_ASSERT_EQUAL_STRING(record_file, dst.recordFile);
free_SchedAdminRequest(&dst);
capn_free(&d);
free(buf);
}
static void test_sched_admin_request_round_trip(void)
{
/* recordStart with a non-empty recordFile. */
sched_admin_request_round_trip_case(SchedAdminAction_recordStart,
"manual-session.replay");
/* recordStop with an empty recordFile (stop takes no filename). */
sched_admin_request_round_trip_case(SchedAdminAction_recordStop, "");
/* recordStatus with an empty recordFile (status is a pure query). */
sched_admin_request_round_trip_case(SchedAdminAction_recordStatus, "");
printf("test_sched_admin_request_round_trip: PASS\n");
}
/* ------------------------------------------------------------------ */
/* Test 17: SchedAdminReply round-trip (scheduler-core task 7.4, */
/* Req 5 AC 10) -- encodes and decodes a SchedAdminReply for both an */
/* ok=true success case and an ok=false error case, each populating all */
/* six fields (ok, error, recording, file, eventCount, startedAt) with */
/* non-default values, proving every field -- including the two Bool */
/* fields (ok, recording) packed into the same wire byte -- round-trips */
/* independently regardless of the other's value. `error` is modelled */
/* as a general diagnostic-message field (populated with a non-fatal */
/* advisory even on success, an outright failure reason on error) so */
/* both cases can legitimately carry a non-empty value. Same codec- */
/* bridge pattern as Test 9/16. */
/* ------------------------------------------------------------------ */
static void sched_admin_reply_round_trip_case(int ok, const char *error,
int recording, const char *file,
uint64_t event_count,
uint64_t started_at)
{
sched_admin_reply_t src;
memset(&src, 0, sizeof(src));
src.ok = ok;
src.error = (char *)error;
src.recording = recording;
src.file = (char *)file;
src.eventCount = event_count;
src.startedAt = started_at;
struct capn c;
capn_init_malloc(&c);
struct capn_segment *cs = capn_root(&c).seg;
SchedAdminReply_ptr p;
encode_SchedAdminReply_ptr(cs, &p, &src);
capn_setp(capn_root(&c), 0, p.p);
int64_t sz = capn_size(&c);
TEST_ASSERT_TRUE(sz > 0);
uint8_t *buf = (uint8_t *)malloc((size_t)sz);
TEST_ASSERT_NOT_NULL(buf);
capn_write_mem(&c, buf, (size_t)sz, 0);
capn_free(&c);
struct capn d;
TEST_ASSERT_EQUAL_INT(0, capn_init_mem(&d, buf, (size_t)sz, 0));
SchedAdminReply_ptr dp;
dp.p = capn_getp(capn_root(&d), 0, 1);
struct SchedAdminReply wire;
read_SchedAdminReply(&wire, dp);
sched_admin_reply_t dst;
memset(&dst, 0, sizeof(dst));
decode_SchedAdminReply(&dst, &wire);
TEST_ASSERT_EQUAL_INT(ok ? 1 : 0, dst.ok ? 1 : 0);
TEST_ASSERT_EQUAL_STRING(error, dst.error);
TEST_ASSERT_EQUAL_INT(recording ? 1 : 0, dst.recording ? 1 : 0);
TEST_ASSERT_EQUAL_STRING(file, dst.file);
TEST_ASSERT_EQUAL_UINT64(event_count, dst.eventCount);
TEST_ASSERT_EQUAL_UINT64(started_at, dst.startedAt);
free_SchedAdminReply(&dst);
capn_free(&d);
free(buf);
}
static void test_sched_admin_reply_round_trip(void)
{
/* ok=true success case (e.g. a recordStatus reply): all six fields
* non-default. */
sched_admin_reply_round_trip_case(1, "warning: recording directory nearing disk quota",
1, "session-0007.replay",
12345ULL, 1700000000000000000ULL);
/* ok=false error case (e.g. a failed recordStart): all six fields
* still non-default, proving the wire carries every field regardless
* of `ok`. */
sched_admin_reply_round_trip_case(0, "recording directory not writable",
1, "session-0006.replay",
999ULL, 1699999999000000000ULL);
printf("test_sched_admin_reply_round_trip: PASS\n");
}
/* ------------------------------------------------------------------ */
/* Suite entry point */
/* ------------------------------------------------------------------ */
@ -1368,4 +1519,6 @@ void test_e2e_capnp(void)
RUN_TEST(test_preempt_decision_round_trip);
RUN_TEST(test_replay_decision_dispatch_union_round_trip);
RUN_TEST(test_snapshot_data_round_trip);
RUN_TEST(test_sched_admin_request_round_trip);
RUN_TEST(test_sched_admin_reply_round_trip);
}