feat(ha-wal-replication): HA→replicator standby registration bridge (tasks 2.1-2.3)

Register standbys with replicator on connect (election promotion +
late-joiner), deregister on disconnect (vote err handler + standby err
handler). repl_add_standby now idempotent (duplicate ep returns
existing slot).

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-06-10 08:35:02 +00:00
parent 18e351ee1b
commit 81dbc4d7b4
4 changed files with 542 additions and 18 deletions

View file

@ -9,6 +9,18 @@
#include <unity/unity.h>
#include "repl.h"
#include "config.h"
#include "typemap.h"
#include "wal_types.h"
/* ============================================================
* Test seam for repl_broadcast send path (task 1.1)
* Declared as extern here; defined in repl.c under XCE_TESTING.
* ============================================================ */
#ifdef XCE_TESTING
extern int (*repl_broadcast_send_hook)(void *ep,
wal_catchup_entry_t **entries,
int n_entries);
#endif
static void test_repl_init_zeroes_nstandbys(void) {
replicator_t r;
@ -160,6 +172,218 @@ static void test_wait_ack_woken_by_background_ack(void) {
printf("test_wait_ack_woken_by_background_ack: PASS\n");
}
/* ============================================================
* Task 1.1: repl_broadcast send path tests
* ============================================================ */
/* Shared hook state for send-path tests */
static int g_hook_call_count;
static void *g_hook_last_ep;
static uint64_t g_hook_last_lsn;
static uint8_t g_hook_last_entry_type;
static const void *g_hook_last_payload;
static size_t g_hook_last_payload_len;
static int g_hook_return_val; /* 0 = success, negative = error */
static int test_broadcast_hook(void *ep, wal_catchup_entry_t **entries,
int n_entries)
{
g_hook_call_count++;
g_hook_last_ep = ep;
if (n_entries >= 1 && entries && entries[0]) {
g_hook_last_lsn = entries[0]->lsn;
g_hook_last_entry_type = entries[0]->entry_type;
g_hook_last_payload = entries[0]->raw_payload;
g_hook_last_payload_len = entries[0]->raw_payload_len;
}
return g_hook_return_val;
}
static void reset_hook_state(void)
{
g_hook_call_count = 0;
g_hook_last_ep = NULL;
g_hook_last_lsn = 0;
g_hook_last_entry_type = 0;
g_hook_last_payload = NULL;
g_hook_last_payload_len = 0;
g_hook_return_val = 0;
}
/* Test: no standbys — broadcast returns 0, hook not called */
static void test_broadcast_no_standbys_returns_zero(void)
{
#ifdef XCE_TESTING
replicator_t r;
repl_init(&r);
reset_hook_state();
repl_broadcast_send_hook = test_broadcast_hook;
wal_entry_hdr_t hdr = { .lsn = 10, .type = WAL_JOB_SUBMIT };
uint8_t payload[] = { 0xAA, 0xBB };
int rc = repl_broadcast(&r, &hdr, payload, sizeof(payload));
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(0, g_hook_call_count);
repl_broadcast_send_hook = NULL;
repl_fini(&r);
printf("test_broadcast_no_standbys_returns_zero: PASS\n");
#else
printf("test_broadcast_no_standbys_returns_zero: SKIPPED (no XCE_TESTING)\n");
#endif
}
/* Test: one standby — hook called once with correct lsn, type, payload */
static void test_broadcast_one_standby_sends_entry(void)
{
#ifdef XCE_TESTING
replicator_t r;
int dummy_ep = 0;
repl_init(&r);
repl_add_standby(&r, (void *)&dummy_ep, "sb1", 7001);
reset_hook_state();
repl_broadcast_send_hook = test_broadcast_hook;
wal_entry_hdr_t hdr = { .lsn = 42, .type = WAL_JOB_SUBMIT };
uint8_t payload[] = { 1, 2, 3, 4 };
int rc = repl_broadcast(&r, &hdr, payload, sizeof(payload));
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(1, g_hook_call_count);
TEST_ASSERT_EQUAL_PTR((void *)&dummy_ep, g_hook_last_ep);
TEST_ASSERT_EQUAL_UINT64(42, g_hook_last_lsn);
TEST_ASSERT_EQUAL_UINT8(WAL_JOB_SUBMIT, g_hook_last_entry_type);
TEST_ASSERT_EQUAL_PTR(payload, g_hook_last_payload);
TEST_ASSERT_EQUAL_UINT(sizeof(payload), g_hook_last_payload_len);
repl_broadcast_send_hook = NULL;
repl_fini(&r);
printf("test_broadcast_one_standby_sends_entry: PASS\n");
#else
printf("test_broadcast_one_standby_sends_entry: SKIPPED (no XCE_TESTING)\n");
#endif
}
/* Test: standby in catch-up is skipped — hook not called for it */
static void test_broadcast_skips_catchup_standby(void)
{
#ifdef XCE_TESTING
replicator_t r;
int ep1 = 0, ep2 = 1;
repl_init(&r);
repl_add_standby(&r, (void *)&ep1, "sb1", 7001);
repl_add_standby(&r, (void *)&ep2, "sb2", 7002);
/* Mark sb1 as in catch-up */
repl_start_catchup(&r, (void *)&ep1, 0);
reset_hook_state();
repl_broadcast_send_hook = test_broadcast_hook;
wal_entry_hdr_t hdr = { .lsn = 99, .type = WAL_JOB_SUBMIT };
uint8_t payload[] = { 0xFF };
int rc = repl_broadcast(&r, &hdr, payload, sizeof(payload));
TEST_ASSERT_EQUAL_INT(0, rc);
/* Only sb2 should be sent to */
TEST_ASSERT_EQUAL_INT(1, g_hook_call_count);
TEST_ASSERT_EQUAL_PTR((void *)&ep2, g_hook_last_ep);
repl_broadcast_send_hook = NULL;
repl_fini(&r);
printf("test_broadcast_skips_catchup_standby: PASS\n");
#else
printf("test_broadcast_skips_catchup_standby: SKIPPED (no XCE_TESTING)\n");
#endif
}
/* Test: standby with pending_ack_count > MAX_PENDING_ACKS is evicted to catch-up */
static void test_broadcast_evicts_overloaded_standby(void)
{
#ifdef XCE_TESTING
replicator_t r;
int dummy_ep = 0;
repl_init(&r);
int idx = repl_add_standby(&r, (void *)&dummy_ep, "sb1", 7001);
/* Manually set pending_ack_count just above the limit */
r.standbys[idx].pending_ack_count = MAX_PENDING_ACKS + 1;
reset_hook_state();
repl_broadcast_send_hook = test_broadcast_hook;
wal_entry_hdr_t hdr = { .lsn = 5, .type = WAL_JOB_SUBMIT };
uint8_t payload[] = { 0x01 };
int rc = repl_broadcast(&r, &hdr, payload, sizeof(payload));
TEST_ASSERT_EQUAL_INT(0, rc);
/* Evicted — hook must NOT be called */
TEST_ASSERT_EQUAL_INT(0, g_hook_call_count);
/* Standby must now be in catch-up */
TEST_ASSERT_NOT_EQUAL(0,
atomic_load_explicit(&r.standbys[idx].in_catch_up, memory_order_acquire));
repl_broadcast_send_hook = NULL;
repl_fini(&r);
printf("test_broadcast_evicts_overloaded_standby: PASS\n");
#else
printf("test_broadcast_evicts_overloaded_standby: SKIPPED (no XCE_TESTING)\n");
#endif
}
/* Test (Task 2.3): repl_add_standby is idempotent for the same ep.
* Calling it twice with the same endpoint must return the same index,
* keep nstandbys==1, and not crash or corrupt state.
* Req 1.3: If repl_add_standby is called with an endpoint already
* registered, the system shall treat it as idempotent (no-op or update-in-place). */
static void test_repl_add_standby_idempotent(void) {
replicator_t r;
int dummy_ep = 0;
repl_init(&r);
int idx1 = repl_add_standby(&r, (void *)&dummy_ep, "sb1", 7001);
TEST_ASSERT_TRUE(idx1 >= 0);
TEST_ASSERT_EQUAL_INT(1, r.nstandbys);
/* Second call with the same ep — must be a no-op */
int idx2 = repl_add_standby(&r, (void *)&dummy_ep, "sb1", 7001);
TEST_ASSERT_TRUE(idx2 >= 0);
/* nstandbys must still be 1 (not 2) */
TEST_ASSERT_EQUAL_INT(1, r.nstandbys);
/* Must return the same slot index */
TEST_ASSERT_EQUAL_INT(idx1, idx2);
repl_fini(&r);
printf("test_repl_add_standby_idempotent: PASS\n");
}
/* Test: send failure (hook returns -EIO) is tolerated — broadcast returns 0 */
static void test_broadcast_send_failure_continues(void)
{
#ifdef XCE_TESTING
replicator_t r;
int ep1 = 0, ep2 = 1;
repl_init(&r);
repl_add_standby(&r, (void *)&ep1, "sb1", 7001);
repl_add_standby(&r, (void *)&ep2, "sb2", 7002);
reset_hook_state();
g_hook_return_val = -EIO; /* First call fails */
repl_broadcast_send_hook = test_broadcast_hook;
wal_entry_hdr_t hdr = { .lsn = 77, .type = WAL_JOB_SUBMIT };
uint8_t payload[] = { 0xDE, 0xAD };
int rc = repl_broadcast(&r, &hdr, payload, sizeof(payload));
/* broadcast should still return 0 — errors are WARN+continue */
TEST_ASSERT_EQUAL_INT(0, rc);
/* Both standbys attempted */
TEST_ASSERT_EQUAL_INT(2, g_hook_call_count);
repl_broadcast_send_hook = NULL;
repl_fini(&r);
printf("test_broadcast_send_failure_continues: PASS\n");
#else
printf("test_broadcast_send_failure_continues: SKIPPED (no XCE_TESTING)\n");
#endif
}
void test_repl_unit(void) {
UNITY_BEGIN();
RUN_TEST(test_repl_init_zeroes_nstandbys);
@ -171,5 +395,13 @@ void test_repl_unit(void) {
RUN_TEST(test_wait_ack_already_acked);
RUN_TEST(test_wait_ack_timeout);
RUN_TEST(test_wait_ack_woken_by_background_ack);
/* Task 2.3: idempotent re-registration */
RUN_TEST(test_repl_add_standby_idempotent);
/* Task 1.1: broadcast send path */
RUN_TEST(test_broadcast_no_standbys_returns_zero);
RUN_TEST(test_broadcast_one_standby_sends_entry);
RUN_TEST(test_broadcast_skips_catchup_standby);
RUN_TEST(test_broadcast_evicts_overloaded_standby);
RUN_TEST(test_broadcast_send_failure_continues);
UNITY_END();
}