feat(ha-repl-hardening): per-standby convergence tracking from heartbeats (task 3.1)

ha_process_standby_state stores (tag, lsn) per standby, evaluates
convergence (tag>=committed && lsn>=committed), sets converged flag,
logs INFO only on transition. Wired into repl_ack_decode_and_dispatch.
5 tests: converged, lsn-behind, tag-behind, regression, multi-peer.
This commit is contained in:
Rongsong Shen 2026-06-04 16:40:44 +00:00
parent 7dc657b1bd
commit aab41fa609
4 changed files with 267 additions and 6 deletions

View file

@ -237,6 +237,146 @@ static void test_ha_init_zeroes_standby_convergence_fields(void)
printf("test_ha_init_zeroes_standby_convergence_fields: PASS\n");
}
/* ── Task 3.1: ha_process_standby_state convergence tracking ─────────────── */
/*
* Declare the function under test. It is defined in ha.c and exposed only in
* XCE_TESTING builds (forward declaration matches the implementation).
*/
#ifdef XCE_TESTING
void ha_process_standby_state(ha_state_t *ha, int peer_idx,
uint64_t reported_tag, uint64_t reported_lsn,
uint64_t committed_tag, uint64_t committed_lsn);
#endif
/* Helper: build a ha_state_t with all convergence fields zeroed. */
static ha_state_t *build_ha_for_convergence_test(void)
{
static ha_state_t ha;
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,
.committing_timeout_ms = HA_DEFAULT_COMMITTING_TIMEOUT_MS,
};
ha_init(&ha, &cfg, 0);
return &ha;
}
/*
* Test: standby with lsn == committed_lsn and tag == committed_tag → converged.
* Fields standby_reported_tag[0] and standby_reported_lsn[0] updated correctly.
* Requirements: 1.2, 2.2 (ha-repl-hardening)
*/
static void test_process_standby_state_converged(void)
{
ha_state_t *ha = build_ha_for_convergence_test();
ha_process_standby_state(ha, 0,
/*reported_tag=*/5, /*reported_lsn=*/100,
/*committed_tag=*/5, /*committed_lsn=*/100);
TEST_ASSERT_EQUAL_UINT64(5, ha->standby_reported_tag[0]);
TEST_ASSERT_EQUAL_UINT64(100, ha->standby_reported_lsn[0]);
TEST_ASSERT_TRUE(ha->standby_converged[0]);
ha_fini(ha);
printf("test_process_standby_state_converged: PASS\n");
}
/*
* Test: standby lsn < committed_lsn → not converged.
* Requirements: 1.2, 2.2
*/
static void test_process_standby_state_lsn_behind(void)
{
ha_state_t *ha = build_ha_for_convergence_test();
ha_process_standby_state(ha, 0,
/*reported_tag=*/5, /*reported_lsn=*/50,
/*committed_tag=*/5, /*committed_lsn=*/100);
TEST_ASSERT_EQUAL_UINT64(5, ha->standby_reported_tag[0]);
TEST_ASSERT_EQUAL_UINT64(50, ha->standby_reported_lsn[0]);
TEST_ASSERT_FALSE(ha->standby_converged[0]);
ha_fini(ha);
printf("test_process_standby_state_lsn_behind: PASS\n");
}
/*
* Test: standby tag < committed_tag → not converged.
* Requirements: 1.2, 2.2
*/
static void test_process_standby_state_tag_behind(void)
{
ha_state_t *ha = build_ha_for_convergence_test();
ha_process_standby_state(ha, 0,
/*reported_tag=*/3, /*reported_lsn=*/100,
/*committed_tag=*/5, /*committed_lsn=*/100);
TEST_ASSERT_EQUAL_UINT64(3, ha->standby_reported_tag[0]);
TEST_ASSERT_EQUAL_UINT64(100, ha->standby_reported_lsn[0]);
TEST_ASSERT_FALSE(ha->standby_converged[0]);
ha_fini(ha);
printf("test_process_standby_state_tag_behind: PASS\n");
}
/*
* Test: convergence regression — standby was converged; primary advances;
* next update with old values should reset standby_converged to false.
* Requirements: 2.5 (converged standby regresses → mark not-converged)
*/
static void test_process_standby_state_regression(void)
{
ha_state_t *ha = build_ha_for_convergence_test();
/* First update: converged at (tag=5, lsn=100) */
ha_process_standby_state(ha, 0,
/*reported_tag=*/5, /*reported_lsn=*/100,
/*committed_tag=*/5, /*committed_lsn=*/100);
TEST_ASSERT_TRUE(ha->standby_converged[0]);
/* Primary advances committed_lsn to 200; standby still reports (5, 100) */
ha_process_standby_state(ha, 0,
/*reported_tag=*/5, /*reported_lsn=*/100,
/*committed_tag=*/5, /*committed_lsn=*/200);
TEST_ASSERT_FALSE(ha->standby_converged[0]);
ha_fini(ha);
printf("test_process_standby_state_regression: PASS\n");
}
/*
* Test: different standby indices updated independently.
* Requirements: 2.2
*/
static void test_process_standby_state_multiple_peers(void)
{
ha_state_t *ha = build_ha_for_convergence_test();
/* peer 0: converged */
ha_process_standby_state(ha, 0,
/*reported_tag=*/5, /*reported_lsn=*/100,
/*committed_tag=*/5, /*committed_lsn=*/100);
/* peer 1: not converged (tag behind) */
ha_process_standby_state(ha, 1,
/*reported_tag=*/3, /*reported_lsn=*/100,
/*committed_tag=*/5, /*committed_lsn=*/100);
TEST_ASSERT_TRUE(ha->standby_converged[0]);
TEST_ASSERT_FALSE(ha->standby_converged[1]);
TEST_ASSERT_EQUAL_UINT64(5, ha->standby_reported_tag[0]);
TEST_ASSERT_EQUAL_UINT64(3, ha->standby_reported_tag[1]);
ha_fini(ha);
printf("test_process_standby_state_multiple_peers: PASS\n");
}
void test_ha_role(void)
{
RUN_TEST(test_ha_init_default_role_is_standby);
@ -252,6 +392,12 @@ void test_ha_role(void)
RUN_TEST(test_ha_init_zeroes_new_fields);
/* Task 1.4: ha-repl-hardening per-standby convergence fields */
RUN_TEST(test_ha_init_zeroes_standby_convergence_fields);
/* Task 3.1: ha_process_standby_state convergence tracking */
RUN_TEST(test_process_standby_state_converged);
RUN_TEST(test_process_standby_state_lsn_behind);
RUN_TEST(test_process_standby_state_tag_behind);
RUN_TEST(test_process_standby_state_regression);
RUN_TEST(test_process_standby_state_multiple_peers);
}
/* test_am_i_primary suite deleted — am_i_primary() removed (election-iomgr-refactor) */