/* test_ha_vote_floor_heartbeat.c — characterization test for * ha-heartbeat-committed-propagation task 3.2 (Req 3.2). * * SAFETY PROPERTY UNDER TEST (no production change in this task): * The EXISTING vote-freshness floor is `max(durable_floor, last_known_committed_lsn)` * (computed by ha_vote_self_freshness; design "Security Considerations"). This * feature only RAISES `last_known_committed_lsn` via a heartbeat * (ha_observe_heartbeat_committed). We prove that raising it through a * heartbeat: * 1. flows UNCHANGED into the floor (floor == max(durable_floor, RAISED)), * 2. REJECTS a vote candidate whose last-log LSN is below the raised floor, * 3. ACCEPTS a candidate at/above it, * 4. never LOWERS the floor below the durable floor (monotone strengthening). * * This is a VERIFICATION/CHARACTERIZATION test: it must PASS against the * already-landed code with NO production edit. If it failed, the safety * property would NOT hold — a spec finding, not a green-on-new-code cycle. * * The gate functions exercised are the SAME ones the existing vote tests use * (handle_request_vote + ha_vote_self_freshness in ha.c) — no new gate invented. * * Covers Requirement 3.2 (advisory committed must not LOWER/WEAKEN the * durability-backed vote floor; raising it only strengthens Leader Completeness). */ #define _GNU_SOURCE #include #include #include #include #include #include #include "ha.h" #include "xce/cluster_def.h" /* full cluster_t (embedded ha_state_t) */ /* Functions under test — the EXISTING vote gate (declared to match ha.c). */ int handle_request_vote(ha_state_t *ha, uint64_t term, const char *candidate_id, uint64_t config_tag, uint64_t last_lsn, uint64_t my_config_tag, uint64_t my_last_lsn, int in_catch_up_flag); void ha_vote_self_freshness(ha_state_t *ha, uint64_t local_config_tag, uint64_t local_applied_lsn, uint64_t *out_tag, uint64_t *out_lsn); /* ── shared config ──────────────────────────────────────────────── */ static ha_config_t vff_cfg(void) { ha_config_t cfg = { .heartbeat_interval_ms = 3000, .heartbeat_timeout_ms = 10000, .election_timeout_ms = 2000, .probe_connect_timeout_ms = 500, .max_promotion_retries = 5, }; return cfg; } /* Open the role gate so handle_request_vote falls through to the freshness * floor: STANDBY, no live primary connection (fd<0), stale heartbeat. */ static void vff_open_role_gate(ha_state_t *ha) { atomic_store_explicit(&ha->role, (int32_t)CTRL_STANDBY, memory_order_release); atomic_store_explicit(&ha->term, 1ULL, memory_order_release); ha->voted_for_term = 0; ha->primary_ctrl_fd = -1; struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); ha->last_activity = now.tv_sec - 60; /* 60 s ago → stale */ } /* The durable floor enters the gate as the `local_applied_lsn` / `my_last_lsn` * argument (production cluster.c feeds persist_durable_floor there — see * test_ha_vote_gate.c task-4.2 tests). We model that durable floor as a plain * constant input so this test stays focused on the heartbeat→floor flow without * a real persist fixture. */ #define DURABLE_FLOOR 5ULL #define INITIAL_LEARNED 3ULL /* < DURABLE_FLOOR: floor starts at durable */ #define RAISED_LSN 9ULL /* heartbeat raises learned ABOVE durable */ /* ── (1) the heartbeat value flows into the floor and RAISES it ──────────── */ static void test_heartbeat_raises_vote_floor_to_max(void) { cluster_t c; memset(&c, 0, sizeof(c)); ha_config_t cfg = vff_cfg(); TEST_ASSERT_EQUAL_INT(0, ha_init(&c.ha, &cfg, 0)); /* Initial learned-committed is BELOW the durable floor → floor==durable. */ atomic_store_explicit(&c.ha.last_known_committed_lsn, INITIAL_LEARNED, memory_order_release); uint64_t tag0 = 0, lsn0 = 0; ha_vote_self_freshness(&c.ha, 0ULL /*local tag*/, DURABLE_FLOOR, &tag0, &lsn0); TEST_ASSERT_EQUAL_UINT64_MESSAGE(DURABLE_FLOOR, lsn0, "before the heartbeat the floor must be max(durable=5, learned=3) = 5"); /* A heartbeat raises the learned committed ABOVE the durable floor. The tag * argument is the heartbeat's config_tag (advisory, orthogonal here). */ ha_observe_heartbeat_committed(&c, RAISED_LSN, 0xAB); TEST_ASSERT_EQUAL_UINT64_MESSAGE(RAISED_LSN, atomic_load_explicit(&c.ha.last_known_committed_lsn, memory_order_acquire), "heartbeat must have raised last_known_committed_lsn to 9"); /* The EXISTING floor now consumes the raised value: max(5, 9) = 9. */ uint64_t tag1 = 0, lsn1 = 0; ha_vote_self_freshness(&c.ha, 0ULL, DURABLE_FLOOR, &tag1, &lsn1); TEST_ASSERT_EQUAL_UINT64_MESSAGE(RAISED_LSN, lsn1, "after the heartbeat the floor must be max(durable=5, learned=9) = 9 — " "the raised last_known_committed_lsn flowed in UNCHANGED (Req 3.2)"); /* Monotone strengthening: the floor was RAISED (9 > 5), never lowered. */ TEST_ASSERT_TRUE_MESSAGE(lsn1 >= lsn0, "the heartbeat may only RAISE the floor, never lower it (Req 3.2)"); TEST_ASSERT_TRUE_MESSAGE(lsn1 >= DURABLE_FLOOR, "the floor must never drop below the durable floor (Req 3.2)"); ha_fini(&c.ha); printf("test_heartbeat_raises_vote_floor_to_max: PASS\n"); } /* ── (2) a candidate BELOW the raised floor is REJECTED ──────────────────── */ static void test_candidate_below_raised_floor_rejected(void) { cluster_t c; memset(&c, 0, sizeof(c)); ha_config_t cfg = vff_cfg(); TEST_ASSERT_EQUAL_INT(0, ha_init(&c.ha, &cfg, 0)); vff_open_role_gate(&c.ha); atomic_store_explicit(&c.ha.last_known_committed_lsn, INITIAL_LEARNED, memory_order_release); /* Heartbeat raises the learned floor to 9 (> durable 5). */ ha_observe_heartbeat_committed(&c, RAISED_LSN, 0xAB); /* Candidate's last-log LSN (7) is ABOVE the durable floor (5) and ABOVE the * stale initial learned (3) but BELOW the raised floor (9). WITHOUT the * heartbeat raising the floor this candidate would have been fresh enough; * because the floor now consumes the raised learned value, it is DENIED. * config_tag axis is held equal (0) so only the LSN axis decides. */ int granted = handle_request_vote(&c.ha, 1ULL, /* term */ "ctrl-laggard", 0ULL, /* cand config_tag == floor tag */ 7ULL, /* cand last_lsn: 5 < 7 < 9 */ 0ULL, /* my local config_tag */ DURABLE_FLOOR, /* my local (durable) lsn = 5 */ 0); TEST_ASSERT_EQUAL_INT_MESSAGE(0, granted, "a candidate (lsn=7) below the heartbeat-raised floor (9) must be DENIED " "— the raised last_known_committed_lsn strengthened the gate (Req 3.2)"); ha_fini(&c.ha); printf("test_candidate_below_raised_floor_rejected: PASS\n"); } /* ── (3) a candidate AT or ABOVE the raised floor is ACCEPTED ────────────── */ static void test_candidate_at_or_above_raised_floor_accepted(void) { /* Candidate AT the raised floor (lsn == 9) → GRANT. */ cluster_t c; memset(&c, 0, sizeof(c)); ha_config_t cfg = vff_cfg(); TEST_ASSERT_EQUAL_INT(0, ha_init(&c.ha, &cfg, 0)); vff_open_role_gate(&c.ha); atomic_store_explicit(&c.ha.last_known_committed_lsn, INITIAL_LEARNED, memory_order_release); ha_observe_heartbeat_committed(&c, RAISED_LSN, 0xAB); int granted_at = handle_request_vote(&c.ha, 1ULL, "ctrl-fresh", 0ULL, RAISED_LSN, /* cand lsn == floor (9) */ 0ULL, DURABLE_FLOOR, 0); TEST_ASSERT_EQUAL_INT_MESSAGE(1, granted_at, "a candidate AT the heartbeat-raised floor (lsn=9) must be GRANTED — " "the floor is satisfiable, no liveness wedge (Req 3.2)"); ha_fini(&c.ha); /* Candidate ABOVE the raised floor (lsn == 12) → GRANT. */ cluster_t c2; memset(&c2, 0, sizeof(c2)); ha_config_t cfg2 = vff_cfg(); TEST_ASSERT_EQUAL_INT(0, ha_init(&c2.ha, &cfg2, 0)); vff_open_role_gate(&c2.ha); atomic_store_explicit(&c2.ha.last_known_committed_lsn, INITIAL_LEARNED, memory_order_release); ha_observe_heartbeat_committed(&c2, RAISED_LSN, 0xAB); int granted_above = handle_request_vote(&c2.ha, 1ULL, "ctrl-fresher", 0ULL, 12ULL, /* cand lsn > floor (9) */ 0ULL, DURABLE_FLOOR, 0); TEST_ASSERT_EQUAL_INT_MESSAGE(1, granted_above, "a candidate ABOVE the heartbeat-raised floor (lsn=12) must be GRANTED"); ha_fini(&c2.ha); printf("test_candidate_at_or_above_raised_floor_accepted: PASS\n"); } /* ── (4) a non-advancing heartbeat NEVER lowers the floor ────────────────── */ static void test_lower_heartbeat_never_lowers_floor(void) { /* Guards the "never weaken" half of Req 3.2: once the floor is raised to 9, * a later heartbeat carrying a LOWER committed (4) must NOT pull the floor * back down — the candidate that was denied (7 < 9) stays denied. */ cluster_t c; memset(&c, 0, sizeof(c)); ha_config_t cfg = vff_cfg(); TEST_ASSERT_EQUAL_INT(0, ha_init(&c.ha, &cfg, 0)); vff_open_role_gate(&c.ha); atomic_store_explicit(&c.ha.last_known_committed_lsn, INITIAL_LEARNED, memory_order_release); ha_observe_heartbeat_committed(&c, RAISED_LSN, 0xAB); /* floor → 9 */ /* A lagging/out-of-order heartbeat carrying committed=4 (< current 9). */ ha_observe_heartbeat_committed(&c, 4ULL, 0xCD); uint64_t tag = 0, lsn = 0; ha_vote_self_freshness(&c.ha, 0ULL, DURABLE_FLOOR, &tag, &lsn); TEST_ASSERT_EQUAL_UINT64_MESSAGE(RAISED_LSN, lsn, "a lower/out-of-order heartbeat must NOT lower the floor below 9 (Req 3.2)"); /* The below-floor candidate (7) is still DENIED — the floor held. */ int granted = handle_request_vote(&c.ha, 1ULL, "ctrl-laggard", 0ULL, 7ULL, 0ULL, DURABLE_FLOOR, 0); TEST_ASSERT_EQUAL_INT_MESSAGE(0, granted, "after a lower heartbeat the raised floor must still DENY the lagging " "candidate (the floor was never weakened) (Req 3.2)"); ha_fini(&c.ha); printf("test_lower_heartbeat_never_lowers_floor: PASS\n"); } /* ── suite entry point ───────────────────────────────────────────── */ void test_ha_vote_floor_heartbeat(void) { UnitySetTestFile(__FILE__); RUN_TEST(test_heartbeat_raises_vote_floor_to_max); RUN_TEST(test_candidate_below_raised_floor_rejected); RUN_TEST(test_candidate_at_or_above_raised_floor_accepted); RUN_TEST(test_lower_heartbeat_never_lowers_floor); }