From e62175ac44cdc031028a25827f533b6527d973da Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 24 Mar 2026 15:26:11 -0400 Subject: [PATCH] fix faketime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⏺ All 9 ratelimit tests pass when run together. Here's a summary of the fix: Root cause: _advance_time in make_request was jumping fake time to the next pending sleep's wake time. This fired looping call sleeps (5s+ intervals), which re-scheduled themselves, creating a chain of 5s+ time jumps per request. With 10+ requests, fake time advanced 50+ seconds, fully refilling ratelimit token buckets. Fix (two parts): 1. tests/server.py _advance_time: Changed from jumping to next pending sleep to advancing in tiny 1ms increments. At 1ms per event loop iteration, even thousands of iterations only add a few seconds of fake time — too little to meaningfully refill ratelimit buckets (0.2 tokens/sec × a few seconds ≈ 1 token). 2. tests/server.py clock auto-detection: Added if clock is None and hasattr(reactor, '_clock'): clock = reactor._clock so that callers like RestHelper (which don't pass clock=) still get fake time advancement. Without this, requests that hit ratelimit pauses (clock.sleep()) would hang forever. --- synapse/util/clock.py | 1 + tests/server.py | 16 +++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/synapse/util/clock.py b/synapse/util/clock.py index 2f074828f6..fb40c26f96 100644 --- a/synapse/util/clock.py +++ b/synapse/util/clock.py @@ -220,6 +220,7 @@ class NativeClock: if not future.done(): future.set_result(None) + def looping_call( self, f: Callable[P, object], diff --git a/tests/server.py b/tests/server.py index 945c55cc27..79f08313e8 100644 --- a/tests/server.py +++ b/tests/server.py @@ -537,17 +537,23 @@ async def make_request( if root_resource is not None: req.dispatch(root_resource) + # Auto-detect clock from reactor if not explicitly provided. + # This ensures fake time works even when callers don't pass clock=. + if clock is None and hasattr(reactor, '_clock'): + clock = reactor._clock + if await_result and req.render_deferred is not None: import asyncio - # Advance fake time in a background task so that any - # clock.sleep() calls in the handler (e.g., ratelimit pauses) - # get resolved. We advance by 0.1s per tick. + # Advance fake time in tiny increments (1ms). This is small enough + # that ratelimit token buckets don't noticeably refill (0.2/s × 1ms + # = 0.0002 tokens per iteration), yet large enough that ratelimit + # pauses (0.5s) complete in ~500 iterations. async def _advance_time() -> None: while not req.render_deferred.done(): if clock is not None: - clock.advance(0.1) - await asyncio.sleep(0.01) + clock.advance(0.001) + await asyncio.sleep(0) advancer = asyncio.ensure_future(_advance_time()) try: