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:
Rongsong Shen 2026-06-22 06:01:30 +00:00
parent ed2443533f
commit 2c5d298cb5
6 changed files with 452 additions and 4 deletions

View file

@ -310,6 +310,102 @@ int repl_broadcast(replicator_t *r, const wal_entry_hdr_t *hdr,
return 0;
}
/* ============================================================
* AppendEntries send (ha-term-safety task 2.3)
*
* The library drives on_send_append_entries(peer_idx, prev tuple,
* leader_commit, n_entries); the ha.c adapter sources the single entry (for
* n_entries==1) from its own WAL and calls this to build + enqueue the
* AppendEntries envelope to the standby at slot peer_idx. Non-blocking: reuses
* the async xce_wal_batch_req_raw transport.
* ============================================================ */
#ifdef XCE_TESTING
repl_ae_capture_t *repl_send_ae_capture_hook = NULL;
#endif
int repl_send_append_entries(replicator_t *r, int peer_idx,
uint64_t prev_log_index, uint64_t prev_log_term,
uint64_t leader_commit, int n_entries,
uint64_t entry_lsn, uint8_t entry_type,
uint64_t entry_term,
const void *payload, uint32_t payload_len)
{
if (r == NULL || peer_idx < 0 || peer_idx >= MAX_STANDBYS)
return -EINVAL;
#ifdef XCE_TESTING
/* Capture seam: record the built envelope without network I/O. */
if (repl_send_ae_capture_hook) {
repl_ae_capture_t *cap = repl_send_ae_capture_hook;
cap->called = 1;
cap->peer_idx = peer_idx;
cap->prev_log_index = prev_log_index;
cap->prev_log_term = prev_log_term;
cap->leader_commit = leader_commit;
cap->n_entries = n_entries;
cap->entry_lsn = (n_entries == 1) ? entry_lsn : 0;
cap->entry_type = (n_entries == 1) ? entry_type : 0;
cap->entry_term = (n_entries == 1) ? entry_term : 0;
if (n_entries == 1 && payload && payload_len > 0) {
uint32_t cpy = payload_len;
if (cpy > sizeof(cap->payload))
cpy = sizeof(cap->payload);
memcpy(cap->payload, payload, cpy);
cap->payload_len = cpy;
} else {
cap->payload_len = 0;
}
return 0;
}
#endif
mylock_lock(&r->lock);
standby_conn_t *s = &r->standbys[peer_idx];
repl_ep_t ep = s->ep;
char host[XCE_MAXNAME_LEN];
uint32_t port = s->port;
strncpy(host, s->host, sizeof(host) - 1);
host[sizeof(host) - 1] = '\0';
mylock_unlock(&r->lock);
if (ep == NULL)
return -ENOENT; /* no live endpoint at this peer slot */
/* Build a single AppendEntries envelope carrying the prev tuple +
* leader_commit and, for n_entries==1, the entry sourced from the leader's
* WAL. The carrier struct already holds the Raft term fields (task 1.1). */
wal_catchup_entry_t entry;
memset(&entry, 0, sizeof(entry));
entry.prev_log_index = prev_log_index;
entry.prev_log_term = prev_log_term;
/* NOTE: leader_commit is accepted as a parameter (and captured by the test
* seam) but is NOT yet wire-encoded — WalCatchupEntry has no leaderCommit
* field. Adding that capnp field and the standby-side intake of leader_commit
* is deferred to task 3.2; no standby acts on it until then.
* A heartbeat is marked by n_entries==0 (zero entries; lsn left 0). */
if (n_entries == 1) {
entry.lsn = entry_lsn;
entry.entry_type = entry_type;
entry.term = entry_term;
entry.raw_payload = (uint8_t *)(uintptr_t)payload;
entry.raw_payload_len = payload_len;
}
wal_catchup_entry_t *ep_arr[1] = { &entry };
int rc = xce_wal_batch_req_raw(ep, ep_arr, n_entries == 1 ? 1 : 0);
if (rc != 0) {
WARN("repl_send_append_entries: send to standby %s:%u failed (rc=%d)",
host, port, rc);
return rc;
}
TRACE("repl_send_append_entries: peer=%d prevIdx=%lu prevTerm=%lu "
"leaderCommit=%lu n=%d to %s:%u",
peer_idx, (unsigned long)prev_log_index, (unsigned long)prev_log_term,
(unsigned long)leader_commit, n_entries, host, port);
return 0;
}
/* ============================================================
* Sync replication — stub in P2-1; wired in P2-3
* ============================================================ */