feat(ha-term-safety): on_commit_advanced barrier trigger + commit-gate ownership (task 2.4)

This commit is contained in:
Rongsong Shen 2026-06-22 07:33:04 +00:00
parent 2c5d298cb5
commit 3470bdfb71
6 changed files with 363 additions and 9 deletions

View file

@ -13,6 +13,7 @@
#include "persist.h"
#include "wal_types.h"
#include "repl.h"
#include "commit.h"
#include "xce/cluster_def.h"
#include "ha.h"
@ -621,6 +622,245 @@ static void test_send_append_entries_heartbeat_zero_entries(void)
printf("test_send_append_entries_heartbeat_zero_entries: PASS\n");
}
/* ================================================================== */
/* Task 2.4 — on_commit_advanced trigger + commit-source switch */
/* ================================================================== */
/*
* Task 2.4 feeds the library-supplied current-term-gated `commit_index` into
* the existing synchronous commit barrier as the commit SOURCE. The barrier
* mechanism is unchanged: committed_lsn still advances only when self+majority
* agree AND the bytes are durable locally (durable_lsn cap). The library commit
* index becomes an ADDITIONAL ceiling so the barrier never advances committed
* past the library's current-term-safe point. on_commit_advanced only TRIGGERS
* (records the ceiling + re-evaluates); it never performs durability itself.
*
* These cases build the commit engine standalone over a stub persist_t +
* replicator (the test_commit_engine.c pattern) so the barrier interaction is
* tested in isolation, plus one case driving the WIRED on_commit_advanced
* callback end-to-end through ha_cb_commit_advanced_for_test.
*
* RED: against the no-op ha_cb_commit_advanced_stub (and the missing
* commit_set_raft_commit_index ceiling), the library commit index is NOT a
* commit source — committed_lsn would advance on durable-majority alone with
* no current-term gate. GREEN: the wired callback feeds the ceiling so the
* barrier holds until BOTH durable majority and the library commit index agree.
*/
/* Stub-persist fixture for the commit-source tests (mirrors test_commit_engine.c). */
static persist_t g_t24_p;
static commit_engine_t g_t24_ce;
static int g_t24_eps[MAX_STANDBYS];
static void t24_engine_setup(int n_peers, int n_standbys, uint64_t appended_lsn)
{
char name[16];
memset(&g_t24_p, 0, sizeof(g_t24_p));
TEST_ASSERT_EQUAL_INT(0, repl_init(&g_t24_p.repl));
for (int i = 0; i < n_standbys; i++) {
snprintf(name, sizeof(name), "sb%d", i);
TEST_ASSERT_EQUAL_INT(
i, repl_add_standby(&g_t24_p.repl, &g_t24_eps[i], name,
7000 + (uint32_t)i));
}
g_t24_p.current_lsn = appended_lsn + 1;
atomic_store(&g_t24_p.committed_lsn, 0);
atomic_store(&g_t24_p.durable_lsn, appended_lsn);
TEST_ASSERT_EQUAL_INT(0, commit_engine_init(&g_t24_ce, &g_t24_p, n_peers, 8));
}
static void t24_engine_teardown(void)
{
commit_engine_fini(&g_t24_ce);
repl_fini(&g_t24_p.repl);
}
static void t24_standby_ack(int idx, uint64_t acked_lsn)
{
atomic_store(&g_t24_p.repl.standbys[idx].acked_lsn, acked_lsn);
commit_on_ack(&g_t24_ce, idx, acked_lsn);
}
static uint64_t t24_committed(void)
{
return atomic_load(&g_t24_p.committed_lsn);
}
/* Test: the library commit index is the commit SOURCE once fed. A durable
* majority covers LSN 3, but the library has only admitted (current-term commit)
* up to LSN 1, so the barrier caps committed at 1 — NOT at the durable-majority
* floor of 3. Raising the library ceiling to 3 then releases the barrier to 3.
* This proves the library current-term commit index gates the commit, replacing
* the term-blind safe-commit (R4.1, R4.5, R5.4).
*
* (raft_commit_index == 0 is the INERT pre-wire state — the library source is
* not yet active and the barrier keeps autonomous durable-majority behavior;
* that flag-OFF fallback is covered by the existing test_commit_engine suite,
* whose autonomous semantics this task must not regress.) */
static void test_commit_advanced_is_commit_source(void)
{
/* 3 peers = self + 2 standbys; majority needs self + 1 standby. */
t24_engine_setup(/*n_peers*/3, /*n_standbys*/2, /*appended*/3);
/* The library is the commit source from the first commit: establish the
* ceiling at LSN 1 BEFORE any ACK drives evaluation (production order — the
* ACK path drives raft_advance_commit, which fires on_commit_advanced, then
* the barrier re-evaluates). */
commit_set_raft_commit_index(&g_t24_ce, 1);
/* A standby ACKs LSN 3; self is durable to LSN 3 → durable majority = 3.
* The barrier must cap committed at the library ceiling (1), NOT at the
* durable majority (3). */
t24_standby_ack(0, 3);
TEST_ASSERT_EQUAL_UINT64(1, t24_committed());
/* Library admits up to LSN 3 → barrier releases to the durable majority (3). */
commit_set_raft_commit_index(&g_t24_ce, 3);
TEST_ASSERT_EQUAL_UINT64(3, t24_committed());
t24_engine_teardown();
printf("test_commit_advanced_is_commit_source: PASS\n");
}
/* Test: the library commit index is a CEILING — committed never advances past
* it even when a durable majority covers a higher LSN (current-term gate). The
* barrier's durable cap is the OTHER ceiling: committed = min(durable-majority,
* raft_commit_index). No gap: raising either ceiling alone cannot overshoot the
* other (R4.1, R4.5). */
static void test_commit_advanced_ceiling_caps_durable_majority(void)
{
t24_engine_setup(/*n_peers*/3, /*n_standbys*/2, /*appended*/5);
/* Establish the library ceiling at 3 BEFORE any ACK (production order). The
* library has only committed up to 3 (higher entries prev-term or not yet
* current-term-agreed). */
commit_set_raft_commit_index(&g_t24_ce, 3);
/* Durable majority covers up to LSN 5 (self durable 5 + standby acked 5).
* committed must cap at the library ceiling (3), not the durable floor (5). */
t24_standby_ack(0, 5);
TEST_ASSERT_EQUAL_UINT64(3, t24_committed());
/* Library advances the ceiling to 5 → committed catches up to the durable
* majority floor (5). Monotonic, no regression. */
commit_set_raft_commit_index(&g_t24_ce, 5);
TEST_ASSERT_EQUAL_UINT64(5, t24_committed());
t24_engine_teardown();
printf("test_commit_advanced_ceiling_caps_durable_majority: PASS\n");
}
/* Test: the library commit index NEVER lets committed advance past what is
* durable locally. With raft_commit_index=4 but self only durable to LSN 2 and
* the standby acked LSN 2, committed must stay at the durable-majority floor (2)
* — the library ceiling does not bypass persist-before-ACK / durable cap
* (R5.4 — barrier durability mechanism unchanged). */
static void test_commit_advanced_does_not_bypass_durable_cap(void)
{
t24_engine_setup(/*n_peers*/3, /*n_standbys*/2, /*appended*/4);
atomic_store(&g_t24_p.durable_lsn, 2); /* self durable only to 2 */
t24_standby_ack(0, 2); /* standby agrees to 2 */
commit_set_raft_commit_index(&g_t24_ce, 4); /* library ceiling well ahead */
commit_on_ack(&g_t24_ce, 0, 2);
/* Durable-majority floor is 2; the library ceiling (4) cannot raise it. */
TEST_ASSERT_EQUAL_UINT64(2, t24_committed());
t24_engine_teardown();
printf("test_commit_advanced_does_not_bypass_durable_cap: PASS\n");
}
/* Test (pins the own-term-gate DECISION — RETAINED, complementary, no deadlock):
* the own-term writable_lsn gate is a WAIT-completion predicate, NOT a cap on
* committed_lsn. It is retained alongside the library commit source. With the
* gate armed at LSN 2 and the library commit index + durable majority both at 2,
* committed advances to 2 AND a WAIT(lsn=2) completes — the two gates cannot
* deadlock (the own-term gate never holds committed_lsn back; the library
* ceiling never holds the WAIT predicate back beyond committed_lsn). */
static void test_own_term_gate_retained_no_deadlock(void)
{
t24_engine_setup(/*n_peers*/3, /*n_standbys*/2, /*appended*/2);
/* Arm the own-term gate at LSN 2 (retained complementary durability gate). */
commit_set_writable_lsn(&g_t24_ce, 2);
/* Library commit source + durable majority both reach LSN 2. */
t24_standby_ack(0, 2);
commit_set_raft_commit_index(&g_t24_ce, 2);
commit_on_ack(&g_t24_ce, 0, 2);
/* committed_lsn advanced to 2 (own-term gate never caps committed_lsn). */
TEST_ASSERT_EQUAL_UINT64(2, t24_committed());
/* A WAIT for LSN 2 completes immediately: committed_lsn (2) >= lsn (2) AND
* >= writable_lsn (2). No deadlock between the gate and the library source. */
TEST_ASSERT_EQUAL_INT(0,
commit_submit(&g_t24_ce, /*lsn*/2, COMMIT_MODE_WAIT, /*timeout_ms*/500));
t24_engine_teardown();
printf("test_own_term_gate_retained_no_deadlock: PASS\n");
}
/* Test: the WIRED on_commit_advanced callback routes commit_index into the
* cluster's commit engine end-to-end (ha_cb_commit_advanced_for_test) as the
* commit SOURCE. A durable majority covers LSN 2, but the wired callback feeds
* commit_index=1 first → the barrier caps committed at 1 (library current-term
* gate), NOT at the durable majority of 2. A second callback at commit_index=2
* releases the barrier to 2. Proves the stub→real transition: the no-op stub
* never fed the source, so committed would have followed the durable majority. */
static void test_on_commit_advanced_wired_feeds_barrier(void)
{
char tmp[256];
strcpy(tmp, "/tmp/xce_ts_commitadv_XXXXXX");
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
cluster_t *c = make_cluster(tmp);
TEST_ASSERT_NOT_NULL(c);
/* Init the cluster's embedded commit engine over the same persist as a
* 3-peer cluster (self + 2 standbys), so a majority is self + 1 standby. */
TEST_ASSERT_EQUAL_INT(0,
commit_engine_init(&c->commit, &c->persist, /*n_peers*/3, /*n_vcpus*/8));
/* Persist two entries BEFORE registering a standby — otherwise the primary
* own-append fans out via repl_broadcast to the dummy int ep (ASAN
* global-buffer-overflow reading ep->peer); with no standby the broadcast
* is a no-op. Append LSN 1,2 with term 4 → durable floor reaches LSN 2. */
atomic_store(&c->ha.role, CTRL_PRIMARY);
uint8_t blob[64];
size_t n1 = pack_entry(blob, WAL_JOB_UPDATE, "a", 1);
TEST_ASSERT_EQUAL_INT(0, ha_cb_persist_entry_for_test(&c->ha, 1, 4, blob, n1));
size_t n2 = pack_entry(blob, WAL_JOB_UPDATE, "b", 1);
TEST_ASSERT_EQUAL_INT(0, ha_cb_persist_entry_for_test(&c->ha, 2, 4, blob, n2));
TEST_ASSERT_EQUAL_INT(0, persist_flush(&c->persist));
/* Now register the standby so a majority (self + 1) is reachable. The ACK
* below is driven directly into the registry + commit_on_ack (no network). */
TEST_ASSERT_EQUAL_INT(0,
repl_add_standby(&c->persist.repl, &g_ae_dummy_ep, "sb0", 7000));
/* Fire the WIRED callback FIRST to establish the library ceiling at LSN 1
* (production order: the ACK path drives raft_advance_commit, which fires
* on_commit_advanced before/with the barrier re-evaluation). */
ha_cb_commit_advanced_for_test(&c->ha, 1);
/* Standby ACKs LSN 2 → durable majority covers LSN 2. The wired source caps
* committed at 1, BELOW the durable majority (2): the library current-term
* gate governs (the term-blind safe-commit is retired as the source). */
atomic_store(&c->persist.repl.standbys[0].acked_lsn, 2);
commit_on_ack(&c->commit, 0, 2);
TEST_ASSERT_EQUAL_UINT64(1, atomic_load(&c->persist.committed_lsn));
/* Library admits LSN 2 → barrier releases to the durable majority (2). */
ha_cb_commit_advanced_for_test(&c->ha, 2);
TEST_ASSERT_EQUAL_UINT64(2, atomic_load(&c->persist.committed_lsn));
commit_engine_fini(&c->commit);
teardown_cluster(c);
printf("test_on_commit_advanced_wired_feeds_barrier: PASS\n");
}
/* ------------------------------------------------------------------ */
/* Suite entry point */
/* ------------------------------------------------------------------ */
@ -640,4 +880,9 @@ void test_term_safety_adapter(void)
RUN_TEST(test_on_truncate_from_removes_all);
RUN_TEST(test_send_append_entries_one_entry_from_wal);
RUN_TEST(test_send_append_entries_heartbeat_zero_entries);
RUN_TEST(test_commit_advanced_is_commit_source);
RUN_TEST(test_commit_advanced_ceiling_caps_durable_majority);
RUN_TEST(test_commit_advanced_does_not_bypass_durable_cap);
RUN_TEST(test_own_term_gate_retained_no_deadlock);
RUN_TEST(test_on_commit_advanced_wired_feeds_barrier);
}