编号:B-09 严重级:high 工作线:broker(internal/broker) 来源:审查 I-04、I-05、M-10 依赖:B-05 (#12)(同改 OnDisconnect) 被依赖:B-10 (#17)(同改 handleHello)
审查 I-04、I-05 与 M-10 描述的是同一组竞态,统一由本 issue 修,避免三方各改一半:
connState
closed
hooks.OnDisconnect
handleHello
OnHandshakeComplete
HandleDisconnect
appUplink.OnHandshakeComplete
appUplink.OnDisconnect
msg.OnDisconnect
IsHandshook
SetOffline
审查 M-10 的消息侧兜底(分发时参考 online_since、每秒补宽限)在 C-03 做。
internal/broker/session.go、hooks.go、broker.go;cmd/nixmsg/uplink.go(OnSessionEstablished、OnHandshakeComplete、OnDisconnect 三个函数);internal/app/presence/app.go。
internal/broker/session.go
hooks.go
broker.go
cmd/nixmsg/uplink.go
OnSessionEstablished
OnDisconnect
internal/app/presence/app.go
HandleUplink
publishResp
msg.OnHandshakeComplete
presence.get
以下是本次复审各区审查报告的原文段落。A、M、I、P、S 开头的是原始发现编号(A 管理后台与网页、M 消息核心、I 身份认证群在线、P 传输平台部署、S SDK)。解决方案以本 issue 上方的"结论与统一方案"为准;原文里的方案与之不一致时,按上方执行。
presence.SetOnline
conns.Set
directory.list
group.get
broker/session.go:237-263
broker/hooks.go:196-238
cmd/nixmsg/uplink.go:38-50
uplink.go:52-62
message/session.go:12-26
presence/app.go:205-221
message/push.go:88-99
190-231
closed=true
broker/session.go
broker/hooks.go
broker/broker.go
presence.SetOffline
SetOffline(B)
resolveOnline
since_ms
presence/app.go:224-249
252-270
285-305
cmd/nixmsg/uplink.go:30-36
333-349
presence/service.go:49-50
broker/session.go:130-141
241-251
presence/app.go:209-212
240-243
message/session.go:15-17
37-40
IsOnline
brk.IsHandshook(ep)
CurrentConn
presence/app.go
SetOnline(ep,A)
SetOffline(ep,B)
Get
expire_at
expire_at IS NOT NULL
dispatched
uplink.go:56-61
dispatch.go:148
recover.go:49
offline_since
dispatchFullTx
online_since
expire_at = now + grace
internal/app/message/dispatch.go
recover.go
复审基线:main 4059a15(2026-09-30)。编号说明、各工作线的合并顺序、共享文件归属见总览 #7。
4059a15
已合入 origin/main 0c9b459。落地提交 91e887b fix: 完成 broker 复审 B-03 至 B-12 (#16)。
0c9b459
91e887b
No dependencies set.
The note is not visible to the blocked user.
编号:B-09 严重级:high 工作线:broker(internal/broker) 来源:审查 I-04、I-05、M-10
依赖:B-05 (#12)(同改 OnDisconnect) 被依赖:B-10 (#17)(同改 handleHello)
结论与统一方案
审查 I-04、I-05 与 M-10 描述的是同一组竞态,统一由本 issue 修,避免三方各改一半:
connState增加closed;hooks.OnDisconnect在删除连接表条目的同一临界区置位,并在同一处直接停掉该连接的握手计时器(审查 P-22 第一点:现在先删表再按代号找计时器,永远找不到)。handleHello从"标在线"到OnHandshakeComplete持锁,持锁后先检查closed,已关闭直接返回;HandleDisconnect持同一把锁做离线处理。备选方案是把断线事件放进该端上行队列顺序处理,二选一,推荐加锁。appUplink.OnHandshakeComplete只在 memConns 当前仍是本代号时才更新;appUplink.OnDisconnect先从 memConns 移除本连接,再调用msg.OnDisconnect(审查 M-10 第 1 点)。IsHandshook、当前代号);注入连接表后 presence 只信连接表,内存表只用来取 since;SetOffline代号不符且该端已无已握手连接时,清掉残留条目并发下线通知。消息线分发仍按 DEVELOPMENT 7.4 把"握手中"算在线,不受影响。审查 M-10 的消息侧兜底(分发时参考 online_since、每秒补宽限)在 C-03 做。
改动文件
internal/broker/session.go、hooks.go、broker.go;cmd/nixmsg/uplink.go(OnSessionEstablished、OnHandshakeComplete、OnDisconnect三个函数);internal/app/presence/app.go。与其他问题的交互 / 冲突说明
OnDisconnect)、B-10(handleHello)同文件:本条在 B-05 之后、B-10 之前合入。HandleUplink、B-06 改publishResp,互不重叠。msg.OnHandshakeComplete/msg.OnDisconnect启停,本条保证这两个回调的调用顺序与"当前连接"判断正确。验收与测试
presence.get返回离线。问题明细(各区审查原文,证据含文件与行号)
[I-04] 握手和断线没有串行,断开的连接会被重新标成在线,不保留的消息也不再按宽限作废
presence.SetOnline(再写一次)→ 置 handshook →OnHandshakeComplete。中途不复查连接是否还在。断线处理则同步跑在 mochi 的连接 goroutine 里。appUplink.OnHandshakeComplete无条件执行conns.Set,把已经死掉的连接登记回 memConns;presence.get、directory.list、group.get里一直显示在线,订阅者最后收到的是「上线」。conns.Set还会覆盖 B 的登记。如果 B 在 hello 前断开,状态同样卡死。broker/session.go:237-263。broker/hooks.go:196-238。cmd/nixmsg/uplink.go:38-50;断线时uplink.go:52-62又用 memConns 重新判断是否当前连接,可能和 broker 的判断不一致。message/session.go:12-26。presence/app.go:205-221。message/push.go:88-99、190-231。closed字段;hooks.OnDisconnect在删除连接表条目的同一临界区里置closed=true,并记下是否已握手。handleHello从「标在线」到OnHandshakeComplete这一段持锁;持锁后先检查closed,已关闭就直接返回。HandleDisconnect持同一把锁做离线处理。appUplink.OnHandshakeComplete只在 memConns 当前仍是本代号时才更新。broker/session.go、broker/hooks.go、broker/broker.go、cmd/nixmsg/uplink.go。[I-05] 在线状态的几个来源不一致:内存表残留、握手中也算在线、时间戳写三遍
presence.SetOffline发现内存表里记的连接代号不是本次断开的代号,就直接返回。SetOffline(B)会因为 A≠B 提前返回。resolveOnline会回退到这张表,于是一直显示在线,订阅者也收不到下线。since_ms还是上一次会话的时间。presence/app.go:224-249、252-270、285-305。cmd/nixmsg/uplink.go:30-36、333-349。presence/service.go:49-50。broker/session.go:130-141、241-251;presence/app.go:209-212、240-243;message/session.go:15-17、37-40。IsOnline用brk.IsHandshook(ep),CurrentConn只返回已握手的当前代号。消息线仍用 memConns 做分发,保持不变。SetOffline遇到代号不符时,如果连接表显示该端已没有已握手连接,就删掉残留条目并发下线通知。cmd/nixmsg/uplink.go、presence/app.go、broker/session.go。SetOnline(ep,A)后,SetOffline(ep,B)且连接表为空:Get返回离线,订阅者收到下线。presence.get返回离线。[M-10] 断线写库与分发之间有竞态,会产生永不过期的「僵尸」不保留投递
appUplink.OnDisconnect先调用msg.OnDisconnect(入队写操作并等待提交),然后才从连接表里移除这个连接。expire_at为空的不保留投递。expire_at IS NOT NULL的投递,所以它永远不会被丢弃。接收端不再上线的话,消息永远停在dispatched,正文永不删除,还占着双方配额。uplink.go:56-61(先写库、后移除);dispatch.go:148(事务内查内存连接表);recover.go:49(清理条件)。OnDisconnect先计算 isCurrent,再从连接表移除,最后调msg.OnDisconnect。会话层写offline_since发生在此之前,所以之后的分发能按新的离线时刻正确计算宽限。dispatchFullTx的离线分支同时读online_since:如果数据库里仍显示在线,就按「刚断线」处理,expire_at = now + grace。expire_at为空的投递补上宽限截止时间。cmd/nixmsg/uplink.go;internal/app/message/dispatch.go、recover.go。OnDisconnect,随后提交一条不保留消息,再移除连接;运行兜底后投递得到宽限,宽限过后变为 dropped。复审基线:main
4059a15(2026-09-30)。编号说明、各工作线的合并顺序、共享文件归属见总览 #7。已合入 origin/main
0c9b459。落地提交91e887bfix: 完成 broker 复审 B-03 至 B-12 (#16)。