forked from lei.chen/myxce
feat(ha-commit-durability): publish durable floor from recovered WAL tip (task 3.4)
This commit is contained in:
parent
e3fc42c11e
commit
c1c35ab1b3
4 changed files with 271 additions and 1 deletions
|
|
@ -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". */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue