myxce/.kiro/specs/ha-config-bootstrap/research.md
2026-05-29 15:00:06 +08:00

277 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Gap Analysis: ha-config-bootstrap
*Generated: 2026-05-27. Branch: persistent_ha.*
---
## Analysis Summary
- **Scope**: 10 requirements across 8 distinct changes (A–H). The codebase has strong foundational infrastructure (iomgr, repl slot management, WAL, SSL) but the HA election core, quorum commit, and control channel are mostly absent or stubbed.
- **Largest gaps**: Raft-style quorum election (Req 2) and persistent control channel with full state sync (Req 6) are full rewrites/additions with no current equivalent logic; majority-commit WAL (Req 5) requires implementing the two stub functions at the heart of replication.
- **Strong reusable assets**: `repl_add_standby/remove/start_catchup/complete_catchup/needs_snapshot` are real implementations; the SSL connect pattern (`connect_ssl_raw`), xorshift64 PRNG, `ha_do_promote/demote`, and iomgr epoll machinery are all directly reusable.
- **Risk concentration**: Req 2 and Req 6 carry High risk; Req 5 carries High risk due to `committed_lsn` cross-cutting `persist_t`, `repl.c`, and `ha_state_t`. The remaining requirements are Medium or Low.
- **Recommended approach**: Hybrid (Option A) — implement changes A→D→B→C→E→F→G→H in dependency order, writing C tests first per TDD, one atomic commit per change.
---
## 1. Current State Investigation
### 1.1 HA Core (ha.h / ha.c)
**`ctrl_role_t` enum** — only two values: `CTRL_PRIMARY = 0`, `CTRL_STANDBY = 1`.
Missing: `CTRL_INIT`, `CTRL_ELECTING` (required by Req 1).
**`ha_state_t`** — current fields of interest:
| Field | Status | Notes |
|---|---|---|
| `term` (_Atomic uint64_t) | Exists | Used for fencing |
| `role` (_Atomic int32_t) | Exists | Cast to ctrl_role_t |
| `my_priority` | Exists | LSF priority index — kept for compat but unused in Raft |
| `applied_lsn` (_Atomic uint64_t) | Exists | Reusable for vote freshness check |
| `primary_host[XCE_MAXNAME_LEN]` | Exists | Reusable |
| `primary_port` | Exists | Reusable |
| `last_heartbeat` (struct timespec) | Exists | Replaces with `EPOLLRDHUP` path in Req 6 |
| `in_catch_up` (_Atomic int) | Exists | Reusable for Clarification 6 vote exclusion |
| `jitter_seed` (uint64_t) | Exists | xorshift64, directly reusable |
| `ssl_ctx` (void*) | Exists | Reusable |
| `standby_endpoints` (zarray_t*) | Exists | Superseded by per-standby fd table in Req 6 |
| `voted_for_term` (uint64_t) | **MISSING** | Req 9 |
| `voted_for_name` (char[]) | **MISSING** | Req 9 |
| `primary_ctrl_fd` (int) | **MISSING** | Req 6 |
| `defer_config_fetch` (bool) | **MISSING** | Req 3 |
| `elect_ctx` (_Atomic ptr) | **MISSING** | Req 2 |
| `last_known_committed_lsn` (_Atomic uint64_t) | **MISSING** | Req 5 |
| `heartbeat_seen` (sem_t) | **MISSING** | Req 1 |
**`ha_config_t`** — missing: `commit_timeout_ms`. All election fields present (election_timeout_ms, jitter_max_ms, etc.) but values tuned for LSF model.
**`am_i_primary()`** — LSF priority probe via per-peer pthreads. No quorum, no config_tag/LSN in vote, no term/voted_for persistence. Must be replaced entirely with `am_i_primary_quorum()`.
**`ha_check_priority_stepdown()`** — permanently unreachable once term > 0; exists only for the term=0 simultaneous-boot race. Must be retired (Req 2).
**`ha_check_heartbeat_nocluster()`** — timestamp polling with configurable timeout. Replaced by `EPOLLRDHUP` fast path + heartbeat timeout vtask (Req 6). Function itself can be deleted.
**`ha_watchdog_thread()`** — sets `failover_requested` but cannot call `ha_promote_to_primary()` (no `cluster_t`). Partially functional stub. Replaced by vtask-based heartbeat monitor (Req 6).
**`start_heartbeat_listener()` / `stop_heartbeat_listener()`** — no-op stubs with comment "full implementation in cluster.c Task 7". **Missing** (Req 1).
**`ha_fini()`** — missing `SSL_CTX_free(ha->ssl_ctx)`. ASAN-visible leak (~5 KB).
### 1.2 Startup Sequence (myapp.c / main.c)
**myapp.c current order**:
```
cluster_init_phase1()
initialize_openssl() + create_ssl_context()
am_i_primary() ← election here
load_primary_config() / fetch_config_from_primary()
cluster_init_phase2() ← iomgr, vtaskmgr, bind+listen inside
cluster_start() ← more network init
cluster_run()
```
**Bind+listen** happens inside `cluster_init_phase2()` or `cluster_start()` — **after** election. Gap: concurrent-startup nodes probe an unbound port (Req 1).
**No `bootstrap_config_if_needed()`** — standby path goes straight to `fetch_config_from_primary()` post-election without checking whether local config_tag is 0 or whether peers have config. Gap (Req 3).
**main.c**:
- `--vcpus N` arg: **MISSING**. vcpus comes from cluster config read inside phase2. This creates a dependency that prevents vtaskmgr from running before election (Req 1, 4).
- Peer count validation: **MISSING**. Even-N peer lists accepted silently (Req 4).
- Cert pre-flight `access()` check: **MISSING**. Cert paths computed but SSL context created later in myapp.c, not with early-exit error in main.c (Req 4).
### 1.3 Replication (repl.c / repl.h)
| Function | Status | Notes |
|---|---|---|
| `repl_init / fini` | Real | Lifecycle ok |
| `repl_add_standby` | Real | Slot management implemented |
| `repl_remove_standby` | Real | Implemented |
| `repl_start_catchup` | Real | Sets in_catch_up=1 |
| `repl_complete_catchup` | Real | Clears in_catch_up, broadcasts cond |
| `repl_needs_snapshot` | Real | Compares standby_lsn vs oldest_wal_lsn |
| `repl_on_ack` | Real | Updates acked_lsn, signals cond |
| `repl_trim_lsn` | Real | WAL trim guard implemented |
| `repl_broadcast` | **STUB** | Returns 0 without sending anything (Req 5, 8) |
| `repl_wait_ack` | Partial | Waits for ANY non-catch-up standby, not majority (Req 5) |
| `repl_wait_majority_ack` | **MISSING** | Req 5, 8 |
**`standby_conn_t`** — missing `pending_ack_count` for backpressure (Req 5).
### 1.4 Persistence (persist.h / persist.c)
- `current_lsn` and `checkpoint_lsn` exist. **`committed_lsn`** — **MISSING** (Req 5).
- `persist_wal_sync_append()` uses `repl_wait_ack()` (any-standby) — must upgrade to `repl_wait_majority_ack()`.
- `persist_truncate_wal_above()` — **MISSING** (Req 5, 6).
- `persist_config_t.sync_timeout_ms` exists — analogous to `commit_timeout_ms` needed for majority-commit.
### 1.5 Config Replication (config_repl.c)
- `do_one_config_fetch()` — **wired** (recent commit `2aae45f` on branch: "wire do_one_config_fetch() — full SSL roundtrip"). Not a stub.
- `bootstrap_config_if_needed()` — **MISSING** (Req 3).
- `xce_probe_config_tag()` — **MISSING**. New lightweight RPC needed (Req 3).
- `push_config_to_standbys()` — declared in config_repl.h; likely fire-and-forget (no majority wait). **Gap**: must upgrade to majority-commit (Req 8).
- `OP_probeConfigTag` Cap'n Proto schema — **MISSING** (Req 3).
- `OP_upgradeConfigAck` Cap'n Proto schema — **MISSING** (Req 8).
- `committed_config_tag` — **MISSING** (Req 8).
### 1.6 Control Channel (ha.c / cluster.c)
- No persistent standby-to-primary TCP connection. Each config fetch and WAL entry opens a new SSL connection via `connect_ssl_raw()`.
- `ha_find_and_connect_to_primary()` — **MISSING** (Req 6).
- Extended `OP_ctrlHello` with applied_lsn, config_tag — **MISSING** (Req 6).
- `OP_snapshotTransfer`, `OP_walCatchupEntry` — **MISSING** (Req 6).
- `handle_ctrlhello()` extended handler — **MISSING** (Req 6).
- Three-tier primary discovery — **MISSING** (Req 6).
- EPOLLRDHUP failover detection — **MISSING** (Req 6).
- Heartbeat timeout vtask replacing watchdog thread — **MISSING** (Req 6).
### 1.7 Election State Durability
- `ha-term.bin` file — **does not exist**.
- `ha_persist_term()` — **MISSING** (Req 9).
- `ha_init()` zeros term and has no `ha-term.bin` read path.
### 1.8 libxce Client Retry (src/lib/)
- `ha_should_redirect()` / `ha_no_primary_known()` exist in ha.c and are used by join.c.
- No transparent retry loop for `XCE_ERR_NOT_READY`, `XCE_ERR_STANDBY`, `ECONNREFUSED`.
- `xce_retry_config_t` — **MISSING** (Req 7).
- `redirect_host` / `redirect_port` fields in `XCE_ERR_STANDBY` error reply — **MISSING** (Req 7).
---
## 2. Requirement-to-Asset Map
| Req | Key Gap | Key Reusable Asset | Gap Tag |
|---|---|---|---|
| 1 — Listener before election | `CTRL_INIT`/`CTRL_ELECTING` enum values; pre-dispatch gate in vtaskmgr; extract bind+listen from phase2; `ha_wait_for_heartbeat_or_timeout()`; `heartbeat_seen` semaphore | iomgr epoll, vtaskmgr dispatch switch, xorshift64 jitter | **Missing** |
| 2 — Quorum election | Entire `am_i_primary_quorum()` and vote protocol; `ha_vote_entry_t`, `ha_elect_ctx_t`; `handle_request_vote()` / `_reply()`; `ha_elect_evaluate_and_signal()`; OP_requestVote capnp schema | `ha_do_promote/demote`, jitter PRNG, `applied_lsn`, iomgr async connect | **Missing** |
| 3 — Config bootstrap | `bootstrap_config_if_needed()`; `xce_probe_config_tag()`; OP_probeConfigTag capnp; `defer_config_fetch` flag in ha_state_t | `do_one_config_fetch()` (wired), `connect_ssl_raw()` | **Missing** |
| 4 — Startup validation | `--vcpus N` CLI arg; even-N peer count check; cert pre-flight `access()`; `SSL_CTX_free` ASAN fix | argp option parsing pattern in main.c | **Missing** |
| 5 — Majority-commit WAL | `repl_broadcast()` implementation; `repl_wait_majority_ack()`; `committed_lsn` in persist_t; `persist_truncate_wal_above()`; `committed_lsn` in heartbeat; `last_known_committed_lsn` in ha_state_t | `repl_wait_ack()` logic, `standby_conn_t`, `repl_start/complete_catchup`, WAL sync path | **Missing** / **Stub** |
| 6 — Persistent channel | Persistent TCP fd per standby; `ha_find_and_connect_to_primary()`; EPOLLRDHUP failover; extended OP_ctrlHello; OP_snapshotTransfer; OP_walCatchupEntry; WAL streaming loop; heartbeat vtask; three-tier discovery | iomgr EPOLLIN/EPOLLRDHUP, `repl_needs_snapshot()`, `repl_start/complete_catchup()` | **Missing** |
| 7 — Client retry | Retry loop in libxce; `xce_retry_config_t`; redirect fields in error reply | `ha_should_redirect()`, `ha_no_primary_known()` | **Missing** |
| 8 — Majority-commit config | `repl_wait_majority_ack()` (shared with Req 5); OP_upgradeConfigAck; `committed_config_tag`; heartbeat piggybacking | `push_config_to_standbys()` (fire-and-forget → upgrade) | **Missing** |
| 9 — State durability | `ha_persist_term()`; `ha-term.bin` read in `ha_init()`; `voted_for_term`/`voted_for_name` in ha_state_t | Atomic write+rename pattern (existing in config_repl.c) | **Missing** |
| 10 — Observability | Structured log statements throughout all new code paths | Existing INFO/WARN/ERROR macros (xcelog.h) | **Missing** (alongside) |
---
## 3. Implementation Approach Options
### Option A: Change-at-a-time (recommended)
Implement changes in dependency order: D → A → B → C → E → H → F → G → (observability woven in).
**Rationale for ordering**:
1. D first: `--vcpus N` removes vtaskmgr's config dependency, unblocking A.
2. A before B: listener must be open before election dispatch works.
3. B before C: config bootstrap needs the proven election winner from quorum election.
4. E and H together: both use `repl_broadcast()` + `repl_wait_majority_ack()`; implement infrastructure once.
5. F after E: persistent channel carries WAL entries; broadcast must send over that channel.
6. G last: retry makes sense only after the controller states it retries against are implemented.
**Files touched per change**:
- D: `main.c`, `cluster.c` (remove vcpus from config), `ha.c` (`SSL_CTX_free`)
- A: `ha.h` (enum + semaphore + fields), `myapp.c` (startup reorder), `cluster.c` (extract listener), `ha.c` (wait function, dispatch gate)
- B: `ha.h` (new types), `ha.c` (replace `am_i_primary`), capnp schema, `cluster.c` (dispatch handler)
- C: `config_repl.c` (`bootstrap_config_if_needed`, `xce_probe_config_tag`), capnp schema, `myapp.c` (integration)
- E: `repl.c` (`repl_broadcast`, `repl_wait_majority_ack`), `persist.h`/`persist.c` (`committed_lsn`, `persist_truncate_wal_above`), `ha.h`/`ha.c` (`last_known_committed_lsn`)
- H: `config_repl.c` (upgrade majority-commit), capnp schema, `ha.h` (`committed_config_tag`)
- F: `ha.c`/`cluster.c` (channel connect, extended hello, streaming, EPOLLRDHUP)
- G: `src/lib/` (retry loop, xce_retry_config_t, redirect fields)
**Trade-offs**:
- ✅ Each atomic commit is testable before the next starts
- ✅ Follows existing TDD convention (failing test first)
- ❌ Long sequential dependency chain; F and G can only start late
### Option B: Infrastructure-first parallel tracks
Track 1 (election+bootstrap): D → A → B → C
Track 2 (replication): E → H
Join (channel): F using Track 1 + Track 2 artifacts
Final: G
**Trade-offs**:
- ✅ Track 1 and Track 2 are mostly independent until F
- ❌ Track 2 cannot be fully validated without Track 1 (no quorum without election)
- ❌ More coordination overhead between developers on shared ha.h
### Option C: Stub-forward then wire
Add all new stubs with `-ENOSYS` first, then wire each one. Matches the existing codebase pattern (many stubs already present).
**Trade-offs**:
- ✅ Compiles throughout; no long broken-build windows
- ❌ Stub-first delays real testability; TDD mandate disfavors this
**Recommendation**: Option A. The dependency chain is well-defined and single-developer serial implementation is the realistic mode for this C codebase.
---
## 4. Effort and Risk Summary
| Req | Effort | Risk | Justification |
|---|---|---|---|
| 1 — Listener before election | M (3–5d) | Medium | Refactor startup order; must remove vtaskmgr/config dependency first |
| 2 — Quorum election | XL (2+ wks) | High | Full algorithm replacement; new wire protocol; FSM extension; concurrent dispatch |
| 3 — Config bootstrap | M (3–5d) | Medium | New function + probe RPC; depends on do_one_config_fetch (wired) |
| 4 — Startup validation | S (1d) | Low | Additive CLI + validation; ASAN one-liner |
| 5 — Majority-commit WAL | L (1–2 wks) | High | `committed_lsn` cross-cuts persist+repl+ha; `repl_broadcast` must become real |
| 6 — Persistent channel | XL (2+ wks) | High | New connection lifecycle; full state sync protocol; 3-tier discovery |
| 7 — Client retry | M (3d) | Low | Well-understood retry pattern; additive libxce change |
| 8 — Majority-commit config | M (3d) | Medium | Shares Req 5 infrastructure; mainly wiring push_config_to_standbys |
| 9 — State durability | S (1d) | Low | File write+read; extends ha_init |
| 10 — Observability | S (alongside) | Low | Log statements woven into each change |
**Total estimated effort**: 6–9 weeks for one engineer implementing with full TDD.
**High-risk dependencies**:
- Req 2 must complete before Req 3 can be integrated end-to-end.
- Req 5 (`repl_broadcast` implementation) must complete before Req 8 and before Req 6 (WAL streaming over the channel).
- Req 4 (vcpus CLI) must complete before Req 1 (vtaskmgr before election).
---
## 5. Key Reusable Assets
| Asset | Location | Reuse in |
|---|---|---|
| `repl_add/remove_standby` | repl.c | Req 6 (channel registration) |
| `repl_start/complete_catchup` | repl.c | Req 6 (state sync protocol) |
| `repl_needs_snapshot` | repl.c | Req 6 (snapshot decision) |
| `repl_on_ack` | repl.c | Req 5 (majority-commit ack handler) |
| `repl_trim_lsn` | repl.c | Req 5 (WAL trim with committed_lsn) |
| `connect_ssl_raw()` | netio.c/config_repl.c | Req 3 (probe RPC) |
| `do_one_config_fetch()` | config_repl.c | Req 3 (deferred fetch) |
| `xorshift64_next + ha_seed_jitter` | ha.h / ha.c | Req 1, 2 (jitter) |
| `ha_do_promote / ha_do_demote` | ha.c | Req 2, 6 (role transitions) |
| `ha_should_redirect / ha_no_primary_known` | ha.c | Req 7 (retry redirect) |
| iomgr epoll + EPOLLIN/EPOLLRDHUP | iomgr (cluster.c) | Req 1, 6 |
| Atomic write+rename pattern | config_repl.c | Req 9 (ha-term.bin) |
| INFO/WARN/ERROR macros | xcelog.h | Req 10 |
---
## 6. Constraints for Design Phase
- **C gnu17, no C++, no new external dependencies**.
- ASAN build (`nix build .#xcecore_debug`) must pass after every change.
- **Static peer membership**: `--peers` list is fixed at startup; no dynamic join/leave of controller peers. Quorum denominator is fixed for cluster lifetime.
- **TDD mandatory**: failing test in `src/controller/testsuite/` before each implementation change.
- `make test` must pass without a live server at every commit.
- `commit_timeout_ms` must be configurable (default 5000ms).
- **Single-node mode** (no `--peers`): quorum=1 everywhere; all new majority-commit paths must short-circuit to self-only.
- **WAL write stall window** (operational constraint, not a bug): during standby catch-up after election, `repl_wait_majority_ack()` will time out on every write until at least one standby completes catch-up. This is correct CP behavior; operators must be warned in docs.
---
## 7. Research Needed (Design Phase)
- **vtaskmgr API for pre-election dispatch**: confirm that `vtaskmgr_init(vcpus)` can be called before `cluster_config` is loaded (after removing vcpus from config struct). Verify no hidden config dependency.
- **iomgr async connect pattern for vote requests**: confirm `iomgr_async_vote_request()` can be implemented using existing async connect infrastructure without new socket primitives.
- **capnp schema extension**: confirm the capnp code-generation toolchain can add `OP_requestVote`, `OP_probeConfigTag`, `OP_walAck`, `OP_upgradeConfigAck`, `OP_snapshotTransfer`, `OP_walCatchupEntry` without breaking existing decode paths.
- **`persist_truncate_wal_above()` safety**: verify WAL segment file structure supports efficient truncation at arbitrary LSN boundary without full segment rewrite.
- **libxce client API surface**: identify the exact function in src/lib/ that wraps xce_encode()+send+recv+xce_decode() to locate the correct insertion point for retry logic.