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:
parent
e0c5e8b7a4
commit
9e68335aee
6 changed files with 322 additions and 8 deletions
|
|
@ -1628,8 +1628,17 @@ static int persist_write_checkpoint_marker(persist_t* p)
|
|||
* [uint32_t job_count]
|
||||
* [job_count * { uint32_t payload_len; uint8_t capnp_Job_bytes[payload_len] }]
|
||||
* [uint32_t queue_count]
|
||||
* [queue_count * { uint32_t name_len; char name[name_len]; uint32_t status }] */
|
||||
#define SNAPSHOT_FORMAT_VERSION 1u
|
||||
* [queue_count * { uint32_t name_len; char name[name_len]; uint32_t status }]
|
||||
*
|
||||
* config-upgrade-snapshot task 1 (Req 1.9, 5.7): bumped 1->2 to reserve the
|
||||
* version for the HostAdminState/QueueAdminState capnp migration -- the
|
||||
* queue-frame layout above (name_len/name/status raw bytes) is the PRIOR
|
||||
* layout this version now hard-rejects; the byte-layout migration itself
|
||||
* (new host section, queue frames re-encoded as capnp) lands in tasks
|
||||
* 2.1/2.2. A binary built against version 2 must never misinterpret a
|
||||
* checkpoint written under version 1's raw queue-frame layout (Design
|
||||
* Invariant 4: fail loud, never silently reinterpret). */
|
||||
#define SNAPSHOT_FORMAT_VERSION 2u
|
||||
|
||||
typedef struct {
|
||||
uint8_t* buf;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -375,7 +375,8 @@ static void test_snapent_fixture_seeds_jobs_and_queues(void)
|
|||
/* ------------------------------------------------------------------ */
|
||||
|
||||
#define SNAPENT_LEGACY_HEADER_BYTES (16 + 1 + 8 + 32)
|
||||
#define SNAPENT_TEST_FORMAT_VERSION 1u
|
||||
#define SNAPENT_TEST_FORMAT_VERSION 2u /* must match persist.c's SNAPSHOT_FORMAT_VERSION
|
||||
* (config-upgrade-snapshot task 1 bumped 1->2) */
|
||||
#define SNAPENT_MAX_DECODE_PAYLOAD (16u * 1024 * 1024)
|
||||
|
||||
/* Decodes every job frame in the checkpoint at `path` into `out`
|
||||
|
|
|
|||
|
|
@ -443,6 +443,25 @@ typedef struct {
|
|||
uint32_t prevCrc;
|
||||
} capnp_repl_envelope_t;
|
||||
|
||||
/* ── Upgrade snapshot admin-state frames (config-upgrade-snapshot task 1,
|
||||
* Req 1.9/5.7) ───────────────────────────────────────────────────────────
|
||||
* Frame-only wire types for the checkpoint's host/queue admin-state
|
||||
* sections -- carry only the administratively-set (name, status) pair,
|
||||
* never the full xce_worker_t/xce_queue_t (design.md Design Invariant 5: a
|
||||
* snapshot frame must never inherit a live struct's dynamic fields just
|
||||
* because reusing it is convenient). Neither has a corresponding "live" C
|
||||
* struct, so each gets its own frame-only counterpart here, same as
|
||||
* capnp_snap_hdr_t/capnp_persist_cfg_t/capnp_repl_envelope_t above. */
|
||||
typedef struct {
|
||||
char* name;
|
||||
uint32_t status;
|
||||
} capnp_host_admin_state_t;
|
||||
|
||||
typedef struct {
|
||||
char* name;
|
||||
uint32_t status;
|
||||
} capnp_queue_admin_state_t;
|
||||
|
||||
/* ── Config replication Cap'n Proto mapped types (Phase 1) ──────────────
|
||||
* C-side counterparts for the capnp encode/decode shims for config sync.
|
||||
* Struct tags match the forward declarations near the top of this file. */
|
||||
|
|
|
|||
|
|
@ -752,6 +752,30 @@ struct SnapshotHeader $C.mapname("capnp_snap_hdr_t") {
|
|||
headerChecksum @8 :UInt32;
|
||||
}
|
||||
|
||||
# ── Upgrade snapshot admin-state frames (config-upgrade-snapshot task 1,
|
||||
# Req 1.9/5.7) ──────────────────────────────────────────────────────────
|
||||
# Independently-extensible Cap'n Proto frames for the on-disk checkpoint's
|
||||
# host/queue admin-state sections (SNAPSHOT_FORMAT_VERSION 2, persist.c).
|
||||
# New admin-state fields (e.g. a lock reason or tag) get new ordinals under
|
||||
# capnp's own schema-evolution rules -- no future SNAPSHOT_FORMAT_VERSION
|
||||
# bump needed per field addition. Deliberately NOT mapname'd onto
|
||||
# Worker/QueueConf (design.md Design Invariant 5): those live runtime
|
||||
# structs bundle static attrs and dynamic state together, which a snapshot
|
||||
# frame must never carry (Requirement 1.7 excludes dynamic fields). Each
|
||||
# maps instead onto its own frame-only C counterpart in typemap.h, matching
|
||||
# the existing capnp_snap_hdr_t/capnp_persist_cfg_t/capnp_repl_envelope_t
|
||||
# convention already established below for schema-only wire concepts with
|
||||
# no corresponding "live" C struct.
|
||||
struct HostAdminState $C.mapname("capnp_host_admin_state_t") {
|
||||
name @0: Text;
|
||||
status @1: UInt32;
|
||||
}
|
||||
|
||||
struct QueueAdminState $C.mapname("capnp_queue_admin_state_t") {
|
||||
name @0: Text;
|
||||
status @1: UInt32;
|
||||
}
|
||||
|
||||
# ── HA heartbeat ────────────────────────────────────────────────────
|
||||
struct HeartbeatMsg $C.mapname("heartbeat_msg_t") {
|
||||
senderPriority @0: UInt32 $C.mapname("sender_priority");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue