# Design Document: worker-join-refactor ## Overview This refactor restructures the worker join path in the XCE controller to make the check → create-or-update → status/resource-update sequence explicit and readable at the `handle_worker_join` call site. The current implementation delegates the create-or-update decision to `add_worker`, which in turn delegates it to `makeWorker` in `wobj.c`. This hides the branching and introduces a latent deadlock: `handle_worker_join` holds the `workermgr` write lock while calling `add_worker → makeWorker`, which attempts to re-acquire the same lock. Since `my_rwlock_t` is `pthread_rwlock_t` (non-reentrant), this deadlocks. The refactor eliminates the outer lock, makes existence-checking visible at the join handler, and introduces two narrowly-scoped helpers for the create and rejoin paths. ### Goals - Make the check → create-or-update → status/resource-update control flow visible in `handle_worker_join` - Eliminate the double workermgr write lock acquisition (latent deadlock fix) - Preserve all observable behavior: hooks, `on_worker_join`, admin-bit preservation ### Non-Goals - Changes to `makeWorker` or any other function in `src/lib/wobj.c` - Changes to other join handlers (`handle_client_join`, `handle_scheduler_join`, etc.) - Job dispatch, WAL, or persist subsystem behavior changes --- ## Boundary Commitments ### This Spec Owns - `handle_worker_join` in `src/controller/join.c` — rewritten to expose explicit branching - `add_worker` in `src/controller/worker.c` — replaced by `init_worker`, `rejoin_worker`, and `apply_join_state` - `src/controller/include/worker.h` — updated exported declarations ### Out of Boundary - `makeWorker` / `makeWorker_` / `override_worker` in `src/lib/wobj.c` — must not be modified - Other join handler functions in `join.c` - Persist/WAL hook internals ### Allowed Dependencies - `key_find` from `kvmgr` (via RTTI) for existence check - `makeWorker` from `wobj.c` for first-time worker creation - `find_matched_hostconfig`, `add_hostattrs_resvals`, `set_host_slots`, `mark_host_res` from `worker.c` (already present) - `xce_run_hooks` for BEFORE/AFTER event hooks - `on_worker_join` for post-join config push ### Revalidation Triggers - Changes to `makeWorker` behavior (create vs update path) require re-checking `init_worker` - Changes to `XCE_WORKER_ADMIN_MASK` definition require re-checking the admin-bit preservation in `rejoin_worker` - Adding new hconfig fields requires updating `apply_join_state` --- ## Architecture ### Existing Architecture Analysis **Current flow in `handle_worker_join` (join.c)**: ``` lock(workermgr, WRITE) run_hooks(BEFORE_EVENT) add_worker() ← delegates create-or-update decision makeWorker() lock(workermgr, WRITE) ← DEADLOCK: already held by caller key_find + create-or-override unlock(workermgr) per-worker lock clear UNAVAIL, set ep, apply hconfig per-worker unlock vget(workermgr, idx, &w) run_hooks(AFTER_EVENT) xce_reply(...) unlock(workermgr) on_worker_join(...) ``` **Problems**: 1. Double write lock on `workermgr` deadlocks (pthread_rwlock_t is non-reentrant) 2. Create-or-update branch hidden inside `makeWorker_` — invisible at call site 3. `xce_reply` called while holding the write lock (unnecessary) ### Architecture Pattern & Boundary Map ```mermaid graph TB HWJ[handle_worker_join] FHC[find_matched_hostconfig] HooksBefore[run_hooks BEFORE] KeyFind[key_find workermgr READ lock] InitW[init_worker] RejoinW[rejoin_worker] AJS[apply_join_state] MakeW[makeWorker wobj.c] HooksAfter[run_hooks AFTER] OWJ[on_worker_join] HWJ --> FHC HWJ --> HooksBefore HWJ --> KeyFind KeyFind -->|ENOENT| InitW KeyFind -->|found| RejoinW InitW --> MakeW InitW --> AJS RejoinW --> AJS HWJ --> HooksAfter HWJ --> OWJ ``` **Refactored flow**: - `handle_worker_join` owns the explicit `key_find` branch decision (Req 1) - `init_worker` owns first-time registration (Req 2); delegates creation locking to `makeWorker` - `rejoin_worker` owns in-place attribute/endpoint update (Req 3); uses per-worker write lock - `apply_join_state` is a static shared helper for status and resource update (Req 4) - No outer `workermgr` write lock in `handle_worker_join` — eliminates deadlock **Locking model**: | Scope | Lock | Holder | Purpose | |-------|------|--------|---------| | Existence check | workermgr READ | `handle_worker_join` (brief) | Safe key_find | | Worker creation | workermgr WRITE | `makeWorker` (internal) | key_add atomicity | | Worker update | per-worker WRITE | `rejoin_worker` / `apply_join_state` | Field mutation | ### Technology Stack | Layer | Choice | Role | Notes | |-------|--------|------|-------| | Language | C99 | Implementation | Existing codebase standard | | Locking | pthread_rwlock_t (my_rwlock_t) | Concurrency | Existing primitive; non-reentrant | | Object management | RTTI (key_find, key_add, makeInstance) | Worker lifecycle | wobj.c / kvmgr.c | | Hooks | xce_run_hooks | Persistence integration | Unchanged hook fire points | --- ## File Structure Plan ### Modified Files - `src/controller/join.c` — `handle_worker_join`: remove outer workermgr write lock; add explicit `key_find` check; call `init_worker` or `rejoin_worker` based on result - `src/controller/worker.c` — remove `add_worker`; add `apply_join_state` (static), `init_worker`, `rejoin_worker` - `src/controller/include/worker.h` — replace `add_worker` declaration with `init_worker` and `rejoin_worker` --- ## System Flows ```mermaid sequenceDiagram participant JC as handle_worker_join participant WC as worker.c helpers participant WO as makeWorker (wobj.c) participant HS as hooksys JC->>JC: find_matched_hostconfig (fail fast, no lock) JC->>HS: run_hooks BEFORE_EVENT WORKER_JOIN JC->>JC: lock(workermgr READ) JC->>JC: key_find(workermgr, name, &w) JC->>JC: unlock(workermgr READ) alt host not found (ENOENT) JC->>WC: init_worker(c, ep, name, attrs, hconfig) WC->>WO: makeWorker(workermgr, &tmp_host) Note over WO: lock WRITE, create entry, unlock WRITE WC->>WC: apply_join_state under per-worker WRITE lock WC-->>JC: idx else host found JC->>WC: rejoin_worker(c, w, ep, attrs, hconfig) WC->>WC: per-worker WRITE lock WC->>WC: update hostattrs + ep (preserve admin bits) WC->>WC: apply_join_state (status, resources) WC->>WC: unlock per-worker WC-->>JC: idx else key_find error (not ENOENT) JC-->>JC: reject join, return error end JC->>JC: set priv->u.idx = idx JC->>JC: xce_reply (outside any lock) JC->>HS: run_hooks AFTER_EVENT WORKER_JOIN JC->>WC: on_worker_join(c, ep) ``` **Key decisions**: - `find_matched_hostconfig` fails fast before any lock (Req 4.4 — reject if NULL) - Hooks fire outside the workermgr lock, bracketing the create-or-update step - `xce_reply` and `on_worker_join` execute after all locks are released --- ## Requirements Traceability | Requirement | Summary | Component | Interface | Flow | |-------------|---------|-----------|-----------|------| | 1.1 | key_find before create-or-update | `handle_worker_join` | — | Sequence: key_find step | | 1.2 | Branch on key_find result | `handle_worker_join` | — | alt/else branches | | 1.3 | Reject on non-ENOENT error | `handle_worker_join` | — | else-error branch | | 2.1 | Create via makeWorker | `init_worker` | `init_worker` signature | create branch | | 2.2 | Log failure, set reply status, no on_worker_join | `handle_worker_join` | — | error exit path | | 2.3 | Assign priv->u.idx | `handle_worker_join` | — | after successful join | | 3.1 | Update hostattrs on rejoin | `rejoin_worker` | `rejoin_worker` signature | update branch | | 3.2 | Update ep on rejoin | `rejoin_worker` | — | update branch | | 3.3 | Preserve admin bits | `rejoin_worker` | — | ADMIN_MASK pattern | | 4.1 | Clear XCE_WORKER_UNAVAIL | `apply_join_state` | — | shared path | | 4.2 | Apply hconfig overrides | `apply_join_state` | — | shared path | | 4.3 | Update host-attr resource values | `apply_join_state` | — | shared path | | 4.4 | Reject if find_matched_hostconfig NULL | `handle_worker_join` | — | fail-fast before lock | | 5.1 | BEFORE/AFTER hook order preserved | `handle_worker_join` | — | hook call sites | | 5.2 | on_worker_join still called | `handle_worker_join` | — | post-join step | | 5.3 | Critical section as short as current | Locking model | — | per-lock scoping | | 5.4 | Existing tests pass | All | — | behavioral preservation | --- ## Components and Interfaces ### Summary | Component | Layer | Intent | Req Coverage | Key Dependencies | |-----------|-------|--------|--------------|-----------------| | `handle_worker_join` | join.c | Explicit branch controller | 1.1–1.3, 2.2–2.3, 4.4, 5.1–5.3 | `key_find` (P0), `init_worker` (P0), `rejoin_worker` (P0) | | `init_worker` | worker.c | First-time registration | 2.1–2.3 | `makeWorker` (P0), `apply_join_state` (P0) | | `rejoin_worker` | worker.c | Reconnect update | 3.1–3.3 | `apply_join_state` (P0), per-worker lock (P0) | | `apply_join_state` | worker.c (static) | Status + resource update | 4.1–4.3 | `add_hostattrs_resvals` (P0), `set_host_slots` (P0) | --- ### join.c #### `handle_worker_join` (rewritten) | Field | Detail | |-------|--------| | Intent | Explicit check → create-or-update → status/resource sequence for worker join | | Requirements | 1.1, 1.2, 1.3, 2.2, 2.3, 4.4, 5.1, 5.2, 5.3 | **Responsibilities & Constraints** - Calls `find_matched_hostconfig` first; rejects join with appropriate error code if NULL (Req 4.4) - Fires BEFORE_EVENT hook before the key_find step - Acquires a brief workermgr READ lock for `key_find`; releases before any write path - Branches explicitly on key_find result: `init_worker` or `rejoin_worker` - Fires AFTER_EVENT hook after the create/update completes, outside any lock - Calls `xce_reply` and `on_worker_join` outside any lock - Does NOT hold the workermgr write lock at any point (prevents double-lock deadlock) **Dependencies** - Inbound: `handle_join_` dispatches to this function (P0) - Outbound: `find_matched_hostconfig` — hostconfig lookup (P0) - Outbound: `xce_run_hooks` — BEFORE/AFTER_EVENT (P0) - Outbound: `key_find(c->workermgr, ...)` — existence check (P0) - Outbound: `init_worker` — first-time join path (P0) - Outbound: `rejoin_worker` — rejoin path (P0) - Outbound: `on_worker_join` — config push post-join (P0) **Contracts**: Service [x] / State [x] ##### Service Interface (C) ```c /* Unchanged external signature */ static int handle_worker_join(cluster_t* c, endpoint_t* ep, xce_request_t* request); ``` - Preconditions: `request->body.joinmsg` non-NULL; `ep->data` non-NULL - Postconditions: on success, `priv->u.idx` set; `on_worker_join` invoked; BEFORE/AFTER hooks fired - Error return: negative errno; reply status set to error code; `on_worker_join` not called **Implementation Notes** - Integration: The outer `lock(c->workermgr, RTTI_LOCK_WRITE)` present in the current code is **removed**. The `vget` lookup for the AFTER_EVENT hook (currently inside the lock) moves to after the create/update call. - Validation: All early-exit paths (hostconfig NULL, key_find non-ENOENT error, init/rejoin failure) must set `reply.status` and call `xce_reply` before returning. - Risks: `priv->u.idx` must be set before `on_worker_join` and after the lock is released; the existing order is preserved. --- ### worker.c #### `apply_join_state` (static) | Field | Detail | |-------|--------| | Intent | Shared status and resource update applied to both new and rejoining workers | | Requirements | 4.1, 4.2, 4.3 | **Responsibilities & Constraints** - Called under the per-worker write lock (caller must hold it) - Clears `XCE_WORKER_UNAVAIL` from `w->host.status` (Req 4.1) - Sets `w->ep = ep` - Applies hconfig hosttype/model overrides (dereference + makeString pattern) - Calls `add_hostattrs_resvals` (Req 4.3) - Computes mxj slots and calls `set_host_slots` (Req 4.2) - Replaces `dispatch_window_config` and `thresholds` from hconfig - Loops over `hconfig->nresources` calling `mark_host_res` (bool resources) **Contracts**: Service [x] ##### Service Interface (C) ```c /* static — internal to worker.c */ static int apply_join_state(cluster_t* c, worker_t* w, endpoint_t* ep, xce_hostconfig_t* hconfig); ``` - Preconditions: caller holds per-worker WRITE lock; `w`, `hconfig` non-NULL - Postconditions: worker status, ep, and all resource values updated - Returns: 0 on success; negative errno on resource update failure --- #### `init_worker` | Field | Detail | |-------|--------| | Intent | Register a new worker entry for a first-time joining host | | Requirements | 2.1, 2.2, 2.3 | **Responsibilities & Constraints** - Builds a temporary `xce_worker_t` with name and attrs - Calls `makeWorker(c->workermgr, &tmp)` — delegating write-lock acquisition and allocation to wobj.c - On NULL return: logs failure, returns `-errno` (caller sets reply status) - On success: acquires per-worker write lock, calls `apply_join_state`, unlocks - Returns the worker index (`w->idx`) **Dependencies** - Outbound: `makeWorker` (wobj.c) — allocation + key_add (P0) - Outbound: `apply_join_state` — status/resource setup (P0) **Contracts**: Service [x] ##### Service Interface (C) ```c /* exported — declared in worker.h */ int init_worker(cluster_t* c, endpoint_t* ep, const char* host, hostattrs_t* attrs, xce_hostconfig_t* hconfig); ``` - Preconditions: caller does NOT hold workermgr write lock; `host` non-NULL - Postconditions: new worker registered in workermgr; status and resources initialized - Returns: worker index (≥ 0) on success; negative errno on failure - Error cases: `makeWorker` allocation failure → return `-errno` without calling `on_worker_join` --- #### `rejoin_worker` | Field | Detail | |-------|--------| | Intent | Update an existing worker entry when the same host reconnects | | Requirements | 3.1, 3.2, 3.3 | **Responsibilities & Constraints** - Accepts a `worker_t*` found by the caller's `key_find` - Acquires per-worker write lock - Saves admin status bits: `admin_status = w->host.status & XCE_WORKER_ADMIN_MASK` - Frees existing `hosttype` and `hostmodel` strings; copies new ones from `attrs` - Copies numeric attrs fields (`cpufactor`, `maxcpus`, `maxmem`, `maxswap`, `maxtmp`, `ndisks`) - Restores `w->host.status` admin bits (Req 3.3) - Calls `apply_join_state` (clears UNAVAIL, sets ep, applies hconfig overrides, updates resources) - Releases per-worker write lock - Returns `w->idx` **Dependencies** - Outbound: `apply_join_state` — status/resource setup (P0) - Outbound: `dereference` / `makeString` — string memory management (P0) **Contracts**: Service [x] ##### Service Interface (C) ```c /* exported — declared in worker.h */ int rejoin_worker(cluster_t* c, worker_t* w, endpoint_t* ep, hostattrs_t* attrs, xce_hostconfig_t* hconfig); ``` - Preconditions: caller holds a reference on `w`; does NOT hold per-worker write lock - Postconditions: `w->host.attrs` updated; `w->ep` set to `ep`; admin bits preserved; resources updated - Returns: `w->idx` (≥ 0); negative errno on `apply_join_state` failure - Invariant: `w->host.status & XCE_WORKER_ADMIN_MASK` unchanged from pre-call value ```c #define XCE_WORKER_ADMIN_MASK \ (XCE_WORKER_LOCKEDU | XCE_WORKER_CLOSED | XCE_WORKER_HIDDEN \ | XCE_WORKER_CLOSED_BY_ADMIN) ``` --- ### worker.h #### Exported Declaration Changes Remove: ```c extern int add_worker(void*, endpoint_t*, const char*, hostattrs_t*); ``` Add: ```c extern int init_worker(cluster_t*, endpoint_t*, const char*, hostattrs_t*, xce_hostconfig_t*); extern int rejoin_worker(cluster_t*, worker_t*, endpoint_t*, hostattrs_t*, xce_hostconfig_t*); ``` --- ## Error Handling ### Error Strategy Fail fast, explicit error propagation, no silent fallbacks. ### Error Categories | Error | Source | Handler | Reply Status | |-------|--------|---------|-------------| | `find_matched_hostconfig` returns NULL | No matching hostconfig | Reject join; set `reply.status = -errno` | `-errno` | | `key_find` returns error other than -ENOENT | workermgr internal error | Reject join (Req 1.3) | key_find return code | | `init_worker` returns error | `makeWorker` allocation failure | Log, reject join (Req 2.2) | error code | | `rejoin_worker` returns error | `apply_join_state` failure | Log, reject join | error code | | `apply_join_state` partial failure | Resource update failure | Log with PERROR; continue (matches existing behavior) | 0 | ### Monitoring - Existing `ERROR` / `TRACE` macros used throughout — no changes to log verbosity - `PERROR("update_resval")` in `update_resvals` preserved for resource update failures --- ## Testing Strategy ### Unit Tests (src/controller/testsuite/) 1. **First-time join creates worker**: call `init_worker` with a new hostname; verify `key_find` succeeds afterwards, `XCE_WORKER_UNAVAIL` is clear, and resource values are set. 2. **Rejoin updates attrs and ep**: call `init_worker`, then call `rejoin_worker` with different attrs; verify updated attrs, updated ep, and original admin bits preserved. 3. **Admin bits preserved on rejoin**: set `XCE_WORKER_CLOSED` before rejoin; verify flag still set after `rejoin_worker`. 4. **NULL hostconfig rejects join**: configure cluster with no matching host config; verify `handle_worker_join` path returns error without modifying workermgr. 5. **Non-ENOENT key_find error rejected**: simulate `key_find` returning `-EIO`; verify join rejected and `on_worker_join` not called. ### Integration Tests 1. **BEFORE/AFTER hook order**: verify hooks fire in the correct order relative to workermgr state changes using existing `test_e2e_hooks.c` patterns. 2. **on_worker_join called after successful join**: verify `worker_push_config` executes on both first-time and rejoin paths. 3. **Existing testsuite passes without modification**: `make test` in `src/frontend/` and testsuite build must remain green. ### Behavioral Preservation The existing unit tests in `src/controller/testsuite/` exercise `add_worker` indirectly through cluster setup helpers. Test fixtures that call `add_worker` directly (if any) must be updated to call `init_worker` with the added `hconfig` parameter. All observable behaviors — hook ordering, resource initialization, admin-bit preservation, `on_worker_join` invocation — are explicitly covered by the traceability table and unit tests above.