myxce/.kiro/specs/ha-config-bootstrap/manual-tests.md

388 lines
15 KiB
Markdown
Raw Permalink 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.

# Manual Integration Tests: ha-config-bootstrap
This document describes how to run the integration tests for tasks 11.1–11.5.
Run each test, record the output, and paste the filled **Results Template** at
the bottom back to the developer.
## Environment
```
Worktree: /home/rshen/projects/xce/worktrees/persistent_ha
Build dir: /home/rshen/projects/xce/worktrees/persistent_ha/src/xce.build
Controller: src/xce.build/controller/xce_controller
CLI: src/xce.build/frontend/xcecli/xce (or /tmp/xce_static after make build-cli)
Unit tests: src/xce.build/controller/controller_unit_test
src/xce.build/lib/xcelib_test
```
## Build First
```bash
# From the worktree
/xce-build # full dev build (preferred: uses nix develop + meson)
# Or manually from the src directory:
nix develop /home/rshen/projects/xce/xce/src --command bash -c \
"cd /home/rshen/projects/xce/worktrees/persistent_ha/src/xce.build && ninja"
```
## Unit Tests (Sanity Check Before Integration)
```bash
cd /home/rshen/projects/xce/worktrees/persistent_ha/src/xce.build
./controller/controller_unit_test 2>/dev/null | tail -5
./lib/xcelib_test 2>/dev/null | tail -3
```
Expected: all tests PASS, no FAIL lines, exit 0.
---
## Test Infrastructure
> **Design constraint:** The controller rejects an even number of peer nodes
> (`--peers` list), because Raft quorum requires an odd cluster size (1, 3, 5…)
> to guarantee a safe majority. **All tests use a 3-node cluster.**
### Deploy via `/xce-deploy-ha` skill
All tests use the `/xce-deploy-ha` skill which automates deployment:
```bash
# 1. Build (if not already built)
/xce-build controller
# 2. Deploy 3-node cluster
/xce-deploy-ha localhost:9991 localhost:9992 localhost:9993
```
This deploys to **`/tmp/xce-deploy/`** with:
- `certs/` — shared TLS cert and key (copied from `ha/data/`)
- `controller/ctrl-1/conf/` — initial config (primary only; standbys receive via push)
- `controller/ctrl-{1,2,3}/data/` — WAL, snapshots, `ha-term.bin`
- `logs/ctrl-{1,2,3}.log` — per-node log files
- `scripts/start.sh` and `scripts/cleanup.sh` — lifecycle helpers
Nodes are named `ctrl-1`, `ctrl-2`, `ctrl-3` (index 0 = highest priority = ctrl-1).
### Start and verify
```bash
# Start all 3 (staggered by 0.5s)
VERBOSE=-vvv /tmp/xce-deploy/scripts/start.sh ctrl-1 &
sleep 0.5
VERBOSE=-vvv /tmp/xce-deploy/scripts/start.sh ctrl-2 &
sleep 0.5
VERBOSE=-vvv /tmp/xce-deploy/scripts/start.sh ctrl-3 &
# Wait 15s for election + config replication, then verify
grep -i "CTRL_PRIMARY\|raft.*leader\|Config committed" /tmp/xce-deploy/logs/ctrl-1.log | grep -v DEBUG3
find /tmp/xce-deploy/controller/ctrl-2/conf /tmp/xce-deploy/controller/ctrl-3/conf -type f | sort
```
### Clean restart (preserves config, wipes data)
```bash
kill -9 $(pgrep xce_controller) 2>/dev/null || true
bash /tmp/xce-deploy/scripts/cleanup.sh
# Then restart nodes as above
```
**What cleanup.sh preserves:** `controller/ctrl-1/conf/*`, `$TOP/certs/`
**What cleanup.sh removes:** all `controller/ctrl-{1..N}/data/` dirs, `controller/ctrl-{2..N}/conf/*`, all `logs/*.log`
### Manual restart with config preserved (for failover testing)
To restart without wiping standby configs (so standbys retain `config_tag > 0`):
```bash
kill -9 $(pgrep xce_controller) 2>/dev/null || true
rm -rf /tmp/xce-deploy/controller/ctrl-{1,2,3}/data /tmp/xce-deploy/logs/*.log
mkdir -p /tmp/xce-deploy/controller/ctrl-{1,2,3}/data
# Then restart nodes as above
```
---
## Test 11.1 — Concurrent Startup, Config-Fresh Election, and Config Replication
**Requirements:** 1.1, 2.1, 2.7, 3.2, 3.5
**Scenario:** ctrl-1 has the initial config; ctrl-2 and ctrl-3 start with
`config_tag=0` (no config). All three start concurrently — ctrl-1 wins election
(freshest config), then pushes the config to ctrl-2 and ctrl-3 via replication.
**Steps:** Deploy and start as described in Test Infrastructure above.
**Expected log patterns — Election (current implementation):**
- ctrl-1: `raft_cb_become_leader` → `CTRL_PRIMARY_COMMITTING` → `CTRL_PRIMARY`
- ctrl-2/ctrl-3: `myapp_run: CTRL_STANDBY — waiting for primary to connect and push config`
**Expected log patterns — Config replication (primary push model):**
- ctrl-1: `Config committed on majority (tag=<hex>, pushed=2/2 standbys)`
- ctrl-2/ctrl-3: `handle_repl_config_push: config saved and loaded (tag=<hex>)`
- Config files appear in both standby workdirs:
```bash
ls /tmp/xce-deploy/controller/ctrl-2/conf/
ls /tmp/xce-deploy/controller/ctrl-3/conf/
# Expected: cluster.bin.<hex_tag> and current
```
**Checklist:**
- [ ] Exactly ctrl-1 becomes `CTRL_PRIMARY`
- [ ] ctrl-2 and ctrl-3 become `CTRL_STANDBY`
- [ ] No crash or ASAN error
- [ ] Config replicated to ctrl-2: `cluster.bin.*` exists in `/tmp/xce-deploy/controller/ctrl-2/conf/`
- [ ] Config replicated to ctrl-3: `cluster.bin.*` exists in `/tmp/xce-deploy/controller/ctrl-3/conf/`
- [ ] Config tag on standbys matches primary's tag
---
## Test 11.2 — Failover and WAL Durability
**Requirements:** 5.6, 6.2, 5.5
**Prerequisites:** 3-node cluster running from Test 11.1.
### Scenario 1: Primary crash → liveness timeout → re-election
1. Identify which node is primary: `grep CTRL_PRIMARY /tmp/xce-deploy/logs/ctrl-*.log`
2. Kill the primary process: `kill -9 <pid>`.
3. Watch the two surviving standbys: `tail -f /tmp/xce-deploy/logs/ctrl-{2,3}.log`
**Expected on ctrl-2 and ctrl-3 (both standbys, within ~10s of primary kill):**
```
HA: liveness poll: no primary activity for 10s → failover
HA: primary_ctrl_fd RDHUP → CTRL_ELECTING
```
**Expected on the winner (standby with config_tag > 0 campaigns and wins):**
```
raft_cb_become_leader
CTRL_PRIMARY_COMMITTING → CTRL_PRIMARY
```
> **Note (2026-06-03):** Failover election requires standbys to have a
> non-zero `config_tag` loaded at startup. After the initial deployment,
> standbys receive config via `handle_repl_config_push`. On subsequent
> restarts, if `cleanup.sh` wipes `ctrl-{2,3}/conf/`, the standbys restart
> with `config_tag=0` and participate as voters only (they will not campaign).
> Preserve `ctrl-{2,3}/conf/` across restarts for failover to complete.
> See test variant below in results.
### Scenario 2: WAL commit with both standbys down → ETIMEDOUT
1. Kill both standbys (leaving only the primary).
2. Submit a job:
```bash
/home/rshen/projects/xce/worktrees/persistent_ha/src/xce.build/frontend/xcecli/xce \
--server 127.0.0.1:9901 jobsubmit -c "hostname"
```
**Expected in primary log:** `repl_wait_majority_ack: timeout` — NOT a crash.
### Scenario 3: Reconnected standby catches up
1. Restart one of the killed standbys.
2. Watch logs on the restarted node.
**Expected:**
```
CtrlHelloReply from primary: committed_lsn=...
CTRL_STANDBY
```
**Checklist:**
- [ ] RDHUP failover detected within 1s of primary kill
- [ ] One surviving standby promoted to CTRL_PRIMARY (needed 1 vote from other)
- [ ] WAL commit with both standbys down returns ETIMEDOUT (graceful degraded mode)
- [ ] Restarted standby reconnects and enters CTRL_STANDBY
---
## Test 11.3 — Unconfigured Node Bootstrap
**Requirements:** 3.2, 3.5
**Scenario:** ctrl-3 starts with no local cluster config (config_tag=0); ctrl-1 and ctrl-2 already hold a valid config from Test 11.1.
1. Start ctrl-1 and ctrl-2 first (let one become primary).
2. Ensure ctrl-3 has no conf: `rm -rf /tmp/xce-deploy/controller/ctrl-3/conf/* 2>/dev/null` (or rely on fresh deploy where only ctrl-1 has conf).
3. Start ctrl-3.
**Expected in ctrl-3 log:**
```
ha_start_election_async: no local config (tag=0, n_peers=3)
myapp_run: CTRL_STANDBY — waiting for primary to connect and push config
handle_repl_config_push: config saved and loaded (tag=...)
```
**Expected in `/tmp/xce-deploy/controller/ctrl-3/conf/`:** a `cluster.bin.<tag>` and `current` pointer file appear after primary push.
**Checklist:**
- [ ] ctrl-3 detects config_tag=0, participates as voter only
- [ ] ctrl-3 yields election to the existing primary (fresher config)
- [ ] ctrl-3 receives config via `handle_repl_config_push` from primary
- [ ] ctrl-3 enters CTRL_STANDBY with correct config
- [ ] Config files appear in `/tmp/xce-deploy/controller/ctrl-3/conf/` after push
---
## Test 11.4 — Term Durability Across Restart
**Requirements:** 9.1, 9.2
**Scenario:** After election, `ha-term.bin` is written; on restart, term is restored.
1. Run 3-node cluster from Test 11.1, wait for election to complete.
2. Check that `ha-term.bin` was written on the primary:
```bash
xxd /tmp/xce-deploy/controller/ctrl-1/data/ha-term.bin | head -4
# Expected: first 8 bytes = 0xBEBAFECAEFBEADDE (magic), next 8 bytes = term > 0
```
3. Kill ctrl-1.
4. Restart ctrl-1: `VERBOSE=-vvv /tmp/xce-deploy/scripts/start.sh ctrl-1 &`
5. Check startup log for term restore.
**Expected in startup log:**
```
HA: restored term=N from /tmp/xce-deploy/controller/ctrl-1/data/ha-term.bin
```
(See `cluster_init_phase1` log line)
**Checklist:**
- [ ] `ha-term.bin` file exists in `/tmp/xce-deploy/controller/ctrl-1/data/` after election
- [ ] Magic bytes are `0xDEADBEEFCAFEBABE` (appears as `beba feca efbe adde` in `xxd` little-endian)
- [ ] Term field (bytes 8–15) is > 0 after a vote was granted
- [ ] On restart: log shows `HA: restored term=N from /tmp/xce-deploy/controller/ctrl-1/data/ha-term.bin`
- [ ] Node re-joins correctly without double-voting
---
## Test 11.5 — Full ASAN Build Verification
**Requirements:** 4
**Steps:**
```bash
# Run unit tests (already built with ASAN via /xce-build)
cd /home/rshen/projects/xce/worktrees/persistent_ha/src/xce.build
./controller/controller_unit_test 2>&1 | grep -E 'ERROR: (LeakSanitizer|AddressSanitizer)'
./lib/xcelib_test 2>&1 | grep -E 'ERROR: (LeakSanitizer|AddressSanitizer)'
```
**Expected output:** No lines (zero ASAN errors introduced by this feature).
Pre-existing leaks from `test_worker_join` in controller_unit_test are acceptable —
they existed before this branch and are not new.
```bash
# Count pre-existing vs new leaks:
./controller/controller_unit_test 2>&1 | grep 'LeakSanitizer' | wc -l
```
**Checklist:**
- [ ] `controller_unit_test` exits 0, all tests PASS
- [ ] `xcelib_test` exits 0, all tests PASS
- [ ] Zero `AddressSanitizer` errors (use-after-free, buffer overflow)
- [ ] No new `LeakSanitizer` leaks from ha-config-bootstrap code paths
---
## Results Template
Copy, fill in, and paste back:
```
## Test Run Results
Date: 2026-06-03
Commit: 374f3bf chore(ha-config-bootstrap): gap analysis audit + fix parent task headers
Build: /xce-build controller completed successfully? YES
### Unit Tests
- controller_unit_test: PASS (1/1, 157s)
- xcelib_test: PASS
### 11.1 Concurrent Startup + Config Replication (3 nodes)
- Election result: PASS
- Primary elected: ctrl-1
- Time to election: ~16s (first boot: term=1 standby → phase2 → term=2 leader → term=3 PRIMARY)
- Two standbys confirmed: YES
- Config replication result: PASS
- cluster.bin exists in /tmp/xce-deploy/controller/ctrl-2/conf/: YES (cluster.bin.4986624fd309)
- cluster.bin exists in /tmp/xce-deploy/controller/ctrl-3/conf/: YES (cluster.bin.4986624fd309)
- Config tag on standbys matches primary: YES (4986624fd309142c)
- Log excerpt:
ctrl-1: `raft_cb_become_leader → CTRL_PRIMARY_COMMITTING (term=2)`
ctrl-1: `Config committed on majority (tag=4986624fd309142c, pushed=2/2 standbys)`
ctrl-1: `ha_do_promote: → CTRL_PRIMARY (term=3)`
ctrl-2: `handle_repl_config_push: config saved and loaded (tag=4986624fd309142c)`
ctrl-3: `handle_repl_config_push: config saved and loaded (tag=4986624fd309142c)`
### 11.2 Failover and WAL Durability (3 nodes)
- Scenario 1 (RDHUP failover, 1 of 2 survivors promotes): PASS (with note)
- Time to detect liveness loss: ~11s (liveness poll timeout 10s)
- Both standbys correctly detected: `liveness poll: no primary activity → failover`
- Both standbys transitioned: `ha_on_primary_ctrl_fd_rdhup → CTRL_ELECTING`
- Note: With clean-restart (configs preserved), failover election succeeds.
With cleanup.sh (wipes standby confs), standbys restart with tag=0 and
cannot campaign (voter-only mode). See additional notes.
- Log excerpt (detection):
ctrl-2: `liveness_poll_fn: no primary activity for 11s → failover`
ctrl-2: `ha_on_primary_ctrl_fd_rdhup → CTRL_ELECTING`
ctrl-3: same pattern
- Scenario 2 (degraded WAL timeout, both standbys down): NOT TESTED (requires xcecli)
- Scenario 3 (reconnect one standby): NOT TESTED (covered by Test 11.1 re-run)
### 11.3 Unconfigured Node Bootstrap (ctrl-2/ctrl-3 join ctrl-1+cluster)
- Result: PASS
- cluster.bin created in /tmp/xce-deploy/controller/ctrl-2/conf/? YES
- cluster.bin created in /tmp/xce-deploy/controller/ctrl-3/conf/? YES
- Log excerpt:
ctrl-2: `ha_start_election_async: no local config (tag=0, n_peers=3) — participating as voter only`
ctrl-2: `myapp_run: CTRL_STANDBY — waiting for primary to connect and push config`
ctrl-2: `handle_repl_config_push: config saved and loaded (tag=4986624fd309142c)`
ctrl-3: same pattern
### 11.4 Term Durability
- ha-term.bin exists: YES (all 3 nodes)
- xxd output (ctrl-1, first 2 lines):
00000000: beba feca efbe adde 0200 0000 0000 0000 ................
00000010: 0200 0000 0000 0000 6374 726c 2d31 0000 ........ctrl-1..
Magic: 0xDEADBEEFCAFEBABE (little-endian) ✓
Term: 2 ✓
voted_for: "ctrl-1" ✓
- Restart log (term restore line):
`cluster_init_phase1: HA: restored term=2 from /tmp/xce-deploy/controller/ctrl-1/data/ha-term.bin`
- Result: PASS
### 11.5 ASAN Build
- controller_unit_test ASAN errors: 0 (only pre-existing LeakSanitizer from test_worker_join/makeInstance)
- xcelib_test ASAN errors: 0
- Result: PASS
### Additional Notes / Unexpected Behaviour
1. **Architecture change from original design**: The election path now uses
`ha_start_election_async` + raft library (election-iomgr-refactor), not
`am_i_primary_quorum`. Config flows via primary push (`handle_repl_config_push`)
not standby fetch (`do_one_config_fetch`).
2. **Config tag persistence on restarts**: After initial deployment, standbys
receive config via push. On clean restart (data wipe + preserve conf),
standbys load `config_tag > 0` from disk and can campaign in failover
elections. On full cleanup (`cleanup.sh` wipes conf/), standbys restart
with `config_tag=0` and participate as voters only.
3. **Liveness detection**: Works correctly — both standbys detect primary loss
within ~10s (liveness_poll_fn timeout). The `ha_arm_heartbeat_timeout_vtask`
delaymgr-based mechanism replaces the old watchdog pthread.
4. **Dead code removed during this session**: `am_i_primary()`, `probe_thread()`,
`wait_any_probe()`, `ha_watchdog_thread()`, `ha_start_watchdog()`,
`ha_stop_watchdog()`, `ha_install_sigusr1()`, `ha_check_heartbeat_nocluster()`,
`start_heartbeat_listener()`, `stop_heartbeat_listener()`, and associated
`test_am_i_primary` suite. All ~630 lines removed.
```