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

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.