feat(ha-term-safety): on_send_append_entries callback body (task 2.3)
Replaces the stub: builds a WalCatchupEntry AppendEntries to standbys[peer_idx].ep carrying prev_log_index/prev_log_term; for n_entries==1 sources the entry at prev_log_index+1 from the local WAL (term via persist_entry_term, type+payload via ha_read_wal_entry_at); n_entries==0 emits a zero-entry heartbeat. Async send (xce_wal_batch_req_raw), payload freed on all paths. 2 tests via a capture seam. Review-applied: corrected the misleading repl.c comment — leader_commit is accepted but NOT yet wire-encoded (deferred to task 3.2: WalCatchupEntry leaderCommit field + standby intake). Reviewer APPROVED; per-group 0 failures.
This commit is contained in:
parent
ed2443533f
commit
2c5d298cb5
6 changed files with 452 additions and 4 deletions
|
|
@ -12,6 +12,7 @@
|
|||
#include "config.h"
|
||||
#include "persist.h"
|
||||
#include "wal_types.h"
|
||||
#include "repl.h"
|
||||
#include "xce/cluster_def.h"
|
||||
#include "ha.h"
|
||||
|
||||
|
|
@ -494,6 +495,132 @@ static void test_on_truncate_from_removes_all(void)
|
|||
printf("test_on_truncate_from_removes_all: PASS\n");
|
||||
}
|
||||
|
||||
/* ================================================================== */
|
||||
/* Task 2.3 — on_send_append_entries callback body */
|
||||
/* ================================================================== */
|
||||
|
||||
/* A dummy non-NULL endpoint pointer for a registered standby slot. The send
|
||||
* is diverted to the capture hook, so the pointer is never dereferenced. */
|
||||
static int g_ae_dummy_ep;
|
||||
|
||||
/* Register one standby in the cluster's embedded replicator at slot 0, wiring
|
||||
* ha.repl so the callback maps peer_idx 0 → this endpoint. */
|
||||
static void register_one_standby(cluster_t *c)
|
||||
{
|
||||
c->ha.repl = &c->persist.repl;
|
||||
TEST_ASSERT_EQUAL_INT(0,
|
||||
repl_add_standby(&c->persist.repl, &g_ae_dummy_ep, "sb0", 7000));
|
||||
}
|
||||
|
||||
/* Test: on_send_append_entries for n_entries==1 builds an envelope carrying the
|
||||
* correct (prev_log_index, prev_log_term, leader_commit, n_entries) and, for the
|
||||
* single entry sourced from the leader's own WAL at prev_log_index+1, its term
|
||||
* (== persist_entry_term(prev_log_index+1)) and payload (R1.3, R3.1). */
|
||||
static void test_send_append_entries_one_entry_from_wal(void)
|
||||
{
|
||||
char tmp[256];
|
||||
strcpy(tmp, "/tmp/xce_ts_ae1_XXXXXX");
|
||||
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
|
||||
|
||||
cluster_t *c = make_cluster(tmp);
|
||||
TEST_ASSERT_NOT_NULL(c);
|
||||
atomic_store(&c->ha.role, CTRL_PRIMARY);
|
||||
|
||||
/* Populate the leader's own WAL BEFORE registering a standby — otherwise the
|
||||
* persist append fans out via repl_broadcast to the dummy int ep (ASAN
|
||||
* global-buffer-overflow reading ep->peer). With no standby, the append is a
|
||||
* no-op broadcast. The entry to be sent is at prev_log_index+1 == LSN 2. */
|
||||
uint8_t blob[64];
|
||||
size_t n1 = pack_entry(blob, WAL_JOB_UPDATE, "p", 1);
|
||||
TEST_ASSERT_EQUAL_INT(0,
|
||||
ha_cb_persist_entry_for_test(&c->ha, /*index*/1, /*term*/4, blob, n1));
|
||||
size_t n2 = pack_entry(blob, WAL_JOB_UPDATE, "QQ", 2);
|
||||
TEST_ASSERT_EQUAL_INT(0,
|
||||
ha_cb_persist_entry_for_test(&c->ha, /*index*/2, /*term*/6, blob, n2));
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_flush(&c->persist));
|
||||
|
||||
/* Now register the standby so peer_idx 0 maps to a slot; the AE send below
|
||||
* is diverted to the capture hook, so the dummy ep is never dereferenced. */
|
||||
register_one_standby(c);
|
||||
|
||||
/* The entry-term oracle the send must carry for LSN 2. */
|
||||
uint64_t expect_term = persist_entry_term(&c->persist, 2);
|
||||
TEST_ASSERT_EQUAL_UINT64(6, expect_term);
|
||||
|
||||
/* Drive the callback: prev_log_index=1, prev_log_term=4 (term of LSN 1),
|
||||
* leader_commit=1, n_entries=1 → sends the entry at LSN 2. */
|
||||
repl_ae_capture_t cap;
|
||||
memset(&cap, 0, sizeof(cap));
|
||||
repl_send_ae_capture_hook = ∩
|
||||
|
||||
int rc = ha_cb_send_append_entries_for_test(&c->ha, /*peer*/0,
|
||||
/*prev_idx*/1, /*prev_term*/4,
|
||||
/*leader_commit*/1, /*n*/1);
|
||||
repl_send_ae_capture_hook = NULL;
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(1, cap.called);
|
||||
TEST_ASSERT_EQUAL_INT(0, cap.peer_idx);
|
||||
TEST_ASSERT_EQUAL_UINT64(1, cap.prev_log_index);
|
||||
TEST_ASSERT_EQUAL_UINT64(4, cap.prev_log_term);
|
||||
TEST_ASSERT_EQUAL_UINT64(1, cap.leader_commit);
|
||||
TEST_ASSERT_EQUAL_INT(1, cap.n_entries);
|
||||
|
||||
/* The single entry is sourced from the local WAL at prev_log_index+1 == 2. */
|
||||
TEST_ASSERT_EQUAL_UINT64(2, cap.entry_lsn);
|
||||
TEST_ASSERT_EQUAL_UINT64(expect_term, cap.entry_term);
|
||||
TEST_ASSERT_EQUAL_UINT8(WAL_JOB_UPDATE, cap.entry_type);
|
||||
TEST_ASSERT_EQUAL_UINT32(2, cap.payload_len);
|
||||
TEST_ASSERT_EQUAL_MEMORY("QQ", cap.payload, 2);
|
||||
|
||||
teardown_cluster(c);
|
||||
printf("test_send_append_entries_one_entry_from_wal: PASS\n");
|
||||
}
|
||||
|
||||
/* Test: on_send_append_entries for n_entries==0 emits a heartbeat carrying the
|
||||
* prev tuple + leader_commit and ZERO entries (R1.3). */
|
||||
static void test_send_append_entries_heartbeat_zero_entries(void)
|
||||
{
|
||||
char tmp[256];
|
||||
strcpy(tmp, "/tmp/xce_ts_ae0_XXXXXX");
|
||||
TEST_ASSERT_NOT_NULL(mkdtemp(tmp));
|
||||
|
||||
cluster_t *c = make_cluster(tmp);
|
||||
TEST_ASSERT_NOT_NULL(c);
|
||||
atomic_store(&c->ha.role, CTRL_PRIMARY);
|
||||
|
||||
/* One entry on disk so prev_log_index=1 is a real tip; heartbeat sends none.
|
||||
* Persist BEFORE registering the standby (see one-entry test note). */
|
||||
uint8_t blob[64];
|
||||
size_t n1 = pack_entry(blob, WAL_JOB_UPDATE, "h", 1);
|
||||
TEST_ASSERT_EQUAL_INT(0,
|
||||
ha_cb_persist_entry_for_test(&c->ha, /*index*/1, /*term*/3, blob, n1));
|
||||
TEST_ASSERT_EQUAL_INT(0, persist_flush(&c->persist));
|
||||
|
||||
register_one_standby(c);
|
||||
|
||||
repl_ae_capture_t cap;
|
||||
memset(&cap, 0, sizeof(cap));
|
||||
repl_send_ae_capture_hook = ∩
|
||||
|
||||
int rc = ha_cb_send_append_entries_for_test(&c->ha, /*peer*/0,
|
||||
/*prev_idx*/1, /*prev_term*/3,
|
||||
/*leader_commit*/1, /*n*/0);
|
||||
repl_send_ae_capture_hook = NULL;
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(1, cap.called);
|
||||
TEST_ASSERT_EQUAL_UINT64(1, cap.prev_log_index);
|
||||
TEST_ASSERT_EQUAL_UINT64(3, cap.prev_log_term);
|
||||
TEST_ASSERT_EQUAL_UINT64(1, cap.leader_commit);
|
||||
TEST_ASSERT_EQUAL_INT(0, cap.n_entries);
|
||||
/* No entry sourced for a heartbeat. */
|
||||
TEST_ASSERT_EQUAL_UINT32(0, cap.payload_len);
|
||||
|
||||
teardown_cluster(c);
|
||||
printf("test_send_append_entries_heartbeat_zero_entries: PASS\n");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Suite entry point */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
|
@ -511,4 +638,6 @@ void test_term_safety_adapter(void)
|
|||
RUN_TEST(test_on_get_entry_term_present_legacy_absent);
|
||||
RUN_TEST(test_on_truncate_from_drops_suffix);
|
||||
RUN_TEST(test_on_truncate_from_removes_all);
|
||||
RUN_TEST(test_send_append_entries_one_entry_from_wal);
|
||||
RUN_TEST(test_send_append_entries_heartbeat_zero_entries);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue