feat(ha-term-safety): WAL term seam + recovery read-back + legacy-0 rule (task 1.3)
persist_set_term sets the live election term so appends stamp hdr.term (fixes the G1 term-0 stamping); replay_wal builds an in-memory per-LSN term index (freed in persist_fini, no on-disk change); persist_entry_term reads it back (0=absent). Legacy-0 -> WAL_LEGACY_TERM=1 at read-back only (raw 0 retained so present-legacy-0 is distinguishable from absent). 40-byte wal_entry_hdr_t preserved (value fix, R1.7). 5 new tests (test_term_safety_adapter.c); reviewer APPROVED. Feeds task 2.2. All controller test groups report 0 failures; the meson full-suite timeout is the bug-924 overlayfs /tmp fdatasync artifact, not this task.
This commit is contained in:
parent
a95d57f733
commit
d540ebd183
6 changed files with 420 additions and 1 deletions
229
src/controller/testsuite/test_term_safety_adapter.c
Normal file
229
src/controller/testsuite/test_term_safety_adapter.c
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
#define _GNU_SOURCE
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "persist.h"
|
||||
#include "wal_types.h"
|
||||
|
||||
/* ha-term-safety task 1.3 — WAL term seam + recovery read-back + legacy-0 rule.
|
||||
*
|
||||
* The seam under test:
|
||||
* - persist_set_term(p, T) sets the controller's live election term so a
|
||||
* subsequent append stamps hdr.term = T (R1.1, R1.6).
|
||||
* - replay_wal retains each entry's hdr.term per LSN in an in-memory index
|
||||
* (R1.5), read back via persist_entry_term(p, lsn).
|
||||
* - persist_entry_term returns 0 for an absent LSN; a legacy entry carrying
|
||||
* hdr.term == 0 maps to the single lowest-known real Raft term (1) so a
|
||||
* legacy-0 entry is never spuriously divergent from another legacy-0 entry
|
||||
* and recovery never fails on legacy-0 (R1.8).
|
||||
* - the on-disk wal_entry_hdr_t stays 40 bytes (R1.7).
|
||||
*/
|
||||
|
||||
/* The defined legacy-0 mapping (lowest real Raft term). Kept local to the test
|
||||
* so the assertion is independent of the implementation constant. */
|
||||
#define LEGACY_LOWEST_TERM 1u
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static persist_t* make_fresh_persist(const char* tmpdir)
|
||||
{
|
||||
persist_t* p = calloc(1, sizeof(persist_t));
|
||||
if (!p) return NULL;
|
||||
|
||||
persist_config_t cfg = persist_config_defaults();
|
||||
cfg.wal_sync_ms = 5000; /* slow fsync — no races in tests */
|
||||
cfg.snap_keep_n = 2;
|
||||
|
||||
persist_set_workdir(p, tmpdir, "ctrl-test");
|
||||
|
||||
if (persist_init(p, &cfg, NULL) != 0) {
|
||||
free(p);
|
||||
return NULL;
|
||||
}
|
||||
if (persist_start(p) != 0) {
|
||||
persist_fini(p);
|
||||
free(p);
|
||||
return NULL;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
static void teardown_persist(persist_t* p)
|
||||
{
|
||||
persist_stop(p);
|
||||
persist_fini(p);
|
||||
free(p);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Test: header layout preserved (R1.7) — value fix, not a format */
|
||||
/* change. The on-disk WAL entry header must stay exactly 40 bytes. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void test_wal_entry_hdr_size_is_40(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_UINT64(40u, (uint64_t)sizeof(wal_entry_hdr_t));
|
||||
printf("test_wal_entry_hdr_size_is_40: PASS\n");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Test: persist_set_term makes a subsequent append stamp hdr.term */
|
||||
/* (R1.1, R1.6), recovered back via persist_entry_term. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void test_set_term_stamps_append(void)
|
||||
{
|
||||
char tmp[256];
|
||||
strcpy(tmp, "/tmp/xce_term_set_XXXXXX");
|
||||
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
|
||||
|
||||
const uint64_t T = 7;
|
||||
|
||||
/* Phase 1: set the live term, then append — the append stamps hdr.term=T. */
|
||||
{
|
||||
persist_t* p = make_fresh_persist(tmp);
|
||||
TEST_ASSERT_NOT_NULL(p);
|
||||
|
||||
persist_set_term(p, T);
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_wal_async(p, WAL_JOB_UPDATE, "a", 1));
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_flush(p));
|
||||
|
||||
/* Live read-back of the just-appended entry's term (LSN 1). */
|
||||
TEST_ASSERT_EQUAL_UINT64(T, persist_entry_term(p, 1));
|
||||
|
||||
teardown_persist(p);
|
||||
}
|
||||
|
||||
/* Phase 2: fresh persist_t recovers the WAL and reads the term back. */
|
||||
{
|
||||
persist_t* p2 = make_fresh_persist(tmp);
|
||||
TEST_ASSERT_NOT_NULL(p2);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_recover(p2));
|
||||
TEST_ASSERT_EQUAL_UINT64(T, persist_entry_term(p2, 1));
|
||||
|
||||
teardown_persist(p2);
|
||||
}
|
||||
|
||||
printf("test_set_term_stamps_append: PASS\n");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Test: replay_wal of a WAL with mixed real-term and legacy-0 */
|
||||
/* entries — persist_entry_term returns the real term for real */
|
||||
/* entries and the defined lowest-known term for legacy-0 (R1.5, */
|
||||
/* R1.8). Recovery never fails on legacy-0 entries. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void test_recover_mixed_real_and_legacy_terms(void)
|
||||
{
|
||||
char tmp[256];
|
||||
strcpy(tmp, "/tmp/xce_term_mixed_XXXXXX");
|
||||
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
|
||||
|
||||
/* Phase 1: write LSN 1,2 with term 0 (legacy — the pre-feature default),
|
||||
* then LSN 3,4 with real term 5. The append path stamps hdr.term=p->term,
|
||||
* so leaving the term unset (0) produces genuine legacy-0 entries. */
|
||||
{
|
||||
persist_t* p = make_fresh_persist(tmp);
|
||||
TEST_ASSERT_NOT_NULL(p);
|
||||
|
||||
/* Legacy-0 entries: no persist_set_term call (p->term stays 0). */
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_wal_async(p, WAL_JOB_UPDATE, "l", 1)); /* lsn 1 */
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_wal_async(p, WAL_JOB_UPDATE, "m", 1)); /* lsn 2 */
|
||||
|
||||
/* Real-term entries. */
|
||||
persist_set_term(p, 5);
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_wal_async(p, WAL_JOB_UPDATE, "r", 1)); /* lsn 3 */
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_wal_async(p, WAL_JOB_UPDATE, "s", 1)); /* lsn 4 */
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_flush(p));
|
||||
teardown_persist(p);
|
||||
}
|
||||
|
||||
/* Phase 2: fresh recover — read back each LSN's term. */
|
||||
{
|
||||
persist_t* p2 = make_fresh_persist(tmp);
|
||||
TEST_ASSERT_NOT_NULL(p2);
|
||||
|
||||
/* Recovery must NOT fail on the legacy-0 entries. */
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_recover(p2));
|
||||
TEST_ASSERT_EQUAL_INT(RECOVER_CLEAN, persist_recovery_verdict(p2));
|
||||
|
||||
/* Legacy-0 entries map to the single lowest-known real term (1) — both
|
||||
* the same, so two legacy-0 entries are never seen as divergent. */
|
||||
TEST_ASSERT_EQUAL_UINT64(LEGACY_LOWEST_TERM, persist_entry_term(p2, 1));
|
||||
TEST_ASSERT_EQUAL_UINT64(LEGACY_LOWEST_TERM, persist_entry_term(p2, 2));
|
||||
|
||||
/* Real-term entries return their recorded term verbatim. */
|
||||
TEST_ASSERT_EQUAL_UINT64(5, persist_entry_term(p2, 3));
|
||||
TEST_ASSERT_EQUAL_UINT64(5, persist_entry_term(p2, 4));
|
||||
|
||||
teardown_persist(p2);
|
||||
}
|
||||
|
||||
printf("test_recover_mixed_real_and_legacy_terms: PASS\n");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Test: persist_entry_term returns 0 for an absent LSN (no entry). */
|
||||
/* Distinguishes "absent" (0) from a present legacy-0 entry (mapped */
|
||||
/* to lowest-known), which is the precision REVIEW ISSUE 2 requires. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void test_entry_term_absent_lsn_is_zero(void)
|
||||
{
|
||||
char tmp[256];
|
||||
strcpy(tmp, "/tmp/xce_term_absent_XXXXXX");
|
||||
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
|
||||
|
||||
persist_t* p = make_fresh_persist(tmp);
|
||||
TEST_ASSERT_NOT_NULL(p);
|
||||
|
||||
persist_set_term(p, 3);
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_wal_async(p, WAL_JOB_UPDATE, "a", 1)); /* lsn 1 */
|
||||
|
||||
/* LSN 0 is reserved (never an entry) and LSN 2 was never appended. */
|
||||
TEST_ASSERT_EQUAL_UINT64(0, persist_entry_term(p, 0));
|
||||
TEST_ASSERT_EQUAL_UINT64(0, persist_entry_term(p, 2));
|
||||
TEST_ASSERT_EQUAL_UINT64(0, persist_entry_term(p, 999999));
|
||||
|
||||
/* The present entry still answers its real term. */
|
||||
TEST_ASSERT_EQUAL_UINT64(3, persist_entry_term(p, 1));
|
||||
|
||||
teardown_persist(p);
|
||||
printf("test_entry_term_absent_lsn_is_zero: PASS\n");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Test: persist_entry_term is NULL-safe. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void test_entry_term_null_safe(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_UINT64(0, persist_entry_term(NULL, 1));
|
||||
/* persist_set_term must not crash on NULL. */
|
||||
persist_set_term(NULL, 9);
|
||||
printf("test_entry_term_null_safe: PASS\n");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Suite entry point */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void test_term_safety_adapter(void)
|
||||
{
|
||||
RUN_TEST(test_wal_entry_hdr_size_is_40);
|
||||
RUN_TEST(test_set_term_stamps_append);
|
||||
RUN_TEST(test_recover_mixed_real_and_legacy_terms);
|
||||
RUN_TEST(test_entry_term_absent_lsn_is_zero);
|
||||
RUN_TEST(test_entry_term_null_safe);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue