Investigation & fixes (failover committed-job loss): - root-cause bug-851 on an owned 3-node cluster: chaotic failover causes silent committed-prefix divergence (WAL hdr.term always 0; two leaders per term; no (term,lsn) reconcile). Evidence in repro-851/ + INVESTIGATION. - live-validated fixes 848/849/850 (recovery-on-promotion, advertise real applied_lsn, persist-layer contiguity guard); add waldump WAL inspector. Specs (strict raft.tla conformance — library owns the log): - TLA-gap-analysis.md: impl vs raft/raft.tla, gaps G1-G6. - raft-log-replication, raft-commit-safety, raft-sim-conformance: extend src/lib/raft to own log/commit/verification via callbacks (specs generated, cross-spec review passed). - ha-term-safety: controller-side adapter + config marker (R1-R7); design done. - config-upgrade-snapshot: R8 descoped here (deps entity-bearing-snapshots, bug-857). - roadmap + buglog (851/854/857) + .wolf knowledge updated.
346 lines
30 KiB
Markdown
346 lines
30 KiB
Markdown
# Research Log: worker-join-refactor
|
||
|
||
## Summary
|
||
|
||
**Discovery scope**: Light discovery (extension of existing C subsystem).
|
||
|
||
**Key findings**:
|
||
1. **Latent deadlock in current code**: `handle_worker_join` acquires `lock(c->workermgr, WRITE)` then calls `add_worker → makeWorker`, which also calls `lock(workermgr, WRITE)`. `my_rwlock_t` is `pthread_rwlock_t` (non-reentrant), so this double-acquisition deadlocks. The outer lock was added for persistence-hook work on this branch and is the root cause of the "confusing locking structure."
|
||
2. **`makeWorker` in wobj.c is out of scope but must still be called**: It manages the create-or-update decision internally (`makeWorker_`). For the create path, the refactored design must NOT hold the `workermgr` write lock when calling `makeWorker`.
|
||
3. **`override_worker` already preserves admin bits**: The `XCE_WORKER_ADMIN_MASK` (`LOCKEDU | CLOSED | HIDDEN | CLOSED_BY_ADMIN`) is preserved by `override_worker` in wobj.c. The rejoin update path must replicate this guarantee when updating attrs in-place, bypassing `makeWorker`.
|
||
|
||
---
|
||
|
||
## Research Log
|
||
|
||
### Topic 1: Lock Reentrancy
|
||
|
||
**Question**: Is `my_rwlock_t` reentrant? Is the existing double-lock intentional?
|
||
|
||
**Investigation**: `src/include/mylock.h` defines `my_rwlock_t` as `pthread_rwlock_t`. POSIX `pthread_rwlock_wrlock` on an already-held write lock by the same thread is undefined behavior (most implementations deadlock). The outer write lock in `handle_worker_join` was introduced on branch `persistent_ha` for the AFTER_EVENT persist hook (the `vget` lookup comment confirms this). This is a newly introduced bug.
|
||
|
||
**Implication**: The refactored design must eliminate the outer `workermgr` write lock in `handle_worker_join` before any path reaches `makeWorker`.
|
||
|
||
---
|
||
|
||
### Topic 2: Existence Check and Locking Strategy
|
||
|
||
**Question**: How should the explicit `key_find` check be locked?
|
||
|
||
**Investigation**: `worker_ctrl` uses `lock(workermgr, READ)` → `key_find` → `reference(w)` → `unlock`. Other paths use read locks for existence checks and upgrade to per-object write locks for mutation. The workermgr write lock is only needed for structural changes (insert/delete of entries), not for updating existing worker fields.
|
||
|
||
**Decision**: Use a brief `READ` lock for the `key_find` check. For the create path, delegate write locking to `makeWorker`. For the rejoin path, use a per-worker write lock.
|
||
|
||
**TOCTOU note**: A brief window exists between the read-lock check and `makeWorker`'s write lock. This is benign: `makeWorker_` calls `key_find` internally under the write lock and, if the worker was created by another thread between our check and our call, will call `override_worker` (update) instead of `makeInstance` (create). The behavior remains correct.
|
||
|
||
---
|
||
|
||
### Topic 3: Rejoin Update — Replicating `override_worker` Semantics
|
||
|
||
**Question**: For the rejoin path (explicit update), how should attrs be updated in-place without calling `override_worker` (which is static in wobj.c)?
|
||
|
||
**Investigation**: `override_worker` in wobj.c:
|
||
1. Saves admin bits (`status & XCE_WORKER_ADMIN_MASK`)
|
||
2. Calls `kill_worker` (frees name, attrs, dispatch_window, thresholds, resvals)
|
||
3. Calls `copy_worker` (re-allocates from new src)
|
||
4. Restores admin bits and idx
|
||
|
||
For the rejoin path in worker.c, we can replicate this semantic without calling `override_worker` directly. The update needs to: free old `hostattrs` fields (hosttype/model strings), copy new attrs, update `ep`, and preserve admin bits on `host.status`. The existing `dereference/makeString` pattern is already used in `add_worker` for hosttype/model override, so the pattern is established.
|
||
|
||
**Decision**: Implement `rejoin_worker` in worker.c that replicates the admin-bit-preserving update logic inline, avoiding the static `override_worker` dependency.
|
||
|
||
---
|
||
|
||
### Topic 4: Resource and Status Update — Shared Path
|
||
|
||
**Question**: Can the status/resource update logic in `add_worker` (req 4) be shared between create and rejoin paths?
|
||
|
||
**Investigation**: The current `add_worker` applies the following after `makeWorker` returns, under a per-worker write lock:
|
||
- `w->host.status &= ~XCE_WORKER_UNAVAIL`
|
||
- `w->ep = ep`
|
||
- hconfig hosttype/model override
|
||
- `add_hostattrs_resvals` (resource vals for type, model, cpuf, etc.)
|
||
- `set_host_slots` (mxj or maxcpus)
|
||
- `dispatch_window_config` copy
|
||
- `thresholds` copy
|
||
- bool resource marking loop
|
||
|
||
This block is identical for both create and rejoin. Extracting it into `static int apply_join_state(cluster_t*, worker_t*, endpoint_t*, xce_hostconfig_t*)` eliminates duplication.
|
||
|
||
---
|
||
|
||
### Topic 5: Hook Firing and Lock Relationship
|
||
|
||
**Question**: Must BEFORE_EVENT and AFTER_EVENT hooks fire while the workermgr write lock is held?
|
||
|
||
**Investigation**: The current code fires both hooks under the outer workermgr write lock. However, `mark_worker_unavail` and `mark_worker_back` fire hooks while holding only the per-worker write lock (not the workermgr lock). The persist hook signature takes `(c, w)` — it reads from `w` directly, not through the workermgr index. There is no architectural requirement for the workermgr write lock to be held during hook dispatch.
|
||
|
||
**Decision**: Hooks fire in `handle_worker_join` outside any workermgr lock. BEFORE_EVENT fires before `key_find`. AFTER_EVENT fires after the create/update completes. This matches the behavioral contract from req 5.1.
|
||
|
||
---
|
||
|
||
## Architecture Decision: Removing `add_worker`, Adding `init_worker` + `rejoin_worker`
|
||
|
||
The current `add_worker` is a monolithic function that internally calls `makeWorker` (which hides the create-or-update branch). The refactored design replaces it with:
|
||
|
||
- `static int apply_join_state(cluster_t*, worker_t*, endpoint_t*, xce_hostconfig_t*)` — shared status/resource update logic (per-worker write lock)
|
||
- `int init_worker(cluster_t*, endpoint_t*, const char*, hostattrs_t*, xce_hostconfig_t*)` — first-time join: calls `makeWorker` + `apply_join_state`
|
||
- `int rejoin_worker(cluster_t*, worker_t*, endpoint_t*, hostattrs_t*, xce_hostconfig_t*)` — rejoin: updates attrs/ep in-place + `apply_join_state`
|
||
|
||
`add_worker` declaration in `worker.h` is replaced with `init_worker` and `rejoin_worker`.
|
||
|
||
---
|
||
|
||
## Risk Assessment
|
||
|
||
| Risk | Likelihood | Mitigation |
|
||
|------|-----------|-----------|
|
||
| TOCTOU between key_find and makeWorker | Very low (same host reconnecting twice simultaneously) | makeWorker_ handles gracefully via override |
|
||
| Admin bits not preserved in rejoin path | Medium if not explicit | Explicit mask applied in rejoin_worker |
|
||
| Hook ordering regression | Low | Hooks remain in handle_worker_join, same relative order |
|
||
| Existing test breakage | Low | Tests exercise add_worker indirectly; signature change requires test fixture update |
|
||
|
||
---
|
||
|
||
# Gap Analysis (2026-05-20)
|
||
|
||
## Analysis Summary
|
||
|
||
- **Scope**: Narrow brownfield refactor — one C file (`worker.c`) with three missing function bodies. All surrounding scaffolding is already in place.
|
||
- **Key challenge**: None blocking; the design and test contract are locked. Only implementation is missing.
|
||
- **Recommendation**: Option A (extend existing `worker.c`) is the only appropriate approach.
|
||
- **Effort**: S (1–3 days). All design decisions and lock semantics resolved; code to write is a mechanical extraction from the existing `add_worker` body.
|
||
- **Risk**: Low. Existing `add_worker` contains the exact logic to extract. Tests in `test_worker_join.c` specify every assertion.
|
||
|
||
## Current Implementation State
|
||
|
||
| Component | File | Status |
|
||
|-----------|------|--------|
|
||
| `handle_worker_join` (rewritten) | `join.c` | ✅ Done — explicit `key_find` branch, calls `init_worker`/`rejoin_worker` |
|
||
| `worker.h` declarations | `include/worker.h` | ✅ Done — `init_worker`, `rejoin_worker` exported; `add_worker` removed |
|
||
| TDD tests | `testsuite/test_worker_join.c` | ✅ Done — 7 tests for `apply_join_state`, `init_worker`, `rejoin_worker` |
|
||
| Test registered | `testsuite/controller_test.c` | ✅ Done — `test_worker_join` registered |
|
||
| `apply_join_state` (static helper) | `worker.c` | ❌ Missing — body not yet extracted |
|
||
| `init_worker` | `worker.c` | ❌ Missing — stub only; function body not yet written |
|
||
| `rejoin_worker` | `worker.c` | ❌ Missing — stub only; function body not yet written |
|
||
| `add_worker` removal | `worker.c` | ❌ Pending — old function body still present (unused by join.c) |
|
||
|
||
## Requirement-to-Asset Map
|
||
|
||
| Requirement | Asset Needed | Status | Gap Tag |
|
||
|-------------|-------------|--------|---------|
|
||
| 1.1 Explicit key_find lookup | `handle_worker_join` explicit key_find | ✅ Present in join.c | — |
|
||
| 1.2 Branch on key_find result | `init_worker`/`rejoin_worker` dispatch in join.c | ✅ Present in join.c | — |
|
||
| 1.3 Reject non-ENOENT errors | Error branch in join.c | ✅ Present in join.c | — |
|
||
| 2.1 Create via makeWorker | `init_worker` calling `makeWorker` | ❌ Missing implementation | Missing |
|
||
| 2.2 Log failure, no on_worker_join | Error path in join.c | ✅ Present in join.c | — |
|
||
| 2.3 Assign priv->u.idx | join.c post-join assignment | ✅ Present in join.c | — |
|
||
| 3.1 Update hostattrs on rejoin | `rejoin_worker` attrs copy | ❌ Missing implementation | Missing |
|
||
| 3.2 Update ep on rejoin | `rejoin_worker` ep assignment | ❌ Missing implementation | Missing |
|
||
| 3.3 Preserve admin bits | `XCE_WORKER_ADMIN_MASK` in `rejoin_worker` | ❌ Missing implementation | Missing |
|
||
| 4.1 Clear UNAVAIL | `apply_join_state` status clear | ❌ Missing implementation | Missing |
|
||
| 4.2 Apply hconfig overrides | `apply_join_state` hosttype/model/slots | ❌ Missing implementation | Missing |
|
||
| 4.3 Update host-attr resources | `apply_join_state` calling `add_hostattrs_resvals` | ❌ Missing implementation | Missing |
|
||
| 4.4 Reject if hostconfig NULL | join.c fail-fast before lock | ✅ Present in join.c | — |
|
||
| 5.1 BEFORE/AFTER hook order | join.c hook call sites | ✅ Present in join.c | — |
|
||
| 5.2 on_worker_join still called | join.c post-join call | ✅ Present in join.c | — |
|
||
| 5.3 Critical section brevity | Locking model in join.c | ✅ Present in join.c | — |
|
||
| 5.4 Existing tests pass | Test suite build | ❌ Pending — add_worker still present, init/rejoin stubs fail tests | Constraint |
|
||
|
||
## Implementation Approach Options
|
||
|
||
### Option A: Extend Existing worker.c (Recommended)
|
||
|
||
**Rationale**: All design decisions are locked. The logic to implement already exists verbatim in `add_worker`. The task is extraction and reorganization:
|
||
|
||
1. Add `apply_join_state` as a static helper — extract the `~XCE_WORKER_UNAVAIL` + ep + hconfig override + `add_hostattrs_resvals` + `set_host_slots` + `dispatch_window_config` + `thresholds` + bool resources loop from the bottom half of `add_worker`.
|
||
2. Implement `init_worker` — build a `xce_worker_t` from name/attrs, call `makeWorker`, then call `apply_join_state` under per-worker write lock.
|
||
3. Implement `rejoin_worker` — save admin bits, update attrs/ep in-place, call `apply_join_state`, restore admin bits.
|
||
4. Remove `add_worker` (now dead code).
|
||
|
||
**Files changed**: `src/controller/worker.c` only.
|
||
|
||
Trade-offs:
|
||
- ✅ Minimal surface — one file, pure extraction
|
||
- ✅ Tests already written and specify exact postconditions
|
||
- ✅ Lock semantics fully resolved in design.md
|
||
- ❌ None — this is the obvious approach
|
||
|
||
### Option B: New file (not appropriate)
|
||
|
||
These are not new responsibilities — they replace `add_worker` in the same file. Creating a new file adds navigation overhead with no benefit.
|
||
|
||
### Option C: Hybrid (not applicable)
|
||
|
||
No external integration or phased rollout needed. Pure in-file refactor.
|
||
|
||
## Implementation Complexity & Risk
|
||
|
||
- **Effort**: S (1–3 days) — logic is a mechanical extraction from `add_worker`; all design decisions resolved
|
||
- **Risk**: Low — no new patterns, no new dependencies, no new locking decisions; tests specify exact assertions
|
||
|
||
## Recommendations for Design Phase
|
||
|
||
Design phase is **already approved**. Proceed directly to implementation:
|
||
|
||
1. Implement `apply_join_state` first (tasks 2 / task 1.1 green)
|
||
2. Implement `init_worker` (task 3.1 / task 1.2 green)
|
||
3. Implement `rejoin_worker` (task 3.2 / task 1.3 green)
|
||
4. Remove `add_worker` (task 4.1 cleanup)
|
||
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`.
|
||
|
||
---
|
||
|
||
# Gap Analysis — Validation Pass (2026-06-09)
|
||
|
||
## Analysis Summary
|
||
|
||
- **Status**: Implementation is **complete and committed** (`93ed282 feat(worker-join-refactor): implement apply_join_state, init_worker, rejoin_worker`). Working tree is clean for all spec-owned files. All tasks 1.1–5 are `[x]` in tasks.md.
|
||
- **Code verification**: Every requirement was re-checked against the actual source on branch `persistent_ha`. `handle_worker_join` (src/controller/join.c:33–119) now exposes the explicit check → create-or-update → status/resource-update flow; `apply_join_state` / `init_worker` / `rejoin_worker` are implemented in src/controller/worker.c:279–397; `add_worker` is fully removed (no remaining references anywhere in src/).
|
||
- **Only open item**: Req 5.4 (full testsuite green) could not be re-confirmed in this pass — no build artifact exists in the worktree. One `/xce-build controller` run is the remaining verification step.
|
||
|
||
## Current State Investigation
|
||
|
||
| Asset | Location | Finding |
|
||
|-------|----------|---------|
|
||
| `handle_worker_join` | `src/controller/join.c:33` | Rewritten. Fail-fast `find_matched_hostconfig` (no lock) → BEFORE_EVENT hook → brief workermgr READ lock + `key_find` + `reference(w)` → branch (`-ENOENT`→`init_worker`, found→`rejoin_worker`, other error→reject) → `vget` for AFTER_EVENT hook → `xce_reply` outside any lock → `priv->u.idx` set → `on_worker_join`. Outer workermgr WRITE lock is gone — the latent double-lock deadlock is eliminated. |
|
||
| `apply_join_state` | `src/controller/worker.c:283–337` | Static (exported under `-DXCE_TESTING`, which meson.build:148 sets for the test binary). Clears `XCE_WORKER_UNAVAIL`, sets `w->ep`, applies hosttype/model `"!"`-guarded overrides, `add_hostattrs_resvals`, mxj/maxcpus `set_host_slots`, dispatch-window + thresholds replacement, bool-resource loop. Precondition (per-worker WRITE lock held by caller) documented in comment. |
|
||
| `init_worker` | `src/controller/worker.c:339–364` | Builds temp `xce_worker_t`, calls `makeWorker` (which owns its own workermgr WRITE lock), `-errno` on NULL with ERROR log, then per-worker WRITE lock + `apply_join_state`, returns `w->idx`. |
|
||
| `rejoin_worker` | `src/controller/worker.c:366–397` | Per-worker WRITE lock; saves `admin_status = status & XCE_WORKER_ADMIN_MASK` (LOCKEDU\|CLOSED\|HIDDEN\|CLOSED_BY_ADMIN, defined worker.c:275); dereference/makeString replacement of hosttype/hostmodel; copies cpufactor/maxcpus/maxmem/maxswap/maxtmp/ndisks; `apply_join_state`; restores admin bits; returns `w->idx`. |
|
||
| `worker.h` | `src/controller/include/worker.h:11–14` | `init_worker` and `rejoin_worker` exported with `xce_hostconfig_t*` param; `add_worker` declaration removed (only unrelated `add_worker_to_xlist` remains in cluster.h). |
|
||
| Tests | `src/controller/testsuite/test_worker_join.c` | 7 `RUN_TEST` cases (3 × apply_join_state, 2 × init_worker, 2 × rejoin_worker), registered in `controller_test.c:154` and built via `meson.build:103`. |
|
||
| Out-of-scope guard | `src/lib/wobj.c` | `makeWorker`/`override_worker` untouched by the refactor commit (wobj.c admin-mask logic at lines 85–105 predates it, commit febb4a6). Boundary respected. |
|
||
|
||
## Requirement-to-Asset Map
|
||
|
||
| Req | Summary | Asset (file:line) | Status |
|
||
|-----|---------|-------------------|--------|
|
||
| 1.1 | key_find before create-or-update | join.c:75–79 (READ lock + key_find + reference) | Implemented |
|
||
| 1.2 | Branch on lookup result | join.c:81–97 (error / NULL / found branches) | Implemented |
|
||
| 1.3 | Reject non-ENOENT key_find error | join.c:81–84 (`idx = rc`, reply.status set, returned) | Implemented |
|
||
| 2.1 | Create via makeWorker | worker.c:349 (`makeWorker(c->workermgr, &tmp)`) | Implemented |
|
||
| 2.2 | Log failure, set reply status, skip on_worker_join | worker.c:350–353 + join.c:87–89, 107–112 (`if (idx < 0) return idx` before on_worker_join) | Implemented |
|
||
| 2.3 | Assign priv->u.idx | join.c:115 (before `on_worker_join`) | Implemented |
|
||
| 3.1 | Update hostattrs on rejoin | worker.c:376–386 (string + numeric attr copy) | Implemented |
|
||
| 3.2 | Update ep on rejoin | worker.c:294 (`w->ep = ep` inside apply_join_state, called from rejoin) | Implemented |
|
||
| 3.3 | Preserve admin bits | worker.c:374, 390 (save mask / restore after apply_join_state) | Implemented |
|
||
| 4.1 | Clear XCE_WORKER_UNAVAIL | worker.c:293 | Implemented |
|
||
| 4.2 | Apply hconfig overrides (type/model/mxj/window/thresholds/bool res) | worker.c:296–336 | Implemented |
|
||
| 4.3 | Host-attr resource values | worker.c:308 (`add_hostattrs_resvals`) | Implemented |
|
||
| 4.4 | Reject when hostconfig NULL, workermgr untouched | join.c:60–68 (fail-fast before BEFORE_EVENT hook and before any lock) | Implemented |
|
||
| 5.1 | BEFORE/AFTER hook order at same event points | join.c:71–72 (BEFORE, pre-key_find) and join.c:103–104 (AFTER, post-update, w via vget) | Implemented |
|
||
| 5.2 | on_worker_join after successful join (both paths) | join.c:116 (single post-branch call site) | Implemented |
|
||
| 5.3 | Critical section no longer than before | Locking narrowed vs. pre-refactor: brief READ for lookup, makeWorker-internal WRITE for create, per-worker WRITE for mutation; xce_reply outside all locks | Implemented |
|
||
| 5.4 | Existing testsuite passes unmodified | test_worker_join.c registered; no build dir in worktree to confirm a green run in this pass | Unknown — verify with `/xce-build controller` |
|
||
|
||
**Constraints noted (no action required)**:
|
||
- `apply_join_state` visibility is toggled by `XCE_TESTING` (worker.c:282–288) — tests link against the non-static symbol. This is the established testsuite pattern; keep the meson `-DXCE_TESTING` arg in sync.
|
||
- TOCTOU window between READ-lock `key_find` and `makeWorker` remains by design; `makeWorker_` degrades to `override_worker` (admin-bit-preserving since febb4a6), so behavior stays correct.
|
||
|
||
**Minor observation (LOW, optional)**: in the join.c error branch (rc < 0 && rc != -ENOENT), if `key_find` ever returned an error while also setting `w`, the `reference(w)` taken at join.c:77 would not be released. Current `key_find` semantics make this unreachable (`w` stays NULL on error), so this is a defensive-coding nit only.
|
||
|
||
## Implementation Approach Options
|
||
|
||
### Option A: Close out the spec as-is (Recommended)
|
||
Verify req 5.4 with one `/xce-build controller` run; if green, mark the spec `implemented` in spec.json and archive.
|
||
- ✅ Zero code change; everything else is verified at source level
|
||
- ✅ Matches the prior audit conclusion (2026-06-08) — now re-confirmed line-by-line
|
||
- ❌ None
|
||
|
||
### Option B: Add the defensive dereference in the key_find error branch before closing
|
||
One-line hardening (`if (w) { dereference(w); w = NULL; }` in the error branch) plus rebuild.
|
||
- ✅ Removes the theoretical reference leak
|
||
- ❌ Touches a committed, behavior-frozen handler for an unreachable path; out of the spec's acceptance criteria
|
||
|
||
### Option C: Fold verification into the next controller-wide build
|
||
Skip a dedicated build; rely on the next `/xce-build` from any active spec (e.g., HA work on this branch) to exercise `test_worker_join`.
|
||
- ✅ No extra build cycle
|
||
- ❌ Spec closure blocked on unrelated work; a regression would be attributed late
|
||
|
||
## Effort & Risk
|
||
|
||
- **Effort: S** — remaining work is one build/test run (Option A) or that plus a one-line nit (Option B); no design or implementation gaps remain.
|
||
- **Risk: Low** — pure verification of already-committed code following established patterns; all 16 functional criteria confirmed at source level; only the test-run evidence is missing.
|
||
|
||
## Recommendations
|
||
|
||
1. Run `/xce-build controller` to confirm the 7 `test_worker_join` cases and the rest of the controller testsuite pass (closes Req 5.4).
|
||
2. On green, update `spec.json` phase to `implemented` (or run `/kiro-verify-completion worker-join-refactor`).
|
||
3. Optionally apply the Option B one-line defensive dereference if the file is touched again for other reasons; do not reopen the spec for it.
|
||
|
||
## Research Needed
|
||
|
||
- None for this spec. Single carry-forward verification item: green `/xce-build controller` run on current HEAD (worker.c/join.c unmodified since 93ed282, but HA-branch changes to ha.c/raft.c in the working tree share the controller test binary — confirm no link/test interference).
|
||
|
||
---
|
||
|
||
## Gap Re-Audit (2026-06-18)
|
||
|
||
**Mode:** POST-IMPLEMENTATION GAP AUDIT (spec.json phase=implemented; tasks 12/12 [x]).
|
||
**Method:** EARS requirement → on-disk code verification via codegraph + targeted Read. Cites file:line at current HEAD.
|
||
**Scope reminder (roadmap line 26):** this spec covers **attribute/endpoint update on rejoin only**. The job-state resync (running/finished jobs re-reported on rejoin) is the SEPARATE, not-yet-started spec **worker-rejoin-resync** (planned, no effort). That is explicitly NOT a gap of THIS spec — see Boundary note below.
|
||
|
||
### Requirement-to-Asset Map
|
||
|
||
| Req | Statement (abbrev) | Asset (file:line) | Status |
|
||
|-----|--------------------|-------------------|--------|
|
||
| 1.1 | `key_find` before create-or-update | `handle_worker_join` brief READ lock + `key_find` — join.c:91-95 | **CLOSED** |
|
||
| 1.2 | Branch on lookup result (new vs exists) | `if (w==NULL) init_worker … else rejoin_worker` — join.c:101-113 | **CLOSED** |
|
||
| 1.3 | Reject on non-ENOENT `key_find` error | `if (rc<0 && rc!=-ENOENT){ … idx=rc; }` — join.c:97-100 | **CLOSED** |
|
||
| 2.1 | Create new entry via `makeWorker` | `init_worker` builds tmp `xce_worker_t`, calls `makeWorker(c->workermgr,&tmp)` — worker.c:339-354 | **CLOSED** |
|
||
| 2.2 | On create fail: log, set status, no `on_worker_join` | NULL→`ERROR`+`return -errno`; caller sets `reply.status=idx`, `if(idx<0) return idx` before `on_worker_join` — worker.c:351-353, join.c:104-105/138-147 | **CLOSED** |
|
||
| 2.3 | Assign new idx to `priv->u.idx` | `priv->u.idx = idx;` before `on_worker_join` — join.c:146 | **CLOSED** |
|
||
| 3.1 | Update `hostattrs` on rejoin | `rejoin_worker` frees+`makeString` hosttype/hostmodel, copies numeric attrs — worker.c:376-386 | **CLOSED** |
|
||
| 3.2 | Update `ep` on rejoin | `apply_join_state` sets `w->ep = ep` (worker.c:295), invoked by rejoin path — worker.c:388 | **CLOSED** |
|
||
| 3.3 | Preserve admin status bits | `admin_status = w->host.status & XCE_WORKER_ADMIN_MASK` (worker.c:374) restored `w->host.status |= admin_status` (worker.c:390); mask = LOCKEDU\|CLOSED\|HIDDEN\|CLOSED_BY_ADMIN — worker.c:275-277 | **CLOSED** |
|
||
| 4.1 | Clear `XCE_WORKER_UNAVAIL` | `w->host.status &= ~XCE_WORKER_UNAVAIL;` — worker.c:294 | **CLOSED** |
|
||
| 4.2 | Apply hconfig overrides (type/model/mxj slots/dispatch window/thresholds/bool res) | `apply_join_state` worker.c:297-334: hosttype/model "!"-guarded override, `set_host_slots(mxj, fallback maxcpus)`, dispatch_window dup, thresholds dup, `mark_host_res` loop | **CLOSED** |
|
||
| 4.3 | Update host-attr resource values (type,model,cpuf,maxcpus,maxmem,maxswap,maxtmp,ndisks,slots) | `add_hostattrs_resvals` (worker.c:223-250, 8 attrs) + `set_host_slots` (slots) — called worker.c:307,312 | **CLOSED** |
|
||
| 4.4 | Reject if `find_matched_hostconfig`==NULL, no workermgr mutation | fail-fast before any lock: `hconfig==NULL → reply.status=-errno/-EINVAL; return` — join.c:77-84 | **CLOSED** |
|
||
| 5.1 | BEFORE before mutation, AFTER after | `HOOK_BEFORE_EVENT` at join.c:87-88 (before key_find); `HOOK_AFTER_EVENT` at join.c:120-121 (after create/update, outside lock) | **CLOSED** |
|
||
| 5.2 | `on_worker_join` after successful join (both paths) | `on_worker_join(c, ep)` — join.c:147 (reached for both init and rejoin when idx>=0) | **CLOSED** |
|
||
| 5.3 | Critical section as short as current | No outer workermgr WRITE lock; only brief READ lock for key_find (join.c:91-95); mutation under per-worker WRITE lock in init/rejoin (worker.c:356-358, 372-392) | **CLOSED** |
|
||
| 5.4 | Existing tests pass; `add_worker` removed | Controller `add_worker` (old join path) deleted from worker.c & worker.h:11-14 (only `init_worker`/`rejoin_worker` declared); new `test_worker_join.c` registered in controller_test.c:37,166. NOTE: residual unrelated `add_worker` symbols exist — `add_worker_to_xlist` (misc.c:11) and worker-daemon `add_worker` (xcecd_worker_utils.c:12); both are distinct functions, not the refactored one | **CLOSED (verify build)** |
|
||
|
||
**Score: 16/16 acceptance criteria CLOSED in code.** All 12 tasks have on-disk evidence. Design's planned architecture (explicit branch, no outer write lock, static `apply_join_state`, `init_worker`/`rejoin_worker`) matches the implementation exactly.
|
||
|
||
### Implementation deltas vs design (not gaps — improvements/additions)
|
||
|
||
1. **`validate_host` gate added** (join.c:65-72) — rejects unknown hosts with `-EPERM` before hostconfig lookup. Not in the original design; a correct hardening, no requirement conflict.
|
||
2. **`persist_commit_barrier` added** (join.c:129-136) — the success reply is gated on the join WAL entry being majority-committed (ha-commit-barrier-rollout Req 1.1-1.4). This is the cross-spec commit-durability integration; barrier failure becomes the error reply and skips `on_worker_join`, preserving Req 2.2/5.2 semantics. Landed after this spec's design was written; consistent with it.
|
||
3. **`apply_join_state` linkage** is `static` in production, non-`static` under `#ifdef XCE_TESTING` (worker.c:283-289) — enables the unit test to call it directly while keeping production encapsulation. Matches design intent.
|
||
4. **`reference(w)`/`dereference(w)` discipline** around the found worker (join.c:93-94, 109) — refcount safety across the lock release between key_find and rejoin. Correct; not specified but required given the read-lock-then-release pattern.
|
||
|
||
### Residual / Deferred Gaps
|
||
|
||
**No open requirement gaps for this spec.** All criteria are met in code. Residual items are verification-only or out-of-boundary:
|
||
|
||
- **R1 (verification-only, carried from prior audit): green build/test on current HEAD.** worker.c/join.c are unmodified by HA-branch work, but join.c now depends on `persist_commit_barrier` and the controller test binary is shared with HA-branch changes (ha.c/raft.c) in the working tree. Action: run `/xce-build controller` and confirm `test_worker_join` (7 cases) plus `test_commit_barrier_reply` pass and no link interference. Effort: S (~1 build). Risk: Low.
|
||
- **R2 (latent, MEDIUM — was flagged in prior audit body): `apply_join_state` resource-update failures are swallowed.** `update_resvals` returns 0 unconditionally (worker.c:133-146; logs via `PERROR` but discards rc), and `mark_host_res` rc in the `apply_join_state` loop (worker.c:333-334) is ignored. So Req 4.3 "update all resource values" can partially fail silently and the join still reports success. This MATCHES pre-refactor behavior (Error Handling table, design.md:423 — "log with PERROR; continue"), so it is **not a regression and not a spec gap** — it is a pre-existing design-accepted behavior. Logged here only as a known limitation.
|
||
- **R3 (out-of-boundary — DO NOT count as this spec's gap): worker job-state resync on rejoin.** `rejoin_worker` updates attributes/endpoint/resources only. It does NOT recover running/finished job facts that were lost during the disconnect/failover window. `xce_join_req` carries only `hostattrs_t` (no job state). This is owned by the separate **worker-rejoin-resync** spec (roadmap line 26: "planned long ago, no effort yet"; dependencies: ha-consistency-invariants commit barrier + idempotency guards). Roadmap explicitly records this spec as "Extends: worker-join-refactor (implemented — covers attribute/endpoint update on rejoin only)." **Boundary is clean and intentional.**
|
||
|
||
### Options for the one actionable open item (R1)
|
||
|
||
| Option | Description | Effort | Risk |
|
||
|--------|-------------|--------|------|
|
||
| **A (recommended)** | Run `/xce-build controller` on current HEAD; confirm `test_worker_join` + `test_commit_barrier_reply` green and no link interference with HA-branch ha.c/raft.c changes. Record result; close R1. | S (~1 build cycle) | Low |
|
||
| **B** | Defer until next controller build for any reason; treat R1 as a standing CI gate rather than a spec action. | None now | Low (build drift risk if join.c's `persist_commit_barrier` dep changes upstream) |
|
||
| **C** | Additionally harden R2: make `update_resvals`/`mark_host_res` propagate rc so a join with failed resource setup fails loud (per fail-loud preference). Re-scopes Req 4.3 error semantics — would need a spec amendment since current behavior is design-accepted. | M (touches worker.c shared helpers + tests; affects `restore_worker`/`mark_worker_unavail` callers) | Medium (behavior change to a shared path; out of original scope) |
|
||
|
||
**Recommendation: Option A.** The spec is functionally complete (16/16 criteria CLOSED); only a build/test confirmation on the integrated HEAD remains. R2 is a pre-existing accepted behavior — pursue Option C only if a separate fail-loud hardening spec is opened. R3 belongs to worker-rejoin-resync.
|