- 变更规模:新增 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/ 知识文件,按口径入库)
4.7 KiB
4.7 KiB
交付物 · 扇出背压修复(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 buildrc=0node --test test/im-backpressure.test.mjs= 8 / 8 过 / 0 败npm test= 626 / 616 过 / 5 败 / 5 跳过(基线 618/608 ⇒ +8 全为本棒;5 败仍是投放线tar EBUSY,未增一败)npm run check:layeringrc=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",那是语义变更、需重新拍板 —— 我按"不污染观测"选了当前做法。 - ⛔ 未登记下一棒。