test(channelbridge): 窗口夹具按截止时刻推钟 + 去掉无法证实的替身锁(M33 r1 复审)

P1-1(channelbridge 的 -race 门仍红,红在用例不确定性而不是竞态):
- coalesceTimers 改成**截止时刻**语义:每次 after(d) 记 at = 注入时钟 + d,
  推钟只把时钟推进到「当前最早的截止时刻」再触发它。旧写法按 FIFO 逐个触发并
  按请求时长累加,实测 12 条消息内攒到 5 个废弃定时器把时钟推 +10s(窗口 2s、
  绝对上限 6s)⇒ 循环头走「到点直接发车、不 takeAll」的分支,队列尾部落到下一批。
- 推钟前强制「车道排空」这条不变量(pump/advanceAndFire/waitIdle):
  时钟只表示时间过去,它一转就可能跳过 takeAll,因此先等 worker 把货收进本批。
- sendSettled:投递后按**序号**等「这次投递之后新挂的定时器」(旧的都已被 worker
  放弃、再没人监听),供「投递与推钟交替」的用例(每 500ms 一条的绝对上限用例)。
- fireLatest/advanceAndFire 带 done 谓词重试,避免「登记与触发之间续臂」导致
  推了钟却没人醒;waitIdle 一边等一边推窗口(worker 停在窗口里时永远走不到退休)。
- 用例侧只补相位与等待,不动断言:TwelveBurst 读历史前等 assistant 行落库;
  准入用例断言「最新一次续臂」(原来按下标取 d[1],隐含臂数假设);
  stale 用例两条都投完再推时钟(车道不会在两条之间退休)。

P2-1(替身锁无法用变异证实)选 A:删掉 stubChatter/stubSessions 的锁与访问器,
退回原字段访问,并写清理由 —— 21 个用例全走同步便捷路径
(b.process → processFrom → runBatch → Loop.Run),DEV HEAD 上 -race 数十次跑
(含把锁删掉的变异)DATA RACE 恒为 0;若将来从车道 worker 触碰这些字段必须加锁
并补 -race 用例。

证据:go test -race -count=3 ./internal/services/channelbridge/ 连跑 6 轮
18/18 全绿、DATA RACE=0;coalesce 子集 -count=80 全绿;整包 -race -count=20 全绿;
全树 go build/vet/test -count=1 全绿。
This commit is contained in:
AskaEth committed 2026-10-24 23:18:45 +08:00
1 parent 7d9b2132bb
commit 1ae9158250
20 files changed
+584 -371

No files matched your search

@@ -204,6 +204,18 @@ func (c *laneClock) advance(d time.Duration) {
c.mu.Unlock()
}
// advanceTo 把时钟推到 t(只前进不后退)。
//
// 窗口夹具用它表达「时间走到了某个截止时刻」:推进量 = 真的过了多久,
// 与「按请求时长累加」相比不会把时钟推过头(见 coalesceTimers 的说明)。
func (c *laneClock) advanceTo(t time.Time) {
c.mu.Lock()
if t.After(c.t) {
c.t = t
}
c.mu.Unlock()
}
// laneRecorder 收集 onRejected 审计(并发安全)。
type laneRecorder struct {
mu sync.Mutex
@@ -379,8 +391,13 @@ func TestLaneDropsStaleInboundAndTracesIt(t *testing.T) {
clock := newLaneClock()
rec := &laneRecorder{}
chat := &laneChatter{}
// 两条都投递完再让时钟越过阈值:第一轮**卡住**等这个信号,车道因此不会在两条之间
// 退休(退休后第二条会进一条新车,arrivedAt 取的是推进后的时钟 ⇒ 它永远不超龄,
// 用例假红)。这不是放宽断言 —— 「第二条超龄被丢」仍然由 stats/审计/日志三处断言。
bothQueued := make(chan struct{})
chat.onRun = func(user string) {
if user == "first" {
<-bothQueued
// 第一轮跑着的时候时间越过阈值:排在后面的那条等到自己开跑时已超龄。
clock.advance(defaultLaneStaleAfter + time.Second)
}
@@ -397,6 +414,7 @@ func TestLaneDropsStaleInboundAndTracesIt(t *testing.T) {
b.onMessage(context.Background(), strangerMsg("200", "first"), ad)
b.onMessage(context.Background(), strangerMsg("200", "stale by the time it runs"), ad)
close(bothQueued)
chat.waitRuns(t, 1, 5*time.Second)
waitLaneIdle(t, b, 2*time.Second)