diff --git a/.kiro/specs/ha-config-bootstrap/tasks.md b/.kiro/specs/ha-config-bootstrap/tasks.md index a5057827..24220fdc 100644 --- a/.kiro/specs/ha-config-bootstrap/tasks.md +++ b/.kiro/specs/ha-config-bootstrap/tasks.md @@ -68,7 +68,7 @@ Dependency order: startup validation (D) unblocks vtaskmgr before election (A); - _Requirements: 1.1, 1.2_ - _Boundary: Startup Sequence (cluster.c, myapp.c)_ -- [ ] 3.3 Implement dispatch gate for CTRL_INIT and CTRL_ELECTING states +- [x] 3.3 Implement dispatch gate for CTRL_INIT and CTRL_ELECTING states - Initialize `ha.heartbeat_seen` semaphore in `ha_init()`; initialize `ha.primary_ctrl_fd = -1` - Add role-check at vtaskmgr dispatch entry: `CTRL_INIT` and `CTRL_ELECTING` pass only `OP_requestVote` and `OP_probeConfigTag`; all others return `XCE_ERR_NOT_READY` - `CTRL_STANDBY` returns `XCE_ERR_STANDBY` with `redirect_host/port` for primary-only ops diff --git a/src/controller/cluster.c b/src/controller/cluster.c index f21291a2..60e80bcd 100644 --- a/src/controller/cluster.c +++ b/src/controller/cluster.c @@ -192,6 +192,17 @@ static int myreq_handler(vtask_t* task) init_reply_by_req(&reply, &request); + /* Dispatch gate: reject ops not allowed in current FSM state (req 1.3, 1.4) */ + rc = ha_dispatch_gate(&c->ha, request.opcode); + if (rc != 0) { + reply.status = rc; + reply.data_which = RequestReply_data_nodata; + xce_reply(ep, c->myhost, &reply); + free_Request(&request); + free(raw_pkt_copy); + return rc; + } + switch (request.opcode) { case OPCode_shutdown: reply.status = 0; diff --git a/src/controller/ha.c b/src/controller/ha.c index 684b4d23..c612edfe 100644 --- a/src/controller/ha.c +++ b/src/controller/ha.c @@ -22,9 +22,15 @@ /* Circular-include safety: cluster_def.h includes ha.h (include-guarded), * and ha.h only forward-declares cluster_t — no cycle. */ #include "xce/cluster_def.h" +#include "xce/errnox.h" +#include "controller.capnp.h" #include "persist.h" #include "utils.h" +/* ── HA dispatch gate error codes (defined here; declared extern in errnox.h) ── */ +int XCE_ERR_NOT_READY = E_XCE_NOT_READY; +int XCE_ERR_STANDBY = E_XCE_STANDBY; + /* ── PRNG seeding ───────────────────────────────────────────── */ void ha_seed_jitter(uint64_t *seed, int my_priority) @@ -82,6 +88,11 @@ int ha_init(ha_state_t *ha, const ha_config_t *cfg, int my_priority) ha->ssl_ctx = NULL; ha->jitter_seed = 0; ha->standby_endpoints = NULL; + ha->primary_ctrl_fd = -1; + + /* heartbeat_seen starts at 0: no heartbeat seen yet. + * ha_wait_for_heartbeat_or_timeout() calls sem_timedwait on this. */ + sem_init(&ha->heartbeat_seen, 0, 0); return 0; } @@ -454,6 +465,7 @@ void ha_fini(ha_state_t *ha) return; ha_stop_watchdog(ha); /* join watchdog thread before destroying mutex */ pthread_mutex_destroy(&ha->lock); + sem_destroy(&ha->heartbeat_seen); /* Fix ASAN leak: free the SSL context if one was allocated */ if (ha->ssl_ctx) { @@ -462,6 +474,70 @@ void ha_fini(ha_state_t *ha) } } +/* ── Dispatch gate (req 1.3, 1.4) ────────────────────────────────────────── + * + * Called from myreq_handler() in cluster.c before the opcode switch. + * Returns 0 when the operation is permitted for the current role. + * + * Primary-only ops are any op that is not one of the always-allowed set: + * nop, reply, hello, join, shutdown, heartbeat, redirect, + * steppingDown, replEntry, replAck, replSnapshot, replCatchUpDone, + * replConfig, replConfigPush, requestVote, probeConfigTag, + * walAck, upgradeConfigAck, snapshotTransfer, walCatchupEntry. + * + * For CTRL_STANDBY the check is: op is in the primary-only set → deny. + * For CTRL_INIT / CTRL_ELECTING the allowlist is tighter: only + * OP_requestVote and OP_probeConfigTag pass through. + */ +int ha_dispatch_gate(ha_state_t *ha, int opcode) +{ + ctrl_role_t role = + (ctrl_role_t)atomic_load_explicit(&ha->role, memory_order_acquire); + + switch (role) { + case CTRL_PRIMARY: + return 0; + + case CTRL_INIT: + case CTRL_ELECTING: + if (opcode == OPCode_requestVote || opcode == OPCode_probeConfigTag) + return 0; + return -XCE_ERR_NOT_READY; + + case CTRL_STANDBY: { + /* Allow ops that standbys legitimately handle */ + switch (opcode) { + case OPCode_nop: + case OPCode_reply: + case OPCode_hello: + case OPCode_join: + case OPCode_shutdown: + case OPCode_heartbeat: + case OPCode_redirect: + case OPCode_steppingDown: + case OPCode_replEntry: + case OPCode_replAck: + case OPCode_replSnapshot: + case OPCode_replCatchUpDone: + case OPCode_replConfig: + case OPCode_replConfigPush: + case OPCode_requestVote: + case OPCode_probeConfigTag: + case OPCode_walAck: + case OPCode_upgradeConfigAck: + case OPCode_snapshotTransfer: + case OPCode_walCatchupEntry: + return 0; + default: + return -XCE_ERR_STANDBY; + } + } + + default: + return 0; + } +} + /* ── Redirect helpers used by join.c ───────────────────────── */ /* diff --git a/src/controller/ha.h b/src/controller/ha.h index 9e85d0ba..611bccac 100644 --- a/src/controller/ha.h +++ b/src/controller/ha.h @@ -277,6 +277,17 @@ void start_heartbeat_listener(cluster_t *c); */ void stop_heartbeat_listener(cluster_t *c); +/* + * ha_dispatch_gate: role-based operation filter for the vtaskmgr dispatch loop. + * Returns: + * 0 — operation is permitted for the current role + * -E_XCE_NOT_READY — role is CTRL_INIT or CTRL_ELECTING and opcode is not + * OP_requestVote or OP_probeConfigTag + * -E_XCE_STANDBY — role is CTRL_STANDBY and opcode is a primary-only op + * CTRL_PRIMARY always returns 0. + */ +int ha_dispatch_gate(ha_state_t *ha, int opcode); + /* ── HA Config Bootstrap & Term Durability (ha-config-bootstrap spec) ── */ /* diff --git a/src/include/xce/errnox.h b/src/include/xce/errnox.h index 41dd7ad5..2442d95a 100644 --- a/src/include/xce/errnox.h +++ b/src/include/xce/errnox.h @@ -21,7 +21,14 @@ enum { E_XCE_INTERNAL, E_XCE_NOMASTER, /* Standby has no known primary — client should retry */ E_XCE_REDIRECT, /* Standby is redirecting client to primary */ + E_XCE_NOT_READY, /* Controller in CTRL_INIT or CTRL_ELECTING state */ + E_XCE_STANDBY, /* Controller in CTRL_STANDBY; op is primary-only */ E_XCE_MAX }; +/* HA dispatch gate error variables — defined in ha.c, declared here so all + * dispatch call sites that include errnox.h get the right names. */ +extern int XCE_ERR_NOT_READY; /* == E_XCE_NOT_READY */ +extern int XCE_ERR_STANDBY; /* == E_XCE_STANDBY */ + #endif