16 KiB
Design: resval-id-decoupling
Feature: resval-id-decoupling Created: 2026-05-12 Status: Design Generated
Architecture Pattern & Boundary Map
Pattern: Targeted replacement of an index-keyed cross-subsystem data carrier with a name-keyed one, following the precedent set by Data_data_resvals (Path C). No new architectural layers are introduced.
Affected boundary: Controller → Scheduler (initial config push) and Controller → Scheduler/API-server (per-worker data push). The wire protocol (controller.capnp) at these two field positions changes from List(ValueWithIdx) to List(NameValue).
Scope:
src/schema/controller.capnp ← 2 field type edits
src/include/xce/types.h ← 1 struct field type change
src/include/typemap.h ← 1 struct field type change
src/include/utils.h ← swap free function declaration
src/controller/include/cluster.h ← remove collect_resval_with_idx declaration
src/controller/dataex.c ← replace collect_resval_with_idx with collect_resval
src/controller/sched.c ← 3 call-site updates
src/controller/apiserver.c ← 2 call-site updates
src/controller/reply.c ← 1 free call update
src/controller/opget.c ← 1 free call update
src/lib/utils.c ← swap free function implementation
src/scheduler/src/scheduler.c ← sched_init_sharedInst update
src/scheduler/src/schedobj.c ← make_sched_worker update
Not in scope: src/scheduler/src/handler.c (Path C untouched), src/frontend/apiserver/ Go code (no direct access to resvals_to_sched in Go layer), capnp generated files (auto-regenerated by ninja -C build after schema edit).
Technology Stack & Alignment
| Aspect | Choice | Rationale |
|---|---|---|
| Language | C99 | Matches existing codebase convention |
| Wire protocol | Cap'n Proto — List(NameValue) replacing List(ValueWithIdx) |
NameValue/xce_name_val_t already defined in schema and types; codec already generated |
| Build | Meson + capnpc-c (existing) | Schema edit triggers automatic regeneration of controller.capnp.[ch] |
| Logging | Existing WARN/ERROR macros |
Consistent with codebase; unknown resource → WARN + skip |
| Iterator pattern | iter_make / iter_next / iter_destroy with read lock |
Matches collect_resources() pattern already in dataex.c |
| Backward compatibility | Not required | No public release; breaking wire change is acceptable |
Components & Interface Contracts
Component 1: controller.capnp — Wire type changes (Req 1, 2, 3)
Two field type replacements in src/schema/controller.capnp. Both field ordinal numbers are preserved (no renumbering); only the element type changes.
Before:
struct Worker { resvals @5: List(ValueWithIdx) ... }
struct Conf4Sched { resvals @6: List(ValueWithIdx) ... }
After:
struct Worker { resvals @5: List(NameValue) ... }
struct Conf4Sched { resvals @6: List(NameValue) ... }
After this edit, ninja -C build regenerates controller.capnp.[ch]. The generated
encode_Conf4Sched and decode_Conf4Sched functions will automatically call
encode_NameValue_list / decode_NameValue_list (already implemented in the generated
code) instead of their ValueWithIdx counterparts.
Requirement traceability: 1.1, 2.1, 3.1, 4.2
Component 2: Type changes in shared headers (Req 1, 2, 3, 4)
src/include/xce/types.h — xce_worker_t.resvals_to_sched
Before:
int nresvals_to_sched;
xce_val_with_idx_t** resvals_to_sched;
After:
int nresvals_to_sched;
xce_name_val_t** resvals_to_sched;
src/include/typemap.h — xce_conf4sched_t.resvals
Before:
int nresvals;
xce_val_with_idx_t** resvals;
After:
int nresvals;
xce_name_val_t** resvals;
Requirement traceability: 1.1, 2.1, 4.2
Component 3: dataex.c — New collector; remove old (Req 1, 2, 5)
Remove: collect_resval_with_idx_ and collect_resval_with_idx
Both the static inner function and its public wrapper are deleted. The declaration in
src/controller/include/cluster.h is also removed.
Add: collect_resval
Iterates resmgr (not the zarray directly) so resource names are always available.
For each named resource, looks up its value by r->idx in the provided zarray_t.
Signature:
int collect_resval(var resmgr, zarray_t* array, xlist_t* l)
Parameters:
resmgr — read-locked resource manager; provides name→idx mapping
array — zarray_t holding xce_val_t* values indexed by r->idx (lock held by caller)
l — output xlist_t; receives xce_name_val_t* entries (caller owns memory)
Behavior:
1. lock(resmgr, RTTI_LOCK_READ)
2. iter_make(&it, resmgr)
3. For each xce_resource_t* r from iter_next:
a. If r == NULL, continue
b. src_val = (xce_val_t*)zarray_get(array, r->idx) [array already caller-locked]
c. If src_val == NULL, continue [resource exists but has no value yet]
d. Allocate xce_name_val_t* nv = calloc(1, sizeof(*nv))
e. nv->name = strdup(r->name)
f. nv->val = calloc(1, sizeof(xce_val_t))
g. Copy value: if svalue, strdup; else memcpy
h. xlist_store(l, nv)
4. iter_destroy(&it)
5. unlock(resmgr)
6. Return l->count
Return value:
>= 0 number of name-val entries collected
-ENOMEM allocation failure (partial list may exist; caller responsible for cleanup)
Note: array lock must be acquired by the caller before calling this function,
consistent with how collect_resval_with_idx_ was called from collect_resval_with_idx
(which held array->lock). This keeps locking responsibility explicit.
Update: collect_shared_resvals
Before:
void collect_shared_resvals(cluster_t* c, collect_check_fn_t fn, xlist_t* l) {
collect_resval_with_idx(&(c->sharedinst), l);
}
After:
void collect_shared_resvals(cluster_t* c, collect_check_fn_t fn, xlist_t* l) {
mylock_lock(&(c->sharedinst.lock));
collect_resval(c->resmgr, &(c->sharedinst), l);
mylock_unlock(&(c->sharedinst.lock));
}
fn parameter is unused (was unused before); kept for signature compatibility with
other collect_* functions. The cluster_t* provides access to both resmgr and
sharedinst.
Update: collect_resvals_workers (inlined into collect_workers)
Before (in collect_resvals_workers):
collect_resval_with_idx(&(w->resvals), &lst);
After:
mylock_lock(&(w->resvals.lock));
collect_resval(resmgr, &(w->resvals), &lst);
mylock_unlock(&(w->resvals.lock));
collect_resvals_workers requires access to resmgr. Pass it as a new parameter, or
access it from the containing cluster_t* context (the function is only called from
collect_workers which receives cluster_t* c). Preferred: pass c->resmgr from
collect_workers.
Updated collect_resvals_workers signature:
static void collect_resvals_workers(var resmgr, xlist_t* l)
Requirement traceability: 1.1, 2.1, 5.1, 5.2
Component 4: src/lib/utils.c and src/include/utils.h — Free function swap (Req 5)
Remove: free_resval_with_idx_list
The existing implementation and declaration are deleted. No remaining callers after the controller-side changes in Component 5.
Add: free_name_val_list
Mirrors the removed function's structure; frees each xce_name_val_t* entry.
Signature:
void free_name_val_list(xce_name_val_t*** vals, int count)
Behavior:
1. resvals = *vals
2. If resvals == NULL or count <= 0, return
3. For i in [0, count):
a. nv = resvals[i]; if NULL, continue
b. FREEUP(nv->name)
c. If nv->val != NULL:
if nv->val->which == Value_u_svalue: FREEUP(nv->val->u.svalue)
FREEUP(nv->val)
d. FREEUP(nv)
4. FREEUP(*vals)
Header declaration (utils.h):
extern void free_name_val_list(xce_name_val_t***, int);
Requirement traceability: 5.1
Component 5: Controller call sites — sched.c and apiserver.c (Req 1, 2, 3)
All call sites that previously called collect_resval_with_idx and
free_resval_with_idx_list are updated to use the new functions and types.
sched.c — xcesched_push_configs (shared resvals, Req 1)
Before:
collect_shared_resvals(c, NULL, &xreslist);
configdata.data.conf4sched->nresvals = xreslist.count;
configdata.data.conf4sched->resvals = (xce_val_with_idx_t**)xreslist.data;
...
free_resval_with_idx_list(&(configdata.data.conf4sched->resvals),
configdata.data.conf4sched->nresvals);
After:
collect_shared_resvals(c, NULL, &xreslist);
configdata.data.conf4sched->nresvals = xreslist.count;
configdata.data.conf4sched->resvals = (xce_name_val_t**)xreslist.data;
...
free_name_val_list(&(configdata.data.conf4sched->resvals),
configdata.data.conf4sched->nresvals);
sched.c — xcesched_push_workerlist (worker resvals, Req 2)
Before:
collect_resval_with_idx(&(w->host.resvals), &resvalist);
w->host.nresvals_to_sched = resvalist.count;
w->host.resvals_to_sched = (xce_val_with_idx_t**)resvalist.data;
...
free_resval_with_idx_list(&(w->host.resvals_to_sched), w->host.nresvals_to_sched);
After:
mylock_lock(&(w->host.resvals.lock));
collect_resval(c->resmgr, &(w->host.resvals), &resvalist);
mylock_unlock(&(w->host.resvals.lock));
w->host.nresvals_to_sched = resvalist.count;
w->host.resvals_to_sched = (xce_name_val_t**)resvalist.data;
...
free_name_val_list(&(w->host.resvals_to_sched), w->host.nresvals_to_sched);
apiserver.c — apiserver_worker_change (Req 3)
Same pattern as the sched.c worker case above. Replace collect and free calls;
change cast to (xce_name_val_t**).
sched.c:226, reply.c:66, opget.c:128 — cleanup-only sites
Replace free_resval_with_idx_list with free_name_val_list at each cleanup site
where w->resvals_to_sched is freed without being re-populated.
Requirement traceability: 1.1, 2.1, 3.1, 5.1
Component 6: Scheduler — sched_init_sharedInst (Req 1, 6)
Receives xce_conf4sched_t.resvals which is now xce_name_val_t**.
For each entry, look up the resource by name in sched->resmgr to get the local idx,
then store into sched->sharedInst.
Critical ordering constraint: sched_init_resources() must be called before
sched_init_sharedInst(). This is already guaranteed by sched_init_config()
(scheduler.c:603-609). This call order must not change.
Before:
static int sched_init_sharedInst(xce_scheduler_t* sched) {
for (i = 0; i < sched->config->nresvals; i++) {
int idx = sched->config->resvals[i]->idx;
xce_val_t* val = NULL;
MOVE_PTR(val, sched->config->resvals[i]->val);
zarray_put(&(sched->sharedInst), idx, val);
}
return 0;
}
After:
static int sched_init_sharedInst(xce_scheduler_t* sched) {
for (i = 0; i < sched->config->nresvals; i++) {
xce_name_val_t* nv = sched->config->resvals[i];
xce_resource_t* r = NULL;
xce_val_t* val = NULL;
if (nv == NULL || nv->name == NULL) continue;
if (key_find(sched->resmgr, (void*)nv->name, (void**)&r) < 0 || r == NULL) {
WARN("%s unknown resource '%s' in resvals, skipping",
__func__, nv->name);
continue;
}
MOVE_PTR(val, nv->val);
zarray_put(&(sched->sharedInst), r->idx, val);
}
return 0;
}
Requirement traceability: 1.2, 1.3, 6.1, 6.2, 6.3
Component 7: Scheduler — make_sched_worker in schedobj.c (Req 2, 6)
Receives xce_worker_t.resvals_to_sched which is now xce_name_val_t**.
For each entry, look up by name in sched->resmgr to get local r->idx.
Before:
for (i = 0; i < w->nresvals_to_sched; i++) {
int idx = w->resvals_to_sched[i]->idx;
xce_val_t* val = NULL;
MOVE_PTR(val, w->resvals_to_sched[i]->val);
zarray_put(&(worker->resvals), idx, val);
}
After:
for (i = 0; i < w->nresvals_to_sched; i++) {
xce_name_val_t* nv = w->resvals_to_sched[i];
xce_resource_t* r = NULL;
xce_val_t* val = NULL;
if (nv == NULL || nv->name == NULL) continue;
if (key_find(sched->resmgr, (void*)nv->name, (void**)&r) < 0 || r == NULL) {
WARN("%s unknown resource '%s' in worker resvals, skipping",
__func__, nv->name);
continue;
}
MOVE_PTR(val, nv->val);
zarray_put(&(worker->resvals), r->idx, val);
}
make_sched_worker already receives xce_scheduler_t* sched as its first argument
(void* arg), cast at top of function — sched->resmgr is available.
Requirement traceability: 2.2, 2.3, 2.4, 6.3
Data Flow
Before (fragile index-keyed path)
Controller Scheduler
---------- ---------
c->sharedinst[r->idx] = val
│
collect_resval_with_idx()
emit {idx=i, val} ──────────────► sched_init_sharedInst()
zarray_put(&sharedInst, idx, val)
↑
ASSUMES controller idx == scheduler idx
(NO ENFORCEMENT — silent corruption if false)
After (name-keyed path, matches Path C)
Controller Scheduler
---------- ---------
c->sharedinst[r->idx] = val
│
collect_resval(resmgr, array)
iter resmgr → r->name, r->idx
val = array->data[r->idx]
emit {name=r->name, val} ────────► sched_init_sharedInst()
key_find(sched->resmgr, name) → r
zarray_put(&sharedInst, r->idx, val)
↑
Uses scheduler's OWN r->idx
No cross-subsystem index assumption
Requirements Traceability
| Requirement | Component(s) |
|---|---|
| 1.1 — Name-keyed shared resvals in config push | Comp 1, 2, 3, 5 |
| 1.2 — Scheduler resolves by name in resmgr | Comp 6 |
| 1.3 — Warn and skip unknown resource names | Comp 6 |
| 1.4 — Config path produces same sharedInst state as Path C | Comp 6 (same key_find → r->idx logic as Path C) |
| 2.1 — Name-keyed worker resvals | Comp 1, 2, 3, 5 |
| 2.2 — Scheduler resolves worker resvals by name | Comp 7 |
| 2.3 — Warn and skip unknown worker resource names | Comp 7 |
| 2.4 — Dynamic resource catalog changes handled correctly | Comp 7 (no cached idx; lookup on each receive) |
| 3.1 — Name-keyed worker resvals to API server | Comp 1, 2, 5 (apiserver.c) |
| 3.2 — API server match by name without numeric index | Comp 2 (type change), Comp 5 |
4.1 — Path C (Data_data_resvals) unchanged |
No changes to handler.c or scheduler.c:770-788 |
| 4.2 — All struct consumers updated consistently | Comp 1, 2, 5, 6, 7 |
| 5.1 — No cross-subsystem slot-indexed resval path remains | Comp 3 (remove old function), Comp 4, 5, 6, 7 |
5.2 — collect_resval_with_idx removed |
Comp 3 |
| 6.1 — Correct values when idx assignments differ | Comp 6, 7 |
| 6.2 — Identical behavior when idx assignments match | Comp 6, 7 |
| 6.3 — Scheduling lookups use scheduler's own indices | Comp 6, 7 |
Open Questions
None — all design decisions resolved in gap analysis and user feedback.