From f5710dea3dd6a69050dc1cfe990c479b5658952b Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 24 Jun 2026 19:57:50 -0500 Subject: [PATCH 01/27] Try `FakeChannel.await_result(...)` changes on their own Split off from https://github.com/element-hq/synapse/pull/19878 --- tests/server.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/tests/server.py b/tests/server.py index ce5eaad63da..52b7dba7491 100644 --- a/tests/server.py +++ b/tests/server.py @@ -101,6 +101,7 @@ from synapse.storage.prepare_database import prepare_database from synapse.types import ISynapseReactor, JsonDict from synapse.util.clock import Clock +from synapse.util.duration import Duration from synapse.util.json import json_encoder from tests.utils import ( @@ -301,15 +302,59 @@ def transport(self) -> "FakeChannel": def await_result(self, timeout_ms: int = 1000) -> None: """ Wait until the request is finished. + + Advances the Twisted reactor clock by 0.1s and suspending execution of the + Python thread (to allow other threads to do work) in a loop until we see a + result. We timeout when both the Twisted reactor clock has been advanced enough + AND we've waited the same amount of in real-time for the specified timeout + before giving up. + + The loop 1) allows `clock.call_later` scheduled callbacks to run if they are + scheduled to run now and 2) will also allow other threads to make progress. This + could be things spawned on the Twisted reactor threadpool or Tokio runtime + (async Rust code). """ - end_time = self._reactor.seconds() + timeout_ms / 1000.0 + timeout = Duration(milliseconds=timeout_ms) + start_time_seconds = self._reactor.seconds() + start_real_time_seconds = time.time() + + # TODO: Why? self._reactor.run() while not self.is_finished(): - if self._reactor.seconds() > end_time: + if ( + # Exceeded the Twisted reactor time timeout + start_time_seconds + timeout.as_secs() < self._reactor.seconds() + # And exceeded the real-time timeout + and start_real_time_seconds + timeout.as_secs() < time.time() + ): raise TimedOutException("Timed out waiting for request to finish.") - self._reactor.advance(0.1) + # Suspend execution of this thread to allow other threads to do work. This + # could be things spawned on the Twisted reactor threadpool or Tokio thread + # pool (async Rust code). + # + # Note: Since we're waiting real-time (`timeout` duration), the tests also + # pass with `time.sleep(0)` commented out because Python has a default + # thread switch interval (5ms for cpython) (see + # `sys.setswitchinterval(interval)`). We still want this here as we're able + # to preempt and cause the thread context swtich to happen faster. + time.sleep(0) + + # Advance the Twisted reactor and run any scheduled callbacks + # + # Don't advance the Twisted reactor clock further than the timeout duration + # as someone should increase the timeout if they expect things to take + # longer. + if start_time_seconds + timeout.as_secs() > self._reactor.seconds(): + self._reactor.advance(0.1) + else: + # But we want to still keep running whatever might be getting scheduled + # to run now. + # + # For example from other threads, they may have scheduled something on + # the reactor to run (like `reactor.callFromThread(...)`) + self._reactor.advance(0) def extract_cookies(self, cookies: MutableMapping[str, str]) -> None: """Process the contents of any Set-Cookie headers in the response From bb9e266454d39a0f63345f897924c7d22812ac79 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 24 Jun 2026 19:59:21 -0500 Subject: [PATCH 02/27] Add changelog --- changelog.d/19879.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/19879.misc diff --git a/changelog.d/19879.misc b/changelog.d/19879.misc new file mode 100644 index 00000000000..83760993ca1 --- /dev/null +++ b/changelog.d/19879.misc @@ -0,0 +1 @@ +Stub. From 0fb37223cf8323bc9fde2ca148329a65aba74236 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 24 Jun 2026 21:13:23 -0500 Subject: [PATCH 03/27] Fix infinite loop getting the tests stuck in CI --- tests/server.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/server.py b/tests/server.py index 52b7dba7491..91d38bc3923 100644 --- a/tests/server.py +++ b/tests/server.py @@ -324,9 +324,13 @@ def await_result(self, timeout_ms: int = 1000) -> None: while not self.is_finished(): if ( # Exceeded the Twisted reactor time timeout - start_time_seconds + timeout.as_secs() < self._reactor.seconds() + # + # We use `>=` for the reactor time condition as it's possible we advance + # exactly the `timeout` amount and we don't want to get stuck in an + # infinite loop + self._reactor.seconds() >= start_time_seconds + timeout.as_secs() # And exceeded the real-time timeout - and start_real_time_seconds + timeout.as_secs() < time.time() + and time.time() > start_real_time_seconds + timeout.as_secs() ): raise TimedOutException("Timed out waiting for request to finish.") @@ -346,7 +350,7 @@ def await_result(self, timeout_ms: int = 1000) -> None: # Don't advance the Twisted reactor clock further than the timeout duration # as someone should increase the timeout if they expect things to take # longer. - if start_time_seconds + timeout.as_secs() > self._reactor.seconds(): + if self._reactor.seconds() < start_time_seconds + timeout.as_secs(): self._reactor.advance(0.1) else: # But we want to still keep running whatever might be getting scheduled From c10f8cb3b3605b2d9f9e6e6538a7e444ee2ce290 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 25 Jun 2026 10:24:38 -0500 Subject: [PATCH 04/27] Fix typo --- tests/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/server.py b/tests/server.py index 91d38bc3923..dcaa58bfc4a 100644 --- a/tests/server.py +++ b/tests/server.py @@ -306,7 +306,7 @@ def await_result(self, timeout_ms: int = 1000) -> None: Advances the Twisted reactor clock by 0.1s and suspending execution of the Python thread (to allow other threads to do work) in a loop until we see a result. We timeout when both the Twisted reactor clock has been advanced enough - AND we've waited the same amount of in real-time for the specified timeout + AND we've waited the same amount of real-time for the specified timeout before giving up. The loop 1) allows `clock.call_later` scheduled callbacks to run if they are From 0281dd2b2cd6925117d45fe172f046a4f437ff11 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 25 Jun 2026 16:14:40 -0500 Subject: [PATCH 05/27] `time.sleep(0.001)` after a few loops Same thing we did in https://github.com/element-hq/synapse/pull/19871 --- tests/server.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/server.py b/tests/server.py index dcaa58bfc4a..2d92d71bdfe 100644 --- a/tests/server.py +++ b/tests/server.py @@ -321,6 +321,7 @@ def await_result(self, timeout_ms: int = 1000) -> None: # TODO: Why? self._reactor.run() + loop_count = 0 while not self.is_finished(): if ( # Exceeded the Twisted reactor time timeout @@ -343,7 +344,16 @@ def await_result(self, timeout_ms: int = 1000) -> None: # thread switch interval (5ms for cpython) (see # `sys.setswitchinterval(interval)`). We still want this here as we're able # to preempt and cause the thread context swtich to happen faster. - time.sleep(0) + # + # After a few cycles, we use `time.sleep(0.001)` instead of `time.sleep(0)` + # to avoid tightlooping on the main thread (CPU 100%) because it's wasteful + # and may starve out other threads. 10 is arbitrary but many cases will have + # none or only a few round-trips so we can just try to go as fast as + # posssible. + if loop_count < 10: + time.sleep(0) + else: + time.sleep(0.001) # Advance the Twisted reactor and run any scheduled callbacks # @@ -360,6 +370,8 @@ def await_result(self, timeout_ms: int = 1000) -> None: # the reactor to run (like `reactor.callFromThread(...)`) self._reactor.advance(0) + loop_count += 1 + def extract_cookies(self, cookies: MutableMapping[str, str]) -> None: """Process the contents of any Set-Cookie headers in the response From 842fd5959410ed95d0b7a78f4f9db11126aa7c68 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 25 Jun 2026 16:15:09 -0500 Subject: [PATCH 06/27] Use 1s real-time instead of `timeout_ms` arg --- tests/server.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/server.py b/tests/server.py index 2d92d71bdfe..2069565a147 100644 --- a/tests/server.py +++ b/tests/server.py @@ -306,16 +306,26 @@ def await_result(self, timeout_ms: int = 1000) -> None: Advances the Twisted reactor clock by 0.1s and suspending execution of the Python thread (to allow other threads to do work) in a loop until we see a result. We timeout when both the Twisted reactor clock has been advanced enough - AND we've waited the same amount of real-time for the specified timeout - before giving up. + AND we've waited the 1s of real-time before giving up. The loop 1) allows `clock.call_later` scheduled callbacks to run if they are scheduled to run now and 2) will also allow other threads to make progress. This could be things spawned on the Twisted reactor threadpool or Tokio runtime (async Rust code). + + Args: + timeout_ms: The Twisted reactor time we wait until we raise a `TimedOutException` """ timeout = Duration(milliseconds=timeout_ms) start_time_seconds = self._reactor.seconds() + + # 1s is an arbitrary small number so we don't have to wait that long when + # something is stuck and because we assume any task on another thread will be + # fast enough. + # + # We don't use the same `timeout_ms` passed in because some tests specify 20s + # and we don't want to be waiting that long unnecessarily. + real_time_timeout = Duration(seconds=1) start_real_time_seconds = time.time() # TODO: Why? @@ -331,7 +341,7 @@ def await_result(self, timeout_ms: int = 1000) -> None: # infinite loop self._reactor.seconds() >= start_time_seconds + timeout.as_secs() # And exceeded the real-time timeout - and time.time() > start_real_time_seconds + timeout.as_secs() + and time.time() > start_real_time_seconds + real_time_timeout.as_secs() ): raise TimedOutException("Timed out waiting for request to finish.") From 3565a7fa53e0c3d84cdba4b6506e4d3cdd2d80c6 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 25 Jun 2026 16:18:44 -0500 Subject: [PATCH 07/27] Fix `tests.rest.client.sliding_sync.test_sliding_sync.SlidingSyncTestCase_new.test_wait_for_sync_token` --- tests/rest/client/sliding_sync/test_sliding_sync.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/rest/client/sliding_sync/test_sliding_sync.py b/tests/rest/client/sliding_sync/test_sliding_sync.py index 2fd18f0e545..c43ed650174 100644 --- a/tests/rest/client/sliding_sync/test_sliding_sync.py +++ b/tests/rest/client/sliding_sync/test_sliding_sync.py @@ -566,7 +566,9 @@ def test_wait_for_sync_token(self) -> None: # timeout with self.assertRaises(TimedOutException): channel.await_result(timeout_ms=9900) - channel.await_result(timeout_ms=200) + # `notifier.wait_for_stream_token(from_token)` only checks every 500ms so we + # need to match that in order to make sure we hit the wake-up for sure. + channel.await_result(timeout_ms=500) self.assertEqual(channel.code, 200, channel.json_body) # We expect the next `pos` in the result to be the same as what we requested From 7c19a47448e56d8909a2515162855f7dba3eb1c1 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 25 Jun 2026 16:37:25 -0500 Subject: [PATCH 08/27] Update changelog --- changelog.d/19879.misc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/19879.misc b/changelog.d/19879.misc index 83760993ca1..be10ee05403 100644 --- a/changelog.d/19879.misc +++ b/changelog.d/19879.misc @@ -1 +1 @@ -Stub. +Update `HomeserverTestCase.get_success(...)` and friends to drive async Rust (Tokio runtime/thread pool). From 81cbffa4e3df4bfd70934c4437ae2d64a1eaeae6 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 6 Jul 2026 15:32:35 -0500 Subject: [PATCH 09/27] More clear explanation --- tests/rest/client/sliding_sync/test_sliding_sync.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/rest/client/sliding_sync/test_sliding_sync.py b/tests/rest/client/sliding_sync/test_sliding_sync.py index c43ed650174..db1d4ac9c91 100644 --- a/tests/rest/client/sliding_sync/test_sliding_sync.py +++ b/tests/rest/client/sliding_sync/test_sliding_sync.py @@ -564,8 +564,13 @@ def test_wait_for_sync_token(self) -> None: ) # Block for 10 seconds to make `notifier.wait_for_stream_token(from_token)` # timeout + # + # First, block for *almost* 10 seconds to make sure we are + # `notifier.wait_for_stream_token(from_token)` with self.assertRaises(TimedOutException): channel.await_result(timeout_ms=9900) + # Then wait for the rest of the 10 second timeout, 9900 + 500 > 10000 + # # `notifier.wait_for_stream_token(from_token)` only checks every 500ms so we # need to match that in order to make sure we hit the wake-up for sure. channel.await_result(timeout_ms=500) From c349a33ae3050f8969bbf311af31830bc2416e71 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 6 Jul 2026 16:20:32 -0500 Subject: [PATCH 10/27] Explain why we `advance(0)` right away --- tests/server.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/server.py b/tests/server.py index 92283eecffb..f8b83140173 100644 --- a/tests/server.py +++ b/tests/server.py @@ -331,6 +331,17 @@ def await_result(self, timeout_ms: int = 1000) -> None: # TODO: Why? self._reactor.run() + # First, get the request started before we start looping and advancing non-zero + # time increments. + # + # Without this, if some request handler had a + # `self.hs.get_clock().sleep(Duration(seconds=1))`, and called + # `channel.await_result(timeout_ms=1000)`, it wouldn't be called because the + # first `self._reactor.advance(0.1)` would be spent first just getting the + # request going, and it would only spend 0.9s in the handler (0.1s shy of the + # sleep finishing) so the request would timeout. + self._reactor.advance(0) + loop_count = 0 while not self.is_finished(): if ( From b64e0672727bdc32df184419ae3e195381d88efe Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 6 Jul 2026 16:27:06 -0500 Subject: [PATCH 11/27] Remove real-time timeout --- tests/server.py | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/tests/server.py b/tests/server.py index f8b83140173..63327e80cbc 100644 --- a/tests/server.py +++ b/tests/server.py @@ -306,7 +306,8 @@ def await_result(self, timeout_ms: int = 1000) -> None: Advances the Twisted reactor clock by 0.1s and suspending execution of the Python thread (to allow other threads to do work) in a loop until we see a result. We timeout when both the Twisted reactor clock has been advanced enough - AND we've waited the 1s of real-time before giving up. + AND we've done at-least 100 iterations (round-trips for other threads to get + work done). The loop 1) allows `clock.call_later` scheduled callbacks to run if they are scheduled to run now and 2) will also allow other threads to make progress. This @@ -319,15 +320,6 @@ def await_result(self, timeout_ms: int = 1000) -> None: timeout = Duration(milliseconds=timeout_ms) start_time_seconds = self._reactor.seconds() - # 1s is an arbitrary small number so we don't have to wait that long when - # something is stuck and because we assume any task on another thread will be - # fast enough. - # - # We don't use the same `timeout_ms` passed in because some tests specify 20s - # and we don't want to be waiting that long unnecessarily. - real_time_timeout = Duration(seconds=1) - start_real_time_seconds = time.time() - # TODO: Why? self._reactor.run() @@ -351,8 +343,10 @@ def await_result(self, timeout_ms: int = 1000) -> None: # exactly the `timeout` amount and we don't want to get stuck in an # infinite loop self._reactor.seconds() >= start_time_seconds + timeout.as_secs() - # And exceeded the real-time timeout - and time.time() > start_real_time_seconds + real_time_timeout.as_secs() + # 100 loops is arbitrary. This also makes the assumption that any work + # on other threads will finish before we give up after sleeping ~0.1s of + # real-time (100 * 0.001). + and loop_count > 100 ): raise TimedOutException("Timed out waiting for request to finish.") @@ -360,11 +354,11 @@ def await_result(self, timeout_ms: int = 1000) -> None: # could be things spawned on the Twisted reactor threadpool or Tokio thread # pool (async Rust code). # - # Note: Since we're waiting real-time (`timeout` duration), the tests also - # pass with `time.sleep(0)` commented out because Python has a default - # thread switch interval (5ms for cpython) (see - # `sys.setswitchinterval(interval)`). We still want this here as we're able - # to preempt and cause the thread context swtich to happen faster. + # Note: Python has a default thread switch interval (5ms for cpython) (see + # `sys.setswitchinterval(interval)`) but we still want this here as we're + # able to preempt and cause the thread context swtich to happen faster. + # Also, without any real-time sleeping, this function would complete before + # the 5ms switch ever happened. # # After a few cycles, we use `time.sleep(0.001)` instead of `time.sleep(0)` # to avoid tightlooping on the main thread (CPU 100%) because it's wasteful From 7a1bd830a34a054f5a5762ab747bd3f3c863ff56 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 6 Jul 2026 17:33:16 -0500 Subject: [PATCH 12/27] Better reasoning Based one explanation from https://github.com/element-hq/synapse/pull/19913#discussion_r3532415025 --- tests/server.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/server.py b/tests/server.py index 63327e80cbc..fdf675cdf5f 100644 --- a/tests/server.py +++ b/tests/server.py @@ -323,15 +323,15 @@ def await_result(self, timeout_ms: int = 1000) -> None: # TODO: Why? self._reactor.run() - # First, get the request started before we start looping and advancing non-zero - # time increments. + # First, run anything that's scheduled now before we start looping and advancing + # non-zero time increments. # - # Without this, if some request handler had a + # Without this, if some request handler had some database queries followed by # `self.hs.get_clock().sleep(Duration(seconds=1))`, and called # `channel.await_result(timeout_ms=1000)`, it wouldn't be called because the - # first `self._reactor.advance(0.1)` would be spent first just getting the - # request going, and it would only spend 0.9s in the handler (0.1s shy of the - # sleep finishing) so the request would timeout. + # first `self._reactor.advance(0.1)` would be first spent driving the database + # queries, and only leaving 0.9s remaining (0.1s shy of the sleep finishing) so + # the request would timeout. self._reactor.advance(0) loop_count = 0 From e9d6aa3194bae1e88d08aa779d1ea31641ab3874 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 6 Jul 2026 17:34:58 -0500 Subject: [PATCH 13/27] Add more reasoning why --- tests/server.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/server.py b/tests/server.py index fdf675cdf5f..cfd82a6f4bd 100644 --- a/tests/server.py +++ b/tests/server.py @@ -332,6 +332,9 @@ def await_result(self, timeout_ms: int = 1000) -> None: # first `self._reactor.advance(0.1)` would be first spent driving the database # queries, and only leaving 0.9s remaining (0.1s shy of the sleep finishing) so # the request would timeout. + # + # The goal is to remove the foot-guns and having to think about this for the + # standard cases. self._reactor.advance(0) loop_count = 0 From 4507561b737e1e273fac1ac62c2bcc481aa1d2df Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 6 Jul 2026 18:04:20 -0500 Subject: [PATCH 14/27] Fix flawed `timestamp` check (when `0`) ``` Error: Traceback (most recent call last): File "/home/runner/work/synapse/synapse/tests/test_mau.py", line 74, in test_simple_deny_mau self.do_sync_for_user(token2) File "/home/runner/work/synapse/synapse/tests/test_mau.py", line 345, in do_sync_for_user raise HttpResponseException( synapse.api.errors.ProxiedRequestError: 403: Monthly Active User Limit Exceeded tests.test_mau.TestMauLimit.test_simple_deny_mau ``` --- synapse/api/auth_blocking.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/api/auth_blocking.py b/synapse/api/auth_blocking.py index 87918e15dca..1aed7f6de26 100644 --- a/synapse/api/auth_blocking.py +++ b/synapse/api/auth_blocking.py @@ -117,7 +117,7 @@ async def check_auth_blocking( # If the user is already part of the MAU cohort or a trial user if user_id: timestamp = await self.store.user_last_seen_monthly_active(user_id) - if timestamp: + if timestamp is not None: return is_trial = await self.store.is_trial_user(user_id) From 5c4a3b3b9da6593c549059a84168d4bf41d64f2c Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 6 Jul 2026 18:30:48 -0500 Subject: [PATCH 15/27] Advance by `CLOCK_SCHEDULE_EPSILON` See https://github.com/element-hq/synapse/pull/19879#discussion_r3532610015 --- tests/server.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/server.py b/tests/server.py index cfd82a6f4bd..e6ff686c593 100644 --- a/tests/server.py +++ b/tests/server.py @@ -100,7 +100,7 @@ from synapse.storage.engines import BaseDatabaseEngine, create_engine from synapse.storage.prepare_database import prepare_database from synapse.types import ISynapseReactor, JsonDict -from synapse.util.clock import Clock +from synapse.util.clock import CLOCK_SCHEDULE_EPSILON, Clock from synapse.util.duration import Duration from synapse.util.json import json_encoder @@ -335,7 +335,13 @@ def await_result(self, timeout_ms: int = 1000) -> None: # # The goal is to remove the foot-guns and having to think about this for the # standard cases. - self._reactor.advance(0) + # + # FIXME: Ideally, we'd advance by `0` but there is a handful of tests that + # assume that time advances in between requests and many requests complete from + # a single advance. For now, we'll just advance by minuscule amount of time. + # It's a balance between test convenience of this helper and materializing test + # expectations so we may never fix this. + self._reactor.advance(CLOCK_SCHEDULE_EPSILON.as_secs()) loop_count = 0 while not self.is_finished(): From 21af1ca3ea1d1242b24744c1f8464abca4177aa2 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 6 Jul 2026 18:54:19 -0500 Subject: [PATCH 16/27] Record `start_time_seconds` after previous one-off advance --- tests/server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/server.py b/tests/server.py index e6ff686c593..fc077feafdf 100644 --- a/tests/server.py +++ b/tests/server.py @@ -318,7 +318,6 @@ def await_result(self, timeout_ms: int = 1000) -> None: timeout_ms: The Twisted reactor time we wait until we raise a `TimedOutException` """ timeout = Duration(milliseconds=timeout_ms) - start_time_seconds = self._reactor.seconds() # TODO: Why? self._reactor.run() @@ -343,6 +342,8 @@ def await_result(self, timeout_ms: int = 1000) -> None: # expectations so we may never fix this. self._reactor.advance(CLOCK_SCHEDULE_EPSILON.as_secs()) + # We only count the looping time (record the start after we advance once above) + start_time_seconds = self._reactor.seconds() loop_count = 0 while not self.is_finished(): if ( From 62bb99a34fdd8691d7caa9ceb28e3730bde4f490 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 6 Jul 2026 18:59:35 -0500 Subject: [PATCH 17/27] We have to advance by 1ms --- tests/server.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/server.py b/tests/server.py index fc077feafdf..f5d4e995a56 100644 --- a/tests/server.py +++ b/tests/server.py @@ -337,10 +337,12 @@ def await_result(self, timeout_ms: int = 1000) -> None: # # FIXME: Ideally, we'd advance by `0` but there is a handful of tests that # assume that time advances in between requests and many requests complete from - # a single advance. For now, we'll just advance by minuscule amount of time. - # It's a balance between test convenience of this helper and materializing test - # expectations so we may never fix this. - self._reactor.advance(CLOCK_SCHEDULE_EPSILON.as_secs()) + # a single advance. Second best, we'd just advance by minuscule amount of time + # (`CLOCK_SCHEDULE_EPSILON`) but some tests assume at-least a millisecond in + # between as our timestamps are often recorded at the millisecond granularity + # (`origin_server_ts`, etc). It's a balance between test convenience of this + # helper and materializing test expectations so we may never fix this. + self._reactor.advance(Duration(milliseconds=1).as_secs()) # We only count the looping time (record the start after we advance once above) start_time_seconds = self._reactor.seconds() From 410710864c1b7a5379ad30bb4bdc2d5ab1c791e1 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 6 Jul 2026 19:13:08 -0500 Subject: [PATCH 18/27] `tests.rest.client.test_msc4388_rendezvous.RendezvousServletTestCase` doesn't care about expiration We don't need to assert exact values. These tests don't care about that --- tests/rest/client/test_msc4388_rendezvous.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/tests/rest/client/test_msc4388_rendezvous.py b/tests/rest/client/test_msc4388_rendezvous.py index e99aeee7a05..7536d68f3ee 100644 --- a/tests/rest/client/test_msc4388_rendezvous.py +++ b/tests/rest/client/test_msc4388_rendezvous.py @@ -175,13 +175,11 @@ def test_rendezvous_public(self) -> None: self.assertEqual(channel.code, 200) rendezvous_id = channel.json_body["id"] sequence_token = channel.json_body["sequence_token"] - expires_in_ms = channel.json_body["expires_in_ms"] - self.assertGreater(expires_in_ms, 0) + self.assertGreater(channel.json_body["expires_in_ms"], 0) session_endpoint = rz_endpoint + f"/{rendezvous_id}" # We can get the data back - # Advances clock by 100ms channel = self.make_request( "GET", session_endpoint, @@ -191,10 +189,9 @@ def test_rendezvous_public(self) -> None: self.assertEqual(channel.code, 200) self.assertEqual(channel.json_body["data"], "foo=bar") self.assertEqual(channel.json_body["sequence_token"], sequence_token) - self.assertEqual(channel.json_body["expires_in_ms"], expires_in_ms - 100) + self.assertGreater(channel.json_body["expires_in_ms"], 0) # We can update the data - # Advances clock by 100ms channel = self.make_request( "PUT", session_endpoint, @@ -221,7 +218,6 @@ def test_rendezvous_public(self) -> None: ) # We should get the updated data - # Advances clock by 100ms channel = self.make_request( "GET", session_endpoint, @@ -231,7 +227,7 @@ def test_rendezvous_public(self) -> None: self.assertEqual(channel.code, 200) self.assertEqual(channel.json_body["data"], "foo=baz") self.assertEqual(channel.json_body["sequence_token"], new_sequence_token) - self.assertEqual(channel.json_body["expires_in_ms"], expires_in_ms - 400) + self.assertGreater(channel.json_body["expires_in_ms"], 0) # We can delete the data channel = self.make_request( @@ -376,8 +372,7 @@ def test_rendezvous_requires_authentication(self) -> None: self.assertEqual(channel.code, 200) rendezvous_id = channel.json_body["id"] sequence_token = channel.json_body["sequence_token"] - expires_in_ms = channel.json_body["expires_in_ms"] - self.assertEqual(expires_in_ms, 120000) + self.assertGreater(channel.json_body["expires_in_ms"], 0) session_endpoint = rz_endpoint + f"/{rendezvous_id}" @@ -391,7 +386,7 @@ def test_rendezvous_requires_authentication(self) -> None: self.assertEqual(channel.code, 200) self.assertEqual(channel.json_body["data"], "foo=bar") self.assertEqual(channel.json_body["sequence_token"], sequence_token) - self.assertEqual(channel.json_body["expires_in_ms"], expires_in_ms - 100) + self.assertGreater(channel.json_body["expires_in_ms"], 0) # We can update the data without authentication channel = self.make_request( @@ -414,7 +409,7 @@ def test_rendezvous_requires_authentication(self) -> None: self.assertEqual(channel.code, 200) self.assertEqual(channel.json_body["data"], "foo=baz") self.assertEqual(channel.json_body["sequence_token"], new_sequence_token) - self.assertEqual(channel.json_body["expires_in_ms"], expires_in_ms - 300) + self.assertGreater(channel.json_body["expires_in_ms"], 0) # We can delete the data without authentication channel = self.make_request( From 15dd368a564443b9dd07fd506415b9b3c868685b Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 6 Jul 2026 19:14:01 -0500 Subject: [PATCH 19/27] Fix lints --- tests/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/server.py b/tests/server.py index f5d4e995a56..26b6bb7a871 100644 --- a/tests/server.py +++ b/tests/server.py @@ -100,7 +100,7 @@ from synapse.storage.engines import BaseDatabaseEngine, create_engine from synapse.storage.prepare_database import prepare_database from synapse.types import ISynapseReactor, JsonDict -from synapse.util.clock import CLOCK_SCHEDULE_EPSILON, Clock +from synapse.util.clock import Clock from synapse.util.duration import Duration from synapse.util.json import json_encoder From 609f7a0391b37450b508c90df3aedac01fc66ba8 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 6 Jul 2026 19:14:48 -0500 Subject: [PATCH 20/27] Remove extra comment --- tests/rest/client/test_msc4388_rendezvous.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/rest/client/test_msc4388_rendezvous.py b/tests/rest/client/test_msc4388_rendezvous.py index 7536d68f3ee..f9b7f578f63 100644 --- a/tests/rest/client/test_msc4388_rendezvous.py +++ b/tests/rest/client/test_msc4388_rendezvous.py @@ -204,7 +204,6 @@ def test_rendezvous_public(self) -> None: new_sequence_token = channel.json_body["sequence_token"] # If we try to update it again with the old etag, it should fail - # Advances clock by 100ms channel = self.make_request( "PUT", session_endpoint, From c08f96fb9dee0d70f0e07753975a26a825721de5 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 6 Jul 2026 19:27:48 -0500 Subject: [PATCH 21/27] No need to test specifics of sticky TTL in `/sync` --- tests/rest/client/test_sync_sticky_events.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/tests/rest/client/test_sync_sticky_events.py b/tests/rest/client/test_sync_sticky_events.py index 7a38debdb95..2e52a0ad167 100644 --- a/tests/rest/client/test_sync_sticky_events.py +++ b/tests/rest/client/test_sync_sticky_events.py @@ -96,10 +96,9 @@ def test_single_sticky_event_appears_in_initial_sync(self) -> None: sticky_event_id, f"Sticky event {sticky_event_id} not found in sync timeline", ) - self.assertEqual( + self.assertGreater( timeline_events[-1]["unsigned"][EventUnsignedContentFields.STICKY_TTL], - # The other 100 ms is advanced in FakeChannel.await_result. - 59_900, + 0, ) self.assertNotIn( @@ -129,7 +128,6 @@ def test_sticky_event_beyond_timeline_in_initial_sync(self) -> None: # that the /sync will get. regular_event_ids = [] for i in range(10): - # (Note: each one advances time by 100ms) response = self.helper.send( room_id=self.room_id, body=f"regular message {i}", @@ -138,7 +136,6 @@ def test_sticky_event_beyond_timeline_in_initial_sync(self) -> None: regular_event_ids.append(response["event_id"]) # Send another sticky event - # (Note: this advances time by 100ms) second_sticky_response = self.helper.send_sticky_event( self.room_id, EventTypes.Message, @@ -185,10 +182,9 @@ def test_sticky_event_beyond_timeline_in_initial_sync(self) -> None: f"Expected exactly 1 item in sticky events section, got {sticky_events}", ) self.assertEqual(sticky_events[0]["event_id"], first_sticky_event_id) - self.assertEqual( - # The 'missing' 1100 ms were elapsed when sending events + self.assertGreater( sticky_events[0]["unsigned"][EventUnsignedContentFields.STICKY_TTL], - 58_800, + 0, ) # Assertions for the second sticky event: should be only in timeline section @@ -197,10 +193,9 @@ def test_sticky_event_beyond_timeline_in_initial_sync(self) -> None: second_sticky_event_id, f"Second sticky event {second_sticky_event_id} not found in sync timeline", ) - self.assertEqual( + self.assertGreater( timeline_events[-1]["unsigned"][EventUnsignedContentFields.STICKY_TTL], - # The other 100 ms is advanced in FakeChannel.await_result. - 59_900, + 0, ) # (sticky section: we already checked it only has 1 item and # that item was the first above) From a7d6999989c73e3b9859e9c48fc326163a844580 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 6 Jul 2026 19:31:50 -0500 Subject: [PATCH 22/27] No need to test specifics of sticky TTL when sending events --- tests/rest/client/test_sticky_events.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/tests/rest/client/test_sticky_events.py b/tests/rest/client/test_sticky_events.py index a6e704fe8c2..015315a307b 100644 --- a/tests/rest/client/test_sticky_events.py +++ b/tests/rest/client/test_sticky_events.py @@ -58,7 +58,7 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: # Create a room self.room_id = self.helper.create_room_as(self.user_id, tok=self.token) - def _assert_event_sticky_for(self, event_id: str, sticky_ttl: int) -> None: + def _assert_event_sticky_for(self, event_id: str) -> None: channel = self.make_request( "GET", f"/rooms/{self.room_id}/event/{event_id}", @@ -75,10 +75,10 @@ def _assert_event_sticky_for(self, event_id: str, sticky_ttl: int) -> None: event["unsigned"], f"No {EventUnsignedContentFields.STICKY_TTL} field in {event_id}; event not sticky: {event}", ) - self.assertEqual( + self.assertGreater( event["unsigned"][EventUnsignedContentFields.STICKY_TTL], - sticky_ttl, - f"{event_id} had an unexpected sticky TTL: {event}", + 0, + f"{event_id} had an unexpected sticky TTL (expected some value greater than 0): {event}", ) def _assert_event_not_sticky(self, event_id: str) -> None: @@ -112,17 +112,15 @@ def test_sticky_event_via_event_endpoint(self) -> None: # If we request the event immediately, it will still have # 1 minute of stickiness - # The other 100 ms is advanced in FakeChannel.await_result. - self._assert_event_sticky_for(event_id, 59_900) + self._assert_event_sticky_for(event_id) - # But if we advance time by 59.799 seconds... - # we will get the event on its last millisecond of stickiness - # The other 100 ms is advanced in FakeChannel.await_result. - self.reactor.advance(59.799) - self._assert_event_sticky_for(event_id, 1) + # But if we advance time by 59 seconds... + # we will get the event on its last second of stickiness + self.reactor.advance(Duration(seconds=59).as_secs()) + self._assert_event_sticky_for(event_id) # Advancing time any more, the event is no longer sticky - self.reactor.advance(0.001) + self.reactor.advance(Duration(seconds=1).as_secs()) self._assert_event_not_sticky(event_id) From fbeb0cc38eaf7eb4848d080e2e85d00b8c5c918a Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 6 Jul 2026 19:42:53 -0500 Subject: [PATCH 23/27] Just use exact timing and move on --- tests/storage/test_client_ips.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/storage/test_client_ips.py b/tests/storage/test_client_ips.py index bd68f2aaa1e..fd138335f0e 100644 --- a/tests/storage/test_client_ips.py +++ b/tests/storage/test_client_ips.py @@ -782,7 +782,11 @@ def _runtest( device_id=device_id, ip=expected_ip, user_agent="Mozzila pizza", - last_seen=123456100, + # Note: The extra 1ms is from `make_request(...)` -> `await_result(...)` + # + # FIXME: This test shouldn't care about this internal detail (don't + # assert exact timing) + last_seen=123456001, ), r, ) From 595670410ff47a16279c669aec733277d8910860 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 7 Jul 2026 02:08:23 -0500 Subject: [PATCH 24/27] Fix requests made in `prepare` affecting `test_send_delayed_event_ratelimit` ``` [FAIL] Traceback (most recent call last): File "/home/runner/.cache/pypoetry/virtualenvs/matrix-synapse-pswDeSvb-py3.10/lib/python3.10/site-packages/parameterized/parameterized.py", line 620, in standalone_func return func(*(a + p.args), **p.kwargs, **kw) File "/home/runner/work/synapse/synapse/tests/rest/client/test_delayed_events.py", line 434, in test_send_delayed_event_ratelimit self.assertEqual(HTTPStatus.OK, channel.code, channel.result) File "/home/runner/.cache/pypoetry/virtualenvs/matrix-synapse-pswDeSvb-py3.10/lib/python3.10/site-packages/twisted/trial/_synctest.py", line 444, in assertEqual super().assertEqual(first, second, msg) File "/opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/unittest/case.py", line 845, in assertEqual assertion_func(first, second, msg=msg) File "/opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/unittest/case.py", line 838, in _baseAssertEqual raise self.failureException(msg) twisted.trial.unittest.FailTest: != 429 : {'version': b'1.1', 'code': b'429', 'reason': b'Unknown Status', 'headers': Headers({b'Server': [b'1'], b'Date': [b'Tue, 07 Jul 2026 00:20:42 GMT'], b'Retry-After': [b'1'], b'Content-Type': [b'application/json'], b'Cache-Control': [b'no-cache, no-store, must-revalidate'], b'Access-Control-Allow-Origin': [b'*'], b'Access-Control-Allow-Methods': [b'GET, HEAD, POST, PUT, DELETE, OPTIONS'], b'Access-Control-Allow-Headers': [b'X-Requested-With, Content-Type, Authorization, Date'], b'Access-Control-Expose-Headers': [b'Synapse-Trace-Id, Server']}), 'body': b'{"errcode":"M_LIMIT_EXCEEDED","error":"Too Many Requests","retry_after_ms":396}', 'done': True} tests.rest.client.test_delayed_events.DelayedEventsTestCase.test_send_delayed_event_ratelimit_0 tests.rest.client.test_delayed_events.DelayedEventsTestCase.test_send_delayed_event_ratelimit_1 ``` --- tests/rest/client/test_delayed_events.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/rest/client/test_delayed_events.py b/tests/rest/client/test_delayed_events.py index c3bfdf7c8d4..3173a90681b 100644 --- a/tests/rest/client/test_delayed_events.py +++ b/tests/rest/client/test_delayed_events.py @@ -26,6 +26,7 @@ from synapse.server import HomeServer from synapse.types import JsonDict from synapse.util.clock import Clock +from synapse.util.duration import Duration from tests import unittest from tests.server import FakeChannel @@ -90,6 +91,10 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: room=self.room_id, user=self.user2_user_id, tok=self.user2_access_token ) + # Advance enough time where any requests we made during `prepare(...)` doesn't + # affect the rate-limits in the test itself + self.reactor.advance(Duration(days=1).as_secs()) + def test_delayed_events_empty_on_startup(self) -> None: self.assertListEqual([], self._get_delayed_events()) From 0042368a414e1a2f8f3996e5ba138ec53986e548 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 7 Jul 2026 02:57:41 -0500 Subject: [PATCH 25/27] Fix `tests.federation.test_federation_sender.FederationSenderDevicesTestCases` ``` [FAIL] Traceback (most recent call last): File "/home/runner/work/synapse/synapse/tests/federation/test_federation_sender.py", line 790, in test_prune_outbound_device_pokes1 self.assertGreaterEqual(mock_send_txn.call_count, 4) File "/opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/unittest/case.py", line 1250, in assertGreaterEqual self.fail(self._formatMessage(msg, standardMsg)) twisted.trial.unittest.FailTest: 2 not greater than or equal to 4 tests.federation.test_federation_sender.FederationSenderDevicesTestCases.test_prune_outbound_device_pokes1 =============================================================================== [FAIL] Traceback (most recent call last): File "/home/runner/work/synapse/synapse/tests/federation/test_federation_sender.py", line 847, in test_prune_outbound_device_pokes2 self.assertGreaterEqual(mock_send_txn.call_count, 3) File "/opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/unittest/case.py", line 1250, in assertGreaterEqual self.fail(self._formatMessage(msg, standardMsg)) twisted.trial.unittest.FailTest: 2 not greater than or equal to 3 tests.federation.test_federation_sender.FederationSenderDevicesTestCases.test_prune_outbound_device_pokes2 =============================================================================== [FAIL] Traceback (most recent call last): File "/home/runner/work/synapse/synapse/tests/federation/test_federation_sender.py", line 743, in test_unreachable_server self.assertGreaterEqual(mock_send_txn.call_count, 4) File "/opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/unittest/case.py", line 1250, in assertGreaterEqual self.fail(self._formatMessage(msg, standardMsg)) twisted.trial.unittest.FailTest: 2 not greater than or equal to 4 tests.federation.test_federation_sender.FederationSenderDevicesTestCases.test_unreachable_server =============================================================================== [FAIL] Traceback (most recent call last): File "/home/runner/work/synapse/synapse/tests/federation/test_federation_sender.py", line 594, in test_upload_signatures self.assertEqual(len(self.edus), 2) File "/home/runner/.cache/pypoetry/virtualenvs/matrix-synapse-pswDeSvb-py3.10/lib/python3.10/site-packages/twisted/trial/_synctest.py", line 444, in assertEqual super().assertEqual(first, second, msg) File "/opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/unittest/case.py", line 845, in assertEqual assertion_func(first, second, msg=msg) File "/opt/hostedtoolcache/Python/3.10.20/x64/lib/python3.10/unittest/case.py", line 838, in _baseAssertEqual raise self.failureException(msg) twisted.trial.unittest.FailTest: 1 != 2 tests.federation.test_federation_sender.FederationSenderDevicesTestCases.test_upload_signatures ``` --- tests/federation/test_federation_sender.py | 88 ++++++++++++---------- 1 file changed, 50 insertions(+), 38 deletions(-) diff --git a/tests/federation/test_federation_sender.py b/tests/federation/test_federation_sender.py index ced98a8b004..2a9c2f0fc41 100644 --- a/tests/federation/test_federation_sender.py +++ b/tests/federation/test_federation_sender.py @@ -37,6 +37,7 @@ from synapse.storage.databases.main.events_worker import EventMetadata from synapse.types import JsonDict, ReadReceipt from synapse.util.clock import Clock +from synapse.util.duration import Duration from tests.unittest import HomeserverTestCase @@ -517,6 +518,24 @@ async def record_transaction( self.edus.extend(data["edus"]) return {} + def wait_for_device_list_updates_to_be_sent(self) -> None: + """ + Wait for the device list update EDU's to get pushed out over federation + + For example, each login does a fire-and-forget (`LoginRestServlet` -> + `register_device` -> `notify_device_update` -> `handle_new_device_update` -> + `send_device_messages(hosts, immediate=False)`) which adds to the + `_DestinationWakeupQueue` which has a background process that sends depending on + how `federation_rr_transactions_per_room_per_second` is configured. + + The default `federation_rr_transactions_per_room_per_second` is `50` (1s/50 -> + 0.02s) + """ + self.reactor.advance( + 1.0 + / self.hs.config.ratelimiting.federation_rr_transactions_per_room_per_second + ) + def test_send_device_updates(self) -> None: """Basic case: each device update should result in an EDU""" # create a device @@ -527,9 +546,7 @@ def test_send_device_updates(self) -> None: self.assertEqual(len(self.edus), 1) stream_id = self.check_device_update_edu(self.edus.pop(0), u1, "D1", None) - # We queue up device list updates to be sent over federation, so we - # advance to clear the queue. - self.reactor.advance(1) + self.wait_for_device_list_updates_to_be_sent() # a second call should produce no new device EDUs self.get_success( @@ -568,7 +585,7 @@ def test_dont_send_device_updates_for_remote_users(self) -> None: ) ) - self.reactor.advance(1) + self.wait_for_device_list_updates_to_be_sent() # We shouldn't see an EDU for that update self.assertEqual(self.edus, []) @@ -590,6 +607,8 @@ def test_upload_signatures(self) -> None: self.login(u1, "pass", device_id="D1") self.login(u1, "pass", device_id="D2") + self.wait_for_device_list_updates_to_be_sent() + # expect two edus self.assertEqual(len(self.edus), 2) stream_id: int | None = None @@ -600,9 +619,7 @@ def test_upload_signatures(self) -> None: device1_signing_key = self.generate_and_upload_device_signing_key(u1, "D1") device2_signing_key = self.generate_and_upload_device_signing_key(u1, "D2") - # We queue up device list updates to be sent over federation, so we - # advance to clear the queue. - self.reactor.advance(1) + self.wait_for_device_list_updates_to_be_sent() # expect two more edus self.assertEqual(len(self.edus), 2) @@ -637,9 +654,7 @@ def test_upload_signatures(self) -> None: e2e_handler.upload_signing_keys_for_user(u1, cross_signing_keys) ) - # We queue up device list updates to be sent over federation, so we - # advance to clear the queue. - self.reactor.advance(1) + self.wait_for_device_list_updates_to_be_sent() # expect signing key update edu self.assertEqual(len(self.edus), 2) @@ -662,9 +677,7 @@ def test_upload_signatures(self) -> None: ) self.assertEqual(ret["failures"], {}) - # We queue up device list updates to be sent over federation, so we - # advance to clear the queue. - self.reactor.advance(1) + self.wait_for_device_list_updates_to_be_sent() # expect two edus, in one or two transactions. We don't know what order the # devices will be updated. @@ -689,9 +702,7 @@ def test_delete_devices(self) -> None: self.login("user", "pass", device_id="D2") self.login("user", "pass", device_id="D3") - # We queue up device list updates to be sent over federation, so we - # advance to clear the queue. - self.reactor.advance(1) + self.wait_for_device_list_updates_to_be_sent() # expect three edus self.assertEqual(len(self.edus), 3) @@ -702,9 +713,7 @@ def test_delete_devices(self) -> None: # delete them again self.get_success(self.device_handler.delete_devices(u1, ["D1", "D2", "D3"])) - # We queue up device list updates to be sent over federation, so we - # advance to clear the queue. - self.reactor.advance(1) + self.wait_for_device_list_updates_to_be_sent() # expect three edus, in an unknown order self.assertEqual(len(self.edus), 3) @@ -730,15 +739,18 @@ def test_unreachable_server(self) -> None: # create devices u1 = self.register_user("user", "pass") self.login("user", "pass", device_id="D1") + # Wait some time in between each device list update as we want each of them to + # be attempted to be sent in their own transaction + self.reactor.advance(Duration(seconds=1).as_secs()) self.login("user", "pass", device_id="D2") + self.reactor.advance(Duration(seconds=1).as_secs()) self.login("user", "pass", device_id="D3") + self.reactor.advance(Duration(seconds=1).as_secs()) # delete them again self.get_success(self.device_handler.delete_devices(u1, ["D1", "D2", "D3"])) - # We queue up device list updates to be sent over federation, so we - # advance to clear the queue. - self.reactor.advance(1) + self.wait_for_device_list_updates_to_be_sent() self.assertGreaterEqual(mock_send_txn.call_count, 4) @@ -748,9 +760,7 @@ def test_unreachable_server(self) -> None: self.hs.get_federation_sender().send_device_messages(["host2"]) ) - # We queue up device list updates to be sent over federation, so we - # advance to clear the queue. - self.reactor.advance(1) + self.wait_for_device_list_updates_to_be_sent() # for each device, there should be a single update self.assertEqual(len(self.edus), 3) @@ -777,16 +787,20 @@ def test_prune_outbound_device_pokes1(self) -> None: # create devices u1 = self.register_user("user", "pass") self.login("user", "pass", device_id="D1") + # Wait some time in between each device list update as we want each of them to + # be attempted to be sent in their own transaction + self.reactor.advance(Duration(seconds=1).as_secs()) self.login("user", "pass", device_id="D2") + self.reactor.advance(Duration(seconds=1).as_secs()) self.login("user", "pass", device_id="D3") + self.reactor.advance(Duration(seconds=1).as_secs()) # delete them again self.get_success(self.device_handler.delete_devices(u1, ["D1", "D2", "D3"])) - # We queue up device list updates to be sent over federation, so we - # advance to clear the queue. - self.reactor.advance(1) + self.wait_for_device_list_updates_to_be_sent() + # Ensure that we tried sending the device list update EDU's out self.assertGreaterEqual(mock_send_txn.call_count, 4) # run the prune job @@ -801,9 +815,7 @@ def test_prune_outbound_device_pokes1(self) -> None: self.hs.get_federation_sender().send_device_messages(["host2"]) ) - # We queue up device list updates to be sent over federation, so we - # advance to clear the queue. - self.reactor.advance(1) + self.wait_for_device_list_updates_to_be_sent() # there should be a single update for this user. self.assertEqual(len(self.edus), 1) @@ -835,15 +847,17 @@ def test_prune_outbound_device_pokes2(self) -> None: mock_send_txn.side_effect = AssertionError("fail") self.login("user", "pass", device_id="D2") + # Wait some time in between each device list update as we want each of them to + # be attempted to be sent in their own transaction + self.reactor.advance(Duration(seconds=1).as_secs()) self.login("user", "pass", device_id="D3") - - # We queue up device list updates to be sent over federation, so we - # advance to clear the queue. - self.reactor.advance(1) + self.reactor.advance(Duration(seconds=1).as_secs()) # delete them again self.get_success(self.device_handler.delete_devices(u1, ["D1", "D2", "D3"])) + self.wait_for_device_list_updates_to_be_sent() + self.assertGreaterEqual(mock_send_txn.call_count, 3) # run the prune job @@ -858,9 +872,7 @@ def test_prune_outbound_device_pokes2(self) -> None: self.hs.get_federation_sender().send_device_messages(["host2"]) ) - # We queue up device list updates to be sent over federation, so we - # advance to clear the queue. - self.reactor.advance(1) + self.wait_for_device_list_updates_to_be_sent() # ... and we should get a single update for this user. self.assertEqual(len(self.edus), 1) From 27b3366cfc07ba7f7e1d2fba991910a7ee445684 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 7 Jul 2026 03:17:31 -0500 Subject: [PATCH 26/27] Fix `possible` typo --- tests/server.py | 2 +- tests/unittest.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/server.py b/tests/server.py index 26b6bb7a871..690d458e96b 100644 --- a/tests/server.py +++ b/tests/server.py @@ -376,7 +376,7 @@ def await_result(self, timeout_ms: int = 1000) -> None: # to avoid tightlooping on the main thread (CPU 100%) because it's wasteful # and may starve out other threads. 10 is arbitrary but many cases will have # none or only a few round-trips so we can just try to go as fast as - # posssible. + # possible. if loop_count < 10: time.sleep(0) else: diff --git a/tests/unittest.py b/tests/unittest.py index 7dccff81c0f..e1079656dfb 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -781,7 +781,7 @@ def _wait_for_deferred( # to avoid tightlooping on the main thread (CPU 100%) because it's wasteful # and may starve out other threads. 10 is arbitrary but many cases will have # none or only a few round-trips so we can just try to go as fast as - # posssible. + # possible. if loop_count < 10: time.sleep(0) else: From d4f4dddd8e5020e799969898c27a9d32d610db6d Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 7 Jul 2026 03:18:04 -0500 Subject: [PATCH 27/27] Fix `switch` typo --- tests/server.py | 2 +- tests/unittest.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/server.py b/tests/server.py index 690d458e96b..15a7661c345 100644 --- a/tests/server.py +++ b/tests/server.py @@ -368,7 +368,7 @@ def await_result(self, timeout_ms: int = 1000) -> None: # # Note: Python has a default thread switch interval (5ms for cpython) (see # `sys.setswitchinterval(interval)`) but we still want this here as we're - # able to preempt and cause the thread context swtich to happen faster. + # able to preempt and cause the thread context switch to happen faster. # Also, without any real-time sleeping, this function would complete before # the 5ms switch ever happened. # diff --git a/tests/unittest.py b/tests/unittest.py index e1079656dfb..202f7120ef0 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -773,7 +773,7 @@ def _wait_for_deferred( # # Note: Python has a default thread switch interval (5ms for cpython) (see # `sys.setswitchinterval(interval)`) but we still want this here as we're - # able to preempt and cause the thread context swtich to happen faster. + # able to preempt and cause the thread context switch to happen faster. # Also, without any real-time sleeping, this function would complete before # the 5ms switch ever happened. #