fix(election-iomgr-refactor): correct task 2.4 review findings

- CRITICAL: single-node shortcircuit enters CTRL_PRIMARY_COMMITTING
  (not CTRL_PRIMARY directly); phase2 transition deferred to task 4.x
- CRITICAL: add n_peers==0 guard in ha_elect_evaluate_and_signal to
  prevent votes[0] out-of-bounds access
- HIGH: wire client_ssl_ctx after endpoint_connect for outgoing votes
- HIGH: add timeout_ctx field to ha_elect_ctx_t for task 4.x cancel
- HIGH: clarify vote_ep_err_handler ownership (no premature free of vctx;
  deferred to ha_election_cleanup in task 5.1)
- Tests updated: single-node shortcircuit asserts COMMITTING not PRIMARY
This commit is contained in:
Rongsong Shen 2026-05-30 09:27:48 +00:00
parent e50afd02f6
commit 543981c505
3 changed files with 80 additions and 13 deletions

View file

@ -901,6 +901,17 @@ ctrl_role_t ha_elect_evaluate_and_signal(ha_state_t *ha, ha_elect_ctx_t *ctx)
if (ctx->done_posted)
return (ctx->result == 1) ? CTRL_PRIMARY : CTRL_STANDBY;
/* Fix 2 (CRITICAL): zero-peer guard — single-node shortcircuit.
* n_peers==0 means no other nodes; votes[] flexible array has 0 entries,
* so accessing votes[0] would be out-of-bounds. Self-vote (votes_granted=1)
* always satisfies quorum=1. No fresher-peer or fatal-gate checks apply. */
if (ctx->n_peers == 0) {
ctx->result = 1;
ctx->done_posted = 1;
sem_post(&ctx->done);
return CTRL_PRIMARY;
}
int total_nodes = ctx->n_peers + 1; /* include self */
int quorum = (total_nodes + 1) / 2; /* ceil(total/2) */
@ -1434,6 +1445,11 @@ static int vote_ep_err_handler(void *ep_raw)
vctx->my_term,
0, /* vote_granted = false */
0, 0);
/* Fix 5 (HIGH): do NOT free vctx here.
* ep->data (vctx) is also referenced by vote_reply_msg_handler via ep->data.
* The endpoint lifetime is managed by iomgr; vctx is freed in
* ha_election_cleanup() (task 5.1) when all per-peer contexts are released.
* Freeing here would cause a use-after-free in vote_reply_msg_handler. */
return 0;
}
@ -1499,14 +1515,17 @@ void ha_start_election_async(ha_state_t *ha, cluster_t *c,
* votes[0] would be out-of-bounds — use a separate approach for the
* evaluate function's my_tag/my_lsn reads). */
/* For single-node, quorum = ceil(1/2) = 1; votes_granted=1 → wins */
/* For single-node, quorum = ceil(1/2) = 1; votes_granted=1 → wins.
* Fix 1 (CRITICAL): enter CTRL_PRIMARY_COMMITTING, not CTRL_PRIMARY.
* With no standbys the committing timeout (task 4.4) will fire and
* promote to CTRL_PRIMARY (degraded/single-node mode, Req 8.4). */
INFO("HA election: outcome=PRIMARY term=%" PRIu64
" votes_granted=1 quorum=1 (single-node async)", cur_term);
INFO("HA FSM: CTRL_ELECTING → CTRL_PRIMARY_COMMITTING "
"(single-node async, term=%" PRIu64 ")", cur_term);
/* Transition to PRIMARY (or COMMITTING if phase2 applies) */
atomic_store_explicit(&ha->role, (int32_t)CTRL_PRIMARY, memory_order_release);
atomic_store_explicit(&ha->role, (int32_t)CTRL_PRIMARY_COMMITTING,
memory_order_release);
pthread_mutex_destroy(&ctx->lock);
sem_destroy(&ctx->done);
@ -1607,6 +1626,26 @@ void ha_start_election_async(ha_state_t *ha, cluster_t *c,
continue;
}
/* Fix 3 (HIGH): endpoint_connect creates the SSL object from iomgr->ssl_ctx
* (server context). For outgoing vote connections we need TLS_client_method.
* After connect, replace the SSL object with one from client_ssl_ctx and
* explicitly set connect (client) state so the handshake goes client-side.
* The fd is already set on the old ssl object — re-set it on the new one. */
if (ha->client_ssl_ctx && ep->ssl) {
SSL_CTX *client_ctx = (SSL_CTX *)ha->client_ssl_ctx;
SSL *new_ssl = SSL_new(client_ctx);
if (new_ssl) {
int ep_fd = SSL_get_fd(ep->ssl);
SSL_free(ep->ssl);
ep->ssl = new_ssl;
SSL_set_fd(ep->ssl, ep_fd);
SSL_set_connect_state(ep->ssl);
}
/* If SSL_new fails, fall through with the original SSL object;
* worst case the handshake uses server context on client side,
* which will fail and trigger vote_ep_err_handler (peer denied). */
}
/* Register with iomgr for async I/O */
if (c) {
rc = iomgr_monitor(ep);
@ -1646,6 +1685,11 @@ void ha_start_election_async(ha_state_t *ha, cluster_t *c,
ha_elect_evaluate_and_signal(ha, ctx);
pthread_mutex_unlock(&ctx->lock);
}
} else {
/* Fix 4 (HIGH): wire timeout_ctx back into ctx so that task 4.1
* (quorum-reached path in handle_request_vote_reply) can cancel
* the watchdog by setting timeout_ctx->cancelled = 1 atomically. */
ctx->timeout_ctx = t;
}
} else {
WARN("ha_start_election_async: calloc election_timeout_ctx_t failed");