Skip to content

Commit 1bddd25

Browse files
authored
Port Clock functions to use Duration class (#19229)
This changes the arguments in clock functions to be `Duration` and converts call sites and constants into `Duration`. There are still some more functions around that should be converted (e.g. `timeout_deferred`), but we leave that to another PR. We also changes `.as_secs()` to return a float, as the rounding broke things subtly. The only reason to keep it (its the same as `timedelta.total_seconds()`) is for symmetry with `as_millis()`. Follows on from #19223
1 parent d143276 commit 1bddd25

File tree

95 files changed

+511
-260
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

95 files changed

+511
-260
lines changed

changelog.d/19229.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Move towards using a dedicated `Duration` type.

rust/src/duration.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* This file is licensed under the Affero General Public License (AGPL) version 3.
3+
*
4+
* Copyright (C) 2025 Element Creations, Ltd
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Affero General Public License as
8+
* published by the Free Software Foundation, either version 3 of the
9+
* License, or (at your option) any later version.
10+
*
11+
* See the GNU Affero General Public License for more details:
12+
* <https://www.gnu.org/licenses/agpl-3.0.html>.
13+
*/
14+
15+
use once_cell::sync::OnceCell;
16+
use pyo3::{
17+
types::{IntoPyDict, PyAnyMethods},
18+
Bound, BoundObject, IntoPyObject, Py, PyAny, PyErr, PyResult, Python,
19+
};
20+
21+
/// A reference to the `synapse.util.duration` module.
22+
static DURATION: OnceCell<Py<PyAny>> = OnceCell::new();
23+
24+
/// Access to the `synapse.util.duration` module.
25+
fn duration_module(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> {
26+
Ok(DURATION
27+
.get_or_try_init(|| py.import("synapse.util.duration").map(Into::into))?
28+
.bind(py))
29+
}
30+
31+
/// Mirrors the `synapse.util.duration.Duration` Python class.
32+
pub struct SynapseDuration {
33+
microseconds: u64,
34+
}
35+
36+
impl SynapseDuration {
37+
/// For now we only need to create durations from milliseconds.
38+
pub fn from_milliseconds(milliseconds: u64) -> Self {
39+
Self {
40+
microseconds: milliseconds * 1_000,
41+
}
42+
}
43+
}
44+
45+
impl<'py> IntoPyObject<'py> for &SynapseDuration {
46+
type Target = PyAny;
47+
type Output = Bound<'py, Self::Target>;
48+
type Error = PyErr;
49+
50+
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
51+
let duration_module = duration_module(py)?;
52+
let kwargs = [("microseconds", self.microseconds)].into_py_dict(py)?;
53+
let duration_instance = duration_module.call_method("Duration", (), Some(&kwargs))?;
54+
Ok(duration_instance.into_bound())
55+
}
56+
}

rust/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use pyo3::prelude::*;
55
use pyo3_log::ResetHandle;
66

77
pub mod acl;
8+
pub mod duration;
89
pub mod errors;
910
pub mod events;
1011
pub mod http;

rust/src/rendezvous/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ use ulid::Ulid;
3535

3636
use self::session::Session;
3737
use crate::{
38+
duration::SynapseDuration,
3839
errors::{NotFoundError, SynapseError},
3940
http::{http_request_from_twisted, http_response_to_twisted, HeaderMapPyExt},
4041
UnwrapInfallible,
@@ -132,6 +133,8 @@ impl RendezvousHandler {
132133
.unwrap_infallible()
133134
.unbind();
134135

136+
let eviction_duration = SynapseDuration::from_milliseconds(eviction_interval);
137+
135138
// Construct a Python object so that we can get a reference to the
136139
// evict method and schedule it to run.
137140
let self_ = Py::new(
@@ -149,7 +152,7 @@ impl RendezvousHandler {
149152
let evict = self_.getattr(py, "_evict")?;
150153
homeserver.call_method0("get_clock")?.call_method(
151154
"looping_call",
152-
(evict, eviction_interval),
155+
(evict, &eviction_duration),
153156
None,
154157
)?;
155158

synapse/api/ratelimiting.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from synapse.storage.databases.main import DataStore
2828
from synapse.types import Requester
2929
from synapse.util.clock import Clock
30+
from synapse.util.duration import Duration
3031
from synapse.util.wheel_timer import WheelTimer
3132

3233
if TYPE_CHECKING:
@@ -100,7 +101,7 @@ def __init__(
100101
# and doesn't affect correctness.
101102
self._timer: WheelTimer[Hashable] = WheelTimer()
102103

103-
self.clock.looping_call(self._prune_message_counts, 15 * 1000)
104+
self.clock.looping_call(self._prune_message_counts, Duration(seconds=15))
104105

105106
def _get_key(self, requester: Requester | None, key: Hashable | None) -> Hashable:
106107
"""Use the requester's MXID as a fallback key if no key is provided."""

synapse/app/phone_stats_home.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -218,13 +218,13 @@ def performance_stats_init() -> None:
218218
# table will decrease
219219
clock.looping_call(
220220
hs.get_datastores().main.generate_user_daily_visits,
221-
Duration(minutes=5).as_millis(),
221+
Duration(minutes=5),
222222
)
223223

224224
# monthly active user limiting functionality
225225
clock.looping_call(
226226
hs.get_datastores().main.reap_monthly_active_users,
227-
Duration(hours=1).as_millis(),
227+
Duration(hours=1),
228228
)
229229
hs.get_datastores().main.reap_monthly_active_users()
230230

@@ -263,29 +263,29 @@ async def _generate_monthly_active_users() -> None:
263263

264264
if hs.config.server.limit_usage_by_mau or hs.config.server.mau_stats_only:
265265
generate_monthly_active_users()
266-
clock.looping_call(generate_monthly_active_users, 5 * 60 * 1000)
266+
clock.looping_call(generate_monthly_active_users, Duration(minutes=5))
267267
# End of monthly active user settings
268268

269269
if hs.config.metrics.report_stats:
270270
logger.info("Scheduling stats reporting for 3 hour intervals")
271271
clock.looping_call(
272272
phone_stats_home,
273-
PHONE_HOME_INTERVAL.as_millis(),
273+
PHONE_HOME_INTERVAL,
274274
hs,
275275
stats,
276276
)
277277

278278
# We need to defer this init for the cases that we daemonize
279279
# otherwise the process ID we get is that of the non-daemon process
280280
clock.call_later(
281-
0,
281+
Duration(seconds=0),
282282
performance_stats_init,
283283
)
284284

285285
# We wait 5 minutes to send the first set of stats as the server can
286286
# be quite busy the first few minutes
287287
clock.call_later(
288-
INITIAL_DELAY_BEFORE_FIRST_PHONE_HOME.as_secs(),
288+
INITIAL_DELAY_BEFORE_FIRST_PHONE_HOME,
289289
phone_stats_home,
290290
hs,
291291
stats,

synapse/appservice/scheduler.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
from synapse.storage.databases.main import DataStore
7878
from synapse.types import DeviceListUpdates, JsonMapping
7979
from synapse.util.clock import Clock, DelayedCallWrapper
80+
from synapse.util.duration import Duration
8081

8182
if TYPE_CHECKING:
8283
from synapse.server import HomeServer
@@ -504,7 +505,7 @@ def __init__(
504505
self.scheduled_recovery: DelayedCallWrapper | None = None
505506

506507
def recover(self) -> None:
507-
delay = 2**self.backoff_counter
508+
delay = Duration(seconds=2**self.backoff_counter)
508509
logger.info("Scheduling retries on %s in %fs", self.service.id, delay)
509510
self.scheduled_recovery = self.clock.call_later(
510511
delay,

synapse/federation/federation_client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
from synapse.types.handlers.policy_server import RECOMMENDATION_OK, RECOMMENDATION_SPAM
7676
from synapse.util.async_helpers import concurrently_execute
7777
from synapse.util.caches.expiringcache import ExpiringCache
78+
from synapse.util.duration import Duration
7879
from synapse.util.retryutils import NotRetryingDestination
7980

8081
if TYPE_CHECKING:
@@ -132,7 +133,7 @@ def __init__(self, hs: "HomeServer"):
132133
super().__init__(hs)
133134

134135
self.pdu_destination_tried: dict[str, dict[str, int]] = {}
135-
self._clock.looping_call(self._clear_tried_cache, 60 * 1000)
136+
self._clock.looping_call(self._clear_tried_cache, Duration(minutes=1))
136137
self.state = hs.get_state_handler()
137138
self.transport_layer = hs.get_federation_transport_client()
138139

synapse/federation/federation_server.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
from synapse.util import unwrapFirstError
9090
from synapse.util.async_helpers import Linearizer, concurrently_execute, gather_results
9191
from synapse.util.caches.response_cache import ResponseCache
92+
from synapse.util.duration import Duration
9293
from synapse.util.stringutils import parse_server_name
9394

9495
if TYPE_CHECKING:
@@ -226,7 +227,7 @@ async def _handle_old_staged_events(self) -> None:
226227
)
227228

228229
# We pause a bit so that we don't start handling all rooms at once.
229-
await self._clock.sleep(random.uniform(0, 0.1))
230+
await self._clock.sleep(Duration(seconds=random.uniform(0, 0.1)))
230231

231232
async def on_backfill_request(
232233
self, origin: str, room_id: str, versions: list[str], limit: int
@@ -301,7 +302,9 @@ async def on_incoming_transaction(
301302
# Start a periodic check for old staged events. This is to handle
302303
# the case where locks time out, e.g. if another process gets killed
303304
# without dropping its locks.
304-
self._clock.looping_call(self._handle_old_staged_events, 60 * 1000)
305+
self._clock.looping_call(
306+
self._handle_old_staged_events, Duration(minutes=1)
307+
)
305308

306309
# keep this as early as possible to make the calculated origin ts as
307310
# accurate as possible.

synapse/federation/send_queue.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
from synapse.metrics import SERVER_NAME_LABEL, LaterGauge
5454
from synapse.replication.tcp.streams.federation import FederationStream
5555
from synapse.types import JsonDict, ReadReceipt, RoomStreamToken, StrCollection
56+
from synapse.util.duration import Duration
5657
from synapse.util.metrics import Measure
5758

5859
from .units import Edu
@@ -137,7 +138,7 @@ def register(queue_name: QueueNames, queue: Sized) -> None:
137138
assert isinstance(queue, Sized)
138139
register(queue_name, queue=queue)
139140

140-
self.clock.looping_call(self._clear_queue, 30 * 1000)
141+
self.clock.looping_call(self._clear_queue, Duration(seconds=30))
141142

142143
def shutdown(self) -> None:
143144
"""Stops this federation sender instance from sending further transactions."""

0 commit comments

Comments
 (0)