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 一律写「远程服务器」。
This commit is contained in:
1 parent
c70d5d860e
commit
5ad755116e
173 files changed
+27632
No files matched your search
@@ -0,0 +1,63 @@
|
||||
# 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)→ 跳过清理(不误伤),仅记日志。
|
||||
Reference in new issue
Block a user