feat(ha-commit-durability): publish durable floor from recovered WAL tip (task 3.4)

This commit is contained in:
Rongsong Shen 2026-06-18 08:24:46 +00:00
parent e3fc42c11e
commit c1c35ab1b3
4 changed files with 271 additions and 1 deletions

View file

@ -990,6 +990,212 @@ static void test_recover_verdict_snapshot_sentinel_clean(void)
printf("test_recover_verdict_snapshot_sentinel_clean: PASS\n");
}
/* ================================================================== *
* task 3.4 — re-derive the durable floor from the recovered WAL tip *
* (Req 2.3) *
* *
* On recovery the member publishes its durable floor as the highest *
* INTACT replayed LSN (the WAL tip / durable last-log index), so the *
* freshness advertisement (task 4.2) is backed by durable on-disk *
* state rather than a volatile in-memory position. The floor is the *
* durable last-log index, NOT the commit position, and NOT a stale *
* higher in-memory value. persist_durable_floor() exposes it (a thin *
* wrapper over the durable_lsn watermark, which recovery sets to the *
* recovered tip). *
* ------------------------------------------------------------------ */
/* Test 3.4a (CRUX, Req 2.3): after a TRUNCATING restart the published
* durable floor equals the highest INTACT LSN (the WAL tip), NOT a stale
* higher in-memory value and NOT the commit position.
*
* Writer lays down 4 durable entries (LSN 1..4). We then corrupt entry 3's
* CRC on disk so the intact prefix is LSN 1..2 — replay stops at the chain
* break and the recovered tip is LSN 2. To make the "not a stale higher
* in-memory value" assertion meaningful we deliberately seed the fresh
* instance's in-memory durable_lsn / committed_lsn to an over-advertised
* value (4) BEFORE recovery; recovery must publish the durable FLOOR at the
* intact tip 2, never the seeded 4. */
static void test_durable_floor_truncating_restart_is_intact_tip(void)
{
char tmp[256];
strcpy(tmp, "/tmp/xce_floortrunc_XXXXXX");
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
/* Writer: four durable entries (LSN 1..4), then die. */
{
persist_t* p = make_durability_persist(tmp);
TEST_ASSERT_NOT_NULL(p);
TEST_ASSERT_EQUAL_INT(0, persist_wal_async(p, WAL_JOB_UPDATE, "a", 1));
TEST_ASSERT_EQUAL_INT(0, persist_wal_async(p, WAL_JOB_UPDATE, "b", 1));
TEST_ASSERT_EQUAL_INT(0, persist_wal_async(p, WAL_JOB_UPDATE, "c", 1));
TEST_ASSERT_EQUAL_INT(0, persist_wal_async(p, WAL_JOB_UPDATE, "d", 1));
TEST_ASSERT_EQUAL_INT(0, persist_flush(p));
/* All four are durable on disk before the truncation. */
TEST_ASSERT_EQUAL_UINT64(4, persist_test_durable_lsn_on_disk(p));
teardown_durability_persist(p);
}
/* Corrupt entry 3 (LSN 3): walk the segment to its offset and flip its
* crc. Each entry is sizeof(hdr) + payload_len (=1) bytes; entries 1 and 2
* precede it. This breaks the chain at LSN 3, so the intact tip is LSN 2. */
{
int fd = open_first_seg_rw(tmp);
TEST_ASSERT_TRUE_MESSAGE(fd >= 0, "first WAL segment must exist");
uint32_t off = 0;
wal_entry_hdr_t hdr;
/* Skip entries 1 and 2 (LSN 1, LSN 2). */
for (int i = 0; i < 2; i++) {
TEST_ASSERT_EQUAL_INT((int)sizeof(hdr),
(int)pread(fd, &hdr, sizeof(hdr), (off_t)off));
off += (uint32_t)(sizeof(hdr) + hdr.payload_len);
}
/* Entry 3 (LSN 3) is at `off`: corrupt its CRC. */
TEST_ASSERT_EQUAL_INT((int)sizeof(hdr),
(int)pread(fd, &hdr, sizeof(hdr), (off_t)off));
TEST_ASSERT_EQUAL_UINT64(3, hdr.lsn);
hdr.crc ^= 0xDEADBEEFu;
TEST_ASSERT_EQUAL_INT((int)sizeof(hdr),
(int)pwrite(fd, &hdr, sizeof(hdr), (off_t)off));
fsync(fd);
close(fd);
}
/* Recover into a fresh instance that we deliberately poison with a stale,
* over-advertised in-memory position (4) BEFORE recovery. */
{
persist_t* p2 = make_durability_persist(tmp);
TEST_ASSERT_NOT_NULL(p2);
/* Stale higher in-memory values: simulate a volatile position that
* over-states what is durable. The published floor must NOT come from
* these — it must come from the recovered intact WAL tip. */
atomic_store(&p2->durable_lsn, 4);
atomic_store(&p2->committed_lsn, 4);
int rc = persist_recover(p2);
TEST_ASSERT_EQUAL_INT(0, rc);
/* Verdict is TRUNCATED (chain break at LSN 3). */
TEST_ASSERT_EQUAL_INT_MESSAGE(
RECOVER_TRUNCATED, persist_recovery_verdict(p2),
"a CRC break at LSN 3 must yield RECOVER_TRUNCATED");
/* The published durable floor == the highest INTACT LSN (2): the WAL
* tip, equal to current_lsn - 1 after replay (replay advanced
* current_lsn only to one past the last intact entry). */
uint64_t floor = persist_durable_floor(p2);
TEST_ASSERT_EQUAL_UINT64_MESSAGE(
2, floor,
"durable floor after a truncating restart must equal the highest "
"intact LSN (the WAL tip), NOT the stale in-memory value");
TEST_ASSERT_EQUAL_UINT64_MESSAGE(
p2->current_lsn - 1, floor,
"durable floor must equal the recovered WAL tip (current_lsn - 1)");
/* Independently confirmed against the on-disk re-read seam. */
TEST_ASSERT_EQUAL_UINT64_MESSAGE(
2, persist_test_durable_lsn_on_disk(p2),
"on-disk re-read agrees: only LSN 1..2 are intact");
teardown_durability_persist(p2);
}
printf("test_durable_floor_truncating_restart_is_intact_tip: PASS\n");
}
/* Test 3.4b: an INTACT-log recovery publishes the FULL tip (no truncation).
* Three durable entries (LSN 1..3) recover cleanly; the floor is the full
* recovered tip 3 == current_lsn - 1. */
static void test_durable_floor_intact_recovery_full_tip(void)
{
char tmp[256];
strcpy(tmp, "/tmp/xce_floorintact_XXXXXX");
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
{
persist_t* p = make_durability_persist(tmp);
TEST_ASSERT_NOT_NULL(p);
TEST_ASSERT_EQUAL_INT(0, persist_wal_async(p, WAL_JOB_UPDATE, "a", 1));
TEST_ASSERT_EQUAL_INT(0, persist_wal_async(p, WAL_JOB_UPDATE, "b", 1));
TEST_ASSERT_EQUAL_INT(0, persist_wal_async(p, WAL_JOB_UPDATE, "c", 1));
TEST_ASSERT_EQUAL_INT(0, persist_flush(p));
teardown_durability_persist(p);
}
{
persist_t* p2 = make_durability_persist(tmp);
TEST_ASSERT_NOT_NULL(p2);
int rc = persist_recover(p2);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(RECOVER_CLEAN, persist_recovery_verdict(p2));
TEST_ASSERT_EQUAL_UINT64_MESSAGE(
3, persist_durable_floor(p2),
"intact recovery publishes the full recovered WAL tip");
TEST_ASSERT_EQUAL_UINT64(p2->current_lsn - 1, persist_durable_floor(p2));
teardown_durability_persist(p2);
}
printf("test_durable_floor_intact_recovery_full_tip: PASS\n");
}
/* Test 3.4c: an EMPTY first-boot WAL publishes floor 0 (nothing durable). */
static void test_durable_floor_empty_recovery_zero(void)
{
char tmp[256];
strcpy(tmp, "/tmp/xce_floorempty_XXXXXX");
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
persist_t* p = make_durability_persist(tmp);
TEST_ASSERT_NOT_NULL(p);
int rc = persist_recover(p);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_UINT64_MESSAGE(
0, persist_durable_floor(p),
"an empty first-boot WAL has nothing durable — floor is 0");
teardown_durability_persist(p);
printf("test_durable_floor_empty_recovery_zero: PASS\n");
}
/* Test 3.4d (snapshot-sentinel edge): a member recovered from a 16-byte
* LSN-sentinel snapshot plus a clean replay above it publishes the recovered
* tip (the highest replayed LSN). The floor is the recovered tip, consistent
* with 3.1's CLEAN classification — "behind-and-must-catch-up", not
* over-advertising. */
static void test_durable_floor_snapshot_sentinel_tip(void)
{
char tmp[256];
strcpy(tmp, "/tmp/xce_floorsnap_XXXXXX");
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
{
persist_t* p = make_durability_persist(tmp);
TEST_ASSERT_NOT_NULL(p);
TEST_ASSERT_EQUAL_INT(0, persist_wal_async(p, WAL_JOB_UPDATE, "a", 1));
TEST_ASSERT_EQUAL_INT(0, persist_wal_async(p, WAL_JOB_UPDATE, "b", 1));
TEST_ASSERT_EQUAL_INT(0, persist_flush(p));
/* Sentinel snapshot at the checkpoint position; then one entry above. */
TEST_ASSERT_EQUAL_INT(0, persist_checkpoint(p));
TEST_ASSERT_EQUAL_INT(0, persist_wal_async(p, WAL_JOB_UPDATE, "c", 1));
TEST_ASSERT_EQUAL_INT(0, persist_flush(p));
teardown_durability_persist(p);
}
{
persist_t* p2 = make_durability_persist(tmp);
TEST_ASSERT_NOT_NULL(p2);
int rc = persist_recover(p2);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(RECOVER_CLEAN, persist_recovery_verdict(p2));
/* The floor is the recovered tip = current_lsn - 1 (whatever the
* highest replayed LSN above the sentinel is). It is the recovered
* tip, not a stale higher value. */
TEST_ASSERT_EQUAL_UINT64_MESSAGE(
p2->current_lsn - 1, persist_durable_floor(p2),
"snapshot-sentinel recovery publishes the recovered WAL tip");
TEST_ASSERT_TRUE_MESSAGE(
persist_durable_floor(p2) > 0,
"a sentinel + clean replay-above has a non-zero recovered tip");
teardown_durability_persist(p2);
}
printf("test_durable_floor_snapshot_sentinel_tip: PASS\n");
}
/* ================================================================== *
* task 3.2 — Boot Policy Decision Table (DECISION ONLY) *
* *
@ -1274,6 +1480,11 @@ void test_commit_durability(void)
RUN_TEST(test_recover_verdict_empty_first_boot_clean);
RUN_TEST(test_recover_verdict_crc_break_first_entry_truncated);
RUN_TEST(test_recover_verdict_snapshot_sentinel_clean);
/* task 3.4 — re-derive the durable floor from the recovered WAL tip. */
RUN_TEST(test_durable_floor_truncating_restart_is_intact_tip);
RUN_TEST(test_durable_floor_intact_recovery_full_tip);
RUN_TEST(test_durable_floor_empty_recovery_zero);
RUN_TEST(test_durable_floor_snapshot_sentinel_tip);
/* task 3.2 — Boot Policy Decision Table skeletons (IGNORED, pending
* 3.3/4.3). Registered so each row is reported as IGNORED, not failed;
* the suite stays GREEN. See design.md "Boot Policy Decision Table". */