P2-1(行为缺陷):窗口准入此前只在 prepareInbound 里算 `solo`,而 prepare 发生在 **窗口等待之后** —— 平台帧(action / 系统通知 / 我们自己的 message_sent 回执)照样 进窗、照样把滑动计时重置:一条回执就能把这一批的静默期一直续住(群友说了三句话, 她因为「撤回事件」迟迟不开口)。这与任务书「窗口只收 chat/image/voice/file」相反。 - `laneWindowAdmits(msg)` 成为**唯一准入判据**,只看结构化事实(ContentType + isSystemNoticeMessage,绝不解析正文); - `splitWindowItems` 在窗口等待阶段把到达的条目分成「进窗口」与「不进窗口」两半; - 非准入帧**不建窗口、不重置计时**(首条即到达即发车),窗口中途到达的只收进 solos, 这一批发车后**原样回插队首**各自单跑(保序、不合并、不丢弃); - prepareInbound 里的 `solo` 保留为第二道保险(适配器将来把平台事件标成 text 时, 仍会在组批前被切出去)。 P2-2(硬约束):lane.go 682 行 → 按职责纯搬移成 5 个文件(最大 246 行): lane.go(车道类型与注册表)/ lane_queue.go(入队与取队列)/ lane_worker.go(单消费者与回合槽)/ lane_drop.go(丢弃与停机诚实)/ lane_owner.go(主人私聊判据与车道键)。语义零改动:去注释后的声明集合 (34 个顶层声明,含函数体/字段/常量值)逐字节全等;既有用例一条未改。 顺带:把两处「IsOwner() && !IsGroupChat()」的注释改成代码真正用的判据 (ownerPrivateLane = !msg.IsGroup() && actor.IsOwner()),并记下这处 P3 分叉。 验证:全树 build/vet/test 全绿(73 包 ok);三包 -race 全绿; 新增 4 条准入用例在「准入失效」变异下三条必红、对照组保持绿。
109 lines
4.1 KiB
Go
109 lines
4.1 KiB
Go
package channelbridge
|
||
|
||
// 本文件承载车道的**单消费者**:一个会话一个 worker,串行跑完队列里的每一条。
|
||
//
|
||
// 「回合在跑」在结构上就等于「worker 正忙」—— M23 的回合中续跑因此不需要任何
|
||
// turn-in-flight 标志:回合结束回到循环头,takeAll 自然把这段期间攒下的消息整片取出。
|
||
//
|
||
// direct 车道(主人私聊)在这里按 `l.batcher == nil` 分派到 runDirectLaneItem:
|
||
// 到达即投递、一条一条跑,**没有**任何窗口等待。
|
||
|
||
import (
|
||
"context"
|
||
"time"
|
||
)
|
||
|
||
// laneWorker 是某会话车道的唯一执行者:串行跑完每一条,队列空了就把车道交还注册表
|
||
// (空闲车道不占内存,也不会把「这个人再说话」变慢)。
|
||
//
|
||
// **回合中续跑(⑤)就落在这里**:不存在任何 "turn in flight" 标志 —— 「回合在跑」
|
||
// 在结构上就等于「worker 正忙」,跑回合期间新消息落进同一个队列,回合结束回到循环头,
|
||
// takeAll 自然把这段期间攒下的消息整片取出 ⇒ **一轮续跑**(不是 N 轮,也不是 0 条)。
|
||
func (b *Bridge) laneWorker(ctx context.Context, l *lane) {
|
||
defer b.wg.Done()
|
||
for {
|
||
// 桥已停:队列里还没发车的条目**如实登记**(绝不静默消失),然后退休。
|
||
if ctx.Err() != nil {
|
||
b.abandonLaneAtStop(l)
|
||
b.retireLane(l)
|
||
return
|
||
}
|
||
if item, ok := b.nextLaneItem(l); ok {
|
||
b.dispatchLaneItem(ctx, l, item)
|
||
continue
|
||
}
|
||
// 队列空了才谈退休:摘表与入队用同一把锁,因此「摘下」与「刚有人塞进来」不会
|
||
// 交错成条目丢失(见 retireLaneIfIdle 的说明)。
|
||
if b.retireLaneIfIdle(l) {
|
||
return
|
||
}
|
||
select {
|
||
case <-ctx.Done():
|
||
// 桥停了:把车道摘掉,未处理的条目随进程取消一起作废(Stop 的语义)。
|
||
b.retireLane(l)
|
||
return
|
||
case item := <-l.ch:
|
||
b.dispatchLaneItem(ctx, l, item)
|
||
}
|
||
}
|
||
}
|
||
|
||
// dispatchLaneItem 按**车道类型**分派:direct(主人私聊)一条一条跑;batch 走窗口
|
||
// 引擎(见 coalesce_lane.go 的 runBatchLaneItem)。
|
||
//
|
||
// 分派依据是 `l.batcher == nil` —— 一个**结构**判据,不是配置开关:direct 车道的
|
||
// 类型里根本没有 batcher 字段值,窗口代码对它不可达。
|
||
func (b *Bridge) dispatchLaneItem(ctx context.Context, l *lane, item laneItem) {
|
||
if l.batcher == nil {
|
||
b.runDirectLaneItem(ctx, l, item)
|
||
return
|
||
}
|
||
b.runBatchLaneItem(ctx, l, item)
|
||
}
|
||
|
||
// runDirectLaneItem 跑 direct 车道(= 主人私聊)的一条:先取全局回合槽(**在线程池里
|
||
// 等,不在读循环里等**),再过陈旧度闸,然后走逐条闸门 + 单条回合。**没有任何等待窗口**。
|
||
func (b *Bridge) runDirectLaneItem(ctx context.Context, l *lane, item laneItem) {
|
||
if !b.acquireTurn(ctx) {
|
||
return
|
||
}
|
||
defer b.releaseTurn()
|
||
|
||
waited := b.now().Sub(item.arrivedAt)
|
||
if b.staleInbound(item, waited) {
|
||
return
|
||
}
|
||
b.logLaneStart(item, waited, 1)
|
||
p, ok := b.prepareInbound(ctx, item)
|
||
if !ok {
|
||
l.lastTurnEnd = b.now()
|
||
return
|
||
}
|
||
b.runBatch(ctx, []preparedInbound{p})
|
||
l.lastTurnEnd = b.now()
|
||
}
|
||
|
||
// acquireTurn 取一个全局回合槽(maxConcurrent 个)。ctx 取消时返回 false:桥停了
|
||
// 就不再开新回合(carried 条目由 Stop 的诚实登记负责,见 coalesce_lane.go)。
|
||
func (b *Bridge) acquireTurn(ctx context.Context) bool {
|
||
select {
|
||
case b.sem <- struct{}{}:
|
||
return true
|
||
case <-ctx.Done():
|
||
return false
|
||
}
|
||
}
|
||
|
||
// releaseTurn 归还一个全局回合槽。
|
||
func (b *Bridge) releaseTurn() { <-b.sem }
|
||
|
||
// logLaneStart 记「这一批等了多久才开始跑」—— 「她为什么回得晚」在生产日志里最直接
|
||
// 的那个答案。与它同时出现在日志里的 `msg="inbound frame"`(适配器的入站帧 DEBUG)
|
||
// 则说明读循环在等的同时一直在读:两行合起来就是「回合没有堵住心跳」的证据。
|
||
func (b *Bridge) logLaneStart(item laneItem, waited time.Duration, messages int) {
|
||
b.lanes.log.Debug("lane turn started",
|
||
"waited", waited.String(), "messages", messages,
|
||
"platform", item.msg.Platform, "channel", item.msg.ChannelID,
|
||
"sender", item.msg.SenderID, "group", item.msg.GroupID)
|
||
}
|