Skip to main content

clawft_kernel/
timer.rs

1//! Timer service: one-shot and repeating timers with message delivery.
2//!
3//! [`TimerService`] complements [`CronService`] (cron expressions, minute
4//! granularity) with sub-second precision timers. Each timer delivers a
5//! [`KernelMessage`] to the owner's PID when it fires.
6
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::time::Duration;
9
10use chrono::{DateTime, Utc};
11use dashmap::DashMap;
12use serde::{Deserialize, Serialize};
13use tokio_util::sync::CancellationToken;
14
15use crate::ipc::{KernelMessage, MessagePayload};
16use crate::process::Pid;
17
18/// Metadata about a timer (serializable snapshot).
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct TimerInfo {
21    /// Timer identifier.
22    pub id: String,
23    /// PID of the timer's owner.
24    pub owner_pid: Pid,
25    /// When the timer is scheduled to fire.
26    pub fire_at: DateTime<Utc>,
27    /// Repeat interval (None for one-shot).
28    pub repeat_interval_ms: Option<u64>,
29    /// Whether the timer has been cancelled.
30    pub cancelled: bool,
31}
32
33/// A timer entry tracked by the service.
34pub struct TimerEntry {
35    /// Timer identifier.
36    pub id: String,
37    /// PID of the timer's owner.
38    pub owner_pid: Pid,
39    /// When the timer is scheduled to fire.
40    pub fire_at: DateTime<Utc>,
41    /// Repeat interval (None for one-shot timers).
42    pub repeat_interval: Option<Duration>,
43    /// Payload to deliver when the timer fires.
44    pub payload: MessagePayload,
45    /// Token used to cancel this timer.
46    pub cancel_token: CancellationToken,
47}
48
49impl TimerEntry {
50    /// Get a serializable info snapshot.
51    pub fn info(&self) -> TimerInfo {
52        TimerInfo {
53            id: self.id.clone(),
54            owner_pid: self.owner_pid,
55            fire_at: self.fire_at,
56            repeat_interval_ms: self.repeat_interval.map(|d| d.as_millis() as u64),
57            cancelled: self.cancel_token.is_cancelled(),
58        }
59    }
60}
61
62/// Timer service: one-shot and repeating timers with message delivery.
63///
64/// Registered as a `SystemService` alongside `CronService`.
65/// Timers deliver messages to the owner's PID via the A2A router
66/// (or whatever delivery mechanism is wired at the kernel level).
67pub struct TimerService {
68    timers: DashMap<String, TimerEntry>,
69    next_id: AtomicU64,
70}
71
72impl TimerService {
73    /// Create a new timer service.
74    pub fn new() -> Self {
75        Self {
76            timers: DashMap::new(),
77            next_id: AtomicU64::new(1),
78        }
79    }
80
81    /// Generate a unique timer ID.
82    fn next_timer_id(&self) -> String {
83        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
84        format!("timer-{id}")
85    }
86
87    /// Create a one-shot timer.
88    ///
89    /// Returns the timer ID and a cancellation token. The timer fires
90    /// after `delay` and delivers a message with the given payload.
91    pub fn create_oneshot(
92        &self,
93        owner_pid: Pid,
94        delay: Duration,
95        payload: MessagePayload,
96    ) -> (String, CancellationToken) {
97        let id = self.next_timer_id();
98        let cancel_token = CancellationToken::new();
99        let fire_at = Utc::now() + chrono::Duration::from_std(delay).unwrap_or_default();
100
101        let entry = TimerEntry {
102            id: id.clone(),
103            owner_pid,
104            fire_at,
105            repeat_interval: None,
106            payload,
107            cancel_token: cancel_token.clone(),
108        };
109
110        self.timers.insert(id.clone(), entry);
111        (id, cancel_token)
112    }
113
114    /// Create a repeating timer.
115    ///
116    /// Returns the timer ID and a cancellation token. The timer fires
117    /// every `interval` starting after the first `interval` elapses.
118    pub fn create_repeating(
119        &self,
120        owner_pid: Pid,
121        interval: Duration,
122        payload: MessagePayload,
123    ) -> (String, CancellationToken) {
124        let id = self.next_timer_id();
125        let cancel_token = CancellationToken::new();
126        let fire_at = Utc::now() + chrono::Duration::from_std(interval).unwrap_or_default();
127
128        let entry = TimerEntry {
129            id: id.clone(),
130            owner_pid,
131            fire_at,
132            repeat_interval: Some(interval),
133            payload,
134            cancel_token: cancel_token.clone(),
135        };
136
137        self.timers.insert(id.clone(), entry);
138        (id, cancel_token)
139    }
140
141    /// Cancel a timer by ID.
142    ///
143    /// Returns `true` if the timer was found and cancelled.
144    pub fn cancel(&self, timer_id: &str) -> bool {
145        if let Some(entry) = self.timers.get(timer_id) {
146            entry.cancel_token.cancel();
147            true
148        } else {
149            false
150        }
151    }
152
153    /// Cancel all timers owned by a specific PID.
154    ///
155    /// Called when an agent exits to clean up its timers.
156    /// Returns the number of timers cancelled.
157    pub fn cancel_for_pid(&self, pid: Pid) -> usize {
158        let mut cancelled = 0;
159        for entry in self.timers.iter() {
160            if entry.owner_pid == pid {
161                entry.cancel_token.cancel();
162                cancelled += 1;
163            }
164        }
165        cancelled
166    }
167
168    /// Remove a timer from the registry (after it fires or is cancelled).
169    pub fn remove(&self, timer_id: &str) -> Option<TimerEntry> {
170        self.timers.remove(timer_id).map(|(_, e)| e)
171    }
172
173    /// Remove all cancelled timers from the registry.
174    pub fn cleanup_cancelled(&self) -> usize {
175        let cancelled: Vec<String> = self
176            .timers
177            .iter()
178            .filter(|e| e.cancel_token.is_cancelled())
179            .map(|e| e.key().clone())
180            .collect();
181        let count = cancelled.len();
182        for id in cancelled {
183            self.timers.remove(&id);
184        }
185        count
186    }
187
188    /// Get info about a specific timer.
189    pub fn info(&self, timer_id: &str) -> Option<TimerInfo> {
190        self.timers.get(timer_id).map(|e| e.info())
191    }
192
193    /// List all timer IDs.
194    pub fn list(&self) -> Vec<String> {
195        self.timers.iter().map(|e| e.key().clone()).collect()
196    }
197
198    /// List info about all timers.
199    pub fn list_info(&self) -> Vec<TimerInfo> {
200        self.timers.iter().map(|e| e.value().info()).collect()
201    }
202
203    /// List timers owned by a specific PID.
204    pub fn list_for_pid(&self, pid: Pid) -> Vec<TimerInfo> {
205        self.timers
206            .iter()
207            .filter(|e| e.owner_pid == pid)
208            .map(|e| e.info())
209            .collect()
210    }
211
212    /// Number of registered timers.
213    pub fn len(&self) -> usize {
214        self.timers.len()
215    }
216
217    /// Whether there are no timers.
218    pub fn is_empty(&self) -> bool {
219        self.timers.is_empty()
220    }
221
222    /// Check which timers are due to fire.
223    ///
224    /// Returns timer entries that have passed their fire_at time
225    /// and have not been cancelled. One-shot timers are removed;
226    /// repeating timers have their fire_at updated.
227    pub fn collect_due(&self) -> Vec<(Pid, KernelMessage)> {
228        let now = Utc::now();
229        let mut due = Vec::new();
230        let mut to_remove = Vec::new();
231        let mut to_reschedule = Vec::new();
232
233        for entry in self.timers.iter() {
234            if entry.cancel_token.is_cancelled() {
235                to_remove.push(entry.key().clone());
236                continue;
237            }
238
239            if entry.fire_at <= now {
240                let msg = KernelMessage::new(
241                    0, // from kernel
242                    crate::ipc::MessageTarget::Process(entry.owner_pid),
243                    entry.payload.clone(),
244                );
245                due.push((entry.owner_pid, msg));
246
247                if let Some(interval) = entry.repeat_interval {
248                    to_reschedule.push((entry.key().clone(), interval));
249                } else {
250                    to_remove.push(entry.key().clone());
251                }
252            }
253        }
254
255        // Remove one-shot timers that fired
256        for id in &to_remove {
257            self.timers.remove(id);
258        }
259
260        // Reschedule repeating timers
261        for (id, interval) in to_reschedule {
262            if let Some(mut entry) = self.timers.get_mut(&id) {
263                entry.fire_at =
264                    Utc::now() + chrono::Duration::from_std(interval).unwrap_or_default();
265            }
266        }
267
268        due
269    }
270}
271
272impl Default for TimerService {
273    fn default() -> Self {
274        Self::new()
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281    use crate::ipc::MessagePayload;
282
283    fn text_payload(msg: &str) -> MessagePayload {
284        MessagePayload::Text(msg.into())
285    }
286
287    #[test]
288    fn create_oneshot_timer() {
289        let service = TimerService::new();
290        let (id, _token) =
291            service.create_oneshot(1, Duration::from_secs(10), text_payload("fire!"));
292
293        assert!(service.info(&id).is_some());
294        assert_eq!(service.len(), 1);
295        let info = service.info(&id).unwrap();
296        assert_eq!(info.owner_pid, 1);
297        assert!(info.repeat_interval_ms.is_none());
298        assert!(!info.cancelled);
299    }
300
301    #[test]
302    fn create_repeating_timer() {
303        let service = TimerService::new();
304        let (id, _token) =
305            service.create_repeating(2, Duration::from_millis(500), text_payload("tick"));
306
307        let info = service.info(&id).unwrap();
308        assert_eq!(info.owner_pid, 2);
309        assert_eq!(info.repeat_interval_ms, Some(500));
310    }
311
312    #[test]
313    fn cancel_timer() {
314        let service = TimerService::new();
315        let (id, _token) = service.create_oneshot(1, Duration::from_secs(10), text_payload("x"));
316
317        assert!(service.cancel(&id));
318        let info = service.info(&id).unwrap();
319        assert!(info.cancelled);
320    }
321
322    #[test]
323    fn cancel_nonexistent() {
324        let service = TimerService::new();
325        assert!(!service.cancel("nope"));
326    }
327
328    #[test]
329    fn cancel_for_pid() {
330        let service = TimerService::new();
331        service.create_oneshot(1, Duration::from_secs(10), text_payload("a"));
332        service.create_oneshot(1, Duration::from_secs(20), text_payload("b"));
333        service.create_oneshot(2, Duration::from_secs(10), text_payload("c"));
334
335        let cancelled = service.cancel_for_pid(1);
336        assert_eq!(cancelled, 2);
337
338        // Timer for PID 2 should not be cancelled
339        let pid2_timers = service.list_for_pid(2);
340        assert_eq!(pid2_timers.len(), 1);
341        assert!(!pid2_timers[0].cancelled);
342    }
343
344    #[test]
345    fn cleanup_cancelled() {
346        let service = TimerService::new();
347        let (id, _) = service.create_oneshot(1, Duration::from_secs(10), text_payload("a"));
348        service.create_oneshot(2, Duration::from_secs(10), text_payload("b"));
349
350        service.cancel(&id);
351        let cleaned = service.cleanup_cancelled();
352        assert_eq!(cleaned, 1);
353        assert_eq!(service.len(), 1);
354    }
355
356    #[test]
357    fn collect_due_oneshot() {
358        let service = TimerService::new();
359        // Create a timer that fires immediately (0 delay)
360        service.create_oneshot(5, Duration::from_millis(0), text_payload("now!"));
361
362        // Small sleep to ensure fire_at is in the past
363        std::thread::sleep(Duration::from_millis(10));
364
365        let due = service.collect_due();
366        assert_eq!(due.len(), 1);
367        assert_eq!(due[0].0, 5); // owner_pid
368
369        // One-shot should be removed
370        assert!(service.is_empty());
371    }
372
373    #[test]
374    fn collect_due_repeating_reschedules() {
375        let service = TimerService::new();
376        let (id, _) = service.create_repeating(3, Duration::from_millis(0), text_payload("repeat"));
377
378        std::thread::sleep(Duration::from_millis(10));
379
380        let due = service.collect_due();
381        assert_eq!(due.len(), 1);
382
383        // Repeating timer should still exist (rescheduled)
384        assert!(service.info(&id).is_some());
385        assert_eq!(service.len(), 1);
386    }
387
388    #[test]
389    fn collect_due_skips_cancelled() {
390        let service = TimerService::new();
391        let (id, _) = service.create_oneshot(1, Duration::from_millis(0), text_payload("x"));
392        service.cancel(&id);
393
394        std::thread::sleep(Duration::from_millis(10));
395
396        let due = service.collect_due();
397        assert!(due.is_empty());
398    }
399
400    #[test]
401    fn collect_due_skips_future() {
402        let service = TimerService::new();
403        service.create_oneshot(1, Duration::from_secs(3600), text_payload("far future"));
404
405        let due = service.collect_due();
406        assert!(due.is_empty());
407        assert_eq!(service.len(), 1);
408    }
409
410    #[test]
411    fn list_and_list_info() {
412        let service = TimerService::new();
413        service.create_oneshot(1, Duration::from_secs(10), text_payload("a"));
414        service.create_oneshot(2, Duration::from_secs(20), text_payload("b"));
415
416        assert_eq!(service.list().len(), 2);
417        assert_eq!(service.list_info().len(), 2);
418    }
419
420    #[test]
421    fn list_for_pid() {
422        let service = TimerService::new();
423        service.create_oneshot(1, Duration::from_secs(10), text_payload("a"));
424        service.create_oneshot(1, Duration::from_secs(20), text_payload("b"));
425        service.create_oneshot(2, Duration::from_secs(10), text_payload("c"));
426
427        assert_eq!(service.list_for_pid(1).len(), 2);
428        assert_eq!(service.list_for_pid(2).len(), 1);
429        assert_eq!(service.list_for_pid(99).len(), 0);
430    }
431
432    #[test]
433    fn remove_timer() {
434        let service = TimerService::new();
435        let (id, _) = service.create_oneshot(1, Duration::from_secs(10), text_payload("x"));
436        assert_eq!(service.len(), 1);
437
438        let entry = service.remove(&id).unwrap();
439        assert_eq!(entry.owner_pid, 1);
440        assert!(service.is_empty());
441    }
442
443    #[test]
444    fn unique_ids() {
445        let service = TimerService::new();
446        let (id1, _) = service.create_oneshot(1, Duration::from_secs(10), text_payload("a"));
447        let (id2, _) = service.create_oneshot(1, Duration::from_secs(10), text_payload("b"));
448        assert_ne!(id1, id2);
449    }
450
451    #[test]
452    fn timer_info_serde_roundtrip() {
453        let info = TimerInfo {
454            id: "timer-1".into(),
455            owner_pid: 42,
456            fire_at: Utc::now(),
457            repeat_interval_ms: Some(1000),
458            cancelled: false,
459        };
460        let json = serde_json::to_string(&info).unwrap();
461        let restored: TimerInfo = serde_json::from_str(&json).unwrap();
462        assert_eq!(restored.id, "timer-1");
463        assert_eq!(restored.owner_pid, 42);
464        assert_eq!(restored.repeat_interval_ms, Some(1000));
465    }
466
467    #[test]
468    fn default_service() {
469        let service = TimerService::default();
470        assert!(service.is_empty());
471    }
472}