myxce/.kiro/specs/worker-join-refactor/research.md
Rongsong Shen 20e59d0b42 checkpoint: bug-851 root cause + raft TLA+ conformance spec family
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.
2026-06-21 14:02:15 +00:00

30 KiB
Raw Blame History

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

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

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
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.