## 现象 `wsutil/outbox.go:281-293` 的写协程把记账只挂在**写返回 error** 的分支: ```go // outbox.go:281 func (o *Outbox) run() { defer o.wg.Done() defer o.Close() for { select { case msg := <-o.queue: if err := o.write(msg); err != nil { o.Close() o.failInFlight() // ← 只有 error 分支记账 return } ``` `write` 一旦 **panic**(不是返回 error),GoSafe 收编后 `failInFlight`/`countPending`/`drain` 全部不执行。而文档承诺(outbox.go:218-219):「Send 返回 true 的每一帧都必然闭合进 Written 或 Dropped 之一——『Send(true) 次数 − Written』恒等于 Dropped」。 探针复现(write 对 "boom" panic,3 帧入队): ``` [async] panic recovered: task=wsutil-outbox: write panic (simulated) [outbox] Send=true/true/true (3 frames accepted) [outbox] Written=1 Dropped=0 Rejected=0 Closed=true [outbox] identity check: Send(true)=3 must equal Written(1)+Dropped(0)=1 [outbox] actually-written frames: ["f1"] ``` ## 失败场景推演 write 实现 panic(websocket Conn 实现 bug、对 nil 字段写、三方库内部断言)→ 正在写的帧与队列里余下的帧(探针中 "boom" 与 "f3")**既不计 Written 也不计 Dropped**,"f3" 更是从未写出。调用方按文档 `Close → Wait → 关底层连接` 收尾,Wait 正常返回(wg.Done 在 unwind 中执行了),以为「都写完了」;监控按恒等式算丢帧差值永远是 0,写坏路径完全失明——这恰是 #105/#117/#123 三轮修复辛苦闭合的口径。 ## 为什么测试没抓到 #105/#117/#123 系列测试只覆盖「写返回 error」与「关闭交错」两个触发面;write panic 路径无用例。且 panic 被 GoSafe 静默收编(只打一行 async 日志),测试断言计数器时很容易恰好不含 panic 场景。 ## 建议修复方向 把 `failInFlight()+countPending()` 提为 `run()` 的 defer(panic 展开时同样闭合记账),或在 `o.write` 调用点包 recover 转 error 走既有失败分支;并在 Dropped 文档补一句「写动作 panic 同样计入 Dropped」。 --- 置信度:高(探针复现:Send(3)=Written(1)+Dropped(0),恒等式差 2;基线 = network 9f3d2c3) **与既有 issue 的差异**:#105/#117/#123 都在闭合同一恒等式,触发面分别是「写失败帧不记账」「关闭交错」「拒收混记」;本条是第四种破口——write **panic** 时整套记账被跳过,机制与修法均不同(delta:#123 后恒等式只对 error 路径成立,对 panic 路径仍假)。
Activity
Merge pull request #148 from oekit/fix/wave1-issue-145-146-147
zy84338719 commentedon Nov 10, 2026
已在
v0.9.2修复并发布 ✅变更内容
本条修复(在
v0.9.2的版本节里定位到):该版本 `v0.9.2` 的其余变更
关联问题单:#145。
关联问题单:#146。
关联问题单:#147。
Release: https://github.com/oekit/network/releases/tag/v0.9.2