refactor(ha): passive standby model — primary connects outbound to all peers

Remove fetch_config_from_primary, ha_find_and_connect_to_primary, and the
tier-1/2/3 discovery path. Standbys never initiate outbound connections to
the primary; the primary owns all inter-controller channels.

Key changes:
- ha.c: add primary heartbeat loop (ha_start_primary_heartbeat) that sends
  OPCode_nop to connected standbys every heartbeat_interval_ms; loop
  self-cancels on demotion via role check before re-arm.
- ha.c: add outbound reconnect in primary_heartbeat_fn — for peers absent
  from standby_endpoints, open fresh outbound TLS connection (on_standby_connected
  sends ctrlhello; standby_ep_ctx_t mirrors vote_ep_ctx_t layout).
- ha.c: remove ha_find_and_connect_to_primary + helpers (ha_raw_tcp_connect,
  ha_tcp_connect_hook); remove election_timeout_fn (superseded by
  raft_election_timeout_fn); remove fetch_config_from_primary call from
  ha_elect_evaluate_and_signal STANDBY branch.
- cluster.c: OPCode_requestVote denial no longer reuses inbound connection;
  late joiners are bootstrapped via primary heartbeat outbound connect.
- config_repl.c: remove connect_ssl_raw, do_one_config_fetch,
  fetch_config_from_primary (~250 LOC).
- myapp.c: remove my_tag > 0 fetch branch; all standbys wait passively.
- ha.h: remove ha_find_and_connect_to_primary declaration + macros +
  ha_tcp_connect_hook seam; add ha_start_primary_heartbeat declaration.
- meson.build: remove test_ha_connect.c (tests deleted function).
- controller_test.c: remove test_ha_connect registration.
- CLAUDE.md, src/controller/CLAUDE.md: document passive standby model,
  inbound-vs-outbound connection ownership, async I/O constraint,
  heartbeat model.
- request.h: add xce_request declaration (removes scattered local externs).
This commit is contained in:
Rongsong Shen 2026-06-01 04:17:05 +00:00
parent 842b9a838e
commit 7b39650229
11 changed files with 716 additions and 919 deletions

View file

@ -252,29 +252,16 @@ int myapp_run(myconf_t *myconf)
skip_election:;
/* Nodes that skipped election (already voted or no config) are now STANDBY.
* Nodes that already have a config actively fetch it so they can connect to
* the primary. Nodes with no config (my_tag == 0) wait passively — the
* primary will push the config via ctrlhello after winning the election
* (Req 7.3). Triggering fetch_config_from_primary for no-config nodes
* only causes retry warnings since the primary isn't up yet. */
/* All standbys wait passively for the primary to connect outbound and push
* config via ctrlhello. fetch_config_from_primary has been removed:
* standbys never initiate outbound connects after an election result. */
{
ctrl_role_t role = (ctrl_role_t)atomic_load_explicit(&c.ha.role,
memory_order_acquire);
uint64_t my_tag = c.myconf ? c.myconf->config_version_tag : 0;
if ((role == CTRL_STANDBY || role == CTRL_INIT) && my_tag > 0) {
if (role == CTRL_STANDBY || role == CTRL_INIT) {
atomic_store_explicit(&c.ha.role, (int32_t)CTRL_STANDBY,
memory_order_release);
extern int fetch_config_from_primary(cluster_t *c);
/* Use delaymgr one-shot so cluster_run is entered before the
* blocking fetch starts (fetch runs in delaymgr worker thread). */
delay_task_ms(&c.delaymgr, 200, 1,
(delay_task_entry_fn_t)fetch_config_from_primary, &c);
} else if (role == CTRL_STANDBY || role == CTRL_INIT) {
/* No config — stay STANDBY, wait for primary push (Req 7.3). */
atomic_store_explicit(&c.ha.role, (int32_t)CTRL_STANDBY,
memory_order_release);
INFO("myapp_run: no local config — waiting for primary to push config");
INFO("myapp_run: CTRL_STANDBY — waiting for primary to connect and push config");
}
}