feat(config-upgrade-snapshot): decode host/queue admin-state frames on checkpoint load
load_snapshot_entities' queue-frame decode migrates from the old raw
{name_len,name,status} layout to {payload_len, capnp QueueAdminState
bytes}, still calling the unmodified restore_queue_state. Adds a new
host-frame section decode after queues, dispatching to
restore_host_state (worker.c, task 3) with the same bounds-checking
and restore-before-advance discipline as the existing job/queue loops.
Un-ignores test_snapent_load_snapshot_restores_entities_before_advancing_position
(blocked since task 2.2) -- now passes for real. Also fixes a genuine
regression in test_snapshot_recv_out_of_order_chunks_assemble_correctly
(test_config_repl.c): its synthetic checkpoint-body filler landed on
the new host_count field's offset, tripping the cluster==NULL guard --
pinned to a well-formed host_count=0, matching that test's existing
fmt/job_count/queue_count precedent. Task 4 of 19.
Requirements: 1.10
This commit is contained in:
parent
5b1600a8b2
commit
00cd26a927
4 changed files with 347 additions and 60 deletions
|
|
@ -3766,11 +3766,17 @@ static void test_snapshot_recv_out_of_order_chunks_assemble_correctly(void)
|
|||
* entity-bearing checkpoint (same reason as
|
||||
* test_snapshot_recv_two_chunks_assembles_and_renames above): legacy
|
||||
* 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
|
||||
* a whole-buffer memcmp, regardless of which physical chunk a given
|
||||
* structural field happens to fall in. */
|
||||
* queue_count=0 + host_count=0 (config-upgrade-snapshot task 4 --
|
||||
* load_snapshot_entities now reads a host section after the queue
|
||||
* section, so it must likewise be pinned to a well-formed 0 here rather
|
||||
* than left as position-dependent filler), 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 a whole-buffer memcmp, regardless of
|
||||
* which physical chunk a given structural field happens to fall in --
|
||||
* except for the 4 structural-field byte ranges explicitly overlaid
|
||||
* below (fmt/job_count/queue_count/host_count), which must stay
|
||||
* well-formed for the load to succeed at all. */
|
||||
uint64_t snap_lsn = 400;
|
||||
size_t total_size = 256;
|
||||
uint8_t body[256];
|
||||
|
|
@ -3783,9 +3789,11 @@ static void test_snapshot_recv_out_of_order_chunks_assemble_correctly(void)
|
|||
* (config-upgrade-snapshot task 1 bumped 1->2) */
|
||||
uint32_t fake_job_count = 0u;
|
||||
uint32_t fake_queue_count = 0u;
|
||||
uint32_t fake_host_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));
|
||||
memcpy(body + 69, &fake_host_count, sizeof(fake_host_count));
|
||||
|
||||
persist_t *p = calloc(1, sizeof(persist_t));
|
||||
TEST_ASSERT_NOT_NULL(p);
|
||||
|
|
|
|||
|
|
@ -140,6 +140,15 @@ static const uint32_t g_snapent_host_admin_statuses[] = {
|
|||
* ever set. */
|
||||
#define SNAPENT_HOST_DYNAMIC_BITS (XCE_WORKER_UNAVAIL | XCE_WORKER_BUSY)
|
||||
|
||||
/* Mask covering exactly the bits g_snapent_host_admin_statuses ever
|
||||
* combines (config-upgrade-snapshot task 4's end-to-end restore test) --
|
||||
* disjoint from SNAPENT_HOST_DYNAMIC_BITS above by construction, so a
|
||||
* restored worker's status can be checked for "contains exactly these
|
||||
* admin bits" without depending on persist.c's own internal
|
||||
* PERSIST_WORKER_ADMIN_MASK #define. */
|
||||
#define SNAPENT_HOST_ADMIN_STATUS_MASK \
|
||||
(XCE_WORKER_CLOSED_BY_ADMIN | XCE_WORKER_LOCKEDU | XCE_WORKER_HIDDEN)
|
||||
|
||||
static void snapent_host_name(int i, char* buf, size_t buf_sz)
|
||||
{
|
||||
snprintf(buf, buf_sz, "snapent-host%d", i);
|
||||
|
|
@ -417,6 +426,21 @@ static queue_t* snapent_find_queue(cluster_t* c, const char* name)
|
|||
return (found > 0) ? q : NULL;
|
||||
}
|
||||
|
||||
/* Lookup helper for host restore assertions (config-upgrade-snapshot task 4)
|
||||
* -- mirrors snapent_find_job/snapent_find_queue above exactly, over
|
||||
* c->workermgr. */
|
||||
static worker_t* snapent_find_worker(cluster_t* c, const char* name)
|
||||
{
|
||||
worker_t* w = NULL;
|
||||
int found = 0;
|
||||
|
||||
lock(c->workermgr, RTTI_LOCK_READ);
|
||||
found = key_find(c->workermgr, (void*)name, (void**)&w);
|
||||
unlock(c->workermgr);
|
||||
|
||||
return (found > 0) ? w : NULL;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Test 1: fixture seeds N jobs (varying state) + M queues (varying */
|
||||
/* status) and both are independently observable via key_find */
|
||||
|
|
@ -1766,21 +1790,12 @@ static void snapent_force_queue_status(cluster_t* c, const char* name,
|
|||
|
||||
static void test_snapent_load_snapshot_restores_entities_before_advancing_position(void)
|
||||
{
|
||||
/* BLOCKED on config-upgrade-snapshot task 4: write_snapshot's queue
|
||||
* trailer was migrated to capnp QueueAdminState frames by task 2.2, but
|
||||
* load_snapshot's queue decode still expects the pre-task-2.2 raw
|
||||
* {name_len, name, status} layout (that decode migration is task 4's
|
||||
* job, design.md "Snapshot Entity/Config Payload"). This test's
|
||||
* m_queues=2 fixture does a REAL persist_checkpoint + persist_load_
|
||||
* snapshot round trip, so it now fails against a format the loader
|
||||
* doesn't understand yet. IGNORE'd so the suite stays green; un-ignore
|
||||
* when task 4 lands the QueueAdminState capnp decode. */
|
||||
TEST_IGNORE_MESSAGE(
|
||||
"BLOCKED on config-upgrade-snapshot task 4 (load_snapshot's queue "
|
||||
"decode still expects the pre-task-2.2 raw layout; this test's "
|
||||
"m_queues=2 fixture now round-trips through a format the loader "
|
||||
"doesn't understand yet — will be re-enabled when task 4 lands the "
|
||||
"QueueAdminState capnp decode)");
|
||||
/* Un-IGNORE'd by config-upgrade-snapshot task 4: load_snapshot_entities'
|
||||
* queue decode is now migrated to the QueueAdminState capnp layout
|
||||
* write_snapshot has written since task 2.2, so this real
|
||||
* persist_checkpoint + persist_load_snapshot round trip (previously
|
||||
* blocked -- see git history for the prior TEST_IGNORE_MESSAGE) now
|
||||
* passes for real. */
|
||||
|
||||
const int n_jobs = 3;
|
||||
const int m_queues = 2;
|
||||
|
|
@ -1872,6 +1887,169 @@ static void test_snapent_load_snapshot_restores_entities_before_advancing_positi
|
|||
printf("test_snapent_load_snapshot_restores_entities_before_advancing_position: PASS\n");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Test (config-upgrade-snapshot task 4, Req 1.10): a checkpoint carrying */
|
||||
/* BOTH queue and host admin-state content, loaded via a REAL */
|
||||
/* persist_checkpoint + persist_load_snapshot round trip, must restore */
|
||||
/* every queue AND every host correctly -- regardless of restore order */
|
||||
/* between the two entity types (design.md "Referential integrity" / */
|
||||
/* Design Invariant 2: re-homing decisions are baked in at materialization */
|
||||
/* time, restore never re-derives them, so each entity type's restore is */
|
||||
/* independent of the other's). Exercises BOTH of restore_host_state's */
|
||||
/* branches (worker.c, task 3) through the real decode-and-dispatch path */
|
||||
/* this task adds: */
|
||||
/* - miss: a host with no local record on the destination must come */
|
||||
/* back as a valid create-if-missing placeholder carrying only */
|
||||
/* the restored admin bits (Requirement 1.10, 1.11). */
|
||||
/* - hit: a host the destination already knows about must have its */
|
||||
/* admin bits OR'd in, with its pre-existing dynamic/attrs */
|
||||
/* state left completely untouched (Requirement 1.7). */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void test_snapent_load_snapshot_restores_hosts_and_queues_end_to_end(void)
|
||||
{
|
||||
const int m_queues = 3;
|
||||
const int n_hosts = 4; /* indices 0,1 pre-exist on dst (hit); 2,3 miss */
|
||||
|
||||
/* Source: produces the entity-bearing checkpoint (queues + hosts, both
|
||||
* carrying real admin content plus, for hosts, dynamic bits that must
|
||||
* NOT survive into the frame -- Requirement 1.7, already covered by
|
||||
* collect_host_frames' own tests; re-seeded here only as realistic
|
||||
* source state, not re-asserted). */
|
||||
snapent_fixture_t* src = snapent_make(0, m_queues);
|
||||
TEST_ASSERT_NOT_NULL(src);
|
||||
snapent_seed_hosts(src->c, n_hosts);
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_checkpoint(&src->c->persist));
|
||||
|
||||
uint64_t expected_lsn = src->c->persist.checkpoint_lsn;
|
||||
char path[4096];
|
||||
snprintf(path, sizeof(path), "%s/snap-%016" PRIx64 ".capnp",
|
||||
src->c->persist.snap_path, expected_lsn);
|
||||
|
||||
/* Destination: queues pre-provisioned with a sentinel status (queues are
|
||||
* config-provisioned -- restore_queue_state is update-only), mirroring
|
||||
* the existing job/queue round-trip test above. Only hosts 0 and 1 are
|
||||
* pre-seeded (the "hit" case); hosts 2 and 3 have no local record at
|
||||
* all (the "miss"/placeholder case) -- exactly the "member that hasn't
|
||||
* locally replayed this host's join record yet" scenario Requirement
|
||||
* 1.10 exists for. */
|
||||
snapent_fixture_t* dst = snapent_make(0, m_queues);
|
||||
TEST_ASSERT_NOT_NULL(dst);
|
||||
for (int i = 0; i < m_queues; i++) {
|
||||
char name[32];
|
||||
snapent_queue_name(i, name, sizeof(name));
|
||||
snapent_force_queue_status(dst->c, name, SNAPENT_PRE_RESTORE_QUEUE_STATUS);
|
||||
}
|
||||
|
||||
/* Pre-existing dynamic bit that must survive restore untouched (Req
|
||||
* 1.7's flip side: restore must never CLEAR a real worker's existing
|
||||
* non-admin state, only OR admin bits into it). XCE_WORKER_BUSY is the
|
||||
* same dynamic bit SNAPENT_HOST_DYNAMIC_BITS already exercises above. */
|
||||
const uint32_t hit_pre_existing_dynamic_bit = XCE_WORKER_BUSY;
|
||||
for (int i = 0; i < 2; i++) {
|
||||
char name[32];
|
||||
snapent_host_name(i, name, sizeof(name));
|
||||
|
||||
hostattrs_t attrs;
|
||||
memset(&attrs, 0, sizeof(attrs));
|
||||
xce_worker_t hdef;
|
||||
memset(&hdef, 0, sizeof(hdef));
|
||||
hdef.name = name;
|
||||
hdef.attrs = &attrs;
|
||||
/* Deliberately NOT the checkpoint's real admin status -- a
|
||||
* post-restore match can then only be explained by a genuine
|
||||
* restore_host_state OR-in, not coincidental pre-existing state. */
|
||||
hdef.status = hit_pre_existing_dynamic_bit;
|
||||
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(makeWorker(dst->c->workermgr, &hdef),
|
||||
"destination hit-case host seeding must succeed");
|
||||
}
|
||||
|
||||
/* Sanity: the miss-case hosts truly have no record on the destination
|
||||
* yet. */
|
||||
for (int i = 2; i < n_hosts; i++) {
|
||||
char name[32];
|
||||
snapent_host_name(i, name, sizeof(name));
|
||||
TEST_ASSERT_NULL(snapent_find_worker(dst->c, name));
|
||||
}
|
||||
|
||||
bool entities_restored = false;
|
||||
int rc = persist_load_snapshot(&dst->c->persist, path, expected_lsn,
|
||||
&entities_restored);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc,
|
||||
"loading a checkpoint carrying both queue and host admin-state "
|
||||
"content must succeed");
|
||||
TEST_ASSERT_TRUE_MESSAGE(entities_restored,
|
||||
"a checkpoint carrying queue/host entities must report "
|
||||
"entities_restored=true");
|
||||
|
||||
/* Every queue's admin status restored -- proves the migrated capnp
|
||||
* QueueAdminState decode still dispatches correctly to
|
||||
* restore_queue_state alongside the new host section. */
|
||||
for (int i = 0; i < m_queues; i++) {
|
||||
char name[32];
|
||||
snapent_queue_name(i, name, sizeof(name));
|
||||
|
||||
queue_t* q = snapent_find_queue(dst->c, name);
|
||||
TEST_ASSERT_NOT_NULL(q);
|
||||
TEST_ASSERT_EQUAL_UINT32_MESSAGE(snapent_queue_status(i), q->queue.status,
|
||||
"queue restore must be unaffected by the new host section "
|
||||
"decoding alongside it in the same checkpoint");
|
||||
}
|
||||
|
||||
/* Hit-case hosts (0, 1): admin bits OR'd into the pre-existing worker;
|
||||
* the pre-existing dynamic bit must survive untouched. */
|
||||
for (int i = 0; i < 2; i++) {
|
||||
char name[32];
|
||||
snapent_host_name(i, name, sizeof(name));
|
||||
|
||||
worker_t* w = snapent_find_worker(dst->c, name);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(w,
|
||||
"a host the destination already knew about must still be "
|
||||
"present after restore (update, not replace)");
|
||||
TEST_ASSERT_EQUAL_UINT32_MESSAGE(snapent_host_admin_status(i),
|
||||
w->host.status & SNAPENT_HOST_ADMIN_STATUS_MASK,
|
||||
"restore must OR the checkpoint's admin bits into the "
|
||||
"pre-existing worker's status");
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
(w->host.status & hit_pre_existing_dynamic_bit) != 0,
|
||||
"restore must never clear a pre-existing worker's own "
|
||||
"non-admin state (Requirement 1.7)");
|
||||
}
|
||||
|
||||
/* Miss-case hosts (2, 3): a minimal, valid placeholder was created,
|
||||
* carrying exactly the restored admin bits plus the UNAVAIL marker a
|
||||
* not-yet-joined placeholder always gets -- never the source's dynamic
|
||||
* bits, which were never part of the encoded frame to begin with. */
|
||||
for (int i = 2; i < n_hosts; i++) {
|
||||
char name[32];
|
||||
snapent_host_name(i, name, sizeof(name));
|
||||
|
||||
worker_t* w = snapent_find_worker(dst->c, name);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(w,
|
||||
"a host with no local record must get a create-if-missing "
|
||||
"placeholder (Requirement 1.10), not be silently dropped");
|
||||
TEST_ASSERT_EQUAL_UINT32_MESSAGE(snapent_host_admin_status(i),
|
||||
w->host.status & SNAPENT_HOST_ADMIN_STATUS_MASK,
|
||||
"placeholder must carry exactly the restored admin bits");
|
||||
TEST_ASSERT_TRUE_MESSAGE((w->host.status & XCE_WORKER_UNAVAIL) != 0,
|
||||
"a create-if-missing placeholder must be marked "
|
||||
"unjoined/dead-until-connected");
|
||||
TEST_ASSERT_TRUE_MESSAGE((w->host.status & XCE_WORKER_BUSY) == 0,
|
||||
"a dynamic/non-admin bit must never travel through the "
|
||||
"checkpoint frame into a restored placeholder (Requirement 1.7)");
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(w->host.attrs,
|
||||
"a placeholder's hostattrs_t must be non-NULL so a later real "
|
||||
"join's rejoin_worker direct dereference does not crash "
|
||||
"(Requirement 1.11)");
|
||||
}
|
||||
|
||||
snapent_teardown(dst);
|
||||
snapent_teardown(src);
|
||||
printf("test_snapent_load_snapshot_restores_hosts_and_queues_end_to_end: PASS\n");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Test 8 (task 5.1, RED): a deliberately corrupted entity frame must fail */
|
||||
/* the load and leave current_lsn/checkpoint_lsn COMPLETELY unchanged -- */
|
||||
|
|
@ -2050,6 +2228,7 @@ void test_persist_snapshot_entities(void)
|
|||
RUN_TEST(test_snapent_checkpoint_config_bytes_section_deterministic_across_checkpoints);
|
||||
RUN_TEST(test_snapent_rotation_preserves_in_flight_checkpoint);
|
||||
RUN_TEST(test_snapent_load_snapshot_restores_entities_before_advancing_position);
|
||||
RUN_TEST(test_snapent_load_snapshot_restores_hosts_and_queues_end_to_end);
|
||||
RUN_TEST(test_snapent_load_snapshot_corrupted_entity_leaves_position_unchanged);
|
||||
RUN_TEST(test_snapent_load_snapshot_legacy_file_reports_position_only);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue