myxce/admin-plane-gaps.md
lei.chen d028c8c315 admin-plane breakpoint 3: done-job live GC
Terminal jobs (DONE/EXIT/PERR) were only filtered from checkpoints and
WAL replay (job_should_retain, finished_retain_s=3600) — the live jobmgr
never dropped them, so joblist and the scheduler's core->jobs view grew
without bound across a controller's uptime (N4).

Three pieces:

1. kvmgr delete path (src/lib/kvmgr.c): xkvmgr_type had .del_key=NULL
   and .vclear=NULL, so key_del()/vclear() on a kvmgr only returned
   -EINVAL. New xkvmgr_del_key (key_find to locate the slot idx ->
   key_del — kvtab's val_killer is NULL so the idx scalar is untouched ->
   vclear(data, idx)) and xkvmgr_vclear forwarder, wired into the
   typeinfo. A missing key propagates -ENOENT (same contract as
   xkvmgr_find).

2. Controller periodic sweep (job.c): job_live_gc_sweep — primary-only
   (standbys rebuild their live table from WAL replay; deleting there
   diverges), collects !job_should_retain jobs under the jobmgr READ
   lock with reference() (check_workers_timeout collect pattern), then
   per-job WRITE-locked key_del. TWO dereferences per purge: the
   collection reference and the table's own makeJob reference — the
   jobmgr's datakiller (job_obj_killer) is a deliberate no-op, so
   key_del transfers the table's ownership duty to the caller; one
   dereference would leak at refcount 1 forever (the very accumulation
   this sweep drains). Registered by cluster_start_job_live_gc with
   ha_start_primary_heartbeat's idempotent static-arm pattern +
   delaymgr.running guard (60s, runcount=-1).

3. Registration on both promotion tails (ha.c): raft road after
   ha_start_primary_heartbeat and the non-raft ha_promote_to_primary.
   NOT cluster_start — it is orphaned on the HA myapp_run path.

Safety: apply_committed_entry's jobstate group and WAL_JOB_UPDATE
already treat a key_find miss as a benign skip (bug-1970, which
explicitly anticipates post-purge facts); persist_verify_apply_correctness
counts only within the boot replay window. The TRANSIENT TTL now takes
live effect too (previously checkpoint-exclusion only).

UTs: kvmgr del INT+STRING (miss propagation, sibling intact, freed-slot
reuse, killer invocation), controller test_job_live_gc.c (expired DONE
purged + refcount 0, fresh/RUN retained, idempotent re-run, EXIT/PERR,
TRANSIENT TTL + transient_since==0 guard, standby no-op, NULL-safe,
arming guard). xcelib 761/761, controller 1354/1354.

E2E (t1): promote arms the sweep; the pre-restart DONE array jobs were
purged across five sweeps (33 jobs) as their 3600s windows elapsed;
submit→RUN→END flows and all daemons healthy throughout.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-23 15:47:10 +00:00

36 KiB
Raw Permalink Blame History

XCE 管理面能力评估(查漏补缺 · 第一步)

生成时间:2026-10-23。评估方法:源码逐模块审计(CLI/REST/controller/scheduler/worker/configs 四路并行)+ t1/t2/t3 真集群 CLI 探针验证。 范围:按用户 10 节检查清单(1.1 管理cluster … 1.10 shared resource)逐项对照。 约定:✓=具备,△=部分具备/有缺口,✗=缺失。调度/性能类问题不在此重复,见 gaps.md 对应条目。 本文档只做记录,未做任何代码改动。

2026-10-23 更新:断点 ①(配置可达性)已实施,见文末《断点修复记录》。


1. 管理 cluster

1.1 使用 cluster

子项 判定 现状与证据
查询 cluster 信息 △ xce id → GET /xce/v1/clusters(routers.go:118,controllers/clusters.go:18)。但实测输出是空壳:My cluster name: <>、The cluster status: <>。ClusterInfo.cluster_admins 注释明言"never populated by config push"(xcecli backend/handler.go:225)。集群名实际存在(xce.yaml cluster_name: eda1),没接到这个端点。
配置管理员 △ RBAC 完整:roles add/delete/assignpermission、users assignrole/unassignrole、permissions list(23 权限、6 角色,rbac.yaml + includes 继承)。缺口:① 用户无 CRUD——CLI/REST 都没有 add/delete user、改密码端点,加用户只能改 auth.yaml + 重启 apiserver;② 角色/用户角色是内存 DataStore,推断不随重启存活(API key 已持久化 state/apikeys.json;.kiro/specs/rbac-persistence 目录已规划未实施);③ 引导方式是 auth.yaml 内置 dev 默认 admin/admin(有告警日志)。
控制 daemons ✗ 见 1.2。
使用命令配置 cluster △ 有整包配置通道:xceadmin config build/push/dump(YAML→cluster.bin→controller,SHA-256 校验、WAL majority barrier、--dry-run 真预检 bug-1506)。没有对象级运行时命令(无 queue create/modify、resource add 之类),改配置=改 YAML→build→push。热生效矩阵见 1.5 与下表。

配置热生效矩阵(关键结论):

变更域 热生效? 证据
新增队列 ✓ reconcile_config_queues 建缺失队列(queue.c:51)
修改既有队列(priority/limits/window) ✗→✓ 队列 reconciliation 曾明确"existing runtime queue: left untouched"(queue.c:51);断点 ① 已修:queue_refresh_def 原地刷新 CONFIG-owned 字段(见文末修复记录)
队列/主机 open/close(运行态) ✓ queue_ctrl/worker_ctrl + WAL 回放 restore_queue_state
controller 心跳参数 ✓ cluster.c:2238 活读 heartbeat_interval×count
worker 参数 △→部分 仅 worker_resync_window_ms 且仅 join 时(controller_connector.c:1852);confvars(heartbeat_interval/inactive_heartbeat_count)断点 ① 已修:激活时刷新 configmgr 条目并随 push 下发
scheduler 的 queues/limits/groups/resources ✗→✓ xcesched_push_configs 曾唯一调用点是 on_scheduler_join(sched.c:139/774);断点 ① 已修:config_activate_committed 激活缝 re-push 到已连接 scheduler(见文末修复记录)
apiserver △ 仅 join 时全量推(apiserver.c:648)

1.2 管理 daemons

✗ 整体缺失(已知且已规划)。

  • 无任何 CLI/REST daemon 生命周期命令;deployment.md:287 原话:"No process supervision is shipped. No systemd unit, no Dockerfile, no docker-compose, no supervisord config exists anywhere in this repo for any of the four daemons."
  • 存在但未暴露:OPCode_ctrlstatus(HA 身份/健康快照:primary/responder/peer 状态 ok/catching-up/dead,capnp:556-574)只在 C client 库(client.c:420 xcecli_ctrlstatus),Go apiserver/xcecli 均未接。
  • xceadmin scheduler record start/stop/status 是唯一"控制运行中组件"的面(事件录制,非 daemon 生命周期)。
  • .kiro/specs/xce-node-manager 有完整 requirements+design(每节点监督 daemon、按角色启停组件、UDS 心跳、controller primary/standby 驱动 scheduler 启停)——规划与缺口精确对齐,未实施。

缺口清单:① daemon 状态/健康查询命令(哪怕只读);② HA 状态查询与 standby 管理(现在只能看 daemon 日志);③ 进程监督层(xce-node-manager spec 落地)。


1.3 管理 hosts

子项 判定 现状与证据
hosts 状态 ✓ 位标志:UNAVAIL/LOCKEDU/LOCKEDW/BUSY/CLOSED/HIDDEN/CLOSED_BY_ADMIN/RECONCILING(types.h:170-186);显示优先级 UNAVAIL>CLOSED_BY_ADMIN>…>OK。实测 hosts close t2 → CLOSED_BY_ADMIN 正确显示。
查询 hosts 信息 ✓ hosts list(-a/-l/-o/-w/--json/-t)+ hosts select -R(resreq 表达式选主机 dry-run)。小缺口:-o 仅 8 字段(hosts.go:121-130),-l 里的 CPUF/负载阈值/dispatch window 不进 -o;-a/--host_info 与 -l/--verbose 绑同一变量(list.go:189,193)。
hosts 控制 ✓ hosts open/close [--all] [-c comment](worker.c:985-1046,WAL 持久化)。实测 close→作业不受影响继续跑、控制命令仍可达(bug-1941 语义)。死枚举:HOST_TAG/HOST_UNTAG 无 C handler(types.h:629-635,默认 -1);无重启主机命令。
增加 host 到集群 △ 路径=hosts.yaml 加条目→config build/push→worker join(join 必须 hostconfig 匹配否则 -EINVAL,bug-1494)。join 后 worker 以 upsert 增量到达 scheduler(XCE_SCHED_OP_UPDATE_WORKERS→core_upsert_worker)→ 新主机可 live 可调度。缺口:validate_host 是 stub(join.c:48);新主机上的 limit/group 类配置受"push 不达 live scheduler"(gaps #12)影响。无 host add 命令,纯文件流。
从集群删除 host ✗ 无任何删除 API/命令。XCE_WORKER_HIDDEN("Admin explicitly removed")定义了但全库无 setter;worker 对象不死(resync.c:253:no code path ever key_del()s;worker_obj_killer 是空函数 cluster.c:1598)。从 hosts.yaml 删掉主机+push:运行中 worker 对象原样保留;其上作业被 re-home 到合成 lost_and_found 主机(config_repl.c:1173-1217,无 worker 端点=永远 PEND)。GC 管道(HIDDEN/UNAVAIL TTL → checkpoint 丢弃)存在但无入口。.kiro/specs/elastic-hosts 已规划未实施。
host groups △ 定义 ✓(hosts.yaml hostgroups,members/exclude_members)、装载 ✓(core->hgmgr,snapshot 也载,bug-1943)、消费仅 limit 域(xcelimit.c:292-341)。不能用于 select[](xce_select.peg 只解析资源名,无组语法)——LSF 式 select[hgroup] 放置能力没有。RBAC 有 hostgroup:view/control 权限但零管理 API;且 hostgroup 域的 host:control 未强制(hosts.go:495 TODO,integration_test.go:556-593 记录为越权 bypass)→ 安全缺口。
host 级作业异常处理 △ 自动侧齐全:worker 死→RUN 变 UNKWN|TRANSIENT(bug-1509)由 rejoin/resync 解决;确认丢失→EXIT(-1 "worker lost");派发失败→rePEND(bug-1494 系);rejoin→RECONCILING 圆(bug-1963)。缺口:① 90 秒检测窗(gaps #9);② 无管理员侧工具——没有"把主机 X 上的作业重排/requeue"命令、没有主机故障恢复策略配置(.kiro/specs/host-failure-job-recovery 已规划);③ UNAVAIL 主机 resInUse 冻结是有意设计但无文档。

1.4 管理作业

子项 判定 现状与证据
作业状态 pend/run/done/exit ✓ 全状态机:PEND/RUN/DONE/EXIT/PSUSP/USUSP/SSUSP/PDONE/PERR/WAIT/TRANSIENT/UNKWN/PROV/ERR(types.h:382-405);迁移强制单调(apply_job_update_transition job.c:3519);终态集 EXIT|DONE|PERR;PERR 合并 post-exec 失败(job.c:3376)。
查询作业信息 ✓ jobs list 过滤器丰富(-a/-u/-q/-p/-r/-d/-s/-J/-Jd/-P/--sla/-A),31 个 -o 字段,--json。缺口:exit code 不上 job API——C 侧已捕获(JobExitInfo.exitcode,bug-1867)、SSE job.finished 事件带 exit_code(broker.go:41),但 REST 行的 ExitStatus 字段无 writer(convert_pure.go:646 不映射,backend/ 零引用),CLI 恒 null;只能从 DONE/EXIT 位或 SSE 推断。已记 memory(prepost-exec 测试)。
作业控制 kill/signal ✓ kill(SIGINT→SIGTERM→SIGKILL 优雅链)/stop(SIGTSTP→SIGSTOP)/resume(SIGCONT)/checkpoint/restart(按原 spec 重提新作业,要求原提交者)/peek(实时输出流);`kill -s <name
job groups ✗ 装饰性字段:JobGroup 在 Go model/控制过滤器里,但不在 wire JobSubmitData(capnp:583-595),modelJobToSubmit 不拷贝,无生命周期管理无消费者。作业依赖(DependCond)同样:纯字符串字段,调度器/controller 零解析零求值。数组作业 ✓(.kiro/specs/job-dependency、gang-groups 已规划未实施)。
作业异常处理 △ PEND reason 全链路存在:9 个命名码(preason.h)scheduler→controller→JSONB→CLI。缺口见 gaps #6(alloc_rc<0 无 reason)、#7(ALTER 子作业 reason 只取一个)、#15(策略性拒绝显示 '-')。实测再证:主机 CLOSED_BY_ADMIN + select(hname==t2) 作业 PEND,pending_reason 显示 '-'。
done 作业清理 △ 配置存在:finished_retain_s=3600(capnp PersistConfig.finishedRetainS)。执行点唯一:collect_job_frames 只把过期作业排除在新 snapshot外(persist.c:2558/8838)。live 表无 GC:设计文档里的 kvtab_gc 未实现(kvmgr.c:279/286 del_key/vclear=NULL;persist.c:7085 恢复期 retention pass 是 no-op);无 purge CLI/API(job 无 DELETE 路由);无归档库。实测证实:集群里 done 作业已积压一天+(Oct 22 03:10 的作业仍在 jobs list -d),长运行 controller 内存里只增不减,跨重启才消失。无回收站/历史查询。.kiro/specs/controller-job-cleanup 已规划未实施。

1.5 管理 queue

子项 判定 现状与证据
queue 状态 ✓ 位标志 OPEN(0)/CLOSED/INACT_WINDOW/INACT_ADMIN/NOPERM/DISC/FAIRSHARE_DEFAULT/HIDDEN(types.h:413-422)。语义正确且对齐 LSF:closed 只挡新提交、PEND 作业照常派发(sched_queue_is_ready 只查 INACT 位,core.c:685-715);inactive 挡派发(INACT_ADMIN→PREASON_QUEUE_DRAINING,INACT_WINDOW→TIME_WINDOW,scheduler.c:499-520);closed 提交被拒 -E_XCE_QUEUE_CLOSED(job.c:2616-2721)。实测两组语义均符合。
查询 queue 信息 △ queues list(-u/-q/-o/-w/-l/--json)。新发现显示缺陷:closed 期间 queues list 仍显示 open:active(实测两次:close night 后提交被拒"queue is closed",同时 list 显示 open:active;重新 open 后提交成功)。原因指向 QUEUE_STAT_OPEN==0x00 是"无位"默认渲染、CLOSED 位没有对应的显示分支,或 queue 控制变更未推送 apiserver 内存库——需定位。另外 sched_delay/accept_interval 是死配置(gaps #4,本次独立复核确认:schema 就没有这两个字段,capnp 有位无源)。
queue 控制 ✓ queues open/close/active/deactive [--all] [-c](queue.c:128-157,WAL 回放 restore_queue_state),四个动作全通 REST。实测 close/open/active 均即时生效。注意动词拼写是 active/deactive(非 activate)。

1.6 host-based resource

子项 判定 现状与证据
概念/定义/配置 ✓ resources.yaml 全谱系:内置负载指数、host 属性(type/model/cpuf/maxmem/…)、static(shared/hostbased)、dynamic、boolean/string、resourcemap;JSON Schema 校验;join 时按 hostconfig 播种(mark_host_res/add_hostattrs_resvals/set_host_slots,worker.c:232-341)。
resource 种类 △ valtype STRING/NUMERIC/BOOLEAN、ordtype INCR/DECR/NA、9 个 flags 齐备(types.h:81-89)。关键接线断裂:YAML→capnp 映射只映射 CONSUMABLE 和 RELEASE 两个 flag(mapping.py:194-200)——DYNAMIC/SHARED/GLOBAL/EXTERNAL/BUILTIN 从 YAML 永远置不上;resourcemap 在 build 时被静默丢弃(build.py:38-65 _UNMAPPED_BUT_VALID_ITEM_KEYS,capnp:192-206 目标字段被注释)。ordtype 存了但无消费者(排序纯 first-fit,关联 t1 亲和)。
load index ✓ 11 个内置指数全部真采集:r15s/r1m/r15m/ut/pg/ls/it/swp/mem/tmp/io,~15s 周期(resource_sync.c:60-106),实现实在(node_information.c:/proc/loadavg、utmp、MemAvailable、statvfs、diskstats…),推送链完整(resvals→controller opupd.c→RESVALS_DELTA→scheduler core_events.c:1228-1263)。实测 distribution list 有 io/it/ls/mem 值。
static resource △ 声明与播种 ✓(hostbased 静态值随 join 落地);但 share 型静态资源的 resourcemap(实例/份额定义)在 build 时被丢(见上),静态 share 资源实际不工作。

1.7 外部 resource(external resource)

✗ 缺失。

  • XCE_RESF_EXTERNAL(0x10) 定义了但全库无 setter。
  • 无 elim 式外挂采集器:worker 没有子进程/脚本采集路径;resource_sync_register_load_index 是进程内 C API(给 xcecd 内部代码用),无任何调用方。
  • dynres1/dynres2 声明了 interval 但无采集者——定义在表里、值永远不会自动出现,只能管理员手工 update_resval 推。
  • scaler.yaml(aws-asg/k8s-pool)映射为 hproviders 推给 scheduler,但 scheduler 零消费者(而且它是自动扩缩 provider,不是资源采集器)。
  • .kiro/specs/custom-resource-reporting 已规划未实施。

1.8 user groups

子项 判定 现状与证据
定义 ✓ users.yaml usergroups(ug1/ug2)→ config push → apiserver store(clib.go:523)+ scheduler core->ugmgr(core.c:675);capnp GroupConfig{name,members,exmembers}。
查询 ✗ 无任何查询通道:REST 无 usergroups 端点、CLI 无命令(RBAC 有 usergroup:view 权限但没地方用)。管理员配了 ug1/ug2 后,除了翻 users.yaml/cluster.bin dump,无法从系统里看到组与成员。
使用 △ 两个消费者:① self_ug 作业可见域(同组成员互见作业,job_scope.go:40,bug-1934);② limit 域——limits.yaml 按 user group 定 per-user/per-user+queue 上限(limit_config.c:139 build_limit(ugmgr,…))。不用于 fairshare(无 fairshare,gaps #3)。另发现:queues.yaml 的 users/hosts 成员表(队列级成员限定)在调度器无消费者——配置 carried but not enforced,属新发现的死配置。

1.9 user mapping(用户映射)

✗ 缺失。

  • 无 submit-as/runs-as 映射实体(无 UserMap 配置、无 exec_user 提交字段)。
  • 执行用户决策链:REST 无 exec_user → apiserver resolveExecUser 取 submit user(convert_pure.go:360-370,bug-1780)→ JobStartInfo.execuser(capnp:317)→ worker 按 /etc/passwd 存在性执行,不存在则静默 fallback 到 nobody(controller_connector.c:1061-1083)。
  • model 的 exec_uid 字段恒 0(job_information.c:27)不填。
  • 关联风险:user mapping 缺失 + nobody fallback 静默,是 gaps #17(执行上下文无显式契约)在账号维度的表现。
  • 另注意:RBAC 用户(auth.yaml,apiserver 域)与集群 usergroups(users.yaml,config 域)是两个独立用户平面,互不引用——管理员视角没有统一的"用户"实体。

1.10 shared resource

△ 机制存在,端到端不通。

  • 机制 ✓:SHARED flag 资源进集群级共享池 c->sharedinst(worker.c:65-143 按 flag 路由);调度两段式——先集群级 precheck(sched_alloc_prepare_shared_,alloc_runner.c:73-132,不满足→无主机 PEND 行+PREASON_NO_RESOURCE/LIMIT_GLOBAL)再逐主机检查;snapshot 以 host==NULL 池形态携带恢复(bug-1923)。limit vres(GLOBAL limit)是当前唯一活的生产者。
  • 断点 ✗:YAML 映射不置 SHARED flag(mapping.py:194-200)+ resourcemap(LSF 式静态份额/实例定义,如 dynres2 host1:50/host2:50)build 时丢弃 → 静态共享资源(license 类场景)声明得、用不上;实测 distribution list 只有 hostbased 行,无共享实例行。
  • .kiro/specs/elastic-shared-resources、resource-instances 已规划未实施。

二、本次新发现缺陷清单(不在 gaps.md 的)

# 缺陷 证据
N1 xce id 空壳:集群名/状态/管理员全空 实测输出 <>;handler.go:225 注释
N2 queues list 显示与准入不一致:closed 队列显示 open:active 实测(close night 后提交被拒、list 仍 open:active)
N3 exit code 不上 job API(REST ExitStatus 死字段) convert_pure.go:646 无映射;SSE 有 exit_code
N4 done 作业 live 无 GC、无 purge API、无归档:长运行 controller 终态作业内存只增不减 persist.c:2558 唯一执行点;kvmgr.c:279 del_key=NULL;实测积压一天+
N5 jobs peek 对已结束作业 501 实测 GET /jobs/:id/output status 501
N6 jobs submit --configfile 不解析配置行,拼进命令行当作业命令执行 实测:job.xce 两行变成 command "queue=short job_name=cfgtest2"
N7 用户无 CRUD:无 add/delete user、无改密码端点,加人=改 auth.yaml+重启 routers.go 全量;users 命令族仅角色指派
N8 hostgroup 域 host:control RBAC 未强制(越权 bypass) hosts.go:495 TODO;integration_test.go:556-593
N9 user groups / host groups 无查询 API/CLI(usergroup:view 权限无处可用) routers.go 无端点;CLI 无命令
N10 queue users/hosts 成员限定配置无消费者 队列 GroupConfig 载入但调度器无检查
N11 HOST_TAG/HOST_UNTAG 枚举无 C handler(死枚举) types.h:629-635;worker.c 默认 -1
N12 ordtype(INCR/DECR 排序意向)无消费者:放置纯 first-fit 关联 t1 亲和(memory xce-firstfit-t1-affinity)
N13 CLI 小毛病:id help 写着 xce clusters;hosts -a/-l 同变量;jobs list -W 死标志 cmd/clusters/list.go:26;hosts/list.go:189,193;jobs/list.go:365
N14 一次观察:closed 队列提交 CLI 挂死 >2min(重试快速失败,未复现,需确认) 本轮探针 02:43 现场
N15 worker 执行用户不存在时静默 fallback nobody controller_connector.c:1061-1083(与 #17 关联)

三、与既有记录的关系

  • gaps.md 一~七节(调度/架构/构建)全部继续有效,本文不重复;交叉引用已标注(#3 fairshare、#4 sched_delay、#6/#7/#15 PEND reason、#9 心跳窗、#12 配置传播、#17 执行上下文)。
  • .kiro/specs 的空目录(elastic-hosts、controller-job-cleanup、job-dependency、gang-groups、resource-instances、custom-resource-reporting、host-failure-job-recovery、elastic-shared-resources、rbac-persistence、project-* 等)正是本评估所列缺口的"已规划未实施"标记;xce-node-manager 有完整 spec。规划面与管理面缺口高度吻合,说明缺口识别一致,缺的是实施。

四、若按用户价值排优先级(供第二步讨论)

  1. 配置可达性:让 config push 真正到达 live scheduler(gaps #12 的一半收益)+ 既有队列编辑热生效——"配了不生效"是管理面最大的信用问题。→ 已作为断点 ① 修复,见下。
  2. 可见性补齐(低风险高价值):queues list 状态显示修复(N2)、exit code 上 API(N3)、cluster info 填实(N1)、user/host group 查询端点(N9)、daemon/HA 状态只读命令(ctrlstatus 已有协议只差暴露)。
  3. 生命周期补齐:done 作业 GC+purge 命令(N4)、host 删除流程(elastic-hosts spec 落地)、用户 CRUD(N7)。
  4. 管理动作补齐:requeue/mig 控制动作(枚举已在、REST 400 挡着)、host 故障作业恢复策略(host-failure-job-recovery spec)。
  5. 资源接线:shared/external resource 的 YAML→capnp 断点(mapping + resourcemap 注释字段)、dynres 采集钩子(custom-resource-reporting spec)。

五、断点修复记录(2026-10-23)

断点 ①:配置可达性(已实施;单测全绿 + t1/t2/t3 真集群 E2E 验证通过)

三处修复(admin-plane-gaps 1.1):

  1. 既有队列原地刷新(src/controller/queue.c):新增 queue_refresh_def()——reconcile_config_queues() 遇到已存在队列时不再跳过,而是在 RTTI_LOCK_WRITE 下原地刷新 CONFIG-owned 字段:resreq/pre_exec/post_exec(interned xstring 换引用)、users/hosts/thresholds/runwindow/dispatchwindow(free_* 旧值 + MOVE_PTR 接管新定义的 capnp 载荷,与 qobj.c queue_init 同一所有权约定)、attrs/status/priority/accept_interval/sched_delay(标量直拷——config 是 status 的 source of truth,WAL 回放的 queue_ctrl 事实仍按回放序生效)。刷新成功的队列走与新建队列相同的 persist_notify_queue_updated_no_wal NO-WAL 通知(t_apply_seam_notifying 守卫,不产生 WAL_QUEUE_CTRL 重复条目)。约定变更:返回值仍只计新建数,刷新数走 TRACE。

  2. confvars 激活刷新(src/controller/cluster.c):cluster_build_config() 改为幂等——boot 建条目(add_config),激活重跑时经新增 config_refresh() key_find 原地换 interned value 串(makeConfig 拒 EEXIST、kvmgr 无 del_key,只能原地换)。collect_confvars 导出的 heartbeat_interval/inactive_heartbeat_count 不再冻结在 boot 值。

  3. 激活缝 re-push(src/controller/config_repl.c 两处:hash-verified 激活路径 + -ENOENT fallback 路径):config_activate_committed() 激活成功后依次 cluster_build_config(confvars 刷新)→ mylock_lock(&c->sched.lock) → xcesched_push_configs(c)(sched.c 去 static,cluster.h 出声明)。锁序安全:该缝已建立 apply_pump_lock → sched.lock 序(WAL_QUEUE_CTRL case 的 hook 触发 sched_queue_update 同序),无反向边(持 sched.lock 者从不取 apply_pump_lock;push 只入 endpoint outq)。推送失败仅 WARN——scheduler 保有旧配置直到下次 join/push。

单测(全绿,controller 套件 1345 tests):

  • test_config_queue_reconcile.c:新增 test_reconcile_refreshes_existing_queue_in_place(同对象指针、status/priority/resreq/pre_exec 原地生效、notify 恰一次);原 bug-1469 测试更新计数语义(建+刷各一次 notify)。
  • test_config_repl.c:新增捕获 fake xcesched_push_configs(controller_unit_test 不链 sched.c)+ 两个测试:test_config_activation_repushes_configs_to_connected_scheduler(sched.ep 置零值 fd=-1 endpoint 时激活恰推一次、二次激活再推;marker commit 自身经 apply pump 触发的激活在 ep=NULL 时不推)与 test_config_activation_deferred_does_not_push_configs(-EAGAIN 未激活不推)。

已知未修(记录):xcesched_push_configs 的 pre-existing 引用泄漏——clist/rlist/xreslist 只锁不放(unlock_collect_queues 仅处理 qlist),每次 push 引用计数累积。与本次改动无关,留待单独清理。

真集群 E2E 验证(2026-10-23,t1/t2/t3,新 controller 已部署):

  • 部署:新 controller 进 AppImage store closure(/nix/store/h7i88.../bin/xce_controller,md5 6121898),补 subprojects/tinylog → /opt/xce-denix/lib 软链(RUNPATH 需要且 libtlog 非 ASAN,md5 cab46c49)。
  • 激活链路:xceadmin config push(单节点 ackedCount==0 是已知误报,实际以 Config committed on majority 为准)→ gen=8→9 → config activated from committed marker → queue xce/normal/night/short updated by configuration activation(4 条全刷)→ re-pushed configs to the connected scheduler → scheduler 侧 sched_init_resources 在轮界触发(sched_apply_pending_config → sched_init_config 原地应用,bug-1886 路径)。第二次 push gen=9→10 复证。
  • 附带修复(发现于 E2E):apiserver Go 侧 handleNotifyData(backend/clib.go)缺 NotifyData_data_schedUpdateQueue case——controller 的 per-queue 通知被静默丢弃,store.Queues 冻结在 join 快照,xce queues list 看不到热应用的优先级/队列状态变化。新增 case → OnQueueUpdated(cQueueToModel(qp));单测 TestOnQueueUpdated_InPlaceRefresh。部署新 apiserver 到 /opt/xce-denix/bin(旧版备份 xce_apiserver.bak.20261023)。
  • 显示面终验:push priority 45→46→47(无 apiserver 重启)后 xce queues list 即时显示 47;作业 xce jobs submit -q short → DONE 全流程过,四 daemon 健康。
  • 运维注意:build.dbg 产物是 system-interpreter + nix-lib 混链(裸跑段错误);部署须用 patchelf --set-interpreter /nix/.../glibc-2.42-47/lib/ld-linux-x86-64.so.2 --set-rpath <nix 库路径> 后再 --help 预检。

断点 ②:YAML flag/resourcemap 接线(已实施;单测全绿 + t1 E2E 全通过)

四处修复(admin-plane-gaps 1.10):

  1. schema(src/schema/controller.capnp):Resource.distributions @8 / hostBased @9 取消注释(此前被注释掉——resourcemap 的目标字段根本不在 wire 上)。ResourceDistribution 结构本就活跃、编解码器早已生成,schema 改动后 encode/decode/free_Resource 自动带上 distributions(decode_ResourceDistribution_list 分配的正是 res_distribution_t*,与 C 结构零胶水)。

  2. resobj 深拷贝(src/lib/resobj.c):resobj_init 的 #if 0 /* FIXME */ 存根替换为真深拷贝——distributions 逐项深拷(value 按 which 拷贝、svalue strdup、hosts strdup),hbased_defined 深拷;resobj_destroy 对应释放。必须深拷不能 MOVE_PTR:源是 myconf->cluster_config(持续持有并在下次激活时 free),MOVE_PTR 会把配置树偷空。新增 resval_deep_copy/resval_free 静态助手(与 update_val_res/resval_obj_killer 同一所有权约定)。调度器 makeResource/resourceApplyDef 路径同文件同修复(推送上来的定义也能带 distributions)。

  3. 控制器 reconcile(src/controller/cluster.c + config_repl.c):新增 reconcile_config_resources(c)——cluster_build_resources(boot 建条目、激活重跑 EEXIST 容忍)+ seed_sharedinst_from_config(对 SHARED|GLOBAL 且 distributions[0] 带 value 的资源,deep-copy 该 value 进 c->sharedinst[live->idx],zarray_put 换值语义=幂等激活直接热更新容量)。seed 用 resmgr 的运行时 idx(key_add 分配),不能用 config 自带的 idx 字段(YAML 来源恒 0)。调用点三处:boot hook(xce_config_init)、config_activate_committed 的 -ENOENT fallback 路径与 hash-verified 路径(均在 xcesched_push_configs 之前,保证下一次 conf4sched push 即携带 seed)。调度器零改动——core_apply_config_resvals/sched_init_sharedInst 本就把 conf4sched.resvals 送进 core->sharedInst,calc_wanted_global_resvals 以 XCE_RESF_GLOBAL 门控 job wants。

    • YAML 映射规则(mapping.py):只有 location: [all] + value 形态置 SHARED|GLOBAL(0x08|0x04,即 license 池场景);其余形态(host 子集/多段/per-host)忠实映射进 distributions 但不置位——host 子集静态共享池需要 elastic-shared-resources 调度器工作,不在本次范围。
  4. Python 工具链(xceadmin):mapping.py 新增 RESOURCE_FLAG_SHARED/_GLOBAL/_SHARED_GLOBAL 常量 + _build_resource_value(YAML 值按类型选 union 成员:bool→bvalue、number→fvalue、string→svalue,经 .u 组访问)+ _build_resource 映射 resourcemap→distributions;build.py 移除 _UNMAPPED_BUT_VALID_ITEM_KEYS["resources"] 的 resourcemap 条目、_format_resource 增加 shared=true 与 distributions=[hosts]=value 渲染((reported) 表示动态无 value 形态)。schema 单点编辑即可——meson-build.sh 构建时把 src/schema/controller.capnp 同步进 xcecommon/schema_files/。

单测(全绿):

  • Python:test_mapping.py 新增 test_map_list_section_resourcemap_maps_distributions_and_all_flag(四形态逐一断言)+ test_map_list_section_resourcemap_real_repo_configs(真 configs/resources.yaml 全量分布数核对);test_config_build.py 的过时 test_build_warns_not_fails_on_schema_valid_unmapped_resourcemap_field 替换为 test_build_maps_resourcemap_to_distributions;full golden expected.bin 再生(5312→6696 字节,distributions 上 wire 的直接证据;minimal fixture 无 resources.yaml 不受影响)。373 用例全绿。
  • C:test_config_resource_reconcile.c 两个用例——注册+seed+深拷非别名断言、SHARED-only/无 value/非 shared 三类不 seed 断言、幂等重跑+容量热更新(100→250)断言;controller 套件 1347 tests 全绿,ASAN 套件 6/6 OK。

语义注意:resourceApplyDef(调度器)只比较 valtype/ordtype/flags/interval——已注册资源定义变更(含 distributions)报 RES_APPLY_CHANGED 但不 apply(重启拾取);而控制器侧 sharedinst seed 走 value 换入,容量热更新。分布结构变更(新增 host 段)目前不热更新到已注册调度器对象,与既有"定义变更需重启"语义一致。

E2E 结果(2026-10-23 t1,14:20–14:28):修复后 controller 部署重启 → boot 双 reconcile 正常(14:20:30 pre-election 激活建 4 队列 + phase2 xce_config_init 二次 reconcile,旧代码在此中毒)→ scheduler 重连,conf4sched push 完整应用 → 作业流全通过:恢复的 12:41 卡死 PEND 作业走到 DONE;rusage[staticres4=1] 作业 PEND→RUN→DONE;30 作业饱和压测(24 槽全占 + 6 PEND,槽释放后全部收敛 DONE);live config push(gen 12 激活,4 队列 "updated by configuration activation" + re-push 到已连接 scheduler)不再崩溃——这正是 12:41:13 旧代码 segfault 的同形场景。全部 4 daemon 健康,t1/t2/t3 OK。

修复中发现并修掉的深层缺陷(queue 载荷所有权):bp2 最初只修了 sched.c 的 double-free;t1 上 12:41:13 的 encode_Threshold_list segfault 根因更深——queue 的 capnp 载荷(users/hosts/thresholds/windows)在创建路径(qobj.c queue_init)被 MOVE_PTR 消耗掉定义,而 reconcile_config_queues 对同一 cluster_config 对象会跑多次(boot:pre-election 激活一次 + phase2 build_queues 又一次),第二次 refresh 读到已掏空的定义:thresholds 是 count>0/array=NULL 的毒对(MOVE_PTR 置空指针但不落 count),generate 的 encode_Threshold_list 无 NULL 卫兵,下次队列/worker push 即崩。修复=创建与刷新两条路径都改深拷贝(定义归 cluster_config 持有并在激活换装时 free;镜像 resobj_init distributions 深拷与 worker.c apply_join_state 的 dup 模式):qobj.c queue_init 深拷 users/hosts/thresholds/windows_config;queue.c queue_refresh_def 深拷同四项(定义里 NULL/0 从此真表示"配置无此列表",刷新清空运行时队列是正确语义);新增 dup_group_config(src/lib/utils.c,生成代码无此 dup)。UT:test_reconcile_thresholds_survive_second_pass 驱动三连 reconcile + 真 encode_QueueConf→wire→decode 回读断言(1348/1348 绿;全套件扫描 sched 99 个/worker/harness/xcelib/xceadmin 375 全绿,sched_repl_trace_baseline_test 一个既有失败经 stash 验证与本改动无关)。

运维观察(未修,记录):CLI config push 单节点 ackedCount==0 误报拒绝(push_config_to_standbys 只数 standby 传输层 ACK,controller 端实际成功——以 controller 日志为准);scheduler 对已注册资源定义变更报 WARN 不 apply(重启拾取——本次 scheduler 重启后 staticres4/res1 正常注册,作业可调度);饱和时 PEND reason 显示 -(PEND-reason 发布链路对 NO_RESOURCE 场景仍未出端到端值,P1 遗留);t1 controller 无 core dump(ulimit 0)——12:41 segfault 现场只能靠 objdump 模式匹配定位。

断点 ③:done 作业 live GC(已完成 2026-10-23)

问题回顾:N4 —— 终态作业(DONE/EXIT/PERR)只有 checkpoint/WAL 回放侧的 job_should_retain 过滤(finished_retain_s,默认 3600s),live jobmgr 从不删除:controller 不重启,joblist 和 scheduler 的 core->jobs 表无界增长。

实现(三处):

  1. kvmgr 补齐删除接口(src/lib/kvmgr.c):xkvmgr_type 原本 .del_key = NULL、.vclear = NULL,key_del()/vclear() 落到 kvmgr 对象只会 -EINVAL。新增 xkvmgr_del_key(先 key_find 定位 idx → key_del(kvtab val_killer 为 NULL,只 unlink 标量)→ vclear(data, idx) 清槽)与 xkvmgr_vclear(转发内部 varray),接入 typeinfo。缺失 key 传播 -ENOENT(与 xkvmgr_find 同契约)。

  2. controller 侧周期清理(src/controller/job.c 尾部):

    • job_live_gc_sweep(c):主角色 gate(ha_is_primary,standby 靠 WAL 回放重建 live 表,删除会分叉)→ jobmgr READ 锁下迭代收集 !job_should_retain(j, &c->persist.config) 的作业并 reference()(check_workers_timeout 同款 collect 模式)→ 逐个 jobmgr WRITE 锁 + key_del 解链。
    • 所有权要点:jobmgr 的 datakiller(job_obj_killer)是刻意空操作,vclear 不会释放 job —— 每次清理需 两次 dereference:①收集时的引用 ②表本身的 makeJob 引用(key_del 把表的持有责任转移给调用方;少一次就是 refcount 1 永久泄漏——恰是本机制要治的积累)。job_destroy 在 refcount 0 触发完整 teardown。
    • 锁纪律(B5):调用方不得持有任何 jobmgr 锁;key_del 需要的 WRITE 锁不能嵌套在收集期的 READ 锁内(check_workers_timeout 模式的直接原因)。
    • cluster_start_job_live_gc(c):幂等注册(ha_start_primary_heartbeat 同款 static _Atomic CAS + delaymgr.running 守卫——UT 零值 cluster_t 安全),delay_task 60s 周期、runcount=-1 无限重复。
  3. 注册点 = 两条 promotion 尾(ha.c):raft road ha_do_promote 尾(ha_start_primary_heartbeat 之后)与 non-raft ha_promote_to_primary 尾各调一次 cluster_start_job_live_gc。不能挂在 cluster_start——HA 路径下它是孤儿函数(myapp_run 不调)。幂等约定与 heartbeat 相同:重复调用不叠任务。

安全性论证:apply_committed_entry jobstate 组与 WAL_JOB_UPDATE case 对 key_find miss 已返回 0(bug-1970 benign skip,注释明确假设"finished_retain_s 过期后 purge 已发生");persist_verify_apply_correctness 只在 boot 回放窗口内比较计数(recovery_complete gated)——post-boot GC 与两者正交。TRANSIENT TTL(transient_since + transient_job_timeout_s)此前只在 checkpoint 排除侧生效,live GC 使其真正生效。

单测(全绿):

  • lib:test_kvmgr.c 新增 test_kvmgr_del_key(STRING 键:删后 find -ENOENT、sibling 完好、双删 -ENOENT、datakiller 清槽触发释放——栈上 key 缓冲避免 UAF)+ test_kvmgr_del_key_int(INT 键:idx 标量清槽、释放槽可复用再 add);xcelib 761/761。
  • controller:新增 testsuite/test_job_live_gc.c(test_job_array_apply 的 bare-cluster fixture 模式):过期 DONE 清除+refcount 归零断言(job_get_refcount)、fresh DONE/RUN 存活、幂等重跑、EXIT/PERR、TRANSIENT(TTL 过期清除 / transient_since==0 保留)、standby 不动表、NULL/mgr 缺失安全、arming 无 delaymgr 安全;controller 1354/1354。

E2E 结果(2026-10-23 t1):新 controller 部署重启 → 15:13:41 promote → job live GC armed (interval=60s, rc=0);重启前 14:2x 的 DONE 数组作业跨过 3600s 保留窗后被逐波清掉(15:24–15:27 五波 1+1+1+10+20 = 33 个,与保留时间线吻合);期间新作业 submit→RUN→DONE 全流程正常,apiserver/scheduler/controller 全健康。近期作业在保留窗内时 sweep 静默(只在 purged>0 打 INFO)——正确。

下游可见性(记录,未改):GC 只清 controller live 表。scheduler 的 core->jobs 与 apiserver 的 store.Jobs 仍各自持有 DONE 作业副本直到自身重启(scheduler 全量 JOBS_PUSH 也不会撤销已删项;Go 侧 OnJobDeleted 已实现但 capnp NotifyData 无 jobDelete 字段、C 侧无发送方)。EDA 习惯观察后再决定是否补 delete 通知链路。