Skip to content

Commit b9527e4

Browse files
committed
refactor: make the session clock shared and monotonic under concurrency
1 parent 0df52c8 commit b9527e4

1 file changed

Lines changed: 75 additions & 4 deletions

File tree

packages/capture/src/clock/session_clock.rs

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,55 @@
1-
use std::time::Instant;
1+
use std::{
2+
sync::{
3+
Arc,
4+
atomic::{AtomicU64, Ordering},
5+
},
6+
time::Instant,
7+
};
28

39
pub trait MonotonicClock: Send + Sync {
410
fn now_ns(&self) -> u64;
511
}
612

13+
#[derive(Debug)]
14+
struct SessionClockInner {
15+
epoch: Instant,
16+
last_ns: AtomicU64,
17+
}
18+
719
#[derive(Debug, Clone)]
820
pub struct SessionClock {
9-
epoch: Instant,
21+
inner: Arc<SessionClockInner>,
1022
}
1123

1224
impl SessionClock {
1325
#[must_use]
1426
pub fn start() -> Self {
1527
Self {
16-
epoch: Instant::now(),
28+
inner: Arc::new(SessionClockInner {
29+
epoch: Instant::now(),
30+
last_ns: AtomicU64::new(0),
31+
}),
32+
}
33+
}
34+
35+
#[must_use]
36+
pub fn elapsed_ns(&self) -> u64 {
37+
u64::try_from(self.inner.epoch.elapsed().as_nanos()).unwrap_or(u64::MAX)
38+
}
39+
40+
fn publish_monotonic(&self, candidate: u64) -> u64 {
41+
let mut last = self.inner.last_ns.load(Ordering::Relaxed);
42+
loop {
43+
let next = candidate.max(last);
44+
match self.inner.last_ns.compare_exchange_weak(
45+
last,
46+
next,
47+
Ordering::Relaxed,
48+
Ordering::Relaxed,
49+
) {
50+
Ok(_) => return next,
51+
Err(observed) => last = observed,
52+
}
1753
}
1854
}
1955
}
@@ -26,6 +62,41 @@ impl Default for SessionClock {
2662

2763
impl MonotonicClock for SessionClock {
2864
fn now_ns(&self) -> u64 {
29-
u64::try_from(self.epoch.elapsed().as_nanos()).unwrap_or(u64::MAX)
65+
self.publish_monotonic(self.elapsed_ns())
66+
}
67+
}
68+
69+
#[cfg(test)]
70+
mod tests {
71+
use std::{sync::Arc, thread};
72+
73+
use super::{MonotonicClock, SessionClock};
74+
75+
#[test]
76+
fn clones_share_one_epoch() {
77+
let clock = SessionClock::start();
78+
let clone = clock.clone();
79+
assert!(clone.now_ns() >= clock.now_ns());
80+
}
81+
82+
#[test]
83+
fn concurrent_reads_never_move_backwards() {
84+
let clock = Arc::new(SessionClock::start());
85+
let handles = (0..4)
86+
.map(|_| {
87+
let clock = clock.clone();
88+
thread::spawn(move || {
89+
let mut previous = 0;
90+
for _ in 0..1_000 {
91+
let current = clock.now_ns();
92+
assert!(current >= previous);
93+
previous = current;
94+
}
95+
})
96+
})
97+
.collect::<Vec<_>>();
98+
for handle in handles {
99+
assert!(handle.join().is_ok());
100+
}
30101
}
31102
}

0 commit comments

Comments
 (0)