[R3-02][medium] 下行积压时 fatal 与停机断开发生在本帧写出之前 #66

Closed
opened 2026-09-30 17:18:25 +08:00 by nixevol · 3 comments
Owner

现象

基线 origin/main 0c9b459。连接下行队列或 mochi outbound 里已经有 msg / resp 时:

  1. PublishThenDisconnect 在 Publish 成功后用 sentPub 比调用前的计数。OnPacketSent 对任意 PUBLISH 都加一。前面那一帧稍后写出就会让等待返回,然后立刻 Disconnect。停用、删除、重置密码的 fatal,以及 logout 的响应,可能还在 outbound 里就被套接字关掉。客户端看不到协议规定的先帧后断。令牌侧多半已经清掉,这不是旧令牌继续可用,而是踢线帧丢失。现有 B-04 测试只有空队列单帧。
  2. Broker.Shutdown 对传入的 context 做了非阻塞 select,几乎立刻 Close()。serve.go 给的 5 秒用不上。有积压时 MQTT 5 0x8B 可能只进了写缓冲就随 Stop 丢掉。空闲连接的停机测试覆盖不到。

位置

  • internal/broker/downlink.go:sendOne 里 before := st.sentPub.Load() 与 waitPacketWritten
  • internal/broker/hooks.go:OnPacketSent 对所有 PUBLISH 增加 sentPub
  • internal/broker/broker.go:Shutdown
  • 调用:internal/broker/session.go 的 logout 与 fatalKick;cmd/nixmsg/serve.go 的 brk.Shutdown

方案

  • 带断开的那一帧用自己的完成信号(例如该次 Publish 对应的 packet id,或只在这一帧的 OnPacketSent 上完成的 channel),不要用连接级 sentPub 总数。
  • Shutdown 在 ctx 截止前等待各连接把 0x8B 写出或超时,再 Close。ctx 已取消时再立即关闭。
  • 补测试:下行队列里先有一帧 QoS 1,再 PublishThenDisconnect,对端应先读到 fatal 或 logout 帧再看到断开。停机时连接上仍有未写出的下行,应能读到 0x8B,或在预算用尽时明确失败而不是空等。

约束

只改 internal/broker/ 与 cmd/nixmsg/serve.go 里调用 Shutdown 的等待方式,以及对应测试。不改 PublishDown 签名,不恢复固定 Sleep。不要合 feat/fix-3-downlink-deadlock。偏差节 ### 复审修复 R3-02。

## 现象 基线 `origin/main` `0c9b459`。连接下行队列或 mochi outbound 里已经有 `msg` / `resp` 时: 1. `PublishThenDisconnect` 在 `Publish` 成功后用 `sentPub` 比调用前的计数。`OnPacketSent` 对任意 `PUBLISH` 都加一。前面那一帧稍后写出就会让等待返回,然后立刻 `Disconnect`。停用、删除、重置密码的 `fatal`,以及 `logout` 的响应,可能还在 outbound 里就被套接字关掉。客户端看不到协议规定的先帧后断。令牌侧多半已经清掉,这不是旧令牌继续可用,而是踢线帧丢失。现有 B-04 测试只有空队列单帧。 2. `Broker.Shutdown` 对传入的 context 做了非阻塞 `select`,几乎立刻 `Close()`。`serve.go` 给的 5 秒用不上。有积压时 MQTT 5 `0x8B` 可能只进了写缓冲就随 `Stop` 丢掉。空闲连接的停机测试覆盖不到。 ## 位置 - `internal/broker/downlink.go`:`sendOne` 里 `before := st.sentPub.Load()` 与 `waitPacketWritten` - `internal/broker/hooks.go`:`OnPacketSent` 对所有 PUBLISH 增加 `sentPub` - `internal/broker/broker.go`:`Shutdown` - 调用:`internal/broker/session.go` 的 logout 与 `fatalKick`;`cmd/nixmsg/serve.go` 的 `brk.Shutdown` ## 方案 - 带断开的那一帧用自己的完成信号(例如该次 `Publish` 对应的 packet id,或只在这一帧的 `OnPacketSent` 上完成的 channel),不要用连接级 `sentPub` 总数。 - `Shutdown` 在 ctx 截止前等待各连接把 `0x8B` 写出或超时,再 `Close`。ctx 已取消时再立即关闭。 - 补测试:下行队列里先有一帧 QoS 1,再 `PublishThenDisconnect`,对端应先读到 fatal 或 logout 帧再看到断开。停机时连接上仍有未写出的下行,应能读到 `0x8B`,或在预算用尽时明确失败而不是空等。 ## 约束 只改 `internal/broker/` 与 `cmd/nixmsg/serve.go` 里调用 `Shutdown` 的等待方式,以及对应测试。不改 `PublishDown` 签名,不恢复固定 `Sleep`。不要合 `feat/fix-3-downlink-deadlock`。偏差节 `### 复审修复 R3-02`。
Author
Owner

已在 feat/fix-r3-66-disconnect (a14ca0d) 修复: 带断开下行改按本帧 OnPacketSent (packet id/载荷) 等待, 不再用连接级 sentPub; Shutdown 先排空再发 0x8B 并在 ctx 内等待连接拆掉. go test ./internal/broker/ ./cmd/nixmsg/ 已通过. 已推送, 未合 main.

已在 `feat/fix-r3-66-disconnect` (`a14ca0d`) 修复: 带断开下行改按本帧 OnPacketSent (packet id/载荷) 等待, 不再用连接级 sentPub; Shutdown 先排空再发 0x8B 并在 ctx 内等待连接拆掉. `go test ./internal/broker/ ./cmd/nixmsg/` 已通过. 已推送, 未合 main.
Author
Owner

已合入 main:ef46563 (#66);当前 origin/main HEAD a7a6e34。

已合入 main:`ef46563` (#66);当前 origin/main HEAD `a7a6e34`。
Author
Owner

复查补强已合入 main ca34344 (#66)。sendOne 在取出帧到返回前记 inSend,排空等待不会把正在等大帧名额的帧当成已经安静。有截止时间时排空预留约 1 秒给 0x8B,排空未完成也会用剩余时间等连接拆掉再 Close。

复查补强已合入 main `ca34344` (#66)。`sendOne` 在取出帧到返回前记 `inSend`,排空等待不会把正在等大帧名额的帧当成已经安静。有截止时间时排空预留约 1 秒给 `0x8B`,排空未完成也会用剩余时间等连接拆掉再 `Close`。
Sign in to join this conversation.