Files
dsh_shenxian/dsh-server-docs/04-调整方案/79-插件启停的两处平台缺陷-uninstall缺await与无效pnpm-flag.md
T

78 lines
7.0 KiB
Markdown
Raw Normal View History

# 79 · 插件启停的两处平台缺陷:`uninstall` 缺 `await`(把整个 orchestrator 带崩)+ `pnpm remove` 用了无效 flag(2026-09-13 发现)
- 日期:2026-09-13
- 状态:✅ **已修复并部署**(2026-09-13 13:4x 逐条核实产物);仅剩**可选的**「加固 b 半应用态主动自愈」未做 —— 见 §七
- 触发:用户在 guest 的「功能管理」启用 `dsh-plugin-mcn-suite` → **安装失败**(2026-09-13 07:4x)
- 影响:**P0** —— 任一次插件启停失败都可能**终止整个 `dshs` 进程**(全体用户瞬断 + 所有实例被停),并把目标 profile 留在**半应用态**(该实例此后每次启动即崩)
> **TL;DR**:D1(缺 `await`)→ 未捕获 Promise 拒绝 → orchestrator 退出;D2(无效 flag)→ 让 D1 必然被触发;D3(内存不够)→ 让流程必然走进触发 D1 的分支。
## 一、现象链(journalctl 原文,逐条可核)
| 时刻 | 事件 |
|---|---|
| 07:41–07:43 | guest 装上 mcn-suite 后反复 `[dsh-plugin-mcn] 榜单查询失败: unable to open database file` |
| 07:43:28/46/56 | 实例侧 `FATAL ERROR: Reached heap limit`(`Mark-Compact 155.3(169.1) -> 153.9 MB` 等)→ `exitCode 134`;`[crash-restart]` 触发 |
| 07:43:58 | `Error: Command failed: … setpriv … pnpm remove dsh-plugin-mcn-suite --ignore-workspace-root-check …`;stderr 解出 **`ERROR Unknown option: 'ignore-workspace-root-check'`**;栈 = `runPnpmAs(…:406) ← uninstall(…:575) ← business-plugins.js:614` |
| 07:43:58 | **`dshs.service: Main process exited, code=exited, status=1/FAILURE`** → systemd 4 秒后自动重启(07:44:02) |
| 07:45–07:46 | 服务重启后 guest 继续被拉起 → 每次都 OOM → attempt 1..5 → **`crash-loop-circuit-open`**(熔断);两个实例均 inactive |
## 二、三个缺陷
| # | 级别 | 位置 | 内容 |
|---|---|---|---|
| **D1** | **P0** | `src/web/routes/business-plugins.ts:682`(部署版 `business-plugins.js:614`) | 隔离定位循环里写的是 `try { uninstall(sel.id) } catch { /* 尽力而为 */ }` —— **缺 `await`**。`uninstall` 是 `async`,其 rejection 逃出**同步** `try/catch` ⇒ **unhandled rejection ⇒ 进程退出**(Node 默认 `--unhandled-rejections=throw`) |
| **D2** | P1 | 同文件 `:644`(部署版 `:575`) | `runPnpmAs(user.id, ['remove', id, '--ignore-workspace-root-check', '--reporter', 'silent'], dir)` —— 该 flag **只存在于 `add` 子命令**,`remove` 不认(pnpm 9.15.9 报 `Unknown option`,提示 `ignore-workspace`)⇒ **禁用路径必然失败**,进而**必然触发 D1** |
| **D3** | P1 | 容量 | 实例 V8 堆限 160 MiB 装不下 `dsh-univer-office` + `dsh-plugin-mcn-suite`(隔离 cgroup 实测分别 **+63.7 / +38.5 MiB rss**)⇒ 探活失败 → 平台进「隔离定位」分支 → **必然走到 D1/D2** |
## 三、连带后果(本身也是缺陷)
- 因进程直接退出,平台兜底的 `restoreProfile()`(在 `try/catch` 里)**没跑** ⇒ profile 停在**半应用态**(mcn-suite 已在 `dependencies` + `bundles`)⇒ 该实例此后**每次启动都 OOM**(熔断循环),用户彻底不可用。
- 本次处置:手工回滚 guest profile(从 deps + bundles 移除 mcn-suite,备份 `package.json.bak-rollback-20260913T0748`)→ 触发一次 `enter` 验证实例恢复(`dsh web:` 正常、`active running`、cgroup **186.5 / 384 MiB**、无 crash-restart)。
## 四、修法(已备好,未落地)
1. **D1**:`try { uninstall(sel.id) } catch {}` → `try { await uninstall(sel.id) } catch {}`(**一行**)。
2. **D2**:`remove` 的参数改成 `['remove', id, '-w', '--reporter', 'silent']`(或直接去掉 `--ignore-workspace-root-check`)。
⚠️ `add` 的 `--ignore-workspace-root-check` 是**合法** flag,**不要一起改**。
3. **加固(建议同批)**:
- 路由最外层 `void (async () => {…})()` 补 `.catch()`(双保险,避免同类遗漏再崩进程);
- 启动/应用前的**半应用态自愈**:检测「`bundles` 里有包但 `node_modules` 装不全」→ 自动摘除或至少审计告警。
4. **验收**:修复后在任一 profile 上重现「启用一个注定失败的插件」→ 期望 = **任务 `failed` + profile 回滚 + 服务不退出**。
## 五、红线与影响
- 改码 + `npm run build` + `systemctl restart dshs` → **中断在线用户数秒** ⇒ 走 **R8 先知会**。
- 回滚:单文件 `git` 回退 + 重新 build + 重启;服务本身有 systemd 自动重启兜底。
## 六、相关
- 档案 65(功能插件启停 ↔ web provider 联动)|档案 16 §2.2(危险操作确认弹窗)|档案 75 维度 B(资源成本,与 D3 同源)|档案 74(V8 堆限 256→160,D3 的背景)。
---
## 七、修正(2026-09-13 13:4x)· **D1 / D2 / 加固 a 已落地并部署**
> 触发:用户问「还有待处理的事项吗」时复核本条,发现**状态已过期** —— 三个缺陷中有两个半已经修完并上线。
**逐条实测(部署产物 `lib/web/routes/business-plugins.js`,只读核对)**:
| 项 | 期望修法(§四) | 产物实测 | 结论 |
|---|---|---|---|
| **D1** 缺 `await` | `try { await uninstall(…) } catch {}` | `:587` 与 `:617` 均为 `await uninstall(sel.id)` | ✅ **已修** |
| **D2** 无效 flag | `remove` 去掉 `--ignore-workspace-root-check` | `:575` = `['remove', id, '-w', '--reporter', 'silent']`;全仓该 flag **仅剩 `add` 一处**(合法) | ✅ **已修** |
| **加固 a** 外层 `.catch()` | 路由最外层补 catch | `:613` `void (async () => {` … `:718` `})().catch((err: unknown) => {` | ✅ **已修** |
| **加固 b** 半应用态主动自愈 | 检测「`bundles` 有包但 `node_modules` 装不全」→ 自动摘除/告警 | 只有**失败路径**的 `restoreProfile()`(`:567` / `:671` / `:710`)回滚到快照 | ⚠️ **未做**(可选加固,非 P0) |
**D3 已消解(不再需要窗口)**:D3 的成因是「V8 堆限写死 160 MiB + `MemoryMax` 写死 384 MiB 装不下 univer + mcn-suite」。**档案 81 · R1-④ 已把两者改成「按插件集合动态计算」**(`instanceMemMb()` / `heapMbFor()`)。2026-09-13 13:3x 实测运行中实例:
```
guest(uid 100002) NODE_OPTIONS=--max-old-space-size=256 scope MemoryMax=544 MiB
admin(uid 114801) NODE_OPTIONS=--max-old-space-size=256 scope MemoryMax=384 MiB
平台进程 env 仍有 DSH_INSTANCE_NODE_OPTIONS=--max-old-space-size=160 ← 已被代码侧 withHeap() 覆盖,不再是生效值
```
⇒ **§五「需 R8 窗口重启」的前提已不成立**:不再需要为修复本条而重启。
**仍未闭环的一项**:§四.4 的端到端验收(**在某个 profile 上重现「启用一个注定失败的插件」→ 期望 任务 `failed` + profile 回滚 + 服务不退出**)**尚未跑过**。代码层面三个修法都已就位,但「服务真的不再被带崩」目前是**代码判据**、不是**实测判据**。建议与 T03 的 guest 启用(天然就是一次"可能失败的启用")合并验收,不必单独造场景。