feat(config-upgrade-snapshot): define HostAdminState/QueueAdminState capnp structs, bump snapshot format version

Adds independently-extensible Cap'n Proto frames for the upgrade
snapshot's host/queue admin-state sections (frame-only, not reusing
Worker/QueueConf which bundle dynamic state) and bumps
SNAPSHOT_FORMAT_VERSION 1->2 so a checkpoint written under the prior
raw queue-frame layout is fail-loud rejected rather than
misinterpreted. Task 1 of 19.

Requirements: 1.9, 5.7
This commit is contained in:
Rongsong Shen 2026-07-24 08:26:49 +00:00
parent e0c5e8b7a4
commit 9e68335aee
6 changed files with 322 additions and 8 deletions

View file

@ -3282,7 +3282,7 @@ static void test_snapshot_recv_two_chunks_assembles_and_renames(void)
* header) but is now correctly rejected as an unrecognized
* format_version. Layout (design.md "Checkpoint File Layout"): the
* 57-byte legacy header (cfgref_present=0 at byte 16, so bytes 17-56 are
* unread and left zero) + format_version=1 + job_count=0 + queue_count=0
* unread and left zero) + format_version=2 + job_count=0 + queue_count=0
* (69 bytes total), padded with zero filler out to 128 bytes to keep the
* two-64-byte-chunk shape this test exercises — the padding is never
* read (load_snapshot_entities stops right after the queue_count=0
@ -3294,7 +3294,8 @@ static void test_snapshot_recv_two_chunks_assembles_and_renames(void)
memcpy(body, "XCESNAP\0", 8);
memcpy(body + 8, &snap_lsn, 8);
/* body[16] (cfgref_present) left 0 by the memset above. */
uint32_t fake_fmt = 1u; /* must match persist.c's SNAPSHOT_FORMAT_VERSION */
uint32_t fake_fmt = 2u; /* must match persist.c's SNAPSHOT_FORMAT_VERSION
* (config-upgrade-snapshot task 1 bumped 1->2) */
uint32_t fake_job_count = 0u;
uint32_t fake_queue_count = 0u;
memcpy(body + 57, &fake_fmt, sizeof(fake_fmt));
@ -3376,6 +3377,107 @@ static void test_snapshot_recv_two_chunks_assembles_and_renames(void)
printf("test_snapshot_recv_two_chunks_assembles_and_renames: PASS\n");
}
/* config-upgrade-snapshot task 1 (Req 1.9, 5.7): SNAPSHOT_FORMAT_VERSION was
* bumped 1->2 to reserve the version for the HostAdminState/QueueAdminState
* capnp migration of the host/queue admin-state frames. A checkpoint written
* under the PRIOR format_version (1 -- the raw queue-frame layout this spec
* migrates off of) must be hard-rejected fail-loud by a binary built against
* the bumped SNAPSHOT_FORMAT_VERSION=2, never silently reinterpreted (Design
* Invariant 4; persist.c:2541's existing `fmt != SNAPSHOT_FORMAT_VERSION`
* check, unchanged logic, new constant). Same well-formed-except-for-
* format-version fixture shape as
* test_snapshot_recv_two_chunks_assembles_and_renames above, but the
* embedded format_version is pinned to the literal PRIOR value (1)
* regardless of whatever persist.c's SNAPSHOT_FORMAT_VERSION currently is —
* this is exactly the "old on-disk checkpoint, new binary" skew the version
* bump must reject. */
static void test_snapshot_recv_old_format_version_rejected_fail_loud(void)
{
char tmpdir[] = "/tmp/xcectest_snaprecv_oldfmt_XXXXXX";
TEST_ASSERT_NOT_NULL(mkdtemp(tmpdir));
char snap_dir[512];
snprintf(snap_dir, sizeof(snap_dir), "%s/snap", tmpdir);
TEST_ASSERT_EQUAL_INT(0, mkdir(snap_dir, 0700));
uint64_t snap_lsn = 200;
size_t total_size = 128;
uint8_t body[128];
memset(body, 0, sizeof(body));
memcpy(body, "XCESNAP\0", 8);
memcpy(body + 8, &snap_lsn, 8);
/* body[16] (cfgref_present) left 0 by the memset above. */
uint32_t old_fmt = 1u; /* the pre-migration raw queue-frame layout --
* deliberately NOT persist.c's current
* SNAPSHOT_FORMAT_VERSION */
uint32_t fake_job_count = 0u;
uint32_t fake_queue_count = 0u;
memcpy(body + 57, &old_fmt, sizeof(old_fmt));
memcpy(body + 61, &fake_job_count, sizeof(fake_job_count));
memcpy(body + 65, &fake_queue_count, sizeof(fake_queue_count));
uint8_t chunk0[64];
uint8_t chunk1[64];
memcpy(chunk0, body, 64);
memcpy(chunk1, body + 64, 64);
persist_t *p = calloc(1, sizeof(persist_t));
TEST_ASSERT_NOT_NULL(p);
persist_config_t pcfg = persist_config_defaults();
persist_set_workdir(p, tmpdir, "ctrl-snaprecv-oldfmt");
persist_init(p, &pcfg, NULL);
persist_start(p);
ha_state_t ha;
snap_recv_ha_init(&ha);
/* Chunk 0 — assembling snap.part never itself parses the body, so this
* must still succeed regardless of what format_version the bytes carry. */
snapshot_transfer_t c0;
memset(&c0, 0, sizeof(c0));
c0.snapshot_lsn = snap_lsn;
c0.chunk_index = 0;
c0.chunk_offset = 0;
c0.total_size = (uint64_t)total_size;
c0.is_last = 0;
int rc = ha_handle_snapshot_chunk(&ha, p, snap_dir, &c0, chunk0, 64);
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "assembling a chunk never parses the body");
/* Chunk 1 (isLast=true) triggers rename + persist_load_snapshot — this is
* where the format-version check fires and must fail loud. */
snapshot_transfer_t c1;
memset(&c1, 0, sizeof(c1));
c1.snapshot_lsn = snap_lsn;
c1.chunk_index = 1;
c1.chunk_offset = 64;
c1.total_size = (uint64_t)total_size;
c1.is_last = 1;
rc = ha_handle_snapshot_chunk(&ha, p, snap_dir, &c1, chunk1, 64);
TEST_ASSERT_TRUE_MESSAGE(rc != 0,
"a checkpoint encoded under the prior SNAPSHOT_FORMAT_VERSION must be "
"rejected fail-loud by a binary built against the bumped version, "
"never silently reinterpreted (Requirement 5.7)");
/* A rejected load must never advance applied_lsn (no partial/silent
* acceptance of mismatched-format bytes). */
uint64_t got_lsn = (uint64_t)atomic_load_explicit(&ha.applied_lsn,
memory_order_acquire);
TEST_ASSERT_EQUAL_UINT64_MESSAGE(0, got_lsn,
"a rejected format-mismatched checkpoint must never advance applied_lsn");
ha_fini(&ha);
persist_stop(p);
persist_fini(p);
free(p);
char cmd2[512];
snprintf(cmd2, sizeof(cmd2), "rm -rf %s", tmpdir);
system(cmd2);
printf("test_snapshot_recv_old_format_version_rejected_fail_loud: PASS\n");
}
/* Test 6.1: a legacy (pre-ha-entity-snapshots) position-only snapshot —
* exactly the 16-byte XCESNAP magic+lsn header, no entity payload — driven
* through the real receive handler end-to-end. persist_load_snapshot's
@ -3571,7 +3673,8 @@ static void test_snapshot_recv_128kb_two_chunks(void)
memcpy(chunk0, "XCESNAP\0", 8);
memcpy(chunk0 + 8, &snap_lsn, 8);
/* chunk0[16] (cfgref_present) already 0 from calloc. */
const uint32_t fake_fmt = 1u; /* must match persist.c's SNAPSHOT_FORMAT_VERSION */
const uint32_t fake_fmt = 2u; /* must match persist.c's SNAPSHOT_FORMAT_VERSION
* (config-upgrade-snapshot task 1 bumped 1->2) */
memcpy(chunk0 + 57, &fake_fmt, sizeof(fake_fmt));
/* ── Chunk 0: not last ── */
@ -3662,7 +3765,7 @@ static void test_snapshot_recv_out_of_order_chunks_assemble_correctly(void)
/* 4 chunks of 64 bytes each = 256-byte body. Must be a WELL-FORMED
* entity-bearing checkpoint (same reason as
* test_snapshot_recv_two_chunks_assembles_and_renames above): legacy
* 57-byte header (cfgref_present=0) + format_version=1 + job_count=0 +
* 57-byte header (cfgref_present=0) + format_version=2 + job_count=0 +
* queue_count=0, then filler out to 256 bytes. The filler is
* position-dependent (distinct per source byte, not per chunk) so ANY
* chunk landing at the wrong file offset is immediately detectable via
@ -3676,7 +3779,8 @@ static void test_snapshot_recv_out_of_order_chunks_assemble_correctly(void)
memcpy(body + 8, &snap_lsn, 8);
for (int i = 16; i < 256; i++)
body[i] = (uint8_t)(i * 3 + 7); /* position-dependent, non-repeating */
uint32_t fake_fmt = 1u;
uint32_t fake_fmt = 2u; /* must match persist.c's SNAPSHOT_FORMAT_VERSION
* (config-upgrade-snapshot task 1 bumped 1->2) */
uint32_t fake_job_count = 0u;
uint32_t fake_queue_count = 0u;
memcpy(body + 57, &fake_fmt, sizeof(fake_fmt));
@ -4268,6 +4372,9 @@ void test_config_repl(void)
RUN_TEST(test_snapshot_transfer_stops_and_propagates_send_failure);
/* Task 3.5c: standby snapshot receive handler */
RUN_TEST(test_snapshot_recv_two_chunks_assembles_and_renames);
/* config-upgrade-snapshot task 1: prior SNAPSHOT_FORMAT_VERSION rejected
* fail-loud by a binary built against the bumped version */
RUN_TEST(test_snapshot_recv_old_format_version_rejected_fail_loud);
/* Task 6.1: legacy position-only file consumed end-to-end via the
* receive handler — entities_restored=false path */
RUN_TEST(test_snapshot_recv_legacy_file_advances_without_entities);