Files
synapse/rust/src/clock.rs
T
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

137 lines
4.8 KiB
Rust

/*
* This file is licensed under the Affero General Public License (AGPL) version 3.
*
* Copyright (C) 2026 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>.
*
*/
//! A clock for the Rust side of Synapse.
//!
//! Rust code often needs the current time somewhere it cannot cheaply ask
//! Python for it, such as on a tokio worker without the GIL. Each
//! homeserver's [`RustRuntime`](crate::runtime::RustRuntime) owns a [`Clock`]
//! that can be read without the GIL.
//!
//! Synapse's unit tests run against a virtual reactor clock, where time only
//! moves when a test says so. The test reactor keeps this clock in step via
//! [`set_virtual_time_msec`]. See `ThreadedMemoryReactorClock` in
//! `tests/server.py`.
use std::{
sync::atomic::{AtomicU64, Ordering},
time::{Duration, SystemTime, UNIX_EPOCH},
};
use pyo3::pyfunction;
/// A sentinel value stored in [`VIRTUAL_MILLIS`] while the clock is
/// reading the real time. This value indicates that the clock should read the
/// real system time rather than a virtual time.
///
/// We choose a value that will never be used in tests, namely the maximum `u64`
/// value. This is 584 million years after the epoch.
const REAL_TIME_SENTINEL: u64 = u64::MAX;
/// The virtual time in milliseconds since the Unix epoch, or
/// [`REAL_TIME_SENTINEL`]. Only ever set by tests, via [`set_virtual_time_msec`].
///
/// We use an AtomicU64 (rather than e.g. `Option<..>`) so that reads are fast
/// and do not require locking.
static VIRTUAL_MILLIS: AtomicU64 = AtomicU64::new(REAL_TIME_SENTINEL);
/// Pin the clock to the given time, in milliseconds since the Unix epoch.
///
/// Only tests call this. Nothing unpins the clock, since a test reactor
/// and the runtimes attached to it live for a single test.
#[pyfunction]
pub fn set_virtual_time_msec(millis: u64) {
if millis == REAL_TIME_SENTINEL {
panic!("Cannot set virtual time to the sentinel value REAL_TIME_SENTINEL");
}
VIRTUAL_MILLIS.store(millis, Ordering::Relaxed);
}
/// The current time as the Rust side of a homeserver sees it.
///
/// Like `synapse.util.clock.Clock` on the Python side, this is a wall clock.
/// While these are two separate, independent implementations, they both read
/// the same system clock (on Linux both are
/// `clock_gettime(CLOCK_REALTIME)`).Code that needs a monotonic clock should
/// use [`std::time::Instant`] directly.
///
/// In future, this may be extended to support per-homeserver looping calls
/// (which is why this is not a global clock).
///
/// See [`crate::clock`] docs for more details.
pub struct Clock {}
impl Default for Clock {
fn default() -> Self {
Self::new()
}
}
impl Clock {
/// A clock reading the real time.
pub fn new() -> Self {
Clock {}
}
/// The current time.
///
/// Reads the same system clock as `synapse.util.clock.Clock.time_msec()`
/// on the Python side. See the module docs.
pub fn now(&self) -> SystemTime {
// Check if virtual time is set, otherwise fall back to real time.
match VIRTUAL_MILLIS.load(Ordering::Relaxed) {
// On Linux this is a vDSO call rather than a syscall, so it is
// cheap enough to make on every read.
REAL_TIME_SENTINEL => SystemTime::now(),
virtual_millis => UNIX_EPOCH + Duration::from_millis(virtual_millis),
}
}
/// [`Clock::now`] in milliseconds since the Unix epoch.
///
/// Panics if the system clock is set before the Unix epoch, 1970. This was
/// checked at startup, so this panic should only ever occur if the system
/// clock is manually set before the Unix epoch during runtime.
pub fn now_millis(&self) -> u64 {
self.now()
.duration_since(UNIX_EPOCH)
.expect("system clock was set before the Unix epoch, 1970")
.as_millis() as u64
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn virtual_time_overrides_the_real_clock() {
// Any real wall clock reads later than this (2020-09-13).
const PLAUSIBLE_WALL_CLOCK_MILLIS: u64 = 1_600_000_000_000;
let clock = Clock::new();
assert!(clock.now_millis() > PLAUSIBLE_WALL_CLOCK_MILLIS);
set_virtual_time_msec(12345);
assert_eq!(clock.now_millis(), 12345);
assert_eq!(clock.now(), UNIX_EPOCH + Duration::from_millis(12345));
// Zero is a legitimate virtual time. The memory reactor starts there.
set_virtual_time_msec(0);
assert_eq!(clock.now_millis(), 0);
}
}