编号:C-02 严重级:high 工作线:消息核心(internal/app/message、serve 的 messageLoops) 来源:审查 M-04 依赖:C-01 (#32)(在途表挂在每连接 worker 上) 被依赖:D-03 (#29)
采用审查 M-04 的方案(测试中观察到的"同一时刻两个 receipt 帧"即此问题):
pushReceipts
ReceiptAck
OnDisconnect
OnPublishDropped
Ack
PublishDown
DEVELOPMENT 6.4"可能重复,SDK 按 receipt_id 去重"仍成立(断线重连后未确认的回执会各重发一次),但不能每秒全量重发。DEVELOPMENT 第 10 节补一句:裸设备要么实现 receipt_ack,要么发送时带 receipt:false。更新 DEVIATIONS M2/M3/M4 第 5 条。
receipt_ack
receipt:false
internal/app/message/push.go、ack.go、session.go、app.go;docs/DEVELOPMENT.md 第 10 节。
internal/app/message/push.go
ack.go
session.go
app.go
docs/DEVELOPMENT.md
test/accept/rest_accept_test.go
drainReceipts
以下是本次复审各区审查报告的原文段落。A、M、I、P、S 开头的是原始发现编号(A 管理后台与网页、M 消息核心、I 身份认证群在线、P 传输平台部署、S SDK)。解决方案以本 issue 上方的"结论与统一方案"为准;原文里的方案与之不一致时,按上方执行。
PushPending
acked=0 ORDER BY receipt_id LIMIT 64
WakePush(发送方)
WakePush
resp
// 简化:未单独记 inflight 回执,按未确认回执取窗口条数 rows, err := a.db.Read.QueryContext(ctx, ` SELECT receipt_id, msg_id, endpoint_id, state, reason, created_at FROM receipts WHERE sender_id = ? AND acked = 0 ORDER BY receipt_id ASC LIMIT ?`, endpointID, window)
push.go:117
push.go:236
ack.go:74-75
ack.go:289-301
test/accept/rest_accept_test.go:863
rcptInflight[connID]map[receiptID]推送时刻
receipt_id
复审基线:main 4059a15(2026-09-30)。编号说明、各工作线的合并顺序、共享文件归属见总览 #7。
4059a15
C-02 已在 feat/fix-message-c01-c03 提交 75f4647cae (#33)
feat/fix-message-c01-c03
75f4647cae
回执按连接去重在途,先收集再发布。未合入 main。
已合入 origin/main 0c9b459。落地提交 bd550f3 fix: 回执按连接去重在途且不改可重复约定 (#33)。
0c9b459
bd550f3
No dependencies set.
The note is not visible to the blocked user.
编号:C-02 严重级:high 工作线:消息核心(internal/app/message、serve 的 messageLoops) 来源:审查 M-04
依赖:C-01 (#32)(在途表挂在每连接 worker 上) 被依赖:D-03 (#29)
结论与统一方案
采用审查 M-04 的方案(测试中观察到的"同一时刻两个 receipt 帧"即此问题):
pushReceipts只发不在途的回执,数量不超过"窗口减在途数",发布前标记、失败撤销;在途超过重发间隔(建议与确认超时一致)才允许重发。ReceiptAck成功后移出在途并唤醒;OnDisconnect清掉该连接的在途表;OnPublishDropped收到 receipt 类型时撤销标记。Ack只在状态真正改变且确实写了回执时才唤醒发送方;重复 ack 不唤醒。pushReceipts改为"先收集结果、关闭 rows、再发布",不在持有读连接时调用PublishDown(D-03 的前提)。DEVELOPMENT 6.4"可能重复,SDK 按 receipt_id 去重"仍成立(断线重连后未确认的回执会各重发一次),但不能每秒全量重发。DEVELOPMENT 第 10 节补一句:裸设备要么实现
receipt_ack,要么发送时带receipt:false。更新 DEVIATIONS M2/M3/M4 第 5 条。改动文件
internal/app/message/push.go、ack.go、session.go、app.go;docs/DEVELOPMENT.md第 10 节。与其他问题的交互 / 冲突说明
test/accept/rest_accept_test.go里为此写的drainReceipts绕行需同步调整(T-02 知悉)。验收与测试
问题明细(各区审查原文,证据含文件与行号)
[M-04] 回执没有在途标记:每次推送都重发最早 64 条未确认回执,并发时必然重复,第 65 条起可能永远发不出
PushPending结尾都会调pushReceipts,它只是查acked=0 ORDER BY receipt_id LIMIT 64,然后全部发出。PushPending的路径有:每秒循环;ack 后的WakePush(发送方);握手;旧连接断开;1 秒重推定时器。WakePush每次调用都起一个新协程,不合并。Ack会对同一个编号WakePush两次,两个协程同时下发同一条回执。这就是你在测试里看到的「同一时刻两个 receipt 帧」。receipt_ack之前,每秒都会重发。SDK 每收到一次重复回执都会再回一次receipt_ack,写库量随之放大。receipt_ack)会在 7 天里每秒收到同样的 64 条回执,第 65 条起永远发不出去。resp。push.go:117、push.go:236:每次PushPending都会调用它。ack.go:74-75:无条件两次WakePush,重复 ack 也会触发。ack.go:289-301:ReceiptAck不唤醒下一批。test/accept/rest_accept_test.go:863里的drainReceipts,就是测试为这个行为做的绕行。rcptInflight[connID]map[receiptID]推送时刻(加锁)。pushReceipts只发不在途的回执,数量不超过「窗口减在途数」,发布前就标记(防止并发重复),发布失败时撤销标记。在途超过重发间隔(例如 60 秒,或与确认超时一致)才允许重发。ReceiptAck成功后移出在途并唤醒该端;OnDisconnect清掉该连接的在途表;OnPublishDropped收到 receipt 类型时撤销标记,1 秒后重推。Ack只在状态真正改变、且确实写了回执时才唤醒发送方;重复 ack 不唤醒。receipt_ack,要么发送时带receipt:false(文档改动)。internal/app/message/push.go、ack.go、session.go、app.go。receipt_id去重,行为兼容。改后重复会少很多,接受测试里的drainReceipts需要跟着调整。PushPending只发出 2 帧。PushPending:每条回执只发一次。复审基线:main
4059a15(2026-09-30)。编号说明、各工作线的合并顺序、共享文件归属见总览 #7。C-02 已在
feat/fix-message-c01-c03提交75f4647cae(#33)回执按连接去重在途,先收集再发布。未合入 main。
已合入 origin/main
0c9b459。落地提交bd550f3fix: 回执按连接去重在途且不改可重复约定 (#33)。