feat(controller-status): allow ctrlstatus through ha_dispatch_gate in all roles incl no-quorum (task 2.2)
This commit is contained in:
parent
a6e03d87ab
commit
e56409cbd4
3 changed files with 89 additions and 3 deletions
|
|
@ -18,7 +18,7 @@
|
|||
- Observable: the unit tests pass for the primary, standby, and unknown-primary cases, and a no-mutation assertion holds.
|
||||
- _Requirements: 1.1, 1.2, 1.3, 2.1, 2.2, 2.3, 3.1, 3.2, 4.1, 4.2, 5.1, 5.2, 5.3, 6.1, 6.2, 6.3, 6.4_
|
||||
- _Boundary: handle_ctrlstatus_
|
||||
- [ ] 2.2 Allow the status query through the HA dispatch gate in every role
|
||||
- [x] 2.2 Allow the status query through the HA dispatch gate in every role
|
||||
- Add the status opcode to the dispatch-gate allow-lists for primary (including the no-quorum branch), standby, electing, and initializing — so a status read always answers (including when fenced or without quorum), rather than being rejected as standby/not-ready. Scope the change strictly to this read-only opcode.
|
||||
- TDD: an integration test confirms the status request is answered (not rejected) on a standby, an electing node, an initializing node, and a primary without quorum; and a guard test confirms no other opcode's gating changed.
|
||||
- Observable: the gate test shows the status query answered in all four role conditions and unchanged behavior for at least one previously primary-only opcode.
|
||||
|
|
|
|||
|
|
@ -2088,6 +2088,10 @@ int ha_dispatch_gate(ha_state_t *ha, int opcode)
|
|||
/* Read-only queries */
|
||||
case OPCode_get:
|
||||
case OPCode_probeConfigTag:
|
||||
/* Read-only controller status (controller-status 7.1, 7.2):
|
||||
* answers the local identity/health view even without quorum —
|
||||
* precisely when an operator most wants it. */
|
||||
case OPCode_ctrlstatus:
|
||||
/* Fact-bearing ops — reports of events that already happened
|
||||
* (worker job transitions/rusage, scheduler pend reasons).
|
||||
* Rejecting them loses the fact forever: the reporter never
|
||||
|
|
@ -2118,7 +2122,10 @@ int ha_dispatch_gate(ha_state_t *ha, int opcode)
|
|||
opcode == OPCode_nop ||
|
||||
opcode == OPCode_replConfigPush ||
|
||||
opcode == OPCode_walCatchupEntry ||
|
||||
opcode == OPCode_snapshotTransfer)
|
||||
opcode == OPCode_snapshotTransfer ||
|
||||
/* Read-only controller status — answerable in every role
|
||||
* (controller-status 7.1, 7.2). */
|
||||
opcode == OPCode_ctrlstatus)
|
||||
return 0;
|
||||
return -XCE_ERR_NOT_READY;
|
||||
|
||||
|
|
@ -2136,7 +2143,10 @@ int ha_dispatch_gate(ha_state_t *ha, int opcode)
|
|||
opcode == OPCode_nop ||
|
||||
opcode == OPCode_replConfigPush ||
|
||||
opcode == OPCode_walCatchupEntry ||
|
||||
opcode == OPCode_snapshotTransfer)
|
||||
opcode == OPCode_snapshotTransfer ||
|
||||
/* Read-only controller status — answerable in every role
|
||||
* (controller-status 7.1, 7.2). */
|
||||
opcode == OPCode_ctrlstatus)
|
||||
return 0;
|
||||
return -XCE_ERR_NOT_READY;
|
||||
|
||||
|
|
@ -2164,6 +2174,9 @@ int ha_dispatch_gate(ha_state_t *ha, int opcode)
|
|||
case OPCode_upgradeConfigAck:
|
||||
case OPCode_snapshotTransfer:
|
||||
case OPCode_walCatchupEntry:
|
||||
/* Read-only controller status — a standby answers with its local
|
||||
* view (it reports the primary it knows) (controller-status 7.1, 7.2). */
|
||||
case OPCode_ctrlstatus:
|
||||
return 0;
|
||||
default:
|
||||
return -XCE_ERR_STANDBY;
|
||||
|
|
|
|||
|
|
@ -791,6 +791,77 @@ static void test_election_stagger_desyncs_by_priority(void)
|
|||
printf("test_election_stagger_desyncs_by_priority: PASS\n");
|
||||
}
|
||||
|
||||
/* ── controller-status task 2.2: dispatch gate admits OPCode_ctrlstatus in
|
||||
* EVERY role — STANDBY, ELECTING, INIT, and PRIMARY-without-quorum — so the
|
||||
* read-only status query always answers (even fenced / without quorum),
|
||||
* rather than being rejected as -XCE_ERR_STANDBY / -XCE_ERR_NOT_READY.
|
||||
*
|
||||
* Requirements: controller-status 7.1, 7.2 (failover-aware, any-node delivery);
|
||||
* design §"Dispatch-gate allow-lists — Service contract".
|
||||
*
|
||||
* Scope guard: the change must be strictly scoped to OPCode_ctrlstatus — no
|
||||
* other opcode's gating may widen. This test also asserts that a previously
|
||||
* primary-only-when-serving read (OPCode_clusterconf) is STILL rejected on a
|
||||
* STANDBY after the change, proving the allow-list was not broadened.
|
||||
*
|
||||
* Note on PRIMARY-without-quorum: ha_has_write_quorum() returns false when the
|
||||
* connected-standby count is below n_peers/2. With n_peers == 0 (default
|
||||
* ha_init) the threshold is 0 and quorum is trivially satisfied, so to exercise
|
||||
* the no-quorum branch we set n_peers high enough that 0 connected standbys
|
||||
* fail the >= n_peers/2 test. */
|
||||
static void test_dispatch_gate_allows_ctrlstatus_in_every_role(void)
|
||||
{
|
||||
ha_state_t ha;
|
||||
ha_config_t cfg = {
|
||||
.heartbeat_interval_ms = 3000,
|
||||
.heartbeat_timeout_ms = 10000,
|
||||
.election_timeout_ms = 2000,
|
||||
.probe_connect_timeout_ms = 500,
|
||||
.max_promotion_retries = 5,
|
||||
};
|
||||
TEST_ASSERT_EQUAL_INT(0, ha_init(&ha, &cfg, 0));
|
||||
|
||||
/* CTRL_STANDBY — must allow ctrlstatus (Req 7.1, 7.2) */
|
||||
atomic_store_explicit(&ha.role, (int32_t)CTRL_STANDBY, memory_order_release);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0,
|
||||
ha_dispatch_gate(&ha, OPCode_ctrlstatus),
|
||||
"CTRL_STANDBY must allow OPCode_ctrlstatus (Req 7.1, 7.2)");
|
||||
|
||||
/* CTRL_ELECTING — must allow ctrlstatus */
|
||||
atomic_store_explicit(&ha.role, (int32_t)CTRL_ELECTING, memory_order_release);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0,
|
||||
ha_dispatch_gate(&ha, OPCode_ctrlstatus),
|
||||
"CTRL_ELECTING must allow OPCode_ctrlstatus (Req 7.1, 7.2)");
|
||||
|
||||
/* CTRL_INIT — must allow ctrlstatus */
|
||||
atomic_store_explicit(&ha.role, (int32_t)CTRL_INIT, memory_order_release);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0,
|
||||
ha_dispatch_gate(&ha, OPCode_ctrlstatus),
|
||||
"CTRL_INIT must allow OPCode_ctrlstatus (Req 7.1, 7.2)");
|
||||
|
||||
/* CTRL_PRIMARY without write quorum — must allow ctrlstatus.
|
||||
* Force the no-quorum branch: with no replicator and zero connected
|
||||
* standby_endpoints, ha_has_write_quorum() returns false once n_peers > 1
|
||||
* (connected 0 < n_peers/2). */
|
||||
ha.n_peers = 3; /* threshold = n_peers/2 = 1; connected = 0 → no quorum */
|
||||
atomic_store_explicit(&ha.role, (int32_t)CTRL_PRIMARY, memory_order_release);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0,
|
||||
ha_dispatch_gate(&ha, OPCode_ctrlstatus),
|
||||
"CTRL_PRIMARY without quorum must allow OPCode_ctrlstatus (Req 7.1, 7.2)");
|
||||
|
||||
/* Scope guard (Req: change strictly scoped to ctrlstatus): a read that is
|
||||
* primary-only-when-serving (OPCode_clusterconf) must STILL be rejected on
|
||||
* a STANDBY — proving the allow-list was not widened for any other opcode. */
|
||||
atomic_store_explicit(&ha.role, (int32_t)CTRL_STANDBY, memory_order_release);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(-XCE_ERR_STANDBY,
|
||||
ha_dispatch_gate(&ha, OPCode_clusterconf),
|
||||
"CTRL_STANDBY must STILL reject OPCode_clusterconf — gate change must "
|
||||
"be scoped strictly to OPCode_ctrlstatus");
|
||||
|
||||
ha_fini(&ha);
|
||||
printf("test_dispatch_gate_allows_ctrlstatus_in_every_role: PASS\n");
|
||||
}
|
||||
|
||||
void test_ha_role(void)
|
||||
{
|
||||
RUN_TEST(test_ha_init_default_role_is_standby);
|
||||
|
|
@ -835,6 +906,8 @@ void test_ha_role(void)
|
|||
RUN_TEST(test_dispatch_gate_allows_prevote_in_voter_roles);
|
||||
/* raft/ha-prevote (bug-606): failover election stagger desyncs survivors */
|
||||
RUN_TEST(test_election_stagger_desyncs_by_priority);
|
||||
/* controller-status task 2.2: dispatch gate admits ctrlstatus in every role */
|
||||
RUN_TEST(test_dispatch_gate_allows_ctrlstatus_in_every_role);
|
||||
}
|
||||
|
||||
/* test_am_i_primary suite deleted — am_i_primary() removed (election-iomgr-refactor) */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue