feat(ha-term-safety): on_get_entry_term + on_truncate_from callback bodies (task 2.2)

on_get_entry_term → persist_entry_term (recorded term, 0=absent, legacy-0→1; caveat
documented per 1.3 review). on_truncate_from → persist_truncate_wal_above(p, index-1)
(delete LSN>=index), returns rc for library/leader retry; non-promotable fence intact;
replaces the committed-boundary heuristic on the library-driven path. New
truncate_term_index_locked keeps the in-memory per-LSN term index consistent on both
truncate paths (under lock, cutoff-aligned) → no stale-term read after truncation.
3 tests, reviewer APPROVED, per-group 0 failures.
This commit is contained in:
Rongsong Shen 2026-06-22 05:29:06 +00:00
parent 8f3d3ce8f4
commit ed2443533f
5 changed files with 210 additions and 5 deletions

View file

@ -378,6 +378,122 @@ static void test_on_persist_entry_failure_propagates_no_ack(void)
printf("test_on_persist_entry_failure_propagates_no_ack: PASS\n");
}
/* ================================================================== */
/* Task 2.2 — on_get_entry_term + on_truncate_from callback bodies */
/* ================================================================== */
/* Persist a couple of replicated entries (each at a 1-based index == LSN)
* through the WIRED on_persist_entry callback, stamping a leader term per
* entry. Returns the (started) cluster the caller tears down. */
static cluster_t* persist_two_entries(const char* tmp, uint64_t t1, uint64_t t2)
{
cluster_t* c = make_cluster(tmp);
if (!c) return NULL;
atomic_store(&c->ha.role, CTRL_STANDBY);
uint8_t blob[64];
size_t n1 = pack_entry(blob, WAL_JOB_UPDATE, "a", 1);
if (ha_cb_persist_entry_for_test(&c->ha, /*index*/1, t1, blob, n1) != 0) {
teardown_cluster(c);
return NULL;
}
size_t n2 = pack_entry(blob, WAL_JOB_UPDATE, "b", 1);
if (ha_cb_persist_entry_for_test(&c->ha, /*index*/2, t2, blob, n2) != 0) {
teardown_cluster(c);
return NULL;
}
return c;
}
/* Test: on_get_entry_term returns the stamped per-LSN term for present entries,
* the lowest-known term for a legacy-0 entry, and 0 for an absent LSN
* (R1.5, R3.1). */
static void test_on_get_entry_term_present_legacy_absent(void)
{
char tmp[256];
strcpy(tmp, "/tmp/xce_ts_get_XXXXXX");
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
cluster_t* c = make_cluster(tmp);
TEST_ASSERT_NOT_NULL(c);
atomic_store(&c->ha.role, CTRL_STANDBY);
uint8_t blob[64];
/* LSN 1: real term 6. */
size_t n = pack_entry(blob, WAL_JOB_UPDATE, "r", 1);
TEST_ASSERT_EQUAL_INT(0,
ha_cb_persist_entry_for_test(&c->ha, /*index*/1, /*term*/6, blob, n));
/* LSN 2: legacy-0 entry (leader term 0) → reads back as WAL_LEGACY_TERM. */
n = pack_entry(blob, WAL_JOB_UPDATE, "l", 1);
TEST_ASSERT_EQUAL_INT(0,
ha_cb_persist_entry_for_test(&c->ha, /*index*/2, /*term*/0, blob, n));
/* Present real-term entry returns its term verbatim. */
TEST_ASSERT_EQUAL_UINT64(6, ha_cb_get_entry_term_for_test(&c->ha, 1));
/* Present legacy-0 entry maps to the lowest-known real term. */
TEST_ASSERT_EQUAL_UINT64(LEGACY_LOWEST_TERM,
ha_cb_get_entry_term_for_test(&c->ha, 2));
/* Absent LSNs (0 reserved, 3 never appended) return 0. */
TEST_ASSERT_EQUAL_UINT64(0, ha_cb_get_entry_term_for_test(&c->ha, 0));
TEST_ASSERT_EQUAL_UINT64(0, ha_cb_get_entry_term_for_test(&c->ha, 3));
teardown_cluster(c);
printf("test_on_get_entry_term_present_legacy_absent: PASS\n");
}
/* Test: on_truncate_from(k) deletes local entries at LSN >= k; the prefix < k
* stays intact and on_get_entry_term returns 0 for the truncated LSNs while the
* term index shrinks consistently (R1.8, R3.1, R3.2). */
static void test_on_truncate_from_drops_suffix(void)
{
char tmp[256];
strcpy(tmp, "/tmp/xce_ts_trunc_XXXXXX");
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
/* Persist LSN 1 (term 3) and LSN 2 (term 5). */
cluster_t* c = persist_two_entries(tmp, /*t1*/3, /*t2*/5);
TEST_ASSERT_NOT_NULL(c);
/* Both present before truncation. */
TEST_ASSERT_EQUAL_UINT64(3, ha_cb_get_entry_term_for_test(&c->ha, 1));
TEST_ASSERT_EQUAL_UINT64(5, ha_cb_get_entry_term_for_test(&c->ha, 2));
TEST_ASSERT_EQUAL_UINT64(3, c->persist.current_lsn);
/* Truncate from LSN 2 → entries at LSN >= 2 are deleted. */
TEST_ASSERT_EQUAL_INT(0, ha_cb_truncate_from_for_test(&c->ha, 2));
/* Prefix < 2 intact, suffix >= 2 gone (term index returns 0). */
TEST_ASSERT_EQUAL_UINT64(3, ha_cb_get_entry_term_for_test(&c->ha, 1));
TEST_ASSERT_EQUAL_UINT64(0, ha_cb_get_entry_term_for_test(&c->ha, 2));
TEST_ASSERT_EQUAL_UINT64(2, c->persist.current_lsn);
teardown_cluster(c);
printf("test_on_truncate_from_drops_suffix: PASS\n");
}
/* Test: on_truncate_from(1) deletes the whole log (LSN >= 1); every entry's
* term reads back absent and the tip collapses to LSN 1. */
static void test_on_truncate_from_removes_all(void)
{
char tmp[256];
strcpy(tmp, "/tmp/xce_ts_trunc_all_XXXXXX");
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
cluster_t* c = persist_two_entries(tmp, /*t1*/2, /*t2*/2);
TEST_ASSERT_NOT_NULL(c);
TEST_ASSERT_EQUAL_INT(0, ha_cb_truncate_from_for_test(&c->ha, 1));
TEST_ASSERT_EQUAL_UINT64(0, ha_cb_get_entry_term_for_test(&c->ha, 1));
TEST_ASSERT_EQUAL_UINT64(0, ha_cb_get_entry_term_for_test(&c->ha, 2));
TEST_ASSERT_EQUAL_UINT64(1, c->persist.current_lsn);
teardown_cluster(c);
printf("test_on_truncate_from_removes_all: PASS\n");
}
/* ------------------------------------------------------------------ */
/* Suite entry point */
/* ------------------------------------------------------------------ */
@ -392,4 +508,7 @@ void test_term_safety_adapter(void)
RUN_TEST(test_on_persist_entry_stamps_term_standby);
RUN_TEST(test_on_persist_entry_stamps_term_primary);
RUN_TEST(test_on_persist_entry_failure_propagates_no_ack);
RUN_TEST(test_on_get_entry_term_present_legacy_absent);
RUN_TEST(test_on_truncate_from_drops_suffix);
RUN_TEST(test_on_truncate_from_removes_all);
}