use core::cell::Cell;
use core::fmt;
#[cfg(atomic_clock)]
pub use atomic::AtomicManualClock;
use derive_more::Debug;
use t2t_core::{TimePoint, Timestamp};
use crate::Clock;
#[derive(Debug, Default)]
#[debug("ManualClock({:?})", reading.get())]
#[debug(bound(P: TimePoint + fmt::Debug))]
pub struct ManualClock<P = Timestamp> {
reading: Cell<P>,
}
impl<P: TimePoint> ManualClock<P> {
#[inline]
#[must_use]
pub const fn new(start: P) -> Self {
Self { reading: Cell::new(start) }
}
#[inline]
pub fn set(&self, point: P) {
self.reading.set(point);
}
#[inline]
#[expect(
clippy::arithmetic_side_effects,
reason = "the point's own addition, which saturates for t2t's"
)]
pub fn advance(&self, span: P::Span) {
self.reading.set(self.reading.get() + span);
}
}
impl<P: TimePoint> Clock for ManualClock<P> {
type Reading = P;
#[inline]
fn now(&self) -> P {
self.reading.get()
}
}
#[cfg(atomic_clock)]
mod atomic {
use core::fmt;
use core::marker::PhantomData;
use derive_more::Debug;
use t2t_core::{TimePoint, Timestamp};
use crate::Clock;
use crate::sync::{AtomicI64, Ordering};
#[derive(Debug)]
#[debug("AtomicManualClock({:?})", self.now())]
#[debug(bound(P: TimePoint + fmt::Debug))]
pub struct AtomicManualClock<P = Timestamp> {
reading: AtomicI64,
marker: PhantomData<fn() -> P>,
}
impl<P: TimePoint> AtomicManualClock<P> {
#[inline]
#[must_use]
pub fn new(start: P) -> Self {
Self { reading: AtomicI64::new(start.count()), marker: PhantomData }
}
#[inline]
pub fn set(&self, point: P) {
self.reading.store(point.count(), Ordering::Release);
}
#[inline]
#[expect(
clippy::arithmetic_side_effects,
reason = "the point's own addition, which saturates for t2t's"
)]
pub fn advance(&self, span: P::Span) {
let mut expected = self.reading.load(Ordering::Relaxed);
loop {
let target = (P::from_count(expected) + span).count();
match self.reading.compare_exchange_weak(
expected,
target,
Ordering::AcqRel,
Ordering::Relaxed,
) {
Ok(_) => return,
Err(actual) => expected = actual,
}
}
}
}
const _: () = {
const fn is_send_and_sync<T: Send + Sync>() {}
is_send_and_sync::<AtomicManualClock<Timestamp>>();
};
impl<P: TimePoint> Clock for AtomicManualClock<P> {
type Reading = P;
#[inline]
fn now(&self) -> P {
P::from_count(self.reading.load(Ordering::Acquire))
}
}
impl<P: TimePoint + Default> Default for AtomicManualClock<P> {
fn default() -> Self {
Self::new(P::default())
}
}
#[cfg(test)]
#[cfg(loom)]
mod model {
use loom::sync::Arc;
use loom::sync::atomic::{AtomicU64, Ordering};
use loom::{model, thread};
use t2t_core::{Timedelta, Timestamp};
use super::AtomicManualClock;
use crate::Clock;
fn clock_and_note() -> (Arc<AtomicManualClock>, Arc<AtomicU64>) {
(Arc::new(AtomicManualClock::new(Timestamp::UNIX_EPOCH)), Arc::new(AtomicU64::new(0)))
}
#[test]
fn a_reader_that_sees_a_set_point_sees_what_was_written_before_it() {
model(|| {
let (clock, note) = clock_and_note();
let setter = {
let (clock, note) = (Arc::clone(&clock), Arc::clone(¬e));
thread::spawn(move || {
note.store(7, Ordering::Relaxed);
clock.set(Timestamp::from_secs(1));
})
};
if clock.now() == Timestamp::from_secs(1) {
assert_eq!(note.load(Ordering::Relaxed), 7, "the note written before the set");
}
setter.join().expect("the setting thread ends");
});
}
#[test]
fn a_reader_that_sees_an_advance_sees_what_was_written_before_it() {
model(|| {
let (clock, note) = clock_and_note();
let advancer = {
let (clock, note) = (Arc::clone(&clock), Arc::clone(¬e));
thread::spawn(move || {
note.store(7, Ordering::Relaxed);
clock.advance(Timedelta::SECOND);
})
};
if clock.now() == Timestamp::from_secs(1) {
assert_eq!(
note.load(Ordering::Relaxed),
7,
"the note written before the advance"
);
}
advancer.join().expect("the advancing thread ends");
});
}
#[test]
fn two_threads_advancing_at_once_both_count() {
model(|| {
let (clock, _) = clock_and_note();
let advancer = {
let clock = Arc::clone(&clock);
thread::spawn(move || clock.advance(Timedelta::SECOND))
};
clock.advance(Timedelta::SECOND);
advancer.join().expect("the advancing thread ends");
assert_eq!(clock.now(), Timestamp::from_secs(2), "neither move lost");
});
}
}
}
#[cfg(test)]
#[cfg(not(loom))]
mod tests {
use alloc::format;
use std::thread;
use t2t_core::{Tickdelta, Tickstamp, Timedelta, Timestamp};
use crate::{AtomicManualClock, Clock, ManualClock};
#[test]
fn a_manual_clock_moves_both_ways_and_saturates() {
let clock = ManualClock::new(Tickstamp::from_ticks(100));
clock.advance(Tickdelta::from_ticks(-40));
assert_eq!(clock.now(), Tickstamp::from_ticks(60), "moved back by a negative span");
clock.set(Tickstamp::MAX);
clock.advance(Tickdelta::from_ticks(1));
assert_eq!(clock.now(), Tickstamp::MAX, "and saturating at the end");
}
#[test]
fn an_atomic_manual_clock_sums_every_threads_moves() {
let clock = AtomicManualClock::new(Timestamp::UNIX_EPOCH);
thread::scope(|scope| {
for _ in 0..8 {
scope.spawn(|| {
for _ in 0..1_000 {
clock.advance(Timedelta::NANOSECOND);
}
});
}
});
assert_eq!(clock.now(), Timestamp::from_nanos(8_000), "no move lost");
}
#[test]
fn a_clock_is_read_through_a_reference_and_debugged_as_its_reading() {
let clock = ManualClock::<Timestamp>::default();
let reference: &dyn Clock<Reading = Timestamp> = &clock;
assert_eq!(reference.now(), Timestamp::UNIX_EPOCH, "through a reference");
let written = format!("{clock:?}");
assert_eq!(written, "ManualClock(1970-01-01T00:00:00.000000000Z)", "as its reading");
let atomic = format!("{:?}", AtomicManualClock::new(Tickstamp::from_ticks(5)));
assert_eq!(atomic, "AtomicManualClock(5 ticks)", "for the shared clock too");
}
}