feat(ha-repl-hardening): populate stateReport in standby heartbeat reply (task 2.3)

OPCode_nop handler now sends stateReport {applied_lsn, config_tag}
instead of nodata. Function in ha.c for unit-test linkability.
Two tests: warm state and zero-state fresh standby (Req 1.4).
This commit is contained in:
Rongsong Shen 2026-06-04 16:08:11 +00:00
parent 6bbcff62ca
commit 7dc657b1bd
5 changed files with 128 additions and 5 deletions

View file

@ -1238,6 +1238,95 @@ static void test_wal_catchup_batch_zero_entries_fallback(void)
printf("test_wal_catchup_batch_zero_entries_fallback: PASS\n");
}
/* ── Task 2.3: OPCode_nop heartbeat reply carries stateReport ──────────── */
#ifdef XCE_TESTING
/* Test seam declared in cluster.c under #ifdef XCE_TESTING —
* fills *sr from ha->applied_lsn / myconf->config_version_tag,
* sets reply_out->data_which = stateReportMsg and reply_out->data.state_report = sr.
* Req 1.1, 1.3, 1.4. */
void ha_nop_reply_state_report(ha_state_t *ha, myconf_t *myconf,
state_report_t *sr, xce_reply_t *reply_out);
/* Test 2.3-A: nop reply carries stateReport with applied_lsn and config_tag.
* Requirements 1.1, 1.3, 1.4: standby populates stateReport in every
* OPCode_nop heartbeat reply with current applied_lsn and config_version_tag. */
static void test_nop_reply_carries_state_report(void)
{
ha_state_t ha;
memset(&ha, 0, sizeof(ha));
ha_config_t cfg;
memset(&cfg, 0, sizeof(cfg));
cfg.heartbeat_timeout_ms = 10000;
cfg.election_timeout_ms = 1500;
cfg.committing_timeout_ms = 10000;
ha_init(&ha, &cfg, 0);
/* Set a known applied_lsn */
atomic_store_explicit(&ha.applied_lsn, 12345ULL, memory_order_release);
/* Build a minimal myconf with a known config_version_tag */
myconf_t myconf;
memset(&myconf, 0, sizeof(myconf));
myconf.config_version_tag = 0xABCD1234ULL;
state_report_t sr;
memset(&sr, 0, sizeof(sr));
xce_reply_t reply;
memset(&reply, 0, sizeof(reply));
ha_nop_reply_state_report(&ha, &myconf, &sr, &reply);
/* Reply must use stateReportMsg discriminant */
TEST_ASSERT_EQUAL_INT(RequestReply_data_stateReportMsg, reply.data_which);
/* state_report pointer must point at our sr */
TEST_ASSERT_EQUAL_PTR(&sr, reply.data.state_report);
/* applied_lsn must match ha->applied_lsn */
TEST_ASSERT_EQUAL_UINT64(12345ULL, sr.applied_lsn);
/* config_tag must match myconf->config_version_tag */
TEST_ASSERT_EQUAL_UINT64(0xABCD1234ULL, sr.config_tag);
ha_fini(&ha);
printf("test_nop_reply_carries_state_report: PASS\n");
}
/* Test 2.3-B: nop reply when applied_lsn==0 (fresh standby, no config).
* Requirement 1.4: config_tag reports zero when no config received yet. */
static void test_nop_reply_zero_state_fresh_standby(void)
{
ha_state_t ha;
memset(&ha, 0, sizeof(ha));
ha_config_t cfg;
memset(&cfg, 0, sizeof(cfg));
cfg.heartbeat_timeout_ms = 10000;
cfg.election_timeout_ms = 1500;
cfg.committing_timeout_ms = 10000;
ha_init(&ha, &cfg, 0);
/* applied_lsn stays at 0; config_version_tag stays at 0 → no config yet */
myconf_t myconf;
memset(&myconf, 0, sizeof(myconf));
state_report_t sr;
memset(&sr, 0, sizeof(sr));
xce_reply_t reply;
memset(&reply, 0, sizeof(reply));
ha_nop_reply_state_report(&ha, &myconf, &sr, &reply);
TEST_ASSERT_EQUAL_INT(RequestReply_data_stateReportMsg, reply.data_which);
TEST_ASSERT_EQUAL_PTR(&sr, reply.data.state_report);
TEST_ASSERT_EQUAL_UINT64(0ULL, sr.applied_lsn);
TEST_ASSERT_EQUAL_UINT64(0ULL, sr.config_tag);
ha_fini(&ha);
printf("test_nop_reply_zero_state_fresh_standby: PASS\n");
}
#endif /* XCE_TESTING */
#endif /* XCE_TESTING */
/* ── dispatch ───────────────────────────────────── */
@ -1275,6 +1364,9 @@ void test_config_repl(void)
RUN_TEST(test_wal_catchup_partial_batch);
RUN_TEST(test_wal_catchup_batch_error_entry_skipped);
RUN_TEST(test_wal_catchup_batch_zero_entries_fallback);
/* Task 2.3: OPCode_nop heartbeat reply carries stateReport */
RUN_TEST(test_nop_reply_carries_state_report);
RUN_TEST(test_nop_reply_zero_state_fresh_standby);
#endif
/* Task A: raw capnp shims */
RUN_TEST(test_replconfig_req_ssl_encode);