fix(test): 去抖 e2e 等任务真跑完,不再猜 sleep 时长 #85

Merged
Yushu merged 1 commit from fix/flaky-debounce-e2e-timing into main 2026-07-27 07:57:27 +00:00
Member

What

dispatch_and_settle(去抖 e2e 的派发辅助)原先派发完事件后真实 sleep(0.2),赌后台 flush 任务能在这个固定余量内跑完。去抖窗口本身只有 0.05s(.env.test),但任务跑完还要走门控 + 整个 Runtime Loop —— 机器负载高时这个余量不够,表现为满负载跑整套测试时 test_debounce_e2e.py 两条偶发失败(-n auto 下可复现,与 issue #78 无关的既有 flake)。

How

改为直接 await 本次派发新排队的那些 _pending_flushes 任务,而不是猜一个「应该够长了」的时长。

识别「新任务」按同一 chat_id 上的 task 对象有没有换过 —— _handle_reactive_message 里 cancel 旧任务与替换字典值是同一步,所以被 cancel 的那些永远不会落进待等列表。这正是去抖语义:同 chat 连发多条时只有最后一个任务该被等。同 test_immediate_followup_e2e.py 里「直接等那个 task」的既有先例。

两处细节:

  • asyncio.wait 的 timeout(10s)只是防挂死兜底,不是「等这么久」—— 正常负载波动不可能触发,真被触发就说明是死锁/任务卡住这类真问题,报错比静默超时有用。
  • task.result() 传播后台异常 —— asyncio.wait 本身会吞掉它们,而后台任务里 should_call_api 不匹配之类的错误直接抛出来,比等到 __aexit__ 报一句「还有期望没被消费」更能指出真正的失败点。

Tests

全仓 1097 通过。串行跑三次 + -n auto 并行满载各一次,均全绿(并行满载正是先前复现失败的条件)。

顺带:整套测试从 ~25s 降到 ~17s,不再无谓等待。

范围

纯测试基建,不碰任何生产行为。

## What `dispatch_and_settle`(去抖 e2e 的派发辅助)原先派发完事件后真实 `sleep(0.2)`,赌后台 flush 任务能在这个固定余量内跑完。去抖窗口本身只有 0.05s(`.env.test`),但任务跑完还要走门控 + 整个 Runtime Loop —— 机器负载高时这个余量不够,表现为满负载跑整套测试时 `test_debounce_e2e.py` 两条偶发失败(`-n auto` 下可复现,与 issue #78 无关的既有 flake)。 ## How 改为直接 await 本次派发新排队的那些 `_pending_flushes` 任务,而不是猜一个「应该够长了」的时长。 识别「新任务」按同一 `chat_id` 上的 task 对象有没有换过 —— `_handle_reactive_message` 里 cancel 旧任务与替换字典值是同一步,所以被 cancel 的那些永远不会落进待等列表。这正是去抖语义:同 chat 连发多条时只有最后一个任务该被等。同 `test_immediate_followup_e2e.py` 里「直接等那个 task」的既有先例。 两处细节: - `asyncio.wait` 的 timeout(10s)只是防挂死兜底,不是「等这么久」—— 正常负载波动不可能触发,真被触发就说明是死锁/任务卡住这类真问题,报错比静默超时有用。 - 补 `task.result()` 传播后台异常 —— `asyncio.wait` 本身会吞掉它们,而后台任务里 `should_call_api` 不匹配之类的错误直接抛出来,比等到 `__aexit__` 报一句「还有期望没被消费」更能指出真正的失败点。 ## Tests 全仓 1097 通过。串行跑三次 + `-n auto` 并行满载各一次,均全绿(并行满载正是先前复现失败的条件)。 顺带:整套测试从 ~25s 降到 ~17s,不再无谓等待。 ## 范围 纯测试基建,不碰任何生产行为。
`dispatch_and_settle` 原先派发完事件后真实 `sleep(0.2)`,赌后台 flush 任务能在
这个固定余量内跑完。去抖窗口本身只有 0.05s(.env.test),但任务跑完还要走门控 +
整个 Runtime Loop——机器负载高时这个余量不够,表现为满负载跑整套测试时
`test_debounce_e2e.py` 两条偶发失败(`-n auto` 下可复现)。

改为直接 await 本次派发新排队的那些 `_pending_flushes` 任务。识别"新任务"按
同一 chat_id 上的 task 对象有没有换过——`_handle_reactive_message` 里 cancel 旧
任务与替换字典值是同一步,所以被 cancel 的那些永远不会落进待等列表(这正是去抖
语义:同 chat 连发多条时只有最后一个任务该被等)。同
`test_immediate_followup_e2e.py` 里"直接等那个 task"的既有先例。

`asyncio.wait` 的 timeout 只是防挂死兜底(10s,正常负载波动不可能触发),真被
触发就说明是死锁/卡住这类真问题。补 `task.result()` 传播后台异常——`asyncio.wait`
本身会吞掉它们,而后台任务里 `should_call_api` 不匹配之类的错误直接抛出来,比等
到 `__aexit__` 报一句"还有期望没被消费"更能指出真正的失败点。

顺带:整套测试从 ~25s 降到 ~17s(不再无谓等待)。全仓 1097 通过,串行跑三次 +
`-n auto` 并行满载各一次均全绿。
Yushu merged commit bc88e437ea into main 2026-07-27 07:57:27 +00:00
Yushu deleted branch fix/flaky-debounce-e2e-timing 2026-07-27 07:57:28 +00:00
Sign in to join this conversation.
No description provided.