forked from lei.chen/myxce
feat(ha-single-leader-role): delete dispatch-gate COMMITTING branch + collapse role predicates to PRIMARY (task 4.2)
This commit is contained in:
parent
a5bf86112a
commit
71ce41f486
3 changed files with 35 additions and 54 deletions
|
|
@ -198,7 +198,7 @@ static int raft_cb_send_vote_request(void *ctx, int peer_idx, uint64_t term)
|
|||
*
|
||||
* from_idx is the absolute candidate index of the requester.
|
||||
* Applies the same checks as handle_request_vote:
|
||||
* - role gate: PRIMARY or COMMITTING → deny
|
||||
* - role gate: PRIMARY → deny
|
||||
* - fresh heartbeat on STANDBY → deny
|
||||
* - in_catch_up → deny
|
||||
* - freshness: candidate config_tag/lsn must be ≥ ours
|
||||
|
|
@ -212,7 +212,7 @@ static int raft_cb_vote_eligible(void *ctx, int from_idx, uint64_t term)
|
|||
/* Role gate */
|
||||
ctrl_role_t role = (ctrl_role_t)atomic_load_explicit(&ha->role,
|
||||
memory_order_acquire);
|
||||
if (role == CTRL_PRIMARY || role == CTRL_PRIMARY_COMMITTING)
|
||||
if (role == CTRL_PRIMARY)
|
||||
return 0;
|
||||
|
||||
/* Fresh heartbeat on STANDBY → primary is alive, deny */
|
||||
|
|
@ -1534,26 +1534,6 @@ int ha_dispatch_gate(ha_state_t *ha, int opcode)
|
|||
return 0;
|
||||
return -XCE_ERR_NOT_READY;
|
||||
|
||||
case CTRL_PRIMARY_COMMITTING:
|
||||
/* Req 5.1, 5.2, 8.1: Primary won election but has not yet received
|
||||
* majority replication ack. Allow only replication-related and
|
||||
* administrative opcodes; deny job dispatch and queue management
|
||||
* with XCE_ERR_NOT_READY. */
|
||||
switch (opcode) {
|
||||
case OPCode_heartbeat:
|
||||
case OPCode_replConfig:
|
||||
case OPCode_replConfigPush:
|
||||
case OPCode_upgradeConfigAck:
|
||||
case OPCode_hello:
|
||||
case OPCode_walAck:
|
||||
case OPCode_replEntry:
|
||||
case OPCode_replAck:
|
||||
case OPCode_replCatchUpDone:
|
||||
return 0;
|
||||
default:
|
||||
return -XCE_ERR_NOT_READY;
|
||||
}
|
||||
|
||||
case CTRL_STANDBY: {
|
||||
/* Allow ops that standbys legitimately handle */
|
||||
switch (opcode) {
|
||||
|
|
@ -1957,7 +1937,7 @@ static int ha_heartbeat_is_fresh(ha_state_t *ha)
|
|||
*
|
||||
* Decision order (fail-fast):
|
||||
* 0. role gate (Req 6.1, 6.2):
|
||||
* - CTRL_PRIMARY or CTRL_PRIMARY_COMMITTING → deny
|
||||
* - CTRL_PRIMARY → deny
|
||||
* - CTRL_STANDBY with fresh heartbeat → deny
|
||||
* 1. in_catch_up → deny (Req 2.5)
|
||||
* 2. req term < my term → deny (Req 2.2)
|
||||
|
|
@ -1988,20 +1968,19 @@ int handle_request_vote(ha_state_t *ha,
|
|||
* Raft §5.1: "If RPC request or response contains term T > currentTerm:
|
||||
* set currentTerm = T, convert to follower."
|
||||
*
|
||||
* Without this ordering, a node in CTRL_PRIMARY_COMMITTING at term=N
|
||||
* would deny a vote request at term=N+1 without ever seeing the higher
|
||||
* term. Both nodes then independently promote to PRIMARY via their
|
||||
* committing timeout → split-brain. */
|
||||
* Without this ordering, a CTRL_PRIMARY node at term=N would deny a vote
|
||||
* request at term=N+1 without ever seeing the higher term, leaving two
|
||||
* nodes both believing they lead → split-brain. */
|
||||
uint64_t my_term = atomic_load_explicit(&ha->term, memory_order_acquire);
|
||||
if (term > my_term) {
|
||||
atomic_store_explicit(&ha->term, term, memory_order_release);
|
||||
ha->voted_for_term = 0;
|
||||
memset(ha->voted_for_name, 0, sizeof(ha->voted_for_name));
|
||||
/* Step down from leader/committing: a higher-term election is in
|
||||
* progress and we must not block it with the role gate below. */
|
||||
/* Step down from leader: a higher-term election is in progress and we
|
||||
* must not block it with the role gate below. */
|
||||
ctrl_role_t cur_role = (ctrl_role_t)atomic_load_explicit(&ha->role,
|
||||
memory_order_acquire);
|
||||
if (cur_role == CTRL_PRIMARY || cur_role == CTRL_PRIMARY_COMMITTING) {
|
||||
if (cur_role == CTRL_PRIMARY) {
|
||||
ha_do_demote(ha);
|
||||
INFO("HA: stepping down (term %" PRIu64 " → %" PRIu64
|
||||
") — higher-term election in progress", my_term, term);
|
||||
|
|
@ -2017,12 +1996,12 @@ int handle_request_vote(ha_state_t *ha,
|
|||
/* Req 6.1, 6.2: role-based deny gate.
|
||||
* Evaluated AFTER the term catch-up above: if we just stepped down due to
|
||||
* a higher term, role is now CTRL_STANDBY and we fall through to voting.
|
||||
* PRIMARY and PRIMARY_COMMITTING at the current term still deny votes
|
||||
* (protecting the live primary from same-term challengers). */
|
||||
* PRIMARY at the current term still denies votes (protecting the live
|
||||
* primary from same-term challengers). */
|
||||
ctrl_role_t my_role = (ctrl_role_t)atomic_load_explicit(&ha->role,
|
||||
memory_order_acquire);
|
||||
if (my_role == CTRL_PRIMARY || my_role == CTRL_PRIMARY_COMMITTING)
|
||||
return 0; /* deny — I am primary (or committing); protect against challengers */
|
||||
if (my_role == CTRL_PRIMARY)
|
||||
return 0; /* deny — I am primary; protect against same-term challengers */
|
||||
if (my_role == CTRL_STANDBY && ha->primary_ctrl_fd >= 0 &&
|
||||
ha_heartbeat_is_fresh(ha))
|
||||
return 0; /* deny — my primary is alive (active primary connection) */
|
||||
|
|
@ -2507,7 +2486,7 @@ static void on_vote_connected(endpoint_t *ep)
|
|||
#endif
|
||||
|
||||
/* Set state-aware dispatcher — routes by ha->role at message-arrival time.
|
||||
* Never reassigned: ELECTING → vote reply; COMMITTING/PRIMARY → repl ack. */
|
||||
* Never reassigned: ELECTING → vote reply; PRIMARY → repl ack. */
|
||||
ep->msg_handler = peer_ctrl_msg_handler;
|
||||
}
|
||||
|
||||
|
|
@ -2642,7 +2621,7 @@ done:
|
|||
|
||||
/*
|
||||
* repl_ack_decode_and_dispatch: decode an inbound packet on a promoted vote
|
||||
* endpoint while role == CTRL_PRIMARY_COMMITTING or CTRL_PRIMARY.
|
||||
* endpoint while role == CTRL_PRIMARY.
|
||||
*
|
||||
* - OPCode_replCatchUpDone → INFO no-op (ack-counting removed in task 4.1)
|
||||
* - OPCode_reply with stateReportMsg → ha_process_standby_state (Task 3.1)
|
||||
|
|
@ -2820,7 +2799,6 @@ static int repl_ack_decode_and_dispatch(endpoint_t *ep, vote_ep_ctx_t *vctx)
|
|||
* peer_ctrl_msg_handler: state-aware iomgr handler set once in on_vote_connected.
|
||||
* Routes by ha->role at dispatch time — no explicit handler reassignment needed.
|
||||
* CTRL_ELECTING → vote_reply_decode_and_dispatch
|
||||
* CTRL_PRIMARY_COMMITTING /
|
||||
* CTRL_PRIMARY → repl_ack_decode_and_dispatch
|
||||
* any other role → drop packet (WARN)
|
||||
*/
|
||||
|
|
@ -2836,7 +2814,6 @@ static int peer_ctrl_msg_handler(void *ep_raw)
|
|||
case CTRL_ELECTING:
|
||||
return vote_reply_decode_and_dispatch(ep, vctx);
|
||||
|
||||
case CTRL_PRIMARY_COMMITTING:
|
||||
case CTRL_PRIMARY:
|
||||
return repl_ack_decode_and_dispatch(ep, vctx);
|
||||
|
||||
|
|
@ -3068,7 +3045,7 @@ void vote_ep_ctx_set_cluster_for_test(endpoint_t *ep, cluster_t *c)
|
|||
/*
|
||||
* raft_cb_become_leader_for_test: drive the leader promotion path without
|
||||
* a live cluster. Passes ha as ctx; sets ha->raft_c = NULL to suppress
|
||||
* production-only calls (phase2, push_config, ctrlhello, committing timer).
|
||||
* production-only calls (phase2, push_config, ctrlhello).
|
||||
*/
|
||||
void raft_cb_become_leader_for_test(ha_state_t *ha, uint64_t term)
|
||||
{
|
||||
|
|
@ -3105,7 +3082,7 @@ static int vote_ep_err_handler(void *ep_raw)
|
|||
ctrl_role_t role = (ctrl_role_t)atomic_load_explicit(&ha->role,
|
||||
memory_order_acquire);
|
||||
|
||||
if (role == CTRL_PRIMARY_COMMITTING || role == CTRL_PRIMARY) {
|
||||
if (role == CTRL_PRIMARY) {
|
||||
/* Promoted connection dropped — remove from standby_endpoints */
|
||||
if (ha->standby_endpoints) {
|
||||
pthread_mutex_lock(&ha->lock);
|
||||
|
|
@ -3902,7 +3879,7 @@ static int primary_heartbeat_fn(void *arg)
|
|||
|
||||
ctrl_role_t role = (ctrl_role_t)atomic_load_explicit(&ha->role,
|
||||
memory_order_acquire);
|
||||
if (role != CTRL_PRIMARY && role != CTRL_PRIMARY_COMMITTING)
|
||||
if (role != CTRL_PRIMARY)
|
||||
return 0; /* demoted — stop sending NOPs */
|
||||
|
||||
/* 1. Heartbeat all connected standbys — idle-triggered NOP (Req 3.1-3.4).
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* Tests verify (Req 6.1, 6.2, 6.4):
|
||||
* Req 6.1: CTRL_PRIMARY → deny all vote requests (return 0)
|
||||
* Req 6.1: CTRL_PRIMARY_COMMITTING → deny all vote requests (return 0)
|
||||
* Req 6.1: CTRL_PRIMARY + fresher same-term challenger → still deny (return 0)
|
||||
* Req 6.2: CTRL_STANDBY + fresh heartbeat → deny (return 0)
|
||||
* Req 6.4: CTRL_STANDBY + stale heartbeat → evaluate Raft gates (not denied by role gate)
|
||||
* Req 6.4: CTRL_ELECTING → evaluate Raft gates (not denied by role gate)
|
||||
|
|
@ -88,16 +88,18 @@ static void test_primary_role_denies_vote_request(void)
|
|||
printf("test_primary_role_denies_vote_request: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Req 6.1: CTRL_PRIMARY_COMMITTING → deny ────────────────────────────── */
|
||||
/* ── Req 6.1: CTRL_PRIMARY denies a fresher same-term challenger ─────────── */
|
||||
|
||||
static void test_primary_committing_role_denies_vote_request(void)
|
||||
static void test_primary_role_denies_fresher_same_term_challenger(void)
|
||||
{
|
||||
/* When the receiving node is CTRL_PRIMARY_COMMITTING (waiting for quorum
|
||||
* replication ack before opening traffic), it must also deny all vote
|
||||
* requests. A challenger must not displace a committing primary. */
|
||||
/* The vote-deny contract that a transitional COMMITTING role used to carry
|
||||
* now lives entirely on CTRL_PRIMARY (single leader role). An elected
|
||||
* primary must deny a same-term challenger even when the challenger looks
|
||||
* FRESHER (higher config_tag / LSN): the role gate fires before any
|
||||
* freshness evaluation, protecting the live primary from displacement. */
|
||||
ha_state_t ha;
|
||||
ha_init(&ha, &g_gate_cfg, 0);
|
||||
atomic_store_explicit(&ha.role, (int32_t)CTRL_PRIMARY_COMMITTING,
|
||||
atomic_store_explicit(&ha.role, (int32_t)CTRL_PRIMARY,
|
||||
memory_order_release);
|
||||
atomic_store_explicit(&ha.term, 3ULL, memory_order_release);
|
||||
ha.voted_for_term = 0;
|
||||
|
|
@ -105,17 +107,17 @@ static void test_primary_committing_role_denies_vote_request(void)
|
|||
int granted = handle_request_vote(&ha,
|
||||
3ULL, /* same term */
|
||||
"ctrl-c",
|
||||
200ULL, /* candidate is fresh */
|
||||
200ULL, /* candidate is fresher */
|
||||
100ULL,
|
||||
100ULL, /* my config_tag */
|
||||
50ULL,
|
||||
0);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, granted,
|
||||
"CTRL_PRIMARY_COMMITTING must deny all vote requests (Req 6.1)");
|
||||
"CTRL_PRIMARY must deny a fresher same-term challenger (Req 6.1)");
|
||||
|
||||
ha_fini(&ha);
|
||||
printf("test_primary_committing_role_denies_vote_request: PASS\n");
|
||||
printf("test_primary_role_denies_fresher_same_term_challenger: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Req 6.2: CTRL_STANDBY + fresh heartbeat → deny ────────────────────── */
|
||||
|
|
@ -234,8 +236,8 @@ void test_ha_vote_gate(void)
|
|||
/* Req 6.1 — PRIMARY denies */
|
||||
RUN_TEST(test_primary_role_denies_vote_request);
|
||||
|
||||
/* Req 6.1 — PRIMARY_COMMITTING denies */
|
||||
RUN_TEST(test_primary_committing_role_denies_vote_request);
|
||||
/* Req 6.1 — PRIMARY denies a fresher same-term challenger */
|
||||
RUN_TEST(test_primary_role_denies_fresher_same_term_challenger);
|
||||
|
||||
/* Req 6.2 — STANDBY + fresh heartbeat denies */
|
||||
RUN_TEST(test_standby_fresh_heartbeat_denies_vote_request);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue