Files
dsh_shenxian/dsh-server-docs/04-调整方案/30-编排器孤儿实例清理.md
T
admin 5ad755116e chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
   保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
   工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
   必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
   + ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
   ⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
   验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
2026-09-15 18:47:13 +08:00

3.7 KiB
Raw Blame History

30 · 编排器孤儿实例清理(单实例保证从内存升到 OS 层)

  • 日期:2026-09-11
  • 触发:admin 对话窗口报「模型连接异常」
  • 状态:✅ 已实施并 live 验证(2026-09-11)

一、现象与根因

现象:admin 的 dsh 对话窗口「模型连接异常」。排查发现 admin 有 2 个实例同时运行:

  • dsh-114801-bf8afe93(port 35685)= 编排器认的 main;
  • dsh-114801-f7ea9f97(port 36449)= 孤儿残留(编排器不认识)。

两实例共享同一 profile(DSH_HOME)→ session/settings 写冲突 → 模型配置读取异常。(KEY 注入、网络均正常。)

根因(已读源码定位):编排器的「单实例保证」只在内存层:

位置 事实
launch() L123 if (this.mains.has(userId)) throw AlreadyRunningError —— 只查内存 map
constructor() L60-72 只建 portGuard + reap timer,不扫描 OS 层已有 scope
teardown() L230 只 stop map 内实例,不清理未纳管的 scope
全文 无任何 systemctl list-units 扫描(对 OS 层不感知)

根因链:

  1. 实例真实生命周期在 OS 层(systemd scope),但编排器只靠内存 map 追踪;
  2. portal 重启 → map 清空,而 systemd scope 不随 portal 死(继续运行)→ 旧实例变孤儿;
  3. 重启后再 enter/launch 时不检查 OS 层 → 直接 spawn 新实例 → 新旧并存;
  4. 两实例共享 profile → 写冲突 → 模型连接异常。

触发条件:portal 每次重启都可能留孤儿(本次由白名单部署重启 + 此前熔断测试/A1 重启叠加命中)。

二、方案(治本:单实例保证升到 OS 层)

改动文件:src/supervisor/orchestrator.ts

  1. spawn 前清孤儿(核心):spawnInstance 前,用 resolveUid(userId) 拼 scope 前缀 dsh-<uid>-,systemctl list-units --type=scope 扫描 → 对该 uid 名下、且不等于当前 map 实例 unit 的 scope systemctl stop。
  2. portal 启动时清一次(防重启留孤儿):LocalSpawner 启动(构造后)扫描所有 dsh-<数字>-*.scope 并清理(编排器重启后无法接管它们,安全清)。
  3. kill 后校验(可选):killInstance 后确认 scope 已消失,否则再 stop 一次。

安全边界:

  • 只清理 dsh-<uid>-*(严格前缀),不误伤 portal 自身(dshs.service 名字不同)与其它服务;
  • 清理是"停止后交给 systemd 回收",不改任何数据。

三、验证结果(live,2026-09-11)

  • 场景 A(启动时清):造假孤儿 dsh-114801-deadbeef.scope → restart portal → scope 数 0(cleanAllStaleScopes 生效)→ enter 后只剩 1 个 ✅;
  • 场景 B(spawn 前清):造孤儿 deadbeef → 停真实 main 触发退避重启 → spawn 前清理生效,deadbeef 残留 0、只剩 1 个新实例 ✅;
  • npm run build 通过。

三之二、原验证计划(留档)

  • 单测:模拟 systemctl list-units 输出含孤儿 → 断言 stop 被调用且前缀匹配;
  • live:手工造孤儿(systemd-run --scope 起一个假的)→ 触发 launch → 断言孤儿被清、只剩 1 个实例;
  • 回归:portal 重启后 enter,断言实例数 = 1(此前会 2)。

四、红线遵守

  1. 不碰官方 dsh 主程序(R2 ✅);2. 不升级 dsh(R1 ✅);3. 只改本地编排器(dshs)。

五、风险

  • systemctl list-units 在每次 spawn 前调用 → 轻微开销(~几十 ms),可接受;
  • 若 uid 解析失败(用户无 uid)→ 跳过清理(不误伤),仅记日志。