Files
dsh_ai1net_server/交付物/扇出背压修复-20260926.md
admin c1b5e4d966 chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)
- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次)
- .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…),
  目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪
- .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/)
- .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*)
- .gitignore 补:备份件(*.bak-*)
- 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

74 lines
4.7 KiB
Markdown
Raw Permalink 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.
# 交付物 · 扇出背压修复(2026-09-25/26 · 第 15 棒)
> **工作区**:`E:/ProgramData/AIProject/aliyun-dsh-server`|**基线**:`D:/github/dsh_shenxian`
> **用户口径**:「**A 方案**」= 现在就修背压
> **上游**:`交付物/端口与容量瓶颈评估-20260925.md`(本棒即其实施)
---
## §0 一句话
修掉一处**单点即可打垮服务器**的隐患:消息扇出的投递点原先**丢弃** `socket.write()` 的返回值、
也不看水位 ⇒ **慢客户端(弱网 / 故意不读)让待写缓冲无界增长**。现在:水位满**记账**、
超上限**具名断开**、且**正常路径语义一字未变**。
## §1 交付(1 新 3 改 · 改动形态极干净)
| 文件 | 改动 | 形态 |
|---|---|---|
| `src/im/ws.ts` | 新增 `IM_SEND_BUFFER_LIMIT_BYTES`(4 MiB)+ 可注入阈值;`send()` 加背压三道判定 | **+84 / −1** —— 🔴 **被删改的既有行只有 1 行,正是 `socket.write(textFrame(text))`**(可机械断言) |
| `src/im/hub.ts` | `HubStats` 加 3 个背压字段;新增 `noteBackpressure()` / `noteBackpressureClose()`(只记账、不抛、不做 I/O) | **+36 / −0**(纯插入) |
| **`test/im-backpressure.test.mjs`** | 8 例(含假 socket 精确控水位) | 新增 |
| `package.json` | `test` / `verify` 各登记 1 处 | 2/2 |
## §2 三条行为与判据
| 行为 | 判据 | 为什么这么定 |
|---|---|---|
| `write()` 返回 `false` ⇒ 记一次背压,**仍算已投递** | `delivered === onlineIn` 不变 | false 的含义是"**水位满**",**数据已排队、会送达** ⇒ 若记成丢帧,"送达数"会随客户端网速变小、污染观测 |
| 待写缓冲 **> 上限** ⇒ **具名断开**(`1013 send-buffer-overflow`)+ warn 日志 | 连接真被销毁;该帧计 `skipped` | 这是防内存无界增长的**唯一硬闸** |
| 恰好 **等于** 上限 ⇒ **不断开** | 判据是 `>`,⛔ 不是 `>=` | 边界明确、可断言 |
## §3 判据读数
- `npm run build` **rc=0**
- `node --test test/im-backpressure.test.mjs` = **8 / 8 过 / 0 败**
- `npm test` = **626 / 616 过 / 5 败 / 5 跳过**(基线 618/608 ⇒ +8 全为本棒;5 败**仍是**投放线 `tar EBUSY`,未增一败)
- `npm run check:layering` **rc=0 ✅ 无新增违规**
- `src/im/store.ts` **零改动**
## §4 阈值:当前是**保守起点**,待标定
- 缺省 **4 MiB**,依据 = 单帧上限(1 MiB)的 4 倍 ⇒「允许积压约 4 个满帧」。
- ⚠️ **未做真实水位标定** ⇒ 阈值**可注入**(运行时选项)⇒ 标定后**改参数即可,⛔ 不必改代码**。
- **观测面**(`GET /api/im/stats`,admin):`backpressureEvents` · **`peakPendingBytes`(最有价值,它决定阈值该定多大)** · `closedByBackpressure`。
- **建议**:真实流量跑一段后取 `peakPendingBytes` 的分位值校准。
## §5 判据演进(已写进 `09 §8`)
**「修 bug」与「新增能力」的判据不同**:
- **新增能力** ⇒ 一律**纯插入**(未接线时行为逐字不变)。
- **修 bug** ⇒ **允许改既有逻辑**(否则没法修),但必须同时满足三条:
① 既有语义有**回归用例** ② 新行为**可观测** ③ 阈值**可注入**。
内核改动现为**四档**:`store.ts` 零改动 | `hub.ts` 纯插入 | `ws.ts` 允许改但**被删的行只限投递点** | `routes/im.ts` 纯插入。
142 §五-6 的**意图不变**:⛔ 不为场景改内核,只许"接扩展点"+"修内核自身缺陷"。
## §6 ⚠️ 一条实测教训(值得复用)
**测试跑的是 `lib/` 编译产物** ⇒ 我改完 `ws.ts` 后**忘记重新 build**,导致 `hub.ts`(新)与 `ws.ts`(旧)混跑 ⇒
测试连挂两次,症状是"断言该断开却投递了",而**诊断输出显示注入值完全正确** ⇒ 一度误判为 Node 属性遮蔽问题。
✅ **定位法**:打印**两个独立观测点**(注入值 ✅ 但 `backpressureEvents = 0`)⇒ 一眼看出"值对、但走的是旧代码"。
⇒ 🔴 **改完必 build**;⛔ 别被"注入看起来没生效"骗到去改实现。
(附带一条真知识:Node 的 `writableLength` 是**原型 getter**、非实例自有属性;实例级 `Object.defineProperty` 可覆盖。)
## §7 未做 / 边界 / 连锁影响
- ⛔ 未部署、未动 47/106、未 commit/push、未动云安全组、未改 A–E 五单结论。
- ⚠️ **生产标定未做**(阈值仍保守)。
- 🔴 **一条连锁影响需你知悉**:设计上**慢客户端不虚减 `delivered`**(理由见 §2)⇒ 若你希望"慢客户端单独计入 `skipped`",那是**语义变更**、需重新拍板 —— 我按"不污染观测"选了当前做法。
- ⛔ **未登记下一棒**。