From a8e300d96a0fadc432fe8cd725cd8f00a22fcf8c Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Wed, 1 Jul 2026 07:56:57 +0000 Subject: [PATCH] test(ha-entity-snapshots): RED tests for load_snapshot restore contract (task 5.1) Extends persist_load_snapshot's public signature with an out_entities_restored bool*, per design.md's documented contract. The static load_snapshot function -- the real restore logic -- is completely untouched (still today's watermark-only implementation, byte-identical to before this task); only a thin wrapper was added that naively maps *out_entities_restored = (rc==0), deliberately scaffolding so the 3 new RED tests fail for genuine reasons rather than accidentally passing. ha_handle_snapshot_chunk's sole call site gets a mechanical declare-and-pass update -- confirmed inert, never reads the new out-param (task 6.1's job). Three tests against persist_load_snapshot directly (unlike prior tasks' tests, which deliberately stayed on the raw-byte-parsing side to avoid this exact function): restore-before-advance (fails at the job-lookup assertion -- nothing is ever restored today), corrupted-entity-leaves- position-unchanged (fails at rc!=0 -- today's code ignores the corrupted section entirely, since load_snapshot never reads past the legacy header), legacy-file-reports-position-only (fails at entities_restored== false -- the naive wrapper claims restoration for every rc==0 case, including legacy files). This is the exact function/call site that caused task 2.2's rejection (a live position-advances-before-restore-validates hazard). Independent review verified load_snapshot's body is byte-identical to commit d52eb94, the wrapper does nothing beyond the naive mapping, and ha.c's change never reads the new out-param anywhere -- no repeat of that mistake here. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JDYFbrgyDHbvNYcxApKhrX --- .kiro/specs/ha-entity-snapshots/tasks.md | 3 +- src/controller/ha.c | 9 +- src/controller/persist.c | 31 +- .../test_persist_snapshot_entities.c | 287 ++++++++++++++++++ src/include/persist.h | 13 +- 5 files changed, 336 insertions(+), 7 deletions(-) diff --git a/.kiro/specs/ha-entity-snapshots/tasks.md b/.kiro/specs/ha-entity-snapshots/tasks.md index 6d330bd7..b189da22 100644 --- a/.kiro/specs/ha-entity-snapshots/tasks.md +++ b/.kiro/specs/ha-entity-snapshots/tasks.md @@ -70,7 +70,7 @@ - _Requirements: 3.1, 3.2, 3.3_ - [ ] 5. Core: entity restoration on load -- [ ] 5.1 RED: restore-before-advance, failure-safety, and legacy-format tests +- [x] 5.1 RED: restore-before-advance, failure-safety, and legacy-format tests - Assert loading a checkpoint with N jobs + M queues restores all of them before the member's position advances - Assert a deliberately corrupted entity record causes the load to fail with the member's @@ -133,3 +133,4 @@ - (3.2) Pre-existing ASan leak in the test fixture itself (`snapent_make` -> `makeInstance`/`makeJob`/`makeQueue`/`xkvmgr_add`, ~18KB/113 allocations), unrelated to any production code -- present since task 1.1, masked by the project's standing `ASAN_OPTIONS=detect_leaks=0` convention for this suite. Not blocking any task; worth a dedicated fixture-teardown cleanup pass if this test file keeps growing. - (4.1) `snapshot_inflight_lsn[peer_idx]` clears to 0 BEFORE `snapshot_inflight[peer_idx]` clears to 0 in every path (completion + all 3 failure sites in `ha_snapshot_send_dispatch`). Task 4.3's `rotate_snapshots` consultation must therefore treat `snapshot_inflight_lsn[i] != 0` as the "protected" signal directly (checkpoint LSNs are never 0), not "check flag==1 first, then trust LSN" -- there's a narrow window where the flag is still 1 but the LSN has already cleared. - (execution note, after 4.1) Remaining tasks split by risk given a mid-session spend-limit hit: low-risk/small/isolated tasks (4.2, 4.3, 7.1, 8.1) executed directly in the main context with self-review (build + test + boundary/TBD/secrets grep), no subagent dispatch. High-risk tasks (5.1/5.2 `load_snapshot` restore -- the exact function that caused task 2.2's rejection; 6.1 wiring into the live `ha_handle_snapshot_chunk` production path) keep the full implementer + independent-reviewer subagent cycle, since that adversarial review already caught a real live-hazard bug once this spec (task 2.2) and the remaining risk is concentrated there, not spread evenly. +- (5.1) `persist_load_snapshot`'s wrapper currently maps `*out_entities_restored = (rc==0)` -- a deliberately naive stopgap so the 3 RED tests fail for the right reasons. `load_snapshot`'s real body is completely untouched (still watermark-only, byte-identical to before this task). Task 5.2 must replace the wrapper's naive mapping wholesale with real format-detection + restore-then-gate-position logic; it owns turning `load_snapshot` from a no-op-on-entities function into the real restore path. diff --git a/src/controller/ha.c b/src/controller/ha.c index a8974f04..a8ede8f6 100644 --- a/src/controller/ha.c +++ b/src/controller/ha.c @@ -2593,8 +2593,13 @@ int ha_handle_snapshot_chunk(ha_state_t *ha, persist_t *p, return -err; } - /* Load the snapshot — sets p->checkpoint_lsn and p->current_lsn */ - int rc = persist_load_snapshot(p, final_path, chunk->snapshot_lsn); + /* Load the snapshot — sets p->checkpoint_lsn and p->current_lsn. + * ha-entity-snapshots task 5.1: call-site signature compat only for the + * new out-param — entities_restored itself is not yet consumed here + * (that is task 6.1's job, once 5.2 makes the flag meaningful). */ + bool entities_restored = false; + int rc = persist_load_snapshot(p, final_path, chunk->snapshot_lsn, + &entities_restored); if (rc != 0) { ERROR("ha_handle_snapshot_chunk: persist_load_snapshot failed rc=%d", rc); return rc; diff --git a/src/controller/persist.c b/src/controller/persist.c index ab5deb21..fc8d44bb 100644 --- a/src/controller/persist.c +++ b/src/controller/persist.c @@ -2204,10 +2204,35 @@ static int load_snapshot(persist_t* p, const char* path, uint64_t lsn) return 0; } -/* Public wrapper for load_snapshot — exposed via persist.h for ha.c (Task 3.5c). */ -int persist_load_snapshot(persist_t *p, const char *path, uint64_t lsn) +/* Public wrapper for load_snapshot — exposed via persist.h for ha.c (Task 3.5c). + * + * ha-entity-snapshots task 5.1 (RED, signature-compat stopgap ONLY — task 5.2 + * owns the real restore-before-advance implementation): `load_snapshot` above + * is UNCHANGED, still today's watermark-only behavior (never inspects/restores + * any entity payload). This wrapper's *out_entities_restored mapping is + * deliberately the most naive one possible — "the underlying call reported + * success" — precisely so it does NOT mask the two real gaps task 5.2 must + * close: (1) a legacy (position-only) file will report entities_restored=true + * here even though it never carried an entity, since load_snapshot() returns + * 0 for ANY well-formed header regardless of what (if anything) follows it — + * a live instance of the exact false-completeness hazard Requirement 2.4 + * exists to prevent; (2) a corrupted entity frame is never even inspected — + * load_snapshot() returns 0 and already advances p->checkpoint_lsn/ + * current_lsn before this wrapper is reached, so the "position unchanged on + * failure" contract (Requirement 2.3) does not hold yet either. Both are + * exactly the behaviors the task 5.1 RED tests assert against. */ +int persist_load_snapshot(persist_t *p, const char *path, uint64_t lsn, + bool *out_entities_restored) { - return load_snapshot(p, path, lsn); + int rc = load_snapshot(p, path, lsn); + + /* Contract: only set on rc==0 (design.md "load_snapshot (extended)" — + * "*out_entities_restored is only set when rc==0"); left untouched + * otherwise so a failed call never overwrites a caller's own state. */ + if (rc == 0 && out_entities_restored != NULL) + *out_entities_restored = true; + + return rc; } /* P2-2: Standby WAL replay — apply a single replicated entry to in-memory state. diff --git a/src/controller/testsuite/test_persist_snapshot_entities.c b/src/controller/testsuite/test_persist_snapshot_entities.c index d8c6597d..55131721 100644 --- a/src/controller/testsuite/test_persist_snapshot_entities.c +++ b/src/controller/testsuite/test_persist_snapshot_entities.c @@ -996,6 +996,290 @@ static void test_snapent_rotation_preserves_in_flight_checkpoint(void) printf("test_snapent_rotation_preserves_in_flight_checkpoint: PASS\n"); } +/* ─────────────────────────────────────────────────────────────────────── + * ha-entity-snapshots Task 5.1 (Req 2.1, 2.2, 2.3, 2.4, 5.1, 5.2): + * restore-before-advance, failure-safety, and legacy-format RED tests. + * + * Unlike every test above (which deliberately parses write_snapshot's raw + * bytes to stay clear of load_snapshot's not-yet-implemented restore + * behavior — task 2.2 remediation note), these three tests call + * persist_load_snapshot DIRECTLY: it is the exact function under test for + * this task (design.md "load_snapshot (extended)"). + * + * They are written against persist_load_snapshot's NEW 4-argument + * signature (with the out_entities_restored parameter) even though + * load_snapshot's internal restore behavior is still today's + * watermark-only implementation — see persist.c's persist_load_snapshot + * wrapper comment for exactly why that stopgap wrapper makes all three of + * these genuinely fail (RED) for the right reason, not a compile error: + * 1. entities are never actually restored into jobmgr/qmgr; + * 2. a corrupted entity frame is never inspected, so failure is never + * detected and the position is wrongly advanced anyway; + * 3. a legacy (position-only) file is falsely reported as fully + * restored (entities_restored=true) instead of false — the exact + * Requirement 2.4 hazard this feature exists to close. + * ─────────────────────────────────────────────────────────────────────── */ + +/* A status value outside every value in g_snapent_queue_statuses[] (which + * only ever sets bits 0x00/0x01/0x04) -- so a post-restore match against the + * checkpoint's real status can only be explained by a genuine + * restore_queue_state call, never by coincidental pre-existing state. */ +#define SNAPENT_PRE_RESTORE_QUEUE_STATUS 0xDEAD0000u + +static void snapent_force_queue_status(cluster_t* c, const char* name, + uint32_t status) +{ + queue_t* q = snapent_find_queue(c, name); + TEST_ASSERT_NOT_NULL(q); + lock(q, RTTI_LOCK_WRITE); + q->queue.status = status; + unlock(q); +} + +/* ------------------------------------------------------------------ */ +/* Test 7 (task 5.1, RED): a checkpoint with N jobs + M queues, loaded via */ +/* persist_load_snapshot into a SEPARATE destination member, must restore */ +/* every entity into the destination's live jobmgr/qmgr, report */ +/* entities_restored=true, and only then reflect the checkpoint's position */ +/* (Requirements 2.1, 2.2). */ +/* ------------------------------------------------------------------ */ + +static void test_snapent_load_snapshot_restores_entities_before_advancing_position(void) +{ + const int n_jobs = 3; + const int m_queues = 2; + + /* Source: produces the entity-bearing checkpoint file (a real, already- + * working write_snapshot, tasks 2.2/3.2). */ + snapent_fixture_t* src = snapent_make(n_jobs, m_queues); + TEST_ASSERT_NOT_NULL(src); + 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: models a rejoining/far-behind member. jobmgr starts + * EMPTY -- jobs are inserted FRESH by restore_job (job.c:96), never + * pre-provisioned (unlike queues). qmgr has the SAME queue names + * pre-provisioned (restore_queue_state, queue.c:208, returns -ENOENT + * for a queue that doesn't already exist -- queues are config-owned, + * per design.md), but seeded with a status that deliberately does NOT + * match the checkpoint's real value, so a post-load match can only be + * explained by a genuine restore_queue_state call. */ + 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); + } + + /* Sanity: destination truly starts without any of the source's jobs. */ + for (int i = 0; i < n_jobs; i++) + TEST_ASSERT_NULL(snapent_find_job(dst->c, snapent_jobid(i))); + + 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 well-formed entity-bearing checkpoint must succeed"); + TEST_ASSERT_TRUE_MESSAGE(entities_restored, + "a checkpoint carrying job/queue entities must report " + "entities_restored=true (Requirement 2.1, 2.2)"); + + /* Requirement 2.1/2.2: every job restored into the live jobmgr, + * field-identical to what was checkpointed. */ + for (int i = 0; i < n_jobs; i++) { + char expect_queue[48], expect_user[48], expect_cmdline[96]; + snapent_job_queue_name(i, expect_queue, sizeof(expect_queue)); + snapent_job_user(i, expect_user, sizeof(expect_user)); + snapent_job_cmdline(i, expect_cmdline, sizeof(expect_cmdline)); + + job_t* j = snapent_find_job(dst->c, snapent_jobid(i)); + TEST_ASSERT_NOT_NULL_MESSAGE(j, + "every job in the checkpoint must be restored into the " + "member's live jobmgr before the position advances"); + TEST_ASSERT_EQUAL_UINT64(snapent_jobid(i), j->job.jobid); + TEST_ASSERT_EQUAL_UINT32(snapent_job_state(i), j->job.state); + TEST_ASSERT_NOT_NULL(j->job.submit); + TEST_ASSERT_EQUAL_STRING(expect_queue, j->job.submit->queue); + TEST_ASSERT_EQUAL_STRING(expect_user, j->job.submit->user); + TEST_ASSERT_NOT_NULL(j->job.submit->start_info); + TEST_ASSERT_EQUAL_STRING(expect_cmdline, + j->job.submit->start_info->cmdline); + } + + /* Requirement 1.1/2.1/2.2: every queue's admin status restored -- + * matches the checkpoint's real value, not the pre-restore sentinel. */ + 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, + "restore_queue_state must have been called with the " + "checkpoint's real status, not left at the pre-restore sentinel"); + } + + /* Requirement 2.1/2.2: the position reflecting "caught up" must hold + * TOGETHER with the entities actually being present -- both checked + * above must be simultaneously true of the end state. */ + TEST_ASSERT_EQUAL_UINT64(expected_lsn, dst->c->persist.checkpoint_lsn); + TEST_ASSERT_EQUAL_UINT64(expected_lsn, dst->c->persist.current_lsn); + + snapent_teardown(dst); + snapent_teardown(src); + printf("test_snapent_load_snapshot_restores_entities_before_advancing_position: 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 -- */ +/* not partially advanced, not advanced-then-reverted (Requirement 2.3). */ +/* ------------------------------------------------------------------ */ + +/* Far beyond any small test checkpoint's actual remaining bytes -- + * guarantees any correct decode implementation hits a short read / bounds + * failure on this frame, regardless of its exact internal read strategy. */ +#define SNAPENT_CORRUPT_JOB_PAYLOAD_LEN (10u * 1024u * 1024u) + +/* Byte offset of the FIRST job frame's payload_len field (design.md + * "Checkpoint File Layout (Physical)"): legacy header, then format_version + * (uint32_t), then job_count (uint32_t), then the first frame's own + * payload_len (uint32_t) begins right after. */ +#define SNAPENT_FIRST_JOB_PAYLOAD_LEN_OFFSET \ + (SNAPENT_LEGACY_HEADER_BYTES + (long)sizeof(uint32_t) /* format_version */ \ + + (long)sizeof(uint32_t) /* job_count */) + +static void snapent_corrupt_first_job_payload_len(const char* path) +{ + int fd = open(path, O_RDWR); + TEST_ASSERT_TRUE(fd >= 0); + + uint32_t bogus = SNAPENT_CORRUPT_JOB_PAYLOAD_LEN; + ssize_t w = pwrite(fd, &bogus, sizeof(bogus), + (off_t)SNAPENT_FIRST_JOB_PAYLOAD_LEN_OFFSET); + TEST_ASSERT_EQUAL_INT((ssize_t)sizeof(bogus), w); + close(fd); +} + +static void test_snapent_load_snapshot_corrupted_entity_leaves_position_unchanged(void) +{ + const int n_jobs = 2; + + snapent_fixture_t* src = snapent_make(n_jobs, 0); + TEST_ASSERT_NOT_NULL(src); + TEST_ASSERT_EQUAL_INT(0, persist_checkpoint(&src->c->persist)); + + uint64_t checkpoint_lsn = src->c->persist.checkpoint_lsn; + char path[4096]; + snprintf(path, sizeof(path), "%s/snap-%016" PRIx64 ".capnp", + src->c->persist.snap_path, checkpoint_lsn); + + snapent_corrupt_first_job_payload_len(path); + + snapent_fixture_t* dst = snapent_make(0, 0); + TEST_ASSERT_NOT_NULL(dst); + + /* Sentinel pre-call position -- distinguishable from both 0 and the + * checkpoint's real position, so "unchanged" is a meaningful assertion + * rather than a 0==0 tautology. */ + const uint64_t sentinel_current = 4242; + const uint64_t sentinel_checkpoint = 4141; + dst->c->persist.current_lsn = sentinel_current; + dst->c->persist.checkpoint_lsn = sentinel_checkpoint; + + bool entities_restored = true; /* canary -- must not be trusted on rc!=0 */ + int rc = persist_load_snapshot(&dst->c->persist, path, checkpoint_lsn, + &entities_restored); + + TEST_ASSERT_TRUE_MESSAGE(rc != 0, + "a checkpoint with a corrupted entity frame must fail to load, not " + "silently succeed (Requirement 2.3)"); + TEST_ASSERT_EQUAL_UINT64_MESSAGE(sentinel_current, dst->c->persist.current_lsn, + "a failed restore must leave current_lsn COMPLETELY unchanged -- " + "not partially advanced (Requirement 2.3)"); + TEST_ASSERT_EQUAL_UINT64_MESSAGE(sentinel_checkpoint, dst->c->persist.checkpoint_lsn, + "a failed restore must leave checkpoint_lsn COMPLETELY unchanged -- " + "not partially advanced (Requirement 2.3)"); + + /* *out_entities_restored is unspecified on rc!=0 (design.md: "only set + * when rc==0") -- deliberately NOT asserted here. */ + + snapent_teardown(dst); + snapent_teardown(src); + printf("test_snapent_load_snapshot_corrupted_entity_leaves_position_unchanged: PASS\n"); +} + +/* ------------------------------------------------------------------ */ +/* Test 9 (task 5.1, RED): a checkpoint file predating this feature */ +/* (position marker only -- no format_version/job/queue sections, i.e. */ +/* the file ends EXACTLY at the legacy header) must load without crashing, */ +/* report entities_restored=false, and still advance current_lsn/ */ +/* checkpoint_lsn to its recorded position -- the existing, PRESERVED */ +/* legacy behavior (Requirements 2.4, 5.1, 5.2). */ +/* ------------------------------------------------------------------ */ + +#define SNAPENT_LEGACY_CHECKPOINT_LSN 555555ULL + +/* Hand-builds a pre-ha-entity-snapshots checkpoint file: the exact 57-byte + * legacy sentinel write_snapshot has always written (magic + lsn + optional + * cfgref tail, cfgref_present=0 here) and NOTHING else -- no format_version, + * no job/queue sections. Per design.md, "a file ending exactly at the + * legacy header (no bytes remain) is unambiguously the old format." */ +static void snapent_write_legacy_checkpoint(const char* path, uint64_t lsn) +{ + int fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, 0600); + TEST_ASSERT_TRUE(fd >= 0); + + char header[SNAPENT_LEGACY_HEADER_BYTES]; + memset(header, 0, sizeof(header)); + memcpy(header, "XCESNAP\0", 8); + memcpy(header + 8, &lsn, 8); + /* header[16] (cfgref_present) left 0 -- no config-ref tail. */ + + TEST_ASSERT_EQUAL_INT((int)sizeof(header), + (int)write(fd, header, sizeof(header))); + close(fd); +} + +static void test_snapent_load_snapshot_legacy_file_reports_position_only(void) +{ + snapent_fixture_t* dst = snapent_make(0, 0); + TEST_ASSERT_NOT_NULL(dst); + + char path[4096]; + snprintf(path, sizeof(path), "%s/snap-legacy.capnp", dst->c->persist.snap_path); + snapent_write_legacy_checkpoint(path, SNAPENT_LEGACY_CHECKPOINT_LSN); + + bool entities_restored = true; /* must come back false */ + int rc = persist_load_snapshot(&dst->c->persist, path, + SNAPENT_LEGACY_CHECKPOINT_LSN, + &entities_restored); + + TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, + "a legacy (pre-feature) checkpoint must load without error -- a " + "short file is an expected shape, not corruption (Requirement 5.1, 5.2)"); + TEST_ASSERT_FALSE_MESSAGE(entities_restored, + "a legacy checkpoint carries no entity content -- it must never " + "falsely claim full restoration (Requirement 2.4)"); + TEST_ASSERT_EQUAL_UINT64_MESSAGE(SNAPENT_LEGACY_CHECKPOINT_LSN, + dst->c->persist.checkpoint_lsn, + "a legacy checkpoint still records the committed position it was " + "taken at -- existing, preserved behavior"); + TEST_ASSERT_EQUAL_UINT64_MESSAGE(SNAPENT_LEGACY_CHECKPOINT_LSN, + dst->c->persist.current_lsn, + "a legacy checkpoint still advances current_lsn to its recorded " + "position -- existing, preserved behavior"); + + snapent_teardown(dst); + printf("test_snapent_load_snapshot_legacy_file_reports_position_only: PASS\n"); +} + /* ------------------------------------------------------------------ */ /* Suite entry point */ /* ------------------------------------------------------------------ */ @@ -1022,4 +1306,7 @@ void test_persist_snapshot_entities(void) RUN_TEST(test_snapent_checkpoint_mid_encode_failure_discards_whole_file); RUN_TEST(test_snapent_checkpoint_queue_status_roundtrip); 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_corrupted_entity_leaves_position_unchanged); + RUN_TEST(test_snapent_load_snapshot_legacy_file_reports_position_only); } diff --git a/src/include/persist.h b/src/include/persist.h index 0bb3f962..f4f524b1 100644 --- a/src/include/persist.h +++ b/src/include/persist.h @@ -650,8 +650,19 @@ int apply_wal_entry(persist_t* p, uint32_t type, uint64_t lsn, /* Load a snapshot file from path, set p->checkpoint_lsn and p->current_lsn. * lsn hint is informational — the actual LSN is read from the file header. + * + * ha-entity-snapshots (task 5.2 owns the restore-before-advance + * implementation; this is the public signature it will fill in): + * out_entities_restored reports whether this call actually restored + * job/queue entity state (true) or only observed a legacy position marker + * (false) — set ONLY when this function returns 0. On a non-zero return, + * p->checkpoint_lsn/p->current_lsn are left completely unchanged and + * *out_entities_restored is left untouched (undefined) — callers must not + * read it in that case. + * * Returns 0 on success, -errno or -EINVAL/-EIO on error. */ -int persist_load_snapshot(persist_t* p, const char* path, uint64_t lsn); +int persist_load_snapshot(persist_t* p, const char* path, uint64_t lsn, + bool* out_entities_restored); /* GC retention predicates (P0-5a / P0-8 / P0-13) */ #include