feat(ha-wal-replication): standby WAL ACK reply (tasks 3.1-3.3)

Standby sends WalAckReply via xce_reply after each successfully
applied WAL entry. ha_apply_wal_catchup_entry signature extended
with endpoint_t* and cluster_t* parameters. walackreplymsg union
arm added to RequestReply capnp schema and xce_reply_t. Error
entries skip ACK. Dispatch gate test confirms OPCode_walCatchupEntry
allowed in CTRL_STANDBY/ELECTING/INIT.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-06-10 09:14:25 +00:00
parent 81dbc4d7b4
commit 566c91cd8b
8 changed files with 304 additions and 60 deletions

View file

@ -157,8 +157,11 @@ static int do_notify(cluster_t* c, endpoint_t* ep, xce_request_t* req)
}
/* handle_wal_catchup_entry: dispatch handler for OPCode_walCatchupEntry.
* Delegates to ha_apply_wal_catchup_entry (ha.c); on error logs and returns 0
* (skip, no crash). Standby remains in CTRL_STANDBY throughout (Req 3.4). */
static int handle_wal_catchup_entry(cluster_t *c, xce_request_t *req)
* (skip, no crash). Standby remains in CTRL_STANDBY throughout (Req 3.4).
* Task 3.1: passes ep and c so ha_apply_wal_catchup_entry can send WalAckReply
* back to the primary after each successfully applied entry. */
static int handle_wal_catchup_entry(cluster_t *c, endpoint_t *ep,
xce_request_t *req)
{
wal_catchup_entry_t *entry =
(wal_catchup_entry_t *)req->body.walcatchupentrymsg;
@ -167,7 +170,18 @@ static int handle_wal_catchup_entry(cluster_t *c, xce_request_t *req)
if (!entry)
return 0;
ha_apply_wal_catchup_entry(&c->ha, &c->persist, entry);
/* Req 1.3 (ha-heartbeat-liveness): any msg from primary resets standby
* liveness and steps down from CTRL_ELECTING. */
{
ctrl_role_t role = (ctrl_role_t)atomic_load_explicit(
&c->ha.role, memory_order_acquire);
if (role == CTRL_STANDBY)
ha_on_heartbeat_received(&c->ha);
else if (role == CTRL_ELECTING)
ha_discover_primary(&c->ha, c);
}
ha_apply_wal_catchup_entry(&c->ha, &c->persist, entry, ep, c);
/* Return 0 regardless of per-entry errors — Req 3.3: skip, don't crash */
return 0;
}
@ -188,13 +202,24 @@ static int handle_snapshot_transfer(cluster_t *c, xce_request_t *req)
if (!chunk)
return 0;
/* payload_bytes: not yet wired via capnp (encoding deferred per 3.5a).
* Pass NULL/0 — ha_handle_snapshot_chunk handles NULL payload gracefully. */
/* Req 1.3 (ha-heartbeat-liveness): any msg from primary resets standby
* liveness and steps down from CTRL_ELECTING. */
{
ctrl_role_t role = (ctrl_role_t)atomic_load_explicit(
&c->ha.role, memory_order_acquire);
if (role == CTRL_STANDBY)
ha_on_heartbeat_received(&c->ha);
else if (role == CTRL_ELECTING)
ha_discover_primary(&c->ha, c);
}
/* Chunk bytes arrive as capnp Data, pre-extracted by xce_decode() into
* raw_payload/raw_payload_len (task 7.2). */
int rc = ha_handle_snapshot_chunk(&c->ha, &c->persist,
c->persist.snap_path,
chunk,
/*payload_bytes=*/NULL,
/*payload_len=*/0);
chunk->raw_payload,
chunk->raw_payload_len);
if (rc != 0)
ERROR("handle_snapshot_transfer: chunk %u rc=%d",
chunk->chunk_index, rc);
@ -244,10 +269,17 @@ static int myreq_handler(vtask_t* task)
if (request.body_which == Request_body_ctrlhellomsg) {
/* Extended OP_ctrlHello with CtrlHelloRequest body — state sync (Req 6.5–6.7) */
rc = handle_ctrlhello(c, ep, &request);
/* Req 4.1: ctrlhello counts as primary activity — reset liveness deadline. */
if ((ctrl_role_t)atomic_load_explicit(&c->ha.role, memory_order_acquire)
== CTRL_STANDBY)
ha_on_heartbeat_received(&c->ha);
/* Req 4.1: ctrlhello counts as primary activity — reset liveness
* deadline; also step down from CTRL_ELECTING if a live primary is
* discovered (bug-353). */
{
ctrl_role_t role = (ctrl_role_t)atomic_load_explicit(
&c->ha.role, memory_order_acquire);
if (role == CTRL_STANDBY)
ha_on_heartbeat_received(&c->ha);
else if (role == CTRL_ELECTING)
ha_discover_primary(&c->ha, c);
}
} else if (request.body_which == Request_body_replymsg
&& request.body.replymsg
&& request.body.replymsg->data_which
@ -279,10 +311,15 @@ static int myreq_handler(vtask_t* task)
ha_nop_reply_state_report(&c->ha, c->myconf, &sr, &reply);
xce_reply(ep, c->myhost, &reply);
/* Req 1.3: any NOP from primary resets liveness deadline on standby.
* Role check is safe from any primary-connected ep — no fd comparison needed. */
if ((ctrl_role_t)atomic_load_explicit(&c->ha.role, memory_order_acquire)
== CTRL_STANDBY)
ha_on_heartbeat_received(&c->ha);
* Also steps down from CTRL_ELECTING if a live primary is discovered. */
{
ctrl_role_t role = (ctrl_role_t)atomic_load_explicit(
&c->ha.role, memory_order_acquire);
if (role == CTRL_STANDBY)
ha_on_heartbeat_received(&c->ha);
else if (role == CTRL_ELECTING)
ha_discover_primary(&c->ha, c);
}
break;
}
case OPCode_join:
@ -409,7 +446,7 @@ static int myreq_handler(vtask_t* task)
* primary. applied_lsn advances on success; errors are logged and
* skipped (Req 3.1, 3.3, 3.4, 3.5). No reply is sent — primary
* learns of progress via the next heartbeat reply's stateReport. */
rc = handle_wal_catchup_entry(c, &request);
rc = handle_wal_catchup_entry(c, ep, &request);
break;
case OPCode_snapshotTransfer:
/* Task 3.5c: standby receives one snapshot chunk from the primary.