Files
synapse/tests/util/test_clock.py
Erik Johnston 7ae1d987ef Add a per-homeserver clock to the Rust side (#20294)
This is a prerequisite to being able to add caching on the Rust side.
2026-10-07 11:11:08 +01:00

121 lines
4.0 KiB
Python

#
# This file is licensed under the Affero General Public License (AGPL) version 3.
#
# Copyright (C) 2025 Element Creations Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# See the GNU Affero General Public License for more details:
# <https://www.gnu.org/licenses/agpl-3.0.html>.
#
#
import weakref
from synapse.synapse_rust.runtime import RustRuntime
from synapse.util.duration import Duration
from tests.unittest import HomeserverTestCase
class ClockTestCase(HomeserverTestCase):
def test_looping_calls_are_gced(self) -> None:
"""Test that looping calls are garbage collected after being stopped.
The `Clock` tracks looping calls so to allow stopping of all looping
calls via the clock.
"""
clock = self.hs.get_clock()
# Create a new looping call, and take a weakref to it.
call = clock.looping_call(lambda: None, Duration(seconds=1))
weak_call = weakref.ref(call)
# Stop the looping call. It should get garbage collected after this.
call.stop()
# Delete our strong reference to the call (otherwise it won't get garbage collected).
del call
# Check that the call has been garbage collected.
self.assertIsNone(weak_call())
def test_looping_calls_stopped_on_clock_shutdown(self) -> None:
"""Test that looping calls are stopped when the clock is shut down."""
clock = self.hs.get_clock()
was_called = False
def on_call() -> None:
nonlocal was_called
was_called = True
# Create a new looping call.
call = clock.looping_call(on_call, Duration(seconds=1))
weak_call = weakref.ref(call)
del call # Remove our strong reference to the call.
# The call should still exist.
self.assertIsNotNone(weak_call())
# Advance the clock to trigger the call.
self.reactor.advance(2)
self.assertTrue(was_called)
# Shut down the clock, which should stop the looping call.
clock.shutdown()
# The call should have been garbage collected.
self.assertIsNone(weak_call())
# Advance the clock again; the call should not be called again.
was_called = False
self.reactor.advance(2)
self.assertFalse(was_called)
class RustClockTestCase(HomeserverTestCase):
"""Tests for the Rust side's view of the time (`RustRuntime.time_msec`)."""
def test_follows_the_reactor(self) -> None:
runtime = self.hs.get_rust_runtime()
before = runtime.time_msec()
self.reactor.advance(37)
self.assertEqual(runtime.time_msec(), before + 37 * 1000)
self.assertEqual(runtime.time_msec(), self.hs.get_clock().time_msec())
def test_is_up_to_date_inside_a_looping_call(self) -> None:
"""Rust must see the new time from callbacks fired *during* an advance.
This is why the test reactor moves the Rust clock *before* it calls
`super().advance()`, which is what fires those callbacks.
"""
runtime = self.hs.get_rust_runtime()
seen: list[int] = []
self.hs.get_clock().looping_call(
lambda: seen.append(runtime.time_msec()), Duration(seconds=1)
)
base_time = self.hs.get_clock().time_msec()
self.reactor.advance(1)
self.assertEqual(seen, [base_time + 1000])
self.reactor.advance(1)
self.assertEqual(seen, [base_time + 1000, base_time + 2000])
def test_every_runtime_on_the_reactor_follows_it(self) -> None:
"""Worker tests run several homeservers, each with its own runtime, on
one reactor. All of their clocks must move together."""
other_runtime = RustRuntime(hs=self.hs)
self.reactor.advance(5)
self.assertEqual(other_runtime.time_msec(), self.hs.get_clock().time_msec())