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>
This commit is contained in:
parent
fc33a5da16
commit
d028c8c315
10 changed files with 638 additions and 3 deletions
|
|
@ -222,4 +222,28 @@
|
|||
|
||||
**运维观察(未修,记录)**: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(待做)
|
||||
### 断点 ③: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 通知链路。
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue