finalize configuration replication

This commit is contained in:
Rongsong Shen 2026-06-09 16:40:13 +08:00
parent e1517bbf28
commit fa66d1ba9c
30 changed files with 8867 additions and 1560 deletions

View file

@ -1,4 +0,0 @@
{
"mcpServers": {
}
}

View file

@ -263,3 +263,13 @@ None of these affect correctness or test pass rate. Safe to clean up in a single
## Conclusion
The `election-iomgr-refactor` spec is **fully implemented and verified**. No blocking issues remain. The spec phase is `implemented` and can be closed. The one deferred item (DNS blocking) is tracked under `ha-peer-connection-reuse`.
---
# Cross-Spec Gap Audit (2026-06-08)
> Batch `/kiro-validate-gap` across 9 specs on persistent_ha branch.
## Status: CLOSED — No remaining gaps
All 15 implementation tasks (1.1–5.3) and all 5 validation tasks (6.1–6.5) confirmed done. The spec is in `implemented` phase. Deferred cleanup items (dead code: `am_i_primary`, `ha_wait_for_heartbeat_or_timeout`, `start_heartbeat_listener`/`stop_heartbeat_listener` stubs) are cosmetic and tracked as tech debt.

View file

@ -58,9 +58,9 @@ All tests use the `/xce-deploy-ha` skill which automates deployment:
```
This deploys to **`/tmp/xce-deploy/`** with:
- `ctrl-{1,2,3}/certs/` — TLS cert and key (copied from `ha/data/`)
- `ctrl-1/conf/` — initial config (primary only; standbys receive via push)
- `ctrl-{1,2,3}/data/` — WAL, snapshots, `ha-term.bin`
- `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
@ -78,7 +78,7 @@ 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/ctrl-2/conf /tmp/xce-deploy/ctrl-3/conf -type f | sort
find /tmp/xce-deploy/controller/ctrl-2/conf /tmp/xce-deploy/controller/ctrl-3/conf -type f | sort
```
### Clean restart (preserves config, wipes data)
@ -89,8 +89,8 @@ bash /tmp/xce-deploy/scripts/cleanup.sh
# Then restart nodes as above
```
**What cleanup.sh preserves:** `ctrl-1/conf/*`, all `certs/`
**What cleanup.sh removes:** all `data/` dirs, `ctrl-{2,3}/conf/*`, all `logs/*.log`
**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)
@ -98,8 +98,8 @@ 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/ctrl-{1,2,3}/data /tmp/xce-deploy/logs/*.log
mkdir -p /tmp/xce-deploy/ctrl-{1,2,3}/data
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
```
@ -124,8 +124,8 @@ mkdir -p /tmp/xce-deploy/ctrl-{1,2,3}/data
- 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/ctrl-2/conf/
ls /tmp/xce-deploy/ctrl-3/conf/
ls /tmp/xce-deploy/controller/ctrl-2/conf/
ls /tmp/xce-deploy/controller/ctrl-3/conf/
# Expected: cluster.bin.<hex_tag> and current
```
@ -133,8 +133,8 @@ mkdir -p /tmp/xce-deploy/ctrl-{1,2,3}/data
- [ ] 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/ctrl-2/conf/`
- [ ] Config replicated to ctrl-3: `cluster.bin.*` exists in `/tmp/xce-deploy/ctrl-3/conf/`
- [ ] 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
---
@ -207,7 +207,7 @@ CTRL_STANDBY
**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/ctrl-3/conf/* 2>/dev/null` (or rely on fresh deploy where only ctrl-1 has conf).
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:**
@ -217,14 +217,14 @@ 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/ctrl-3/conf/`:** a `cluster.bin.<tag>` and `current` pointer file appear after primary push.
**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/ctrl-3/conf/` after push
- [ ] Config files appear in `/tmp/xce-deploy/controller/ctrl-3/conf/` after push
---
@ -237,7 +237,7 @@ handle_repl_config_push: config saved and loaded (tag=...)
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/ctrl-1/data/ha-term.bin | head -4
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.
@ -246,15 +246,15 @@ xxd /tmp/xce-deploy/ctrl-1/data/ha-term.bin | head -4
**Expected in startup log:**
```
HA: restored term=N from /tmp/xce-deploy/ctrl-1/data/ha-term.bin
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/ctrl-1/data/` after election
- [ ] `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/ctrl-1/data/ha-term.bin`
- [ ] 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
---
@ -312,8 +312,8 @@ Build: /xce-build controller completed successfully? YES
- 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/ctrl-2/conf/: YES (cluster.bin.4986624fd309)
- cluster.bin exists in /tmp/xce-deploy/ctrl-3/conf/: YES (cluster.bin.4986624fd309)
- 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)`
@ -339,8 +339,8 @@ Build: /xce-build controller completed successfully? YES
### 11.3 Unconfigured Node Bootstrap (ctrl-2/ctrl-3 join ctrl-1+cluster)
- Result: PASS
- cluster.bin created in /tmp/xce-deploy/ctrl-2/conf/? YES
- cluster.bin created in /tmp/xce-deploy/ctrl-3/conf/? YES
- 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`
@ -356,7 +356,7 @@ Build: /xce-build controller completed successfully? YES
Term: 2 ✓
voted_for: "ctrl-1" ✓
- Restart log (term restore line):
`cluster_init_phase1: HA: restored term=2 from /tmp/xce-deploy/ctrl-1/data/ha-term.bin`
`cluster_init_phase1: HA: restored term=2 from /tmp/xce-deploy/controller/ctrl-1/data/ha-term.bin`
- Result: PASS
### 11.5 ASAN Build

View file

@ -71,6 +71,19 @@ No new code needed for either spec.
- **Recommendation**: Run the 5 integration tests from `manual-tests.md`
against a 3-node cluster, then mark the spec `implemented`.
---
# Cross-Spec Gap Audit (2026-06-08)
> Batch `/kiro-validate-gap` across 9 specs on persistent_ha branch.
## Analysis Summary
- **ha-config-bootstrap** is **~95% complete**. All 10 requirements have verified implementation assets. All 22 leaf subtasks (1.1–10.2) are done.
- **Remaining work**: Tasks 11.1–11.5 (manual integration tests documented in `manual-tests.md`).
- **Cross-spec verification**: `election-iomgr-refactor` superseded the blocking-pthread election path while preserving all ha-config-bootstrap requirements. The two specs are consistent. `ha-repl-hardening` extends Req 5 (majority-commit WAL) and Req 8 (majority-commit config) with heartbeat-driven convergence — no conflicts.
- **Cleanup debt**: `am_i_primary()` (ha.c:869), `ha_wait_for_heartbeat_or_timeout`, and `start_heartbeat_listener()`/`stop_heartbeat_listener()` stubs are dead code — safe to delete in a dedicated cleanup commit (tracked in election-iomgr-refactor/research.md).
## Requirement-to-Asset Map — Final State
| Requirement | Verified Key Assets | Status |
@ -446,7 +459,7 @@ Add all new stubs with `-ENOSYS` first, then wire each one. Matches the existing
### Gap 1: Task 11.1 — Concurrent startup and config-fresh election
- **What's needed**: 3-node cluster starts simultaneously; verify only one elects primary; config-fresh node wins.
- **How to run**: See `manual-tests.md` — Test 1 (3-node clean start, verify ctrl-a primary).
- **How to run**: See `manual-tests.md` — Test 1 (3-node clean start, verify ctrl-1 primary).
- **Infrastructure**: `deploy-ha.sh` + `start.sh` scripts; log grep for `CTRL_PRIMARY`.
- **Effort**: ~30 min.

View file

@ -155,3 +155,15 @@ liveness path, not a new subsystem.
`OPCode_walCatchupEntry` sites in `ha.c` for full Req 1.3 literal compliance.
- Run task 4.5 integration test (SIGSTOP/SIGCONT) when a 3-node cluster is
available.
---
# Cross-Spec Gap Audit (2026-06-08)
> Batch `/kiro-validate-gap` across 9 specs on persistent_ha branch.
## Analysis Summary
- **ha-heartbeat-liveness** is **~97% complete**. All core implementation is done. The `hb_timeout_thread` has been replaced with delaymgr-based `liveness_poll_fn`.
- **Remaining gaps**: (1) Req 1.3 partial — `OPCode_replEntry` and `OPCode_walCatchupEntry` dispatch sites in ha.c do not call `ha_on_heartbeat_received`. Low risk: active WAL replication keeps `last_push_time` fresh on primary, suppressing NOPs, and ctrlhello resets the standby's deadline. Adding 2 `ha_on_heartbeat_received` calls in the dispatch sites would achieve full literal compliance. (2) Task 4.5 — SIGSTOP/SIGCONT missed-heartbeat tolerance is an optional manual integration test (script exists at `src/tools/test-heartbeat-tolerance.sh`).
- **Cross-spec relationship**: `ha-repl-hardening` heartbeat-driven convergence tracking relies on this spec's liveness infrastructure. The `primary_heartbeat_fn` idle-triggered NOP suppression is correct and does not interfere with ha-repl-hardening's per-standby state reporting via heartbeat replies.

View file

@ -221,3 +221,17 @@ All seams appear to be in place. The tests should pass, but a build run (`/xce-b
| Task 7.3 integration test | M (1–2 days) | Medium — requires promoted ep + WAL harness |
**Overall**: The core feature is implemented. The remaining work is test validation + one latent correctness issue in the old election path.
---
# Cross-Spec Gap Audit (2026-06-08)
> Batch `/kiro-validate-gap` across 9 specs on persistent_ha branch.
## Analysis Summary
- **ha-peer-connection-reuse** is **~98% complete**. All 7 task groups (1–7) are `[x]` in tasks.md.
- **Prior CRITICAL gap RESOLVED**: The `fetch_config_from_primary` call in `ha_elect_evaluate_and_signal`'s STANDBY path was removed (confirmed: only comments remain in ha.c). The standby is fully passive — no outbound TCP connections from `CTRL_STANDBY`.
- **Integration tests (7.2, 7.3)**: Both marked `[x]`. The two-node integration test and WAL ACK test are done.
- **Remaining**: Build verification (`/xce-build controller`) to confirm all unit tests in `test_ha_connection_reuse.c` pass under ASAN.
- **Cross-spec relationships**: `ha-repl-hardening` commit `0762577` (task 5.1: "reduce handle_ctrlhello to connection setup only") directly depends on this spec's `peer_ctrl_msg_handler` + promoted connection model. The two specs are consistent — ha-repl-hardening moves all replication decisions to the heartbeat decision loop, which works over the promoted connections established by this spec.

View file

@ -133,3 +133,18 @@ Add `OPCode_stateReport` / `OPCode_stateAck` to capnp schema:
- **Capnp schema design**: Whether to extend `RequestReply` with state fields, add a new `stateReport` struct to existing heartbeat messages, or create a new opcode. Evaluate against existing capnp codegen patterns.
- **Heartbeat reply processing**: Currently fire-and-forget; need to add a reply handler on the primary side. Determine best insertion point in the dispatch path.
- **Snapshot wire format**: How to serialize `persist_checkpoint` output for transfer. COW zarray vs raw file transfer.
---
# Cross-Spec Gap Audit (2026-06-08)
> Batch `/kiro-validate-gap` across 9 specs on persistent_ha branch.
## Analysis Summary
- **ha-repl-hardening** is **~98% complete**. All 6 task groups (1–6), 27 subtasks (1.1–6.7) are `[x]` in tasks.md. Recent commits (aab41fa through e1517bb) confirm active implementation. The git log shows all tasks through 6.7 implemented with bug fixes applied.
- **Remaining gaps**: Task groups 1–4 and 5–6 all show `[ ]` as section headers, but all subtasks within them are `[x]`. The section checkboxes are cosmetic.
- **Deferred items**: None. The original research's "Research Needed" items (capnp schema design, heartbeat reply processing, snapshot wire format) were resolved during the design phase.
- **Cross-spec relationships**: This spec extends `ha-config-bootstrap` Req 5 (majority-commit WAL) and Req 8 (majority-commit config) with heartbeat-driven convergence. It depends on `ha-heartbeat-liveness` for the idle-triggered NOP policy on the primary and activity-based liveness detection on the standby. It depends on `ha-peer-connection-reuse` for the promoted connection model — `handle_ctrlhello` was reduced to connection setup only (task 5.1), with all replication decisions moved to the heartbeat decision loop.
- **Single-node mode**: Verified working — `n_peers=1`, immediate convergence, `CTRL_PRIMARY` with no heartbeat wait.
- **Recommendation**: Run `/xce-build controller` to confirm ASAN-clean build, then mark the spec `implemented`.

View file

@ -36,3 +36,17 @@
- Added on_trace callback to raft_callbacks_t for full protocol observability
- Simulator dump log enables post-mortem analysis of split-brain scenarios
- raft_snapshot() provides safe point-in-time state read for crash/failure logging
---
# Cross-Spec Gap Audit (2026-06-08)
> Batch `/kiro-validate-gap` across 9 specs on persistent_ha branch.
## Analysis Summary
- **raft-library** is **~100% complete**. All 7 task groups (1–7), all subtasks (1–2.2, 3.1–3.5, 4.1–4.2, 5.1–5.4, 6.1–6.4, 7) are `[x]` in tasks.md. The `ready_for_implementation` flag is set in spec.json.
- **No remaining gaps**: The library (`src/lib/raft/raft.h`, `raft.c`, `raft_sim.c`) exposes `raft_state_t` + `raft_callbacks_t` with 9 callback hooks. The deterministic simulator (`raft_sim.c`) verifies split-brain freedom across 100 randomized rounds. The controller adapter in ha.c delegates all election state management (term, voted_for, quorum evaluation) to the raft library.
- **Cross-spec integration**: `election-iomgr-refactor` uses `raft_start_election` instead of the old `am_i_primary_quorum`. The adapter callbacks in ha.c wire `on_send_vote_request` → iomgr async connect, `on_become_leader` → `cluster_init_phase2` + config push, `on_become_follower` → `CTRL_STANDBY`. The `on_vote_eligible` callback implements the freshness gate (config_tag + applied_lsn comparison) and role gate (`CTRL_PRIMARY_COMMITTING` → deny).
- **Test coverage**: 34+ Unity test cases across 5 groups (A–H) covering basic election correctness, voted_for invariant, eligibility policy, quorum evaluation, election timeout, RDHUP failover, and split-brain prevention. All pass under ASAN.
- **Recommendation**: The spec can be marked `implemented`.

View file

@ -152,3 +152,17 @@ Ruled out: does not fix the design — just moves the source of truth without el
4. **Removal of `collect_resval_with_idx`**: **Remove entirely.** No cross-subsystem callers will remain after this change, and intra-subsystem callers (e.g., `worker.c` reading from `sharedinst` for API get-resval) access the zarray directly by `r->idx` without going through this function. Delete `collect_resval_with_idx`, `collect_resval_with_idx_`, and remove the declaration from `src/controller/include/cluster.h`.
5. **`free_name_val_list` utility**: **Add to `utils.c`/`utils.h`.** A new `free_name_val_list(xce_name_val_t***, int)` function mirroring `free_resval_with_idx_list` is needed. The capnp-generated `free_NameValue_list` is an option but couples cleanup to the serialization layer — a standalone utility in `utils.c` is cleaner and consistent with the existing pattern.
---
# Cross-Spec Gap Audit (2026-06-08)
> Batch `/kiro-validate-gap` across 9 specs on persistent_ha branch.
## Analysis Summary
- **resval-id-decoupling** is **~100% complete**. All 8 task groups, 22 subtasks (1.1–8.3) are `[x]` in tasks.md.
- **No remaining gaps**: `collect_resval_with_idx` and `free_resval_with_idx_list` are removed (verified by grep: zero matches). `collect_resval` (name-keyed, iterates resmgr not zarray) replaces it. The capnp schema (`Conf4Sched.resvals @6`, `Worker.resvals @5`) now use `List(NameValue)`. The scheduler resolves by name via `key_find(sched->resmgr, name)` → `r->idx`. The Go API server has `cNameValToWorkerResval` in `convert.go` for the new `xce_name_val_t**` type.
- **Task approval gap**: spec.json shows `tasks.approved: false`. All tasks are implemented but the tasks document was never formally approved. This is a process artifact — the implementation matches the approved design.
- **Cross-spec impact**: `scheduler-controller-msg-gap` extends the same scheduler handlers (`sched_init_sharedInst`, `make_sched_worker`) that were modified by this spec. The two specs are consistent — both use name-based resource resolution.
- **Recommendation**: Mark tasks as approved in spec.json, or run `/kiro-spec-status resval-id-decoupling` to sync.

View file

@ -95,4 +95,18 @@ All 19 NotifyData event tags:
4. **Plugin hook for rusage:** Add `plugin_job_rusage_hook_t` typedef and `plugin_job_rusage_update()` global function following existing patterns.
5. **Helper function:** Add `xce_notify_event_name(int)` and `xce_sched_op_name(xce_sched_op_t)` to `src/lib/helper.c` and `src/include/helper.h`.
5. **Helper function:** Add `xce_notify_event_name(int)` and `xce_sched_op_name(xce_sched_op_t)` to `src/lib/helper.c` and `src/include/helper.h`.
---
# Cross-Spec Gap Audit (2026-06-08)
> Batch `/kiro-validate-gap` across 9 specs on persistent_ha branch.
## Analysis Summary
- **scheduler-controller-msg-gap** is **~100% complete**. All 9 task groups, 28 subtasks (1.1–9.9) are `[x]` (9.9 optional).
- **No remaining gaps**: All 8 requirements have verified implementation. The 6 previously stubbed notify/dataset handlers (`schedUpdateJob`, `schedUpdateQueue`, `schedUpdateResvals`, `schedJobRusages`, `Data_data_resources`, `Data_data_resvals`) are wired with full business logic. Dead code (`pushmsg` branches, `sched_handle_jobpush`, `sched_handle_workerpush`) removed. Symbolic logging via `xce_notify_event_name()` and `xce_sched_op_name()` is in place.
- **Optional gap**: Task 9.9 (verify WARN log when handler returns non-zero) is marked optional — requires log capture infrastructure not yet present in the test harness.
- **Cross-spec relationship**: `resval-id-decoupling` uses the same pattern (name-based resource lookup via `sched->resmgr`) in `sched_init_sharedInst`. The `XCE_SCHED_OP_UPDATE_RESVALS_FULL` handler added in this spec also resolves by name — consistency is maintained.
- **Recommendation**: The spec can be marked `implemented`. Task 9.9 can be closed as deferred.

View file

@ -190,3 +190,17 @@ Design phase is **already approved**. Proceed directly to implementation:
5. Run `make test` in controller testsuite (task 5 validation)
**No further research needed.** All unknowns from the original research log are resolved in `design.md`.
---
# Cross-Spec Gap Audit (2026-06-08)
> Batch `/kiro-validate-gap` across 9 specs on persistent_ha branch.
## Analysis Summary
- **worker-join-refactor** is **~100% complete**. All 5 task groups, all subtasks (1.1–5) are `[x]` in tasks.md.
- **Prior gaps RESOLVED**: `init_worker` (worker.c:339), `rejoin_worker` (worker.c:366), and `apply_join_state` (static helper) are all implemented. `add_worker` declaration removed from `worker.h`. The double-lock deadlock (`handle_worker_join` held `workermgr` WRITE lock while `makeWorker` re-acquired it) is eliminated.
- **No remaining gaps**: `handle_worker_join` now has explicit `key_find` → branch → `init_worker`/`rejoin_worker` → `apply_join_state` flow visible at the call site. `BEFORE_EVENT`/`AFTER_EVENT` hooks fire at the same event points. `on_worker_join` called after successful join. Admin status bits (`LOCKEDU | CLOSED | HIDDEN | CLOSED_BY_ADMIN`) preserved in `rejoin_worker`.
- **Build verification**: Run `/xce-build controller` to confirm `test_worker_join.c` (7 test cases) pass.
- **Recommendation**: The spec can be marked `implemented`.