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:
Rongsong Shen 2026-06-13 03:46:29 +00:00
parent 9cdeba5816
commit 5a2fae1d37
12 changed files with 1034 additions and 399 deletions

View file

@ -109,14 +109,38 @@ int handle_config_request(cluster_t *c, endpoint_t *ep, xce_request_t *req);
int handle_repl_config_push(cluster_t *c, endpoint_t *ep, xce_request_t *req);
/*
* push_config_to_standbys: fan-out a new config blob to all connected standby
* controllers in parallel. Best-effort — standbys that miss the push will
* pull on reconnect via handle_config_request().
* push_config_transport_only: best-effort streaming fan-out of a config blob
* to all connected standby endpoints — NO barrier wait (ha-commit-barrier-
* rollout task 5.2 remediation r1). Spawns one push thread per registered
* standby; the sends are non-blocking endpoint_enq()s, so the helper is
* iomgr-safe and is the ONLY config-push entry callers reachable from
* peer_ctrl_msg_handler / io_worker may use (election promotion, heartbeat-
* driven convergence in ha.c, controller join in join.c). Standbys that
* miss the push converge later via the OP_replConfig anti-entropy pull and
* the heartbeat decision loop.
* n_out (may be NULL): receives the registered standby count.
* Returns the number of standbys that ACKed the push at the transport level.
*/
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);
/*
* push_config_to_standbys: transport fan-out (push_config_transport_only)
* PLUS the majority-quorum config ACK barrier on new_tag (ha-commit-barrier-
* rollout task 5.2). The barrier blocks up to CONF_PUSH_TIMEOUT_MS — this
* entry is for VTASK-CONTEXT callers ONLY (the OP_upgradeConfig path,
* do_upgrade_config_with_bytes); it must never run on the iomgr epoll
* thread (no path reachable from peer_ctrl_msg_handler / io_worker — those
* callers use push_config_transport_only above).
* acked_out (may be NULL): receives the number of standbys that successfully
* received the push at the transport level (per-standby push count).
* Returns the number of standbys that successfully received the push.
* (ha-commit-barrier-rollout task 5.2 moves the majority-ACK barrier result
* into the return value; the push count is then only available via acked_out.)
* Returns the barrier result: 0 once 1 (self) + standbys with
* acked_config_lsn >= new_tag form a majority of configured members
* (n_peers <= 1 → immediate 0); -ETIMEDOUT when the quorum never forms
* within CONF_PUSH_TIMEOUT_MS; -EINVAL if the commit engine is not wired.
* On 0 committed_config_tag has been advanced to new_tag; on failure it is
* untouched. The upgrade path treats the result as authoritative.
* Only meaningful when CONF_ENABLE_UPGRADE_CONFIG is set, but always declared
* so callers can reference it without an #if guard.
*/