#![cfg_attr(
not(test),
deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::unreachable,
clippy::todo,
clippy::unimplemented,
clippy::indexing_slicing,
clippy::string_slice,
clippy::arithmetic_side_effects,
)
)]
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use chrono::{DateTime, Utc};
use crate::capsule::capture::current_scope;
use crate::time::ClockSource;
pub struct RecordingClock {
inner: Arc<dyn ClockSource>,
}
impl RecordingClock {
#[must_use]
pub fn new(inner: Arc<dyn ClockSource>) -> Self {
Self { inner }
}
}
impl ClockSource for RecordingClock {
fn now(&self) -> DateTime<Utc> {
let reading = self.inner.now();
if let Some(scope) = current_scope() {
scope.record_clock(reading);
}
reading
}
fn monotonic(&self) -> crate::time::MonotonicInstant {
let reading = self.inner.monotonic();
if let Some(scope) = current_scope() {
scope.record_monotonic(reading.since_origin());
}
reading
}
}
tokio::task_local! {
static REPLAY_REQUEST: ();
}
pub async fn with_replay_request_scope<F: Future>(future: F) -> F::Output {
REPLAY_REQUEST.scope((), future).await
}
fn in_replay_request() -> bool {
REPLAY_REQUEST.try_with(|()| ()).is_ok()
}
pub struct ReplayClock {
readings: Mutex<VecDeque<DateTime<Utc>>>,
last: Mutex<Option<DateTime<Utc>>>,
over_reads: std::sync::atomic::AtomicUsize,
fallback: DateTime<Utc>,
monotonic: Mutex<VecDeque<std::time::Duration>>,
last_monotonic: Mutex<Option<std::time::Duration>>,
}
impl ReplayClock {
#[must_use]
pub fn new(readings: Vec<DateTime<Utc>>, fallback: DateTime<Utc>) -> Self {
Self {
readings: Mutex::new(readings.into()),
last: Mutex::new(None),
over_reads: std::sync::atomic::AtomicUsize::new(0),
fallback,
monotonic: Mutex::new(VecDeque::new()),
last_monotonic: Mutex::new(None),
}
}
#[must_use]
pub fn with_monotonic(self, readings: Vec<std::time::Duration>) -> Self {
Self {
monotonic: Mutex::new(readings.into()),
..self
}
}
#[must_use]
pub fn over_reads(&self) -> usize {
self.over_reads.load(std::sync::atomic::Ordering::Relaxed)
}
#[must_use]
pub fn unconsumed(&self) -> usize {
self.readings.lock().map_or(0, |readings| readings.len())
}
}
impl ClockSource for ReplayClock {
fn now(&self) -> DateTime<Utc> {
if !in_replay_request() {
return self
.last
.lock()
.ok()
.and_then(|last| *last)
.unwrap_or(self.fallback);
}
if let Ok(mut readings) = self.readings.lock()
&& let Some(reading) = readings.pop_front()
{
if let Ok(mut last) = self.last.lock() {
*last = Some(reading);
}
return reading;
}
self.over_reads
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
self.last
.lock()
.ok()
.and_then(|last| *last)
.unwrap_or(self.fallback)
}
fn monotonic(&self) -> crate::time::MonotonicInstant {
use std::time::Duration;
let last_or_origin = || {
crate::time::MonotonicInstant::from_origin_elapsed(
self.last_monotonic
.lock()
.ok()
.and_then(|last| *last)
.unwrap_or(Duration::ZERO),
)
};
if !in_replay_request() {
return last_or_origin();
}
if let Ok(mut readings) = self.monotonic.lock()
&& let Some(reading) = readings.pop_front()
{
if let Ok(mut last) = self.last_monotonic.lock() {
*last = Some(reading);
}
return crate::time::MonotonicInstant::from_origin_elapsed(reading);
}
last_or_origin()
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone as _;
fn at(second: u32) -> DateTime<Utc> {
Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, second)
.single()
.expect("valid timestamp")
}
#[tokio::test]
async fn replay_clock_serves_recorded_readings_in_order() {
let clock = ReplayClock::new(vec![at(1), at(2)], at(0));
with_replay_request_scope(async {
assert_eq!(clock.now(), at(1));
assert_eq!(clock.now(), at(2));
})
.await;
assert_eq!(clock.over_reads(), 0);
}
#[tokio::test]
async fn replay_clock_reports_readings_it_never_served() {
let clock = ReplayClock::new(vec![at(1), at(2), at(3)], at(0));
with_replay_request_scope(async {
assert_eq!(clock.now(), at(1));
})
.await;
assert_eq!(
clock.unconsumed(),
2,
"readings the replayed code never asked for must be countable"
);
}
#[tokio::test]
async fn replay_clock_over_read_reuses_the_last_reading() {
let clock = ReplayClock::new(vec![at(1)], at(0));
with_replay_request_scope(async {
assert_eq!(clock.now(), at(1));
assert_eq!(clock.now(), at(1), "an over-read must repeat, not drift");
})
.await;
assert_eq!(clock.over_reads(), 1);
}
#[tokio::test]
async fn replay_clock_with_no_readings_uses_the_fallback() {
let clock = ReplayClock::new(Vec::new(), at(9));
with_replay_request_scope(async {
assert_eq!(clock.now(), at(9));
})
.await;
assert_eq!(clock.over_reads(), 1);
}
#[tokio::test]
async fn replay_clock_serves_monotonic_readings_scope_gated() {
use std::time::Duration;
let clock = ReplayClock::new(Vec::new(), at(0))
.with_monotonic(vec![Duration::from_millis(10), Duration::from_millis(30)]);
assert_eq!(
clock.monotonic(),
crate::time::MonotonicInstant::ORIGIN,
"an unscoped monotonic read must not touch the queue"
);
with_replay_request_scope(async {
assert_eq!(
clock.monotonic().since_origin(),
Duration::from_millis(10),
"recorded monotonic readings are served in order"
);
assert_eq!(clock.monotonic().since_origin(), Duration::from_millis(30));
assert_eq!(
clock.monotonic().since_origin(),
Duration::from_millis(30),
"an over-read repeats the last reading instead of drifting to \
the real timeline"
);
})
.await;
}
#[tokio::test]
async fn reads_outside_the_replay_request_scope_do_not_consume_the_queue() {
let clock = Arc::new(ReplayClock::new(vec![at(1), at(2)], at(0)));
assert_eq!(clock.now(), at(0), "an unscoped read serves the fallback");
assert_eq!(clock.unconsumed(), 2, "…without consuming");
with_replay_request_scope({
let clock = Arc::clone(&clock);
async move {
assert_eq!(clock.now(), at(1));
let spawned = {
let clock = Arc::clone(&clock);
tokio::spawn(async move { clock.now() })
};
let seen = spawned.await.expect("spawned read");
assert_eq!(
seen,
at(1),
"a spawned read repeats the last served reading, non-consuming"
);
assert_eq!(
clock.now(),
at(2),
"the handler still gets its next reading"
);
}
})
.await;
assert_eq!(clock.over_reads(), 0, "unscoped reads are not drift");
}
#[test]
fn recording_clock_passes_through_outside_a_request() {
let clock = RecordingClock::new(Arc::new(crate::time::FixedClock::at(at(5))));
assert_eq!(clock.now(), at(5));
}
}