feat(raft-adapter-conformance): wire term-adoption into primary-side config-push reply handler

myreq_handler's ReplConfigPushReply branch now routes cack->term
through raft_on_higher_term_observed (same entry point the
heartbeat-reply and WAL-NACK vectors use), regardless of accept/reject,
without altering the existing accept/reject branching.

Adds myreq_handler_for_test (XCE_TESTING-only) to drive the real
dispatcher in tests, following the peer_ctrl_msg_handler_for_test
precedent. Requires cluster.c in controller_unit_test_src plus the
established link-satisfying fake stubs for its unrelated switch-case
dependencies (same pattern already used by test_worker_join.c and
test_commit_barrier_reply.c).
This commit is contained in:
Rongsong Shen 2026-07-29 10:56:48 +00:00
parent ecf2dac6b2
commit 1e7a7312bf
4 changed files with 229 additions and 1 deletions

View file

@ -749,6 +749,26 @@ static int myreq_handler(vtask_t* task)
&& workreply->data.replconfigpushreplymsg) {
repl_config_push_reply_t *cack =
workreply->data.replconfigpushreplymsg;
/* raft-adapter-conformance Req 7.4/7.5: a config-push reply now
* carries the replying member's current term (task 1 schema
* addition + task 2.1 send side) -- route it through the same
* term-discovery entry point the heartbeat-reply and WAL-NACK
* vectors already use (bug-1421; ha.c raft_lock-guarded
* raft_on_higher_term_observed calls) so a stale-term primary
* adopts the higher term and steps down instead of retrying
* forever against a standby that has already moved on. Applies
* regardless of accept/reject below -- a reject can still carry
* a useful higher term. raft_on_higher_term_observed is itself
* strictly-greater-than-only (ha-async-io.md Heartbeat Model),
* so no separate `>` comparison is needed here; raft_lock
* discipline mirrors the vote-request/pre-vote-request branches
* above (bug-1435 -- serialize against concurrent raft_state_t
* access). */
pthread_mutex_lock(&c->ha.raft_lock);
(void)raft_on_higher_term_observed(&c->ha.raft, cack->term);
pthread_mutex_unlock(&c->ha.raft_lock);
if (cack->accepted) {
repl_on_config_ack(&c->persist.repl, ep, cack->version_tag);
TRACE("myreq_handler: config ACK from inbound standby"
@ -779,6 +799,39 @@ static int myreq_handler(vtask_t* task)
return rc;
}
#ifdef XCE_TESTING
/*
* myreq_handler_for_test: expose myreq_handler for tests (peer_ctrl_msg_
* handler_for_test precedent, ha.c). Builds the vtask_t/vtask_data_t wrapper
* vtask_entry constructs internally in production so a test can decode+
* dispatch a real packet through the actual switch -- including the
* ReplConfigPushReply branch's term-adoption check (raft-adapter-
* conformance Req 7.4/7.5). Packet ownership follows myreq_handler's own
* contract: myreq_handler unconditionally calls xce_decode(pkt, ...) as its
* first step, and xce_decode() itself consumes pkt -- it calls
* free_packet(pkt) (frees pkt->data) and FREEUP(pkt) (frees the io_packet_t
* struct) before returning (request.c). Ownership of `pkt` is therefore
* TRANSFERRED into this call: the caller must NOT touch or free `pkt` after
* calling myreq_handler_for_test -- doing so is a use-after-free/double-free.
* (xce_free_request separately releases the decoded xce_request_t, an
* unrelated stack-local object in myreq_handler.)
*/
int myreq_handler_for_test(endpoint_t* ep, io_packet_t* pkt)
{
vtask_data_t data;
memset(&data, 0, sizeof(data));
data.vtype = VTASK_IO;
data.u.iodata.ep = ep;
data.u.iodata.pkt = pkt;
vtask_t task;
memset(&task, 0, sizeof(task));
task.data = &data;
return myreq_handler(&task);
}
#endif /* XCE_TESTING */
static int vtask_entry(vtask_t* task)
{
int rc;