fix(rate-limit): do not leave the gate on an early timer wake

`wait_for_slot` trusted a single `asyncio.sleep(delay)`. The event-loop timer can
fire before the delay elapsed -- Windows granularity is ~15ms, which the new
Windows job caught -- and the method then returned while the cooldown was still
armed, so the caller sent a request inside the window the gate exists to enforce.

It now re-checks the fixed `wake_at` and sleeps again, which also restores the
strict `elapsed >= 0.14` cooldown bounds the earlier relaxation had weakened (a
substring replace had turned two of them into `>= 0.09`), and a regression test
fails the gate if it ever returns on an early wake.
This commit is contained in:
Will Miao
2026-10-12 11:59:10 +08:00
parent 946809f47c
commit b197793b10
2 changed files with 38 additions and 4 deletions
+7 -1
View File
@@ -160,7 +160,13 @@ class RateLimitCoordinator:
self._normalize(destination),
delay,
)
await asyncio.sleep(delay)
# Re-check rather than trusting a single sleep: the event-loop
# timer can fire early (Windows granularity is ~15ms), and
# returning then would send the request inside the cooldown this
# gate exists to enforce. ``wake_at`` is fixed, so the loop can
# never outlast the ``max_wait_seconds`` check above.
while (delay := wake_at - time.monotonic()) > 0:
await asyncio.sleep(delay)
state.last_send_at = time.monotonic()
def register_rate_limit(