Files
dsh_ai1net_server/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

64 lines
3.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)→ 跳过清理(不误伤),仅记日志。