feat(ha-commit-barrier-rollout): config barrier with production ACK ingestion (task 5.2 + r1)
configAck branch: per-push majority (acked_config_lsn >= push_tag, 1+count > n_peers/2 over r->ce->n_peers, n_peers<=1 immediate, ce==NULL -EINVAL); legacy >0 counting and n_active==0 shortcut dead. push_config_to_standbys returns the barrier result; upgrade failure -> send_upgrade_error (rc forwarded), no success ACK, no restart, committed_config_tag success-only. Remediation r1 (two CRITICAL review findings): - F1: wire production config-ACK ingestion — replconfigpushreplymsg decode branch in repl_ack_decode_and_dispatch -> repl_on_config_ack on the promoted-vote ep; + defensive cluster.c OPCode_reply case. Closes the unlanded half of ha-config-bootstrap 8.1; without this the barrier would brick every multi-member config upgrade. No schema change. - F2: keep the barrier off the iomgr thread — export push_config_transport_only (non-blocking) for the 5 ha.c + join.c streaming sites; barrier path now vtask-only (do_upgrade_config). Wire-path test drives real capnp ACK bytes through the production decode branch. 30/30 rollout, 6/6 repl_majority, 46/46 config_repl; census 0 FAIL; pre-existing fixture leak only (less than baseline).
This commit is contained in:
parent
9cdeba5816
commit
5a2fae1d37
12 changed files with 1034 additions and 399 deletions
|
|
@ -68,15 +68,23 @@ typedef struct {
|
|||
cluster_t *c;
|
||||
} committing_timeout_ctx_t;
|
||||
|
||||
/* cluster_init_phase2 / push_config_to_standbys / ha_send_ctrlhello_to_all_standbys:
|
||||
* defined in cluster.c / config_repl.c; only referenced in production builds.
|
||||
* (XCE_TESTING uses hook shims and does not link cluster.c.) */
|
||||
/* cluster_init_phase2 / push_config_transport_only /
|
||||
* ha_send_ctrlhello_to_all_standbys: defined in cluster.c / config_repl.c;
|
||||
* only referenced in production builds.
|
||||
* (XCE_TESTING uses hook shims and does not link cluster.c.)
|
||||
* ha.c uses push_config_transport_only ONLY (task 5.2 remediation r1):
|
||||
* every ha.c push site is reachable from peer_ctrl_msg_handler / io_worker
|
||||
* (vote replies, heartbeat replies), where the push_config_to_standbys
|
||||
* barrier wait would block the single iomgr epoll thread — and the config
|
||||
* ACK it waits for would arrive on that same blocked thread (structural
|
||||
* deadlock). Convergence is owned by the OP_replConfig anti-entropy pull
|
||||
* and the heartbeat decision loop, not by these best-effort pushes. */
|
||||
#ifndef XCE_TESTING
|
||||
extern int cluster_init_phase2(cluster_t *c);
|
||||
extern void ha_send_ctrlhello_to_all_standbys(cluster_t *c);
|
||||
extern int push_config_to_standbys(cluster_t *c, const uint8_t *bytes, size_t blen,
|
||||
const uint8_t sha256[32], uint64_t new_tag,
|
||||
int *acked_out);
|
||||
extern int push_config_transport_only(cluster_t *c, const uint8_t *bytes,
|
||||
size_t blen, const uint8_t sha256[32],
|
||||
uint64_t new_tag, int *n_out);
|
||||
#endif
|
||||
|
||||
static void on_vote_connected(endpoint_t *ep);
|
||||
|
|
@ -567,12 +575,12 @@ static void raft_cb_become_leader(void *ctx, uint64_t term)
|
|||
extern int conf_read_raw(const char *, uint8_t **, size_t *);
|
||||
if (c->myconf && c->myconf->conffile &&
|
||||
conf_read_raw(c->myconf->conffile, &cfg_bytes, &cfg_len) == 0) {
|
||||
push_config_to_standbys(c, cfg_bytes, cfg_len,
|
||||
push_config_transport_only(c, cfg_bytes, cfg_len,
|
||||
c->myconf->config_sha256,
|
||||
c->myconf->config_version_tag, NULL);
|
||||
free(cfg_bytes);
|
||||
} else {
|
||||
push_config_to_standbys(c, NULL, 0, NULL,
|
||||
push_config_transport_only(c, NULL, 0, NULL,
|
||||
c->myconf ? c->myconf->config_version_tag : 0,
|
||||
NULL);
|
||||
}
|
||||
|
|
@ -1362,7 +1370,7 @@ void ha_check_majority_convergence(ha_state_t *ha, cluster_t *c)
|
|||
* (stored by ha_process_standby_state on the same heartbeat reply), calls
|
||||
* ha_evaluate_repl_action, and acts on the result:
|
||||
*
|
||||
* HA_REPL_PUSH_CONFIG → push_config_to_standbys(c, …) [or test hook] (Task 3.3)
|
||||
* HA_REPL_PUSH_CONFIG → push_config_transport_only(c, …) [or test hook] (Task 3.3)
|
||||
* HA_REPL_SEND_SNAPSHOT→ stub (Task 3.5)
|
||||
* HA_REPL_STREAM_WAL → ha_stream_wal_to_standby(c, ha, idx) (Task 3.4)
|
||||
* HA_REPL_CONVERGED → no-op
|
||||
|
|
@ -1407,12 +1415,12 @@ int ha_dispatch_repl_action_for_standby(ha_state_t *ha,
|
|||
size_t cfg_len = 0;
|
||||
if (c->myconf && c->myconf->conffile &&
|
||||
conf_read_raw(c->myconf->conffile, &cfg_bytes, &cfg_len) == 0) {
|
||||
push_config_to_standbys(c, cfg_bytes, cfg_len,
|
||||
push_config_transport_only(c, cfg_bytes, cfg_len,
|
||||
c->myconf->config_sha256,
|
||||
c->myconf->config_version_tag, NULL);
|
||||
free(cfg_bytes);
|
||||
} else {
|
||||
push_config_to_standbys(c, NULL, 0, NULL,
|
||||
push_config_transport_only(c, NULL, 0, NULL,
|
||||
c->myconf ? c->myconf->config_version_tag : 0,
|
||||
NULL);
|
||||
}
|
||||
|
|
@ -2078,7 +2086,7 @@ int handle_request_vote(ha_state_t *ha,
|
|||
|
||||
/* ── Test-only hook shims (XCE_TESTING only) ──────────────────────────────
|
||||
* Type definitions (election_timeout_ctx_t, committing_timeout_ctx_t),
|
||||
* extern declarations (cluster_init_phase2, push_config_to_standbys,
|
||||
* extern declarations (cluster_init_phase2, push_config_transport_only,
|
||||
* ha_send_ctrlhello_to_all_standbys), and static forward declarations
|
||||
* (committing_timeout_fn, ha_heartbeat_is_fresh) have been moved to the
|
||||
* forward-declarations block near the top of this file so that the raft
|
||||
|
|
@ -2101,9 +2109,9 @@ int (*ha_phase2_hook)(void *c) = NULL;
|
|||
int (*ha_send_ctrlhello_hook)(void *c) = NULL;
|
||||
|
||||
/*
|
||||
* ha_push_config_hook: test-only seam for push_config_to_standbys.
|
||||
* ha_push_config_hook: test-only seam for push_config_transport_only.
|
||||
* When non-NULL, the COMMITTING path calls this INSTEAD OF the real function
|
||||
* (config_repl.c's push_config_to_standbys needs a real cluster). Req 7.1.
|
||||
* (config_repl.c's push_config_transport_only needs a real cluster). Req 7.1.
|
||||
*/
|
||||
int (*ha_push_config_hook)(void *c) = NULL;
|
||||
|
||||
|
|
@ -2118,7 +2126,7 @@ int (*ha_nop_req_hook)(endpoint_t *ep) = NULL;
|
|||
* ha_repl_push_config_for_standby_hook: test-only seam for the per-standby
|
||||
* config push triggered by the heartbeat-reply decision loop (Task 3.3).
|
||||
* When non-NULL, ha_dispatch_repl_action_for_standby() calls this INSTEAD OF
|
||||
* push_config_to_standbys() when the decision is HA_REPL_PUSH_CONFIG.
|
||||
* push_config_transport_only() when the decision is HA_REPL_PUSH_CONFIG.
|
||||
* Requirements: 1.4 (ha-repl-hardening)
|
||||
*/
|
||||
int (*ha_repl_push_config_for_standby_hook)(void *c, int peer_idx) = NULL;
|
||||
|
|
@ -2283,7 +2291,7 @@ apply_role:
|
|||
ha_push_config_hook(c);
|
||||
#else
|
||||
if (c != NULL)
|
||||
push_config_to_standbys(c, NULL, 0, NULL, 0, NULL);
|
||||
push_config_transport_only(c, NULL, 0, NULL, 0, NULL);
|
||||
#endif
|
||||
|
||||
/* Send CtrlHelloRequest to all standbys to trigger WAL catch-up (Req 7.2) */
|
||||
|
|
@ -2649,7 +2657,10 @@ done:
|
|||
*
|
||||
* - OPCode_replCatchUpDone → ha_repl_ack_received
|
||||
* - OPCode_reply with stateReportMsg → ha_process_standby_state (Task 3.1)
|
||||
* - OPCode_nop or OPCode_reply without stateReport → silently ignored
|
||||
* - OPCode_reply with walackreplymsg → repl_on_ack (Task 4.1)
|
||||
* - OPCode_reply with replconfigpushreplymsg → repl_on_config_ack
|
||||
* (barrier-rollout task 5.2 r1 / ha-config-bootstrap task 8.1)
|
||||
* - OPCode_nop or OPCode_reply with any other body → silently ignored
|
||||
* - anything else → WARN + drop
|
||||
*/
|
||||
static int repl_ack_decode_and_dispatch(endpoint_t *ep, vote_ep_ctx_t *vctx)
|
||||
|
|
@ -2775,6 +2786,32 @@ static int repl_ack_decode_and_dispatch(endpoint_t *ep, vote_ep_ctx_t *vctx)
|
|||
WARN("repl_ack_decode_and_dispatch: WAL NACK from standby"
|
||||
" lsn=%lu success=0 — ignoring", (unsigned long)acked_lsn);
|
||||
}
|
||||
} else if (rep_s.data_which == RequestReply_data_replconfigpushreplymsg) {
|
||||
/* Config-push ACK from standby (ha-commit-barrier-rollout task
|
||||
* 5.2 remediation r1; completes the unlanded half of
|
||||
* ha-config-bootstrap task 8.1): the standby replies to
|
||||
* OP_replConfigPush with replconfigpushreplymsg carrying
|
||||
* {accepted, versionTag} (config_repl.c:744-752). Decode the
|
||||
* acked tag and apply it to the replicator so the configAck
|
||||
* barrier (repl_wait_majority_ack) can observe the covering
|
||||
* ACK — without this branch every multi-member config upgrade
|
||||
* would time out. read_ReplConfigPushReply reads scalars only
|
||||
* — safe to call before capn_free. */
|
||||
struct ReplConfigPushReply rcp_raw;
|
||||
memset(&rcp_raw, 0, sizeof(rcp_raw));
|
||||
read_ReplConfigPushReply(&rcp_raw, rep_s.data.replconfigpushreplymsg);
|
||||
uint64_t acked_tag = (uint64_t)rcp_raw.versionTag;
|
||||
int accepted = rcp_raw.accepted ? 1 : 0;
|
||||
capn_free(&capn_ctx);
|
||||
|
||||
if (accepted && c) {
|
||||
repl_on_config_ack(&c->persist.repl, ep, acked_tag);
|
||||
TRACE("repl_ack_decode_and_dispatch: config ACK from standby"
|
||||
" tag=%016" PRIx64 " accepted=1", acked_tag);
|
||||
} else if (!accepted) {
|
||||
WARN("repl_ack_decode_and_dispatch: config push rejected by"
|
||||
" standby (tag=%016" PRIx64 ") — ignoring", acked_tag);
|
||||
}
|
||||
} else {
|
||||
capn_free(&capn_ctx);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue