minuteurs/
timer.rs

1//! Types relative to the timer feature.
2
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::Arc;
5use std::time::Duration;
6
7use crate::Deadline;
8
9/* ---------- */
10
11/// A timer that ticks at a periodic time.
12///
13/// On missing ticks, the timer will burst until it catches up
14/// with the defined delay.
15#[derive(Debug)]
16pub struct Timer {
17    /// The inner state of the timer, toggle on each ticks.
18    state: State,
19
20    /// The deadline used to trigger the timer's ticks.
21    deadline: Deadline,
22}
23
24impl Timer {
25    /// Returns a new timer that ticks every `delay`.
26    pub fn new(delay: Duration) -> Self {
27        Self {
28            state: State::new(),
29            deadline: Deadline::repeat(delay),
30        }
31    }
32
33    /// Returns a new watcher associated to `self`.
34    pub fn watcher(&self) -> Watcher {
35        Watcher::new(self.state.clone())
36    }
37
38    /// Blocks the current thread until the next tick and notify the associated watchers.
39    pub fn tick(&mut self) {
40        self.deadline.wait();
41        self.state.toggle();
42    }
43}
44
45/* ---------- */
46
47/// A handle associated to a [`Timer`] that is notified when the timer ticks.
48///
49/// Watchers are safely clonable. A cloned watcher will be associated to the
50/// [`Timer`] of the original one. This is equivalent to calling [`Timer::watcher()`] twice.
51pub struct Watcher {
52    /// The inner state of the associated [`Timer`].
53    state: State,
54
55    /// The prévious value of the state.
56    prev_state: bool,
57}
58
59impl Watcher {
60    /// Returns a new watcher associated to a [`Timer`].
61    fn new(state: State) -> Self {
62        let prev_state = state.value();
63        Self { state, prev_state }
64    }
65
66    /// Returns whether or not the associated [`Timer`] has ticked.
67    pub fn has_ticked(&mut self) -> bool {
68        if self.state.value() != self.prev_state {
69            self.prev_state = !self.prev_state;
70            return true;
71        }
72
73        false
74    }
75}
76
77impl Clone for Watcher {
78    fn clone(&self) -> Self {
79        // FIXME: What happens if the timer ticks between the the clone and the prev_state ?
80        // We might miss 2 ticks here, is it a problem ?
81        // We probably should get the prev_value *before* the clone itself.
82        let state = self.state.clone();
83        let prev_state = state.value();
84
85        Self { state, prev_state }
86    }
87}
88
89/* ---------- */
90
91/// Inner state of the [`Timer`] and [`Watcher`] types.
92#[derive(Debug, Default, Clone)]
93struct State(Arc<AtomicBool>);
94
95impl State {
96    /// Returns a new state with a default value.
97    #[inline]
98    fn new() -> Self {
99        Self::default()
100    }
101
102    /// Flip the state's value.
103    #[inline]
104    fn toggle(&self) {
105        self.0.fetch_xor(true, Ordering::Release);
106    }
107
108    /// Returns the state's inner value.
109    #[inline]
110    fn value(&self) -> bool {
111        self.0.load(Ordering::Acquire)
112    }
113}
114
115#[cfg(test)]
116impl PartialEq<bool> for State {
117    #[inline]
118    fn eq(&self, other: &bool) -> bool {
119        self.value() == *other
120    }
121}
122
123/* ---------- */
124
125#[cfg(test)]
126mod state {
127    use super::*;
128
129    #[test]
130    fn new() {
131        let new = State::new();
132        assert_eq!(new, false);
133    }
134
135    #[test]
136    fn toggle() {
137        let new = State::new();
138        assert_eq!(new, false);
139
140        new.toggle();
141        assert_eq!(new, true);
142    }
143}
144
145#[cfg(test)]
146#[allow(clippy::module_inception)]
147mod timer {
148    use std::time::Instant;
149
150    use super::*;
151
152    #[test]
153    fn tick_delay() {
154        let now = Instant::now();
155        let mut timer = Timer::new(Duration::from_millis(100));
156
157        for count in 0..5 {
158            timer.tick();
159
160            let elapsed = now.elapsed();
161            assert!(
162                now.elapsed() >= Duration::from_millis(100 * count),
163                "elapsed = {elapsed:?}"
164            )
165        }
166    }
167}
168
169#[cfg(test)]
170mod watcher {
171    use std::time::Instant;
172
173    use super::*;
174
175    #[test]
176    fn new() {
177        let mut timer = Timer::new(Duration::from_millis(100));
178        let mut watcher = timer.watcher();
179
180        assert!(
181            !watcher.has_ticked(),
182            "watcher shouldn't have been notified yet"
183        );
184
185        timer.tick();
186        assert!(watcher.has_ticked(), "watcher should have been notified");
187        assert!(
188            !watcher.has_ticked(),
189            "watcher shouldn't have been notified instantly"
190        );
191    }
192
193    #[test]
194    fn cloned() {
195        let mut timer = Timer::new(Duration::from_millis(100));
196
197        let mut watcher = timer.watcher();
198        assert!(
199            !watcher.has_ticked(),
200            "watcher shouldn't have been notified yet"
201        );
202
203        let mut watcher_clone = watcher.clone();
204        assert!(
205            !watcher_clone.has_ticked(),
206            "watcher clone shouldn't have been notified yet"
207        );
208
209        timer.tick();
210        assert!(watcher.has_ticked(), "watcher should have been notified");
211        assert!(
212            watcher_clone.has_ticked(),
213            "watcher clone should have been notified"
214        );
215
216        let mut watcher_clone = watcher.clone();
217        assert!(
218            !watcher_clone.has_ticked(),
219            "2dn watcher clone shouldn't have been notified yet"
220        );
221    }
222
223    #[test]
224    fn thread_sync() {
225        let stop = Arc::new(AtomicBool::default());
226        let now = Instant::now();
227        let mut timer = Timer::new(Duration::from_millis(100));
228        let mut watcher = timer.watcher();
229
230        let stop_clone = Arc::clone(&stop);
231        let watcher_thread = std::thread::spawn(move || {
232            let mut loops = 1;
233
234            while !stop_clone.load(Ordering::Acquire) {
235                if watcher.has_ticked() {
236                    let elapsed = now.elapsed();
237                    let expected = Duration::from_millis(100 * loops);
238
239                    if now.elapsed() < expected {
240                        return Some((elapsed, expected));
241                    }
242
243                    loops += 1;
244                }
245            }
246
247            None
248        });
249
250        for _ in 0..5 {
251            timer.tick();
252        }
253
254        stop.store(true, Ordering::Release);
255        let test_result = watcher_thread.join().unwrap();
256        assert_eq!(
257            test_result, None,
258            "watcher detected a tick before the expected time"
259        );
260    }
261}