Skip to main content

zerodds_corba_ccm/
timer.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! TimerEventService — Spec OMG Time 1.1 §2.2-§2.4.
5//!
6//! Provides one-shot and periodic timers embedded in a container
7//! worker thread. When a timer fires, a `TimerCallback` is invoked.
8
9use alloc::collections::BTreeMap;
10use alloc::sync::Arc;
11use core::sync::atomic::{AtomicBool, AtomicU64, Ordering};
12use std::sync::Mutex;
13use std::thread::{self, JoinHandle};
14use std::time::{Duration, Instant};
15
16/// Timer kind.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum TimerKind {
19    /// One-shot: fires exactly once.
20    OneShot,
21    /// Periodic: fires repeatedly at the period interval.
22    Periodic,
23}
24
25/// Timer handle (assigned by the service).
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
27pub struct TimerHandle(pub u64);
28
29/// Callback trait for timer firing.
30pub trait TimerCallback: Send + Sync {
31    /// Invoked by the service thread when the timer fires.
32    fn fire(&self, handle: TimerHandle);
33}
34
35struct TimerEntry {
36    kind: TimerKind,
37    next_fire: Instant,
38    period: Duration,
39    callback: Arc<dyn TimerCallback>,
40}
41
42struct ServiceInner {
43    next_handle: AtomicU64,
44    timers: Mutex<BTreeMap<TimerHandle, TimerEntry>>,
45    shutdown: AtomicBool,
46}
47
48/// TimerEventService — starts a worker thread.
49pub struct TimerEventService {
50    inner: Arc<ServiceInner>,
51    worker: Option<JoinHandle<()>>,
52}
53
54impl core::fmt::Debug for TimerEventService {
55    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
56        let n = self.inner.timers.lock().ok().map(|g| g.len()).unwrap_or(0);
57        f.debug_struct("TimerEventService")
58            .field("active_timers", &n)
59            .finish()
60    }
61}
62
63impl Default for TimerEventService {
64    fn default() -> Self {
65        Self::new()
66    }
67}
68
69impl TimerEventService {
70    /// Constructor — starts the worker thread.
71    #[must_use]
72    pub fn new() -> Self {
73        let inner = Arc::new(ServiceInner {
74            next_handle: AtomicU64::new(1),
75            timers: Mutex::new(BTreeMap::new()),
76            shutdown: AtomicBool::new(false),
77        });
78        let inner_w = Arc::clone(&inner);
79        let worker = thread::Builder::new()
80            .name("ccm-timer-service".into())
81            .spawn(move || run_worker(&inner_w))
82            .ok();
83        Self { inner, worker }
84    }
85
86    /// Creates a one-shot timer that fires after `delay`.
87    pub fn create_one_shot(&self, delay: Duration, cb: Arc<dyn TimerCallback>) -> TimerHandle {
88        self.create_internal(TimerKind::OneShot, delay, delay, cb)
89    }
90
91    /// Creates a periodic timer with a `period` interval.
92    pub fn create_periodic(&self, period: Duration, cb: Arc<dyn TimerCallback>) -> TimerHandle {
93        self.create_internal(TimerKind::Periodic, period, period, cb)
94    }
95
96    fn create_internal(
97        &self,
98        kind: TimerKind,
99        delay: Duration,
100        period: Duration,
101        callback: Arc<dyn TimerCallback>,
102    ) -> TimerHandle {
103        let handle = TimerHandle(self.inner.next_handle.fetch_add(1, Ordering::Relaxed));
104        let entry = TimerEntry {
105            kind,
106            next_fire: Instant::now() + delay,
107            period,
108            callback,
109        };
110        if let Ok(mut g) = self.inner.timers.lock() {
111            g.insert(handle, entry);
112        }
113        handle
114    }
115
116    /// Cancels a timer.
117    pub fn cancel(&self, handle: TimerHandle) -> bool {
118        self.inner
119            .timers
120            .lock()
121            .map(|mut g| g.remove(&handle).is_some())
122            .unwrap_or(false)
123    }
124
125    /// Number of active timers.
126    #[must_use]
127    pub fn active_count(&self) -> usize {
128        self.inner.timers.lock().map(|g| g.len()).unwrap_or(0)
129    }
130
131    /// Stops the service.
132    pub fn shutdown(mut self) {
133        self.inner.shutdown.store(true, Ordering::Release);
134        if let Some(j) = self.worker.take() {
135            let _ = j.join();
136        }
137    }
138}
139
140impl Drop for TimerEventService {
141    fn drop(&mut self) {
142        self.inner.shutdown.store(true, Ordering::Release);
143        if let Some(j) = self.worker.take() {
144            let _ = j.join();
145        }
146    }
147}
148
149fn run_worker(inner: &Arc<ServiceInner>) {
150    let tick = Duration::from_millis(20);
151    while !inner.shutdown.load(Ordering::Acquire) {
152        let now = Instant::now();
153        let mut to_fire: alloc::vec::Vec<(TimerHandle, Arc<dyn TimerCallback>)> = alloc::vec![];
154        let mut to_remove: alloc::vec::Vec<TimerHandle> = alloc::vec![];
155        if let Ok(mut g) = inner.timers.lock() {
156            for (h, e) in g.iter_mut() {
157                if e.next_fire <= now {
158                    to_fire.push((*h, Arc::clone(&e.callback)));
159                    match e.kind {
160                        TimerKind::OneShot => to_remove.push(*h),
161                        TimerKind::Periodic => e.next_fire = now + e.period,
162                    }
163                }
164            }
165            for h in &to_remove {
166                g.remove(h);
167            }
168        }
169        for (h, cb) in to_fire {
170            cb.fire(h);
171        }
172        thread::sleep(tick);
173    }
174}
175
176#[cfg(test)]
177#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
178mod tests {
179    use super::*;
180    use core::sync::atomic::AtomicUsize;
181
182    struct CountingCallback {
183        fired: Arc<AtomicUsize>,
184    }
185    impl TimerCallback for CountingCallback {
186        fn fire(&self, _: TimerHandle) {
187            self.fired.fetch_add(1, Ordering::Relaxed);
188        }
189    }
190
191    fn waitfor(c: &Arc<AtomicUsize>, target: usize, timeout: Duration) {
192        let start = Instant::now();
193        while c.load(Ordering::Relaxed) < target && start.elapsed() < timeout {
194            thread::sleep(Duration::from_millis(20));
195        }
196    }
197
198    #[test]
199    fn one_shot_fires_once() {
200        let svc = TimerEventService::new();
201        let counter = Arc::new(AtomicUsize::new(0));
202        let cb = Arc::new(CountingCallback {
203            fired: Arc::clone(&counter),
204        });
205        let _ = svc.create_one_shot(Duration::from_millis(50), cb);
206        waitfor(&counter, 1, Duration::from_secs(2));
207        assert_eq!(counter.load(Ordering::Relaxed), 1);
208        // After firing, the one-shot timer is gone.
209        thread::sleep(Duration::from_millis(150));
210        assert_eq!(svc.active_count(), 0);
211    }
212
213    #[test]
214    fn periodic_fires_multiple_times() {
215        let svc = TimerEventService::new();
216        let counter = Arc::new(AtomicUsize::new(0));
217        let cb = Arc::new(CountingCallback {
218            fired: Arc::clone(&counter),
219        });
220        let h = svc.create_periodic(Duration::from_millis(50), cb);
221        waitfor(&counter, 3, Duration::from_secs(3));
222        assert!(counter.load(Ordering::Relaxed) >= 3);
223        svc.cancel(h);
224    }
225
226    #[test]
227    fn cancel_stops_periodic() {
228        let svc = TimerEventService::new();
229        let counter = Arc::new(AtomicUsize::new(0));
230        let cb = Arc::new(CountingCallback {
231            fired: Arc::clone(&counter),
232        });
233        let h = svc.create_periodic(Duration::from_millis(50), cb);
234        thread::sleep(Duration::from_millis(150));
235        assert!(svc.cancel(h));
236        let after = counter.load(Ordering::Relaxed);
237        thread::sleep(Duration::from_millis(200));
238        assert_eq!(counter.load(Ordering::Relaxed), after);
239    }
240
241    #[test]
242    fn cancel_unknown_returns_false() {
243        let svc = TimerEventService::new();
244        assert!(!svc.cancel(TimerHandle(9999)));
245    }
246}