forked from lei.chen/myxce
feat(ha-entity-snapshots): restore-before-advance load_snapshot implementation (task 5.2)
Replaces the naive task-5.1 stopgap with the real restore contract: detects legacy vs entity-bearing checkpoints by remaining file size; restores every job then every queue status via restore_job/ restore_queue_state, in file order, with bounds-checked length-prefixed reads; only after every entity restores successfully does current_lsn/checkpoint_lsn commit to the checkpoint's position. Every failure path (bounds check, decode, restore error, unrecognized format_version) returns before that single commit point -- position is provably untouched on failure, never partially advanced. Also fixes the pre-existing restore_job decode leak (job.c): job_init deep-copies submit (never nulled) and MOVE_PTRs allocs/pidinfo/ pend_reasons (nulled on the one path that runs) -- free_Job(&j) is safe unconditionally on every reachable return path (EEXIST early-return, create_job success, create_job failure), verified against the exact generated free_*_ptr null-check guards. Two pre-existing test_config_repl.c wire-protocol tests needed their synthetic garbage snapshot bodies updated to a well-formed empty-entity tail, since load_snapshot now genuinely parses past the legacy header (previously ignored); their actual assertions (chunk-reassembly correctness) are unchanged. This is the exact function/contract that caused task 2.2's rejection (position advancing before restore validated). Independent review traced the memory-safety fix through the generated capnp code field-by- field, confirmed the test_config_repl.c change doesn't weaken any assertion, and confirmed restore_queue_state's ENOENT-aborts-the-whole- checkpoint behavior is faithful to design.md's unconditional contract (flagged as a watch-item for task 7.1's soak, not a defect). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JDYFbrgyDHbvNYcxApKhrX
This commit is contained in:
parent
a8e300d96a
commit
9fdb6778d4
4 changed files with 297 additions and 56 deletions
|
|
@ -2954,16 +2954,38 @@ static void test_snapshot_recv_two_chunks_assembles_and_renames(void)
|
|||
/* Build a fake 128-byte snapshot body (two 64-byte chunks).
|
||||
* payload_bytes contains the raw file bytes for each chunk.
|
||||
* We skip writing an actual snapshot source file — the receiver
|
||||
* writes snap.part and renames it; we use the resulting file to verify. */
|
||||
* writes snap.part and renames it; we use the resulting file to verify.
|
||||
*
|
||||
* The body must be a WELL-FORMED entity-bearing checkpoint, not opaque
|
||||
* filler (ha-entity-snapshots task 5.2): load_snapshot() now actually
|
||||
* parses everything past the legacy header — a 0xAA/0xBB filler tail
|
||||
* used to be silently ignored (load_snapshot only ever read the 57-byte
|
||||
* 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
|
||||
* (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
|
||||
* loop), so its content is immaterial. */
|
||||
uint64_t snap_lsn = 100;
|
||||
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 fake_fmt = 1u; /* must match persist.c's SNAPSHOT_FORMAT_VERSION */
|
||||
uint32_t fake_job_count = 0u;
|
||||
uint32_t fake_queue_count = 0u;
|
||||
memcpy(body + 57, &fake_fmt, sizeof(fake_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];
|
||||
memset(chunk0, 0xAA, 64);
|
||||
memset(chunk1, 0xBB, 64);
|
||||
/* Embed XCESNAP header in chunk0 so load_snapshot can parse it */
|
||||
memcpy(chunk0, "XCESNAP\0", 8);
|
||||
memcpy(chunk0 + 8, &snap_lsn, 8);
|
||||
memcpy(chunk0, body, 64);
|
||||
memcpy(chunk1, body + 64, 64);
|
||||
|
||||
persist_t *p = calloc(1, sizeof(persist_t));
|
||||
TEST_ASSERT_NOT_NULL(p);
|
||||
|
|
@ -3140,9 +3162,21 @@ static void test_snapshot_recv_128kb_two_chunks(void)
|
|||
TEST_ASSERT_NOT_NULL(chunk0);
|
||||
TEST_ASSERT_NOT_NULL(chunk1);
|
||||
|
||||
/* Embed valid XCESNAP header in chunk0 so persist_load_snapshot succeeds */
|
||||
/* Embed valid XCESNAP header in chunk0 so persist_load_snapshot succeeds.
|
||||
* chunk0/chunk1 are otherwise zero-filled, but a well-formed entity-
|
||||
* bearing checkpoint (ha-entity-snapshots task 5.2) needs format_version
|
||||
* = SNAPSHOT_FORMAT_VERSION right after the 57-byte legacy header, or
|
||||
* load_snapshot() correctly rejects an all-zero format_version as
|
||||
* unrecognized rather than silently ignoring the body as before. The
|
||||
* job_count=0/queue_count=0 that follow are already satisfied by the
|
||||
* zero-fill; the rest of the 128KB body is never read (load_snapshot
|
||||
* stops right after the queue_count=0 loop) so it stays harmless
|
||||
* filler. */
|
||||
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 */
|
||||
memcpy(chunk0 + 57, &fake_fmt, sizeof(fake_fmt));
|
||||
|
||||
/* ── Chunk 0: not last ── */
|
||||
snapshot_transfer_t c0;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue