- 变更规模:新增 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/ 知识文件,按口径入库)
74 lines
4.7 KiB
Markdown
74 lines
4.7 KiB
Markdown
# 交付物 · 扇出背压修复(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`",那是**语义变更**、需重新拍板 —— 我按"不污染观测"选了当前做法。
|
||
- ⛔ **未登记下一棒**。
|