forked from lei.chen/myxce
fix(ha-commit-engine,persist): reserve WAL lsn 0 — first entry starts at lsn 1
Root cause (found via live 3-node E2E + a verification test): lsn was 0-based
(hdr.lsn = current_lsn++ from current_lsn=0), and 0 was overloaded as a
sentinel, causing three cold-start defects on a freshly-promoted leader:
- the first WAL entry (own-term promotion NOOP at lsn 0) was structurally
uncommittable: commit_advance_locked only stores when cand>cur and
committed_lsn starts at 0, so lsn 0 can never advance it;
- commit_set_writable_lsn(noop_lsn=0) set writable_lsn=0, which wait_done
reads as 'gate off' — the own-term safety gate was a no-op on the first leader;
- bug-543 (WAL hole): ha_stream_wal_to_standby filters hdr.lsn>standby_lsn;
a fresh empty standby reports standby_lsn=0 (ambiguous 'empty' vs 'has lsn 0'),
so lsn 0 failed 0>0 and was dropped -> non-contiguous standby WAL.
Fix: a fresh persist store initializes current_lsn=1, reserving lsn 0 as the
none/empty sentinel. The first real entry is lsn 1 -> committable (0->1),
writable_lsn=1 -> gate effective, and standby_lsn=0 unambiguously means 'empty'
so catch-up streams from the first entry with no hole. persist_recover is
data-driven (current_lsn=max(hdr.lsn+1)), so existing on-disk WALs and non-fresh
recovery are unaffected; the change only sets the base for brand-new stores.
No wire/capnp change.
Verification: test_coldstart_lsn0 drives the real commit engine + real streamer
and proves H1 (first entry commits 0->1), H3 (gate effective + blocks when
committed<writable), H4 (empty-standby catch-up is contiguous [1,2], control
standby_lsn=1 gets only [2]). Full controller suite green under ASAN. Tests that
relied on the lsn-0 base were shifted (several strengthened); commit.c /
config_repl.c / ha.c / wire format untouched.
Owning specs: ha-commit-engine / ha-wal-replication / persist. Follow-up:
sweep stale lsn-0 comments (repl.c:429-436, persist.c:1055-1075/1240-1243/1296).
This commit is contained in:
parent
c97c7632e9
commit
7519fa96dd
10 changed files with 480 additions and 31 deletions
|
|
@ -243,8 +243,9 @@ static void test_single_node_committed_lsn_advances(void)
|
|||
/* Wire the engine exactly as cluster_init_phase2 does for n_peers=1 */
|
||||
TEST_ASSERT_EQUAL_INT(0, commit_engine_init(&ce, p, 1, 8));
|
||||
repl_set_commit_engine(&p->repl, &ce);
|
||||
/* Priming append: sync entry below lands at LSN 1 (engine-wired path) */
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_wal_async(p, WAL_JOB_UPDATE, "p", 1));
|
||||
/* Reserved-0 contract: fresh store starts current_lsn=1, so the sync entry
|
||||
* below lands at LSN 1 with no priming append (LSN 0 is the reserved
|
||||
* sentinel — the engine rejects lsn == 0). */
|
||||
|
||||
int sync_rc = -1;
|
||||
int rc = persist_wal_sync_append(p, WAL_JOB_SUBMIT, "job", 3, &sync_rc);
|
||||
|
|
@ -363,10 +364,11 @@ static void* p2_ack_thread(void* arg)
|
|||
* Verify: persist_wal_sync_append returns 0, sync_rc == 0,
|
||||
* and committed_lsn advanced past committed_before.
|
||||
*
|
||||
* Note: fresh persist_t starts with current_lsn=0, so after the first WAL
|
||||
* write written_lsn = current_lsn - 1 = 0. The test therefore captures
|
||||
* committed_before and asserts committed_after > committed_before (advance)
|
||||
* OR committed_after == written_lsn (exact match), rather than asserting > 0.
|
||||
* Note: under the reserved-0 contract a fresh persist_t starts current_lsn=1,
|
||||
* so the first WAL write lands at lsn 1 (written_lsn = current_lsn - 1 = 1).
|
||||
* The standby ACK below is for lsn 1, so it exactly covers the first entry.
|
||||
* The test captures committed_before and asserts committed_after >
|
||||
* committed_before (advance) OR committed_after == written_lsn (exact match).
|
||||
*/
|
||||
static persist_t* g_p9 = NULL;
|
||||
static int g_ep9_a, g_ep9_b;
|
||||
|
|
@ -374,6 +376,8 @@ static struct p2_ack_args g_ack9;
|
|||
|
||||
static void test_committed_lsn_advances_after_majority_ack(void)
|
||||
{
|
||||
commit_engine_t ce;
|
||||
|
||||
g_p9 = make_p2_persist("clsn_ack");
|
||||
TEST_ASSERT_NOT_NULL(g_p9);
|
||||
|
||||
|
|
@ -381,6 +385,14 @@ static void test_committed_lsn_advances_after_majority_ack(void)
|
|||
repl_add_standby(&g_p9->repl, (void *)&g_ep9_a, "sb_x", 7020);
|
||||
repl_add_standby(&g_p9->repl, (void *)&g_ep9_b, "sb_y", 7021);
|
||||
|
||||
/* Wire the commit engine exactly as cluster_init_phase2 does (n_peers=3).
|
||||
* Under the reserved-0 contract the first WAL entry is LSN 1, which routes
|
||||
* through the engine-wired walAck path (commit_submit), so the engine must
|
||||
* be wired — the old engine-less path only "worked" via the LSN-0
|
||||
* immediate-confirm escape hatch, which no longer applies. */
|
||||
TEST_ASSERT_EQUAL_INT(0, commit_engine_init(&ce, g_p9, 3, 8));
|
||||
repl_set_commit_engine(&g_p9->repl, &ce);
|
||||
|
||||
/* Give enough time for the ACK thread to fire before timeout */
|
||||
g_p9->config.sync_timeout_ms = 2000;
|
||||
|
||||
|
|
@ -405,11 +417,10 @@ static void test_committed_lsn_advances_after_majority_ack(void)
|
|||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(0, sync_rc);
|
||||
/* committed_lsn must have advanced to written_lsn (Req 4.1).
|
||||
* written_lsn = current_lsn - 1 after persist_wal_async; for a fresh
|
||||
* persist_t this equals 0 on the very first write. We therefore check
|
||||
* that committed_after > committed_before (advance happened) or, when
|
||||
* both are 0 and written_lsn is also 0, that they are equal (lsn was
|
||||
* committed at 0 → valid: majority ACK was received). */
|
||||
* written_lsn = current_lsn - 1 after persist_wal_async; under the
|
||||
* reserved-0 contract this equals 1 on the very first write. We check
|
||||
* that committed_after > committed_before (advance happened) or that
|
||||
* committed_after == written_lsn (the first entry committed exactly). */
|
||||
uint64_t committed_after =
|
||||
atomic_load_explicit(&g_p9->committed_lsn, memory_order_acquire);
|
||||
|
||||
|
|
@ -422,6 +433,7 @@ static void test_committed_lsn_advances_after_majority_ack(void)
|
|||
committed_after > committed_before || committed_after == written_lsn,
|
||||
"committed_lsn must advance after majority ACK (Req 4.1)");
|
||||
|
||||
commit_engine_fini(&ce);
|
||||
teardown_p2_persist(g_p9);
|
||||
g_p9 = NULL;
|
||||
printf("test_committed_lsn_advances_after_majority_ack: PASS\n");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue