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:
parent
06135954ff
commit
d5a0f9c64d
10 changed files with 757 additions and 283 deletions
|
|
@ -4,7 +4,7 @@
|
|||
> Verify exact line numbers with grep before editing (research.md table has the verified anchors; files drift).
|
||||
|
||||
- [ ] 1. Commit engine core: per-LSN evaluation and commit-index integrity
|
||||
- [ ] 1.1 Write RED tests for per-LSN majority evaluation and commit-index integrity
|
||||
- [x] 1.1 Write RED tests for per-LSN majority evaluation and commit-index integrity
|
||||
- Create `testsuite/test_commit_engine.c` against a stub `persist_t` + `replicator_t` (XCE_TESTING pattern, AAA structure) and register it in `controller_unit_test_src` (meson.build); create `src/controller/include/commit.h` with the umbrella-verbatim interface (types + prototypes only) so tests compile
|
||||
- Tests: with n_peers=3, a standby ACK for LSN M does not commit entry N>M (kills `acked_lsn > 0` counting); ACK watermark >= N from one standby plus self commits N; cumulative/out-of-order watermarks advance `committed_lsn` to the majority median, never past the highest appended LSN, never backwards; one far-lagging standby does not block majority commit; RETAIN mode returns 0 immediately without blocking
|
||||
- Observable completion: build succeeds with the new header and test file; all new test cases FAIL (red) against a stub `commit.c` or unlinked implementation
|
||||
|
|
@ -78,3 +78,10 @@
|
|||
- Audit: grep confirms no production (engine-wired) walAck path reaches the `acked_lsn > 0` counting or the `n_active == 0 → success` branch; both survive only under the deprecated `ce == NULL` test fallback
|
||||
- Observable completion: full testsuite green under ASAN; audit results recorded in the task completion notes
|
||||
- _Requirements: 1.1, 1.2, 1.3, 2.1, 2.2, 2.3, 3.1, 3.2, 3.3, 4.1, 4.2, 4.3, 4.4, 5.1, 6.1, 6.2, 6.3, 7.1, 7.2, 7.3_
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- (1.1) THREE PRE-EXISTING suite failures, masked until now (bug-411): test_committed_lsn_does_not_advance_on_timeout (-110), test_repl_ack_after_promote_gate_allows_jobsub (-10013), test_rdhup_primary_dispatch_gate_opens (-10013). Masking cause: each test group runs its own UNITY_BEGIN/END so the process exit code reflects only the LAST group. They live in repl/persist sync-append and quorum-gate paths — tasks 1.2/2.x/5.x touch those areas and 6.1 requires full green, so they must be fixed (or proven fixed by the engine) along the way.
|
||||
- (1.1) Full-suite output under default ASAN dies at exit via LeakSanitizer (pre-existing leaks in worker_join/rdhup_integration/start_election_async/liveness groups) without flushing stdout — use ASAN_OPTIONS=detect_leaks=0 for full-run logs or run groups standalone with -t.
|
||||
- (1.1) repl_add_standby returns the SLOT INDEX, not 0 (bug-410 note for fixture writers).
|
||||
- (1.1) commit.c is registered ONLY in controller_unit_test_src for now; production target linkage lands with task 5.3.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# anatomy.md
|
||||
|
||||
> Auto-maintained by OpenWolf. Last scanned: 2026-06-11T13:00:00.755Z
|
||||
> Files: 506 tracked | Anatomy hits: 0 | Misses: 0
|
||||
> Auto-maintained by OpenWolf. Last scanned: 2026-06-11T14:16:52.677Z
|
||||
> Files: 511 tracked | Anatomy hits: 0 | Misses: 0
|
||||
|
||||
## ./
|
||||
|
||||
|
|
@ -262,7 +262,7 @@
|
|||
- `requirements.md` — Requirements Document (~2715 tok)
|
||||
- `research.md` — Research Log: ha-commit-engine (~1345 tok)
|
||||
- `spec.json` (~140 tok)
|
||||
- `tasks.md` — Implementation Plan: ha-commit-engine (~2682 tok)
|
||||
- `tasks.md` — Implementation Plan: ha-commit-engine (~2947 tok)
|
||||
|
||||
## .kiro/specs/ha-config-bootstrap/
|
||||
|
||||
|
|
@ -703,3 +703,17 @@
|
|||
- `6664f5dc160a6acfc03af4d9881c53f8f6c650ac384b1ad9b3db47d896800af9.json` (~656 tok)
|
||||
- `669931b38fdf0060e974d68dd28abc43e49a1907405848ce9d8b3b3901518313.json` (~376 tok)
|
||||
- `66cc2d1ad85554725cecb1f215d375511474a58829464647a0d8c109818d9804.json` (~57 tok)
|
||||
|
||||
## src/controller/
|
||||
|
||||
- `commit.c` — STUB commit engine (ha-commit-engine task 1.1 RED): init/fini real, commit_submit -ENOSYS, commit_on_ack no-op; real impl in task 1.2 (~474 tok)
|
||||
- `meson.build` (~1094 tok)
|
||||
|
||||
## src/controller/include/
|
||||
|
||||
- `commit.h` — commit engine service interface (umbrella-verbatim): commit_engine_t, WAIT/RETAIN modes, init/fini/submit/on_ack/set_writable_lsn/on_membership_change (~771 tok)
|
||||
|
||||
## src/controller/testsuite/
|
||||
|
||||
- `controller_test.c` — include <argp.h> (~1323 tok)
|
||||
- `test_commit_engine.c` — RED tests for per-LSN majority + commit-index integrity (Reqs 1.2, 2.1-2.3, 5.1): 7 cases against stub persist_t+replicator_t (~2459 tok)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -34,6 +34,10 @@
|
|||
- Heartbeat freshness source of truth (2026-06-09): `ha_heartbeat_is_fresh` derives from `ha->last_activity` (whole CLOCK_MONOTONIC seconds, written by ha_on_heartbeat_received, lock-free time_t read). The old `last_heartbeat` timespec field was REMOVED from ha_state_t — do not reintroduce it. The standby vote-deny gate is `role==CTRL_STANDBY && primary_ctrl_fd >= 0 && ha_heartbeat_is_fresh(ha)`; RDHUP resets primary_ctrl_fd to -1 so a standby whose primary died can grant votes immediately even if last_activity is still fresh.
|
||||
- raft_init restore contract (2026-06-09): persistence writes (term, voted_for) as a pair, so `raft_init` must set `voted_for_term = initial_term` when `initial_voted_for != -1`. Leaving it 0 lets a restarted node double-vote in the same term (Election Safety violation). Covered by test_proto_H6.
|
||||
|
||||
- repl_add_standby returns the SLOT INDEX (>=0) on success, not 0 (2026-06-11, ha-commit-engine task 1.1): asserting `== 0` breaks on the second standby. Assert `>= 0` or the expected index.
|
||||
- Unity harness quirks in controller_unit_test (2026-06-11): (a) failure lines misattribute the FILE — Unity.TestFile comes from whichever group's UNITY_BEGIN ran, so a failure printed as test_repl_unit.c:294 may live in test_p2_integration.c; locate the actual test by grepping for the test NAME. (b) Each group's UNITY_BEGIN resets counters, so the process exit code only reflects the LAST group — failures in earlier groups are masked (exit=0); meson reports green despite real failures. Grep output for FAIL with `grep FAIL` on the raw log (ANSI codes sit between ':' and 'FAIL', so `grep ":FAIL"` misses them).
|
||||
- 3 pre-existing controller suite failures on persistent_ha baseline (2026-06-11, verified by stash/rebuild without ha-commit-engine changes, see bug-411): test_committed_lsn_does_not_advance_on_timeout (rc -110 vs expected 0 — persist_wal_sync_append now returns -ETIMEDOUT, test expects old rc==0+sync_rc contract), test_repl_ack_after_promote_gate_allows_jobsub and test_rdhup_primary_dispatch_gate_opens (both -10013: ha_has_write_quorum false, standby_endpoints empty with n_peers=2).
|
||||
|
||||
## Do-Not-Repeat
|
||||
|
||||
<!-- Mistakes made and corrected. Each entry prevents the same mistake recurring. -->
|
||||
|
|
|
|||
|
|
@ -860,3 +860,13 @@
|
|||
| 05:57 | Edited .kiro/steering/roadmap.md | 6→8 lines | ~365 |
|
||||
| 11:40 | kiro-spec-batch complete: 4 sub-specs generated (3 waves), cross-spec review passed after 6 doc fixes (1 by fix-agent before spend limit, rest verified/applied); roadmap marked [x] | 4 spec dirs, roadmap.md, umbrella design.md | 51 sub-tasks total, ready for /kiro-impl ha-commit-engine | ~700k |
|
||||
| 05:57 | Session end: 99 writes across 12 files (brief.md, project_ha_consistency_invariants.md, product.md, spec.json, requirements.md) | 69 reads | ~240145 tok |
|
||||
| 06:26 | Created src/controller/include/commit.h | — | ~771 |
|
||||
| 06:27 | Created src/controller/commit.c | — | ~474 |
|
||||
| 06:28 | Created src/controller/testsuite/test_commit_engine.c | — | ~2440 |
|
||||
| 06:28 | Edited src/controller/meson.build | 2→4 lines | ~30 |
|
||||
| 06:28 | Edited src/controller/testsuite/controller_test.c | 1→2 lines | ~20 |
|
||||
| 06:28 | Edited src/controller/testsuite/controller_test.c | 3→4 lines | ~22 |
|
||||
| 06:32 | Edited src/controller/testsuite/test_commit_engine.c | modified for() | ~75 |
|
||||
| 13:30 | Task 1.1 ha-commit-engine RED: commit.h + stub commit.c + test_commit_engine.c (7 failing tests) | src/controller/{include/commit.h,commit.c,testsuite/test_commit_engine.c,meson.build,testsuite/controller_test.c} | 7/7 RED for right reasons; 3 suite failures proven pre-existing via baseline stash run | ~60k |
|
||||
| 07:16 | Edited .kiro/specs/ha-commit-engine/tasks.md | inline fix | ~23 |
|
||||
| 07:16 | Edited .kiro/specs/ha-commit-engine/tasks.md | modified now() | ~344 |
|
||||
|
|
|
|||
68
src/controller/commit.c
Normal file
68
src/controller/commit.c
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* commit.c — STUB for ha-commit-engine task 1.1 (RED phase).
|
||||
*
|
||||
* Behavioral functions are deliberately inert (-ENOSYS / no-op) so the
|
||||
* task 1.1 tests in testsuite/test_commit_engine.c compile, link, and FAIL
|
||||
* on their assertions. init/fini are real enough that tests exercise the
|
||||
* engine's behavioral contract rather than failing at fixture setup.
|
||||
*
|
||||
* The real implementation lands in task 1.2 (per-LSN majority evaluation,
|
||||
* monotonic/contiguous committed_lsn advancement).
|
||||
*/
|
||||
#include <errno.h>
|
||||
|
||||
#include "commit.h"
|
||||
|
||||
int commit_engine_init(commit_engine_t* ce, persist_t* p, int n_peers, int n_vcpus)
|
||||
{
|
||||
if (ce == NULL || p == NULL || n_peers < 0 || n_vcpus < 1) {
|
||||
return -EINVAL;
|
||||
}
|
||||
pthread_mutex_init(&ce->lock, NULL);
|
||||
pthread_cond_init(&ce->cond, NULL);
|
||||
ce->p = p;
|
||||
ce->n_peers = n_peers;
|
||||
ce->n_vcpus = n_vcpus;
|
||||
ce->inflight_waits = 0;
|
||||
ce->writable_lsn = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void commit_engine_fini(commit_engine_t* ce)
|
||||
{
|
||||
if (ce == NULL) {
|
||||
return;
|
||||
}
|
||||
pthread_cond_destroy(&ce->cond);
|
||||
pthread_mutex_destroy(&ce->lock);
|
||||
ce->p = NULL;
|
||||
}
|
||||
|
||||
int commit_submit(commit_engine_t* ce, uint64_t lsn, commit_mode_t mode,
|
||||
uint32_t timeout_ms)
|
||||
{
|
||||
(void)ce;
|
||||
(void)lsn;
|
||||
(void)mode;
|
||||
(void)timeout_ms;
|
||||
return -ENOSYS; /* stub: real WAIT/RETAIN semantics in task 1.2/2.2 */
|
||||
}
|
||||
|
||||
void commit_on_ack(commit_engine_t* ce, int standby_idx, uint64_t acked_lsn)
|
||||
{
|
||||
(void)ce;
|
||||
(void)standby_idx;
|
||||
(void)acked_lsn;
|
||||
/* stub: never advances committed_lsn */
|
||||
}
|
||||
|
||||
void commit_set_writable_lsn(commit_engine_t* ce, uint64_t noop_lsn)
|
||||
{
|
||||
(void)ce;
|
||||
(void)noop_lsn;
|
||||
}
|
||||
|
||||
void commit_on_membership_change(commit_engine_t* ce)
|
||||
{
|
||||
(void)ce;
|
||||
}
|
||||
61
src/controller/include/commit.h
Normal file
61
src/controller/include/commit.h
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
#pragma once
|
||||
/*
|
||||
* commit.h — Commit engine: single authority for per-entry commit evaluation,
|
||||
* committed_lsn advancement, and waiter completion.
|
||||
*
|
||||
* Service interface is umbrella-verbatim from
|
||||
* .kiro/specs/ha-consistency-invariants/design.md (sliced by ha-commit-engine).
|
||||
*
|
||||
* All functions thread-safe; commit_on_ack callable from iomgr (non-blocking).
|
||||
*/
|
||||
#include <pthread.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "persist.h" /* persist_t (owns committed_lsn atomic + replicator_t registry) */
|
||||
|
||||
/* Headroom kept free of blocking commit waits; cap = max(1, n_vcpus - reserved) */
|
||||
#define COMMIT_WAIT_RESERVED 2
|
||||
|
||||
typedef enum { COMMIT_MODE_WAIT, COMMIT_MODE_RETAIN } commit_mode_t;
|
||||
|
||||
/*
|
||||
* Commit engine state — in-memory only (commit state volatile, Raft-conformant).
|
||||
* Per-standby view delegated to standby_conn_t.acked_lsn / in_catch_up via
|
||||
* p->repl (no duplication).
|
||||
*/
|
||||
typedef struct commit_engine {
|
||||
pthread_mutex_t lock;
|
||||
pthread_cond_t cond; /* broadcast on committed_lsn advance / membership change */
|
||||
persist_t* p; /* back-reference: committed_lsn store + repl registry */
|
||||
int n_peers;
|
||||
int n_vcpus;
|
||||
int inflight_waits; /* <= max(1, n_vcpus - COMMIT_WAIT_RESERVED) */
|
||||
uint64_t writable_lsn; /* own-term gate; 0 = no gate */
|
||||
uint64_t scratch[MAX_STANDBYS]; /* sorted matchIndex scratch array */
|
||||
} commit_engine_t;
|
||||
|
||||
/* Lifecycle. init validates n_peers >= 0, n_vcpus >= 1, non-NULL persist_t;
|
||||
* returns -EINVAL otherwise. Call after n_peers and n_vcpus are final. */
|
||||
int commit_engine_init(commit_engine_t* ce, persist_t* p, int n_peers, int n_vcpus);
|
||||
void commit_engine_fini(commit_engine_t* ce);
|
||||
|
||||
/* Register entry at append time. RETAIN returns 0 immediately.
|
||||
* WAIT blocks up to timeout_ms: 0 = committed; -ETIMEDOUT = not committed
|
||||
* (entry remains in log; outcome resolves per umbrella 2.5); -XCE_ERR_NOT_READY =
|
||||
* admission cap exhausted (only cause). An unmet own-term gate does NOT fail
|
||||
* fast: the gate is part of the completion predicate, so WAIT blocks within
|
||||
* timeout_ms until the gate opens, surfacing -ETIMEDOUT if it never does. */
|
||||
int commit_submit(commit_engine_t* ce, uint64_t lsn, commit_mode_t mode,
|
||||
uint32_t timeout_ms);
|
||||
|
||||
/* iomgr path: record standby ACK, evaluate majority, advance committed_lsn
|
||||
* contiguously, wake satisfied waiters. Never blocks. */
|
||||
void commit_on_ack(commit_engine_t* ce, int standby_idx, uint64_t acked_lsn);
|
||||
|
||||
/* Promotion: set own-term gate to the no-op entry's LSN.
|
||||
* (API ships here; no caller in this spec — unset = no gate.) */
|
||||
void commit_set_writable_lsn(commit_engine_t* ce, uint64_t noop_lsn);
|
||||
|
||||
/* Quorum-membership change (standby add/remove/catch-up transition):
|
||||
* re-evaluate pending entries; standbys in catch-up are excluded. */
|
||||
void commit_on_membership_change(commit_engine_t* ce);
|
||||
|
|
@ -119,6 +119,8 @@ controller_unit_test_src = [
|
|||
'testsuite/test_ha_connection_reuse.c',
|
||||
'testsuite/test_ha_rdhup_integration.c',
|
||||
'testsuite/test_ha_liveness.c',
|
||||
'testsuite/test_commit_engine.c',
|
||||
'commit.c',
|
||||
'startup_validation.c',
|
||||
'worker.c',
|
||||
'persist.c',
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ extern void test_ha_repl_ack(void);
|
|||
extern void test_ha_connection_reuse(void);
|
||||
extern void test_ha_rdhup_integration(void);
|
||||
extern void test_ha_liveness(void);
|
||||
extern void test_commit_engine(void);
|
||||
|
||||
void setUp(void) { return; }
|
||||
|
||||
|
|
@ -169,6 +170,7 @@ int main(int argc, char* argv[])
|
|||
TEST(test_ha_connection_reuse);
|
||||
TEST(test_ha_rdhup_integration);
|
||||
TEST(test_ha_liveness);
|
||||
TEST(test_commit_engine);
|
||||
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
|
|||
270
src/controller/testsuite/test_commit_engine.c
Normal file
270
src/controller/testsuite/test_commit_engine.c
Normal 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();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue