450 lines
18 KiB
Markdown
450 lines
18 KiB
Markdown
# 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.
|