myxce/.kiro/specs/resval-id-decoupling/design.md

435 lines
16 KiB
Markdown

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