18 KiB
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
makeWorkeror any other function insrc/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_joininsrc/controller/join.c— rewritten to expose explicit branchingadd_workerinsrc/controller/worker.c— replaced byinit_worker,rejoin_worker, andapply_join_statesrc/controller/include/worker.h— updated exported declarations
Out of Boundary
makeWorker/makeWorker_/override_workerinsrc/lib/wobj.c— must not be modified- Other join handler functions in
join.c - Persist/WAL hook internals
Allowed Dependencies
key_findfromkvmgr(via RTTI) for existence checkmakeWorkerfromwobj.cfor first-time worker creationfind_matched_hostconfig,add_hostattrs_resvals,set_host_slots,mark_host_resfromworker.c(already present)xce_run_hooksfor BEFORE/AFTER event hookson_worker_joinfor post-join config push
Revalidation Triggers
- Changes to
makeWorkerbehavior (create vs update path) require re-checkinginit_worker - Changes to
XCE_WORKER_ADMIN_MASKdefinition require re-checking the admin-bit preservation inrejoin_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:
- Double write lock on
workermgrdeadlocks (pthread_rwlock_t is non-reentrant) - Create-or-update branch hidden inside
makeWorker_— invisible at call site xce_replycalled while holding the write lock (unnecessary)
Architecture Pattern & Boundary Map
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_joinowns the explicitkey_findbranch decision (Req 1)init_workerowns first-time registration (Req 2); delegates creation locking tomakeWorkerrejoin_workerowns in-place attribute/endpoint update (Req 3); uses per-worker write lockapply_join_stateis a static shared helper for status and resource update (Req 4)- No outer
workermgrwrite lock inhandle_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 explicitkey_findcheck; callinit_workerorrejoin_workerbased on resultsrc/controller/worker.c— removeadd_worker; addapply_join_state(static),init_worker,rejoin_workersrc/controller/include/worker.h— replaceadd_workerdeclaration withinit_workerandrejoin_worker
System Flows
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_hostconfigfails fast before any lock (Req 4.4 — reject if NULL)- Hooks fire outside the workermgr lock, bracketing the create-or-update step
xce_replyandon_worker_joinexecute 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_hostconfigfirst; 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_workerorrejoin_worker - Fires AFTER_EVENT hook after the create/update completes, outside any lock
- Calls
xce_replyandon_worker_joinoutside 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)
/* Unchanged external signature */
static int handle_worker_join(cluster_t* c, endpoint_t* ep,
xce_request_t* request);
- Preconditions:
request->body.joinmsgnon-NULL;ep->datanon-NULL - Postconditions: on success,
priv->u.idxset;on_worker_joininvoked; BEFORE/AFTER hooks fired - Error return: negative errno; reply status set to error code;
on_worker_joinnot called
Implementation Notes
- Integration: The outer
lock(c->workermgr, RTTI_LOCK_WRITE)present in the current code is removed. Thevgetlookup 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.statusand callxce_replybefore returning. - Risks:
priv->u.idxmust be set beforeon_worker_joinand 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_UNAVAILfromw->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_configandthresholdsfrom hconfig - Loops over
hconfig->nresourcescallingmark_host_res(bool resources)
Contracts: Service [x]
Service Interface (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,hconfignon-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_twith 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)
/* 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;
hostnon-NULL - Postconditions: new worker registered in workermgr; status and resources initialized
- Returns: worker index (≥ 0) on success; negative errno on failure
- Error cases:
makeWorkerallocation failure → return-errnowithout callingon_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'skey_find - Acquires per-worker write lock
- Saves admin status bits:
admin_status = w->host.status & XCE_WORKER_ADMIN_MASK - Frees existing
hosttypeandhostmodelstrings; copies new ones fromattrs - Copies numeric attrs fields (
cpufactor,maxcpus,maxmem,maxswap,maxtmp,ndisks) - Restores
w->host.statusadmin 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)
/* 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.attrsupdated;w->epset toep; admin bits preserved; resources updated - Returns:
w->idx(≥ 0); negative errno onapply_join_statefailure - Invariant:
w->host.status & XCE_WORKER_ADMIN_MASKunchanged from pre-call value
#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:
extern int add_worker(void*, endpoint_t*, const char*, hostattrs_t*);
Add:
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/TRACEmacros used throughout — no changes to log verbosity PERROR("update_resval")inupdate_resvalspreserved for resource update failures
Testing Strategy
Unit Tests (src/controller/testsuite/)
- First-time join creates worker: call
init_workerwith a new hostname; verifykey_findsucceeds afterwards,XCE_WORKER_UNAVAILis clear, and resource values are set. - Rejoin updates attrs and ep: call
init_worker, then callrejoin_workerwith different attrs; verify updated attrs, updated ep, and original admin bits preserved. - Admin bits preserved on rejoin: set
XCE_WORKER_CLOSEDbefore rejoin; verify flag still set afterrejoin_worker. - NULL hostconfig rejects join: configure cluster with no matching host config; verify
handle_worker_joinpath returns error without modifying workermgr. - Non-ENOENT key_find error rejected: simulate
key_findreturning-EIO; verify join rejected andon_worker_joinnot called.
Integration Tests
- BEFORE/AFTER hook order: verify hooks fire in the correct order relative to workermgr state changes using existing
test_e2e_hooks.cpatterns. - on_worker_join called after successful join: verify
worker_push_configexecutes on both first-time and rejoin paths. - Existing testsuite passes without modification:
make testinsrc/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.