Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
520a4bc
Refactor `get_success(...)` to allow other threads to make progress
MadLittleMods Jun 19, 2026
65a1c59
Refactor `get_failure`
MadLittleMods Jun 19, 2026
fdeed9a
Fix `get_failure(...)` raises docstring
MadLittleMods Jun 19, 2026
5ca9050
Add changelog
MadLittleMods Jun 19, 2026
6e9b2a2
Fix `get_failure` lint
MadLittleMods Jun 19, 2026
c45774c
Reduce timeout so you don't have to wait as long when something goes …
MadLittleMods Jun 19, 2026
ae7e367
Fix test cases that don't need `by=`
MadLittleMods Jun 19, 2026
f54d0c0
Fix `tests/storage/test_background_update.py`
MadLittleMods Jun 20, 2026
997a160
Fix `tests/app/test_homeserver_shutdown.py`
MadLittleMods Jun 20, 2026
b501ad1
Fix `tests/handlers/test_presence.py`
MadLittleMods Jun 20, 2026
9cfd0f9
Fix `tests/handlers/test_send_email.py`
MadLittleMods Jun 20, 2026
66a515b
Explain why remove `get_success_or_raise`
MadLittleMods Jun 20, 2026
a1092da
Extract logic to `_wait_for_deferred`
MadLittleMods Jun 20, 2026
09c91d3
Fix FIXME comment grammar
MadLittleMods Jun 20, 2026
4357aa4
Use 1 second timeout default
MadLittleMods Jun 20, 2026
edce488
Use "deferred" in `_wait_for_deferred` docstring
MadLittleMods Jun 20, 2026
5cc4590
Add example if you need to advance time
MadLittleMods Jun 20, 2026
5b27102
Fix `tests.handlers.test_profile.ProfileTestCase.test_background_upda…
MadLittleMods Jun 20, 2026
44253df
No need to change background update in `tests/app/test_homeserver_shu…
MadLittleMods Jun 20, 2026
47297af
Fix `tests.handlers.test_user_directory.UserDirectoryTestCase.test_pr…
MadLittleMods Jun 20, 2026
cc2c27b
Fix `tests/handlers/test_typing.py`
MadLittleMods Jun 20, 2026
26dc512
Fix `trial tests.replication.test_federation_ack`
MadLittleMods Jun 20, 2026
2bce6e7
Fix lints
MadLittleMods Jun 20, 2026
3425d15
Merge branch 'develop' into madlittlemods/better-get-success
MadLittleMods Jun 20, 2026
ecce873
Remove other `till_deferred_has_result`
MadLittleMods Jun 20, 2026
2c51142
Explain better
MadLittleMods Jun 20, 2026
999d22d
Fix `tests.storage.databases.main.test_events_worker.GetEventCancella…
MadLittleMods Jun 20, 2026
41642be
Remove `wait_on_thread`
MadLittleMods Jun 20, 2026
350b15f
Fix `trial-olddeps`: `builtins.TypeError: 'type' object is not subscr…
MadLittleMods Jun 20, 2026
60ddfc6
Fix `tests.replication.tcp.test_handler.ChannelsTestCase.test_wait_fo…
MadLittleMods Jun 20, 2026
167ad62
Run CI again
MadLittleMods Jun 22, 2026
ad0bbb6
Fix `tests.replication.tcp.test_handler.ChannelsTestCase.test_wait_fo…
MadLittleMods Jun 22, 2026
f0e968a
Run CI again
MadLittleMods Jun 22, 2026
85edec1
Merge branch 'develop' into madlittlemods/better-get-success
MadLittleMods Jun 22, 2026
919a94c
Align default timeout with `get_success`
MadLittleMods Jun 22, 2026
2c8ea3a
Remove double space in comment
MadLittleMods Jun 22, 2026
2300a19
Run CI again
MadLittleMods Jun 22, 2026
a1170c6
Run CI again
MadLittleMods Jun 23, 2026
efcd574
Run CI again
MadLittleMods Jun 23, 2026
749ea8b
Run CI again
MadLittleMods Jun 23, 2026
77ddfcf
Merge branch 'develop' into madlittlemods/better-get-success
MadLittleMods Jun 23, 2026
92346f2
Switch to cross-compatible `time.sleep()`
MadLittleMods Jun 23, 2026
ce1758c
Comment about Python's default thread switch interval
MadLittleMods Jun 23, 2026
0add7cd
Merge branch 'develop' into madlittlemods/better-get-success
MadLittleMods Jun 25, 2026
a871de0
`time.sleep(0.001)` after a few loops
MadLittleMods Jun 25, 2026
74060df
Merge branch 'develop' into madlittlemods/better-get-success
MadLittleMods Jun 25, 2026
28586c6
Remove real-time `timeout`
MadLittleMods Jun 26, 2026
cb91056
Fix lints
MadLittleMods Jun 26, 2026
a975f83
Refactor to make `CLOCK_SCHEDULE_EPSILON` generic
MadLittleMods Jun 26, 2026
34f015f
Use `callLater(0)` for `FakeTransport`
MadLittleMods Jun 26, 2026
e7b09b3
Use `callLater(0)` for `FakeChannel`
MadLittleMods Jun 26, 2026
f11be8b
Remove some no longer necessary `advance(...)` in tests (related to `…
MadLittleMods Jun 26, 2026
453a296
Merge branch 'develop' into madlittlemods/better-get-success
MadLittleMods Jul 2, 2026
2bff819
Update references to `_EPSILON` (now called `CLOCK_SCHEDULE_EPSILON`)
MadLittleMods Jul 2, 2026
a12a9e0
Remove some no longer necessary `advance(...)` in tests (related to `…
MadLittleMods Jul 2, 2026
d034373
Better label fire-and-forget
MadLittleMods Jul 2, 2026
77184c8
Update docstring to be accurate after 100 loop refactor instead of re…
MadLittleMods Jul 2, 2026
5be3534
Better document plan to refactor and discussion where it spawned from
MadLittleMods Jul 2, 2026
77ce7a5
More accurate `test_redact_messages_all_rooms` description
MadLittleMods Jul 2, 2026
bc54b65
Advance by `CLOCK_SCHEDULE_EPSILON`
MadLittleMods Jul 2, 2026
3cb3c66
Fix `tests.crypto.test_keyring`
MadLittleMods Jul 2, 2026
c37b6ce
Fix `tests.util.test_task_scheduler.TestTaskScheduler.test_cancel_run…
MadLittleMods Jul 2, 2026
6e86cf3
Fix `tests.handlers.test_oidc.OidcHandlerTestCase.test_exchange_code_…
MadLittleMods Jul 2, 2026
235c90a
Merge branch 'develop' into madlittlemods/better-get-success
MadLittleMods Jul 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/19871.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Update `HomeserverTestCase.get_success(...)` and friends to drive async Rust (Tokio runtime/thread pool).
10 changes: 2 additions & 8 deletions synapse/http/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,7 @@
from synapse.metrics import SERVER_NAME_LABEL
from synapse.types import ISynapseReactor, StrSequence
from synapse.util.async_helpers import timeout_deferred
from synapse.util.clock import Clock
from synapse.util.duration import Duration
from synapse.util.clock import CLOCK_SCHEDULE_EPSILON, Clock
from synapse.util.json import json_decoder

if TYPE_CHECKING:
Expand Down Expand Up @@ -163,11 +162,6 @@ def _is_ip_blocked(
return False


# The delay used by the scheduler to schedule tasks "as soon as possible", while
# still allowing other tasks to run between runs.
_EPSILON = Duration(microseconds=1)


def _make_scheduler(clock: Clock) -> Callable[[Callable[[], object]], IDelayedCall]:
"""Makes a schedular suitable for a Cooperator using the given reactor.

Expand All @@ -176,7 +170,7 @@ def _make_scheduler(clock: Clock) -> Callable[[Callable[[], object]], IDelayedCa

def _scheduler(x: Callable[[], object]) -> IDelayedCall:
return clock.call_later(
_EPSILON,
CLOCK_SCHEDULE_EPSILON,
x,
)

Expand Down
13 changes: 13 additions & 0 deletions synapse/util/clock.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,19 @@
logging.setLoggerClass(original_logger_class)


CLOCK_SCHEDULE_EPSILON = Duration(microseconds=1)
"""
The smallest value we can use that will schedule tasks "as soon as possible", while
still allowing other tasks to run between runs.

This should be a non-zero value as the Twisted Reactor API does not specify how calls
get scheduled. If we used `0`, a weird reactor implementation could run it immediately
or run it any order with the other calls that are scheduled now.

We want the semantics of run this in the "next reactor iteration".
"""


def _try_wakeup_deferred(d: Deferred) -> None:
"""Try to wake up a deferred, but ignore any exceptions raised by the
callback. This is useful when we want to wake up a deferred that may have
Expand Down
16 changes: 15 additions & 1 deletion tests/app/test_homeserver_shutdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ async def shutdown() -> None:

self.get_success(shutdown())

# XXX: There can be a few already dispatched database queries (from normal
# background tasks in Synapse) and the threadless `ThreadPool` that we use in
# tests uses *untracked* clock calls to pass database results back so `shutdown`
# doesn't cancel those calls. This is a quirk of our test infrastructure
# (threadless `ThreadPool`) so this kind of "hack" is fine.
self.reactor.advance(0)
Comment on lines +79 to +84

@MadLittleMods MadLittleMods Jun 20, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The explanation here is slightly hand-wavey.

The other comment explanations are more concrete at-least in the fact that I've traced some scheduled reactor callback.


# Cleanup the internal reference in our test case
del self.hs

Expand Down Expand Up @@ -106,7 +113,7 @@ def test_clean_homeserver_shutdown_mid_background_updates(self) -> None:
# Pump the background updates by a single iteration, just to ensure any extra
# resources it uses have been started.
store = weakref.proxy(self.hs.get_datastores().main)
self.get_success(store.db_pool.updates.do_next_background_update(False), by=0.1)
self.get_success(store.db_pool.updates.do_next_background_update(False))

hs_ref = weakref.ref(self.hs)

Expand All @@ -127,6 +134,13 @@ async def shutdown() -> None:

self.get_success(shutdown())

# XXX: There can be a few already dispatched database queries (from normal
# background tasks in Synapse) and the threadless `ThreadPool` that we use in
# tests uses *untracked* clock calls to pass database results back so `shutdown`
# doesn't cancel those calls. This is a quirk of our test infrastructure
# (threadless `ThreadPool`) so this kind of "hack" is fine.
self.reactor.advance(0)

# Cleanup the internal reference in our test case
del self.hs

Expand Down
6 changes: 3 additions & 3 deletions tests/crypto/test_keyring.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ async def get_json(destination: str, path: str, **kwargs: Any) -> JsonDict:
res = key_json[testverifykey_id]
self.assertIsNotNone(res)
assert res is not None
self.assertEqual(res.added_ts, self.reactor.seconds() * 1000)
self.assertEqual(res.added_ts, self.clock.time_msec())

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated this to actually use millisecond resolution. Previously, this test passed because the clock was using round numbers but we updated get_success(...) to advance by CLOCK_SCHEDULE_EPSILON, this needed to change.

[FAIL]
Traceback (most recent call last):
  File "/home/runner/work/synapse/synapse/tests/crypto/test_keyring.py", line 617, in test_get_keys_from_perspectives
    self.assertEqual(res.added_ts, self.reactor.seconds() * 1000)
  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: 100000 != 100000.003

tests.crypto.test_keyring.PerspectivesKeyFetcherTestCase.test_get_keys_from_perspectives

self.assertEqual(res.valid_until_ts, VALID_UNTIL_TS)

# we expect it to be encoded as canonical json *before* it hits the db
Expand Down Expand Up @@ -614,7 +614,7 @@ def test_get_keys_from_perspectives(self) -> None:
res = key_json[testverifykey_id]
self.assertIsNotNone(res)
assert res is not None
self.assertEqual(res.added_ts, self.reactor.seconds() * 1000)
self.assertEqual(res.added_ts, self.clock.time_msec())
self.assertEqual(res.valid_until_ts, VALID_UNTIL_TS)

self.assertEqual(res.key_json, canonicaljson.encode_canonical_json(response))
Expand Down Expand Up @@ -732,7 +732,7 @@ def test_get_perspectives_own_key(self) -> None:
res = key_json[testverifykey_id]
self.assertIsNotNone(res)
assert res is not None
self.assertEqual(res.added_ts, self.reactor.seconds() * 1000)
self.assertEqual(res.added_ts, self.clock.time_msec())
self.assertEqual(res.valid_until_ts, VALID_UNTIL_TS)

self.assertEqual(res.key_json, canonicaljson.encode_canonical_json(response))
Expand Down
1 change: 0 additions & 1 deletion tests/handlers/test_federation.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,6 @@ def create_invite() -> EventBase:
event.room_version,
),
exc=LimitExceededError,
by=0.5,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a lot of cases, the by usage didn't seem necessary at all (test still passes) (no need to advance time in the reactor/clock)

)

def _build_and_send_join_event(
Expand Down
77 changes: 12 additions & 65 deletions tests/handlers/test_oauth_delegation.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,13 @@

import json
import threading
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
from typing import Any, ClassVar, Coroutine, Generator, TypeVar, Union
from typing import Any, ClassVar, TypeVar
from unittest.mock import AsyncMock, Mock
from urllib.parse import parse_qs

from parameterized.parameterized import parameterized_class

from twisted.internet.defer import Deferred, ensureDeferred
from twisted.internet.testing import MemoryReactor

from synapse.api.auth.mas import MasDelegatedAuth
Expand Down Expand Up @@ -204,31 +202,6 @@ class MasAuthDelegation(HomeserverTestCase):
def device_scope(self) -> str:
return self.device_scope_prefix + DEVICE

def till_deferred_has_result(
self,
awaitable: Union[
"Coroutine[Deferred[Any], Any, T]",
"Generator[Deferred[Any], Any, T]",
"Deferred[T]",
],
) -> "Deferred[T]":
"""Wait until a deferred has a result.

This is useful because the Rust HTTP client will resolve the deferred
using reactor.callFromThread, which are only run when we call
reactor.advance.
"""
deferred = ensureDeferred(awaitable)
tries = 0
while not deferred.called:
time.sleep(0.1)
self.reactor.advance(0)
tries += 1
if tries > 100:
raise Exception("Timed out waiting for deferred to resolve")

return deferred

def default_config(self) -> dict[str, Any]:
config = super().default_config()
config["public_baseurl"] = BASE_URL
Expand Down Expand Up @@ -278,11 +251,7 @@ def test_simple_introspection(self) -> None:
"expires_in": 60,
}

requester = self.get_success(
self.till_deferred_has_result(
self._auth.get_user_by_access_token("some_token")
)
)
requester = self.get_success(self._auth.get_user_by_access_token("some_token"))

self.assertEqual(requester.user.to_string(), USER_ID)
self.assertEqual(requester.device_id, DEVICE)
Expand All @@ -301,11 +270,7 @@ def test_unexpiring_token(self) -> None:
"username": USERNAME,
}

requester = self.get_success(
self.till_deferred_has_result(
self._auth.get_user_by_access_token("some_token")
)
)
requester = self.get_success(self._auth.get_user_by_access_token("some_token"))

self.assertEqual(requester.user.to_string(), USER_ID)
self.assertEqual(requester.device_id, DEVICE)
Expand All @@ -326,9 +291,7 @@ def test_inexistent_device(self) -> None:
}

failure = self.get_failure(
self.till_deferred_has_result(
self._auth.get_user_by_access_token("some_token")
),
self._auth.get_user_by_access_token("some_token"),
InvalidClientTokenError,
)
self.assertEqual(failure.value.code, 401)
Expand All @@ -343,9 +306,7 @@ def test_inexistent_user(self) -> None:
}

failure = self.get_failure(
self.till_deferred_has_result(
self._auth.get_user_by_access_token("some_token")
),
self._auth.get_user_by_access_token("some_token"),
AuthError,
)
# This is a 500, it should never happen really
Expand All @@ -361,9 +322,7 @@ def test_missing_scope(self) -> None:
}

failure = self.get_failure(
self.till_deferred_has_result(
self._auth.get_user_by_access_token("some_token")
),
self._auth.get_user_by_access_token("some_token"),
InvalidClientTokenError,
)
self.assertEqual(failure.value.code, 401)
Expand All @@ -372,9 +331,7 @@ def test_invalid_response(self) -> None:
self.server.introspection_response = {}

failure = self.get_failure(
self.till_deferred_has_result(
self._auth.get_user_by_access_token("some_token")
),
self._auth.get_user_by_access_token("some_token"),
SynapseError,
)
self.assertEqual(failure.value.code, 503)
Expand All @@ -389,11 +346,7 @@ def test_device_id_in_body(self) -> None:
"device_id": DEVICE,
}

requester = self.get_success(
self.till_deferred_has_result(
self._auth.get_user_by_access_token("some_token")
)
)
requester = self.get_success(self._auth.get_user_by_access_token("some_token"))

self.assertEqual(requester.device_id, DEVICE)

Expand All @@ -406,11 +359,7 @@ def test_admin_scope(self) -> None:
"expires_in": 60,
}

requester = self.get_success(
self.till_deferred_has_result(
self._auth.get_user_by_access_token("some_token")
)
)
requester = self.get_success(self._auth.get_user_by_access_token("some_token"))

self.assertEqual(requester.user.to_string(), USER_ID)
self.assertTrue(self.get_success(self._auth.is_server_admin(requester)))
Expand All @@ -435,17 +384,15 @@ def test_cached_expired_introspection(self) -> None:
request.requestHeaders.getRawHeaders = mock_getRawHeaders()

# The first CS-API request causes a successful introspection
self.get_success(
self.till_deferred_has_result(self._auth.get_user_by_req(request))
)
self.get_success(self._auth.get_user_by_req(request))
self.assertEqual(self.server.calls, 1)

# Sleep for 60 seconds so the token expires.
self.reactor.advance(60.0)

# Now the CS-API request fails because the token expired
self.assertFailure(
self.till_deferred_has_result(self._auth.get_user_by_req(request)),
self.get_failure(
self._auth.get_user_by_req(request),
InvalidClientTokenError,
)
# Ensure another introspection request was not sent
Expand Down
6 changes: 3 additions & 3 deletions tests/handlers/test_oidc.py
Original file line number Diff line number Diff line change
Expand Up @@ -960,7 +960,7 @@ def test_exchange_code_jwt_key(self) -> None:
# advance the clock a bit before we start, so we aren't working with zero
# timestamps.
self.reactor.advance(1000)
start_time = self.reactor.seconds()
start_time_s = int(self.reactor.seconds())

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated this to actually use second resolution. Previously, this test passed because the clock was using round numbers but we updated get_success(...) to advance by CLOCK_SCHEDULE_EPSILON, this needed to change.

[FAIL]
Traceback (most recent call last):
  File "/home/runner/work/synapse/synapse/tests/handlers/test_oidc.py", line 984, in test_exchange_code_jwt_key
    self.assertEqual(claims["iat"], start_time)
  File "/home/runner/.cache/pypoetry/virtualenvs/matrix-synapse-pswDeSvb-py3.14/lib/python3.14/site-packages/twisted/trial/_synctest.py", line 444, in assertEqual
    super().assertEqual(first, second, msg)
  File "/opt/hostedtoolcache/Python/3.14.6/x64/lib/python3.14/unittest/case.py", line 925, in assertEqual
    assertion_func(first, second, msg=msg)
  File "/opt/hostedtoolcache/Python/3.14.6/x64/lib/python3.14/unittest/case.py", line 918, in _baseAssertEqual
    raise self.failureException(msg)
twisted.trial.unittest.FailTest: 1000 != 1000.000001

tests.handlers.test_oidc.OidcHandlerTestCase.test_exchange_code_jwt_key

ret = self.get_success(self.provider._exchange_code(code, code_verifier=""))

self.assertEqual(ret, token)
Expand All @@ -981,8 +981,8 @@ def test_exchange_code_jwt_key(self) -> None:
self.assertEqual(claims["aud"], ISSUER)
self.assertEqual(claims["iss"], "DEFGHI")
self.assertEqual(claims["sub"], CLIENT_ID)
self.assertEqual(claims["iat"], start_time)
self.assertGreater(claims["exp"], start_time)
self.assertEqual(claims["iat"], start_time_s)
self.assertGreater(claims["exp"], start_time_s)

# check the rest of the POSTed data
self.assertEqual(args["grant_type"], ["authorization_code"])
Expand Down
21 changes: 7 additions & 14 deletions tests/handlers/test_presence.py
Original file line number Diff line number Diff line change
Expand Up @@ -951,8 +951,7 @@ def test_external_process_timeout(self) -> None:
self.get_success(
worker_presence_handler.user_syncing(
self.user_id, self.device_id, True, PresenceState.ONLINE
),
by=0.1,
)
)

# Check that if we wait a while without telling the handler the user has
Expand Down Expand Up @@ -1270,8 +1269,7 @@ def test_set_presence_from_syncing_multi_device(
"dev-1",
affect_presence=dev_1_state != PresenceState.OFFLINE,
presence_state=dev_1_state,
),
by=0.01,
)
)

# 2. Wait half the idle timer.
Expand All @@ -1285,8 +1283,7 @@ def test_set_presence_from_syncing_multi_device(
"dev-2",
affect_presence=dev_2_state != PresenceState.OFFLINE,
presence_state=dev_2_state,
),
by=0.01,
)
)

# 4. Assert the expected presence state.
Expand All @@ -1311,8 +1308,7 @@ def test_set_presence_from_syncing_multi_device(
"dev-3",
affect_presence=True,
presence_state=PresenceState.ONLINE,
),
by=0.01,
)
):
pass

Expand Down Expand Up @@ -1507,8 +1503,7 @@ def test_set_presence_from_non_syncing_multi_device(
"dev-1",
affect_presence=dev_1_state != PresenceState.OFFLINE,
presence_state=dev_1_state,
),
by=0.1,
)
)

# 2. Sync with the second device.
Expand All @@ -1518,8 +1513,7 @@ def test_set_presence_from_non_syncing_multi_device(
"dev-2",
affect_presence=dev_2_state != PresenceState.OFFLINE,
presence_state=dev_2_state,
),
by=0.1,
)
)

# 3. Assert the expected presence state.
Expand Down Expand Up @@ -1625,8 +1619,7 @@ def test_set_presence_from_syncing_keeps_busy(
self.get_success(
worker_to_sync_against.get_presence_handler().user_syncing(
self.user_id, self.device_id, True, PresenceState.ONLINE
),
by=0.1,
)
)

# Check against the main process that the user's presence did not change.
Expand Down
Loading
Loading