test(ha-commit-engine): RED tests for per-LSN majority evaluation (task 1.1)

commit.h umbrella-verbatim interface + inert stub commit.c (test target
only); 7 Unity tests covering per-LSN vs acked_lsn>0 counting, majority
median, monotonicity, append clamp, lagging standby, RETAIN immediacy.
All 7 fail RED as intended. Documented 3 pre-existing masked suite
failures (bug-411) and the per-group exit-code masking root cause.
This commit is contained in:
Rongsong Shen 2026-06-11 14:17:09 +00:00
parent 06135954ff
commit d5a0f9c64d
10 changed files with 757 additions and 283 deletions

View file

@ -0,0 +1,270 @@
/*
* test_commit_engine.c — RED-phase tests for the commit engine
* (ha-commit-engine task 1.1: per-LSN majority evaluation and
* commit-index integrity).
*
* The engine is constructed standalone against a stub persist_t +
* replicator_t (XCE_TESTING pattern): the persist_t is a zeroed stack
* struct whose only live parts are committed_lsn, current_lsn, and the
* embedded replicator registry. Standby ACK watermarks are written
* directly into standby_conn_t.acked_lsn before feeding commit_on_ack,
* exactly as repl_on_ack does in production.
*
* Cluster model: n_peers = 3 means primary (self) + 2 standbys.
* Majority for entry N = self + at least n_peers/2 standbys with
* acked_lsn >= N (Raft §5.3, umbrella-verbatim commit rule).
*
* These cases FAIL against the task-1.1 stub commit.c and go green with
* the task-1.2 implementation.
*
* Requirements: 1.2, 2.1, 2.2, 2.3, 5.1
*/
#include <errno.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <unity/unity.h>
#include "commit.h"
#include "persist.h"
#include "repl.h"
/* ── Fixture ──────────────────────────────────────────────────────────────
* Static: persist_t embeds large path buffers, and statics survive Unity's
* longjmp on assertion failure (pattern from test_repl_majority.c). */
static persist_t g_p;
static commit_engine_t g_ce;
/* Dummy endpoints — the replicator only compares pointers */
static int g_eps[MAX_STANDBYS];
/*
* Arrange a fresh engine over a stub persist_t + replicator_t.
* appended_lsn = highest locally appended WAL entry; persist invariant is
* committed_lsn <= current_lsn - 1, so current_lsn = appended_lsn + 1.
*/
static void engine_setup(int n_peers, int n_standbys, uint64_t appended_lsn)
{
int i;
char name[16];
memset(&g_p, 0, sizeof(g_p));
TEST_ASSERT_EQUAL_INT(0, repl_init(&g_p.repl));
for (i = 0; i < n_standbys; i++) {
snprintf(name, sizeof(name), "sb%d", i);
/* repl_add_standby returns the slot index (>= 0) on success */
TEST_ASSERT_EQUAL_INT(
i, repl_add_standby(&g_p.repl, &g_eps[i], name, 7000 + (uint32_t)i));
}
g_p.current_lsn = appended_lsn + 1;
atomic_store(&g_p.committed_lsn, 0);
TEST_ASSERT_EQUAL_INT(0, commit_engine_init(&g_ce, &g_p, n_peers, 8));
}
static void engine_teardown(void)
{
commit_engine_fini(&g_ce);
repl_fini(&g_p.repl);
}
/* Register appended entries 1..up_to with the engine (RETAIN = bookkeeping
* only, no blocking). Return codes asserted in the dedicated RETAIN test. */
static void append_entries(uint64_t up_to)
{
uint64_t lsn;
for (lsn = 1; lsn <= up_to; lsn++) {
(void)commit_submit(&g_ce, lsn, COMMIT_MODE_RETAIN, 0);
}
}
/* Standby ACK watermark arrives: update the registry (what repl_on_ack
* does in production) and feed the engine. */
static void standby_ack(int idx, uint64_t acked_lsn)
{
atomic_store(&g_p.repl.standbys[idx].acked_lsn, acked_lsn);
commit_on_ack(&g_ce, idx, acked_lsn);
}
static uint64_t committed(void)
{
return atomic_load(&g_p.committed_lsn);
}
static uint64_t mono_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u;
}
/* ── Test 1: per-LSN evaluation — ACK for LSN M never commits N > M ───────
* Kills the `acked_lsn > 0` ("ACKed anything ever") counting. (Req 1.2) */
static void test_ack_for_lower_lsn_does_not_commit_higher_entry(void)
{
/* Arrange: 3-member cluster, entries 1..3 appended */
engine_setup(3, 2, 3);
append_entries(3);
/* Act: standby 0 ACKs only LSN M=1 */
standby_ack(0, 1);
/* Assert: entry N=3 is NOT committed (dishonest counting would have
* counted "ACKed anything" and committed 3); entry 1 IS committed
* (self + standby 0 cover it) */
TEST_ASSERT_TRUE_MESSAGE(committed() < 3,
"ACK for LSN 1 must not commit entry 3");
TEST_ASSERT_EQUAL_UINT64(1, committed());
engine_teardown();
printf("test_ack_for_lower_lsn_does_not_commit_higher_entry: PASS\n");
}
/* ── Test 2: ACK watermark >= N from one standby plus self commits N ──────
* (Req 1.2, 2.1) */
static void test_ack_at_entry_plus_self_commits_entry(void)
{
/* Arrange: 3-member cluster, entries 1..2 appended */
engine_setup(3, 2, 2);
append_entries(2);
/* Act: standby 0's watermark covers entry 2 (self + sb0 = 2/3) */
standby_ack(0, 2);
/* Assert: entry 2 committed */
TEST_ASSERT_EQUAL_UINT64(2, committed());
engine_teardown();
printf("test_ack_at_entry_plus_self_commits_entry: PASS\n");
}
/* ── Test 3: cumulative / out-of-order watermarks → majority median ───────
* committed_lsn always advances to the highest LSN covered by self + a
* majority of standby watermarks, contiguously. (Req 2.1, 2.3) */
static void test_out_of_order_watermarks_advance_to_majority_median(void)
{
/* Arrange: 3-member cluster, entries 1..5 appended */
engine_setup(3, 2, 5);
append_entries(5);
/* Act + Assert, step by step (cumulative watermarks, mixed order) */
/* sb1 ACKs watermark 2 → matchIndex {self=5, sb0=0, sb1=2}: median 2 */
standby_ack(1, 2);
TEST_ASSERT_EQUAL_UINT64(2, committed());
/* sb0 ACKs watermark 4 → {self=5, sb0=4, sb1=2}: median 4 — entry 5
* still lacks majority and must NOT be committed */
standby_ack(0, 4);
TEST_ASSERT_EQUAL_UINT64(4, committed());
/* sb0 ACKs watermark 5 → {self=5, sb0=5, sb1=2}: median 5 */
standby_ack(0, 5);
TEST_ASSERT_EQUAL_UINT64(5, committed());
engine_teardown();
printf("test_out_of_order_watermarks_advance_to_majority_median: PASS\n");
}
/* ── Test 4: committed_lsn never exceeds the highest appended LSN ─────────
* (Req 2.3) */
static void test_committed_lsn_never_exceeds_appended_log(void)
{
/* Arrange: 3-member cluster, only entries 1..3 appended locally */
engine_setup(3, 2, 3);
append_entries(3);
/* Act: both standbys report watermarks beyond our appended log */
standby_ack(0, 10);
standby_ack(1, 10);
/* Assert: clamped to the highest appended LSN */
TEST_ASSERT_EQUAL_UINT64(3, committed());
engine_teardown();
printf("test_committed_lsn_never_exceeds_appended_log: PASS\n");
}
/* ── Test 5: committed_lsn is monotonic — never moves backwards ───────────
* (Req 2.2) */
static void test_committed_lsn_never_regresses(void)
{
/* Arrange: 3-member cluster, entries 1..4 appended; commit up to 4 */
engine_setup(3, 2, 4);
append_entries(4);
standby_ack(0, 4);
TEST_ASSERT_EQUAL_UINT64(4, committed());
/* Act: a regressing watermark arrives (stale/duplicate ACK) — the
* majority median over current registry state would now be 1 */
standby_ack(0, 1);
/* Assert: commit index never decreases */
TEST_ASSERT_EQUAL_UINT64(4, committed());
engine_teardown();
printf("test_committed_lsn_never_regresses: PASS\n");
}
/* ── Test 6: one far-lagging standby does not block majority commit ───────
* (Req 5.1) */
static void test_lagging_standby_does_not_block_majority_commit(void)
{
/* Arrange: 3-member cluster, entries 1..3 appended; standby 1 is far
* behind and never ACKs (watermark stays 0) */
engine_setup(3, 2, 3);
append_entries(3);
/* Act: standby 0 alone covers entry 3 — self + sb0 = majority */
standby_ack(0, 3);
/* Assert: full replication is not a confirmation precondition */
TEST_ASSERT_EQUAL_UINT64(3, committed());
engine_teardown();
printf("test_lagging_standby_does_not_block_majority_commit: PASS\n");
}
/* ── Test 7: RETAIN mode returns 0 immediately without blocking ───────────
* (interface contract; commit happens later via ACKs, Req 2.1) */
static void test_retain_mode_returns_zero_immediately(void)
{
int rc;
uint64_t t0, elapsed_ms;
/* Arrange: 3-member cluster, entry 1 appended, no ACKs outstanding */
engine_setup(3, 2, 1);
/* Act: RETAIN submit with a large timeout that must NOT be honored */
t0 = mono_ms();
rc = commit_submit(&g_ce, 1, COMMIT_MODE_RETAIN, 5000);
elapsed_ms = mono_ms() - t0;
/* Assert: immediate success, no blocking, and no commit without ACKs */
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_TRUE_MESSAGE(elapsed_ms < 1000,
"RETAIN submit must not block on the timeout");
TEST_ASSERT_EQUAL_UINT64(0, committed());
engine_teardown();
printf("test_retain_mode_returns_zero_immediately: PASS\n");
}
/* ── Test group entry point ──────────────────────────────────────────────── */
void test_commit_engine(void)
{
UNITY_BEGIN();
RUN_TEST(test_ack_for_lower_lsn_does_not_commit_higher_entry);
RUN_TEST(test_ack_at_entry_plus_self_commits_entry);
RUN_TEST(test_out_of_order_watermarks_advance_to_majority_median);
RUN_TEST(test_committed_lsn_never_exceeds_appended_log);
RUN_TEST(test_committed_lsn_never_regresses);
RUN_TEST(test_lagging_standby_does_not_block_majority_commit);
RUN_TEST(test_retain_mode_returns_zero_immediately);
UNITY_END();
}