Skip to main content

moloch_core/agent/
timestamp.rs

1//! Millisecond-precision UTC timestamp type.
2//!
3//! Provides a type-safe wrapper around raw `i64` millisecond timestamps,
4//! preventing confusion between seconds and milliseconds, and ensuring
5//! timestamp values are always in the correct unit.
6//!
7//! See Section 3.2.2 of the Agent Accountability specification.
8
9use serde::{Deserialize, Serialize};
10use std::time::Duration;
11
12/// Millisecond-precision UTC timestamp.
13///
14/// Wraps a raw `i64` representing milliseconds since the Unix epoch.
15/// All agent module timestamps should use this type for consistency
16/// and type safety.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
18pub struct Timestamp(i64);
19
20impl Timestamp {
21    /// Create a timestamp representing the current time.
22    pub fn now() -> Self {
23        Self(chrono::Utc::now().timestamp_millis())
24    }
25
26    /// Create a timestamp from raw milliseconds since Unix epoch.
27    pub fn from_millis(ms: i64) -> Self {
28        Self(ms)
29    }
30
31    /// Get the raw milliseconds since Unix epoch.
32    pub fn as_millis(&self) -> i64 {
33        self.0
34    }
35
36    /// Get the elapsed duration since this timestamp.
37    ///
38    /// Returns `Duration::ZERO` if the timestamp is in the future.
39    pub fn elapsed(&self) -> Duration {
40        let now = chrono::Utc::now().timestamp_millis();
41        Duration::from_millis((now - self.0).max(0) as u64)
42    }
43
44    /// Check if this timestamp is expired given a time-to-live duration.
45    ///
46    /// Returns `true` if `elapsed() > ttl`.
47    pub fn is_expired(&self, ttl: Duration) -> bool {
48        self.elapsed() > ttl
49    }
50}
51
52impl std::fmt::Display for Timestamp {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        write!(f, "{}ms", self.0)
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn timestamp_now_returns_milliseconds() {
64        let ts = Timestamp::now();
65        // Must be after 2020-01-01 in millis
66        assert!(ts.as_millis() > 1_577_836_800_000);
67    }
68
69    #[test]
70    fn timestamp_ordering() {
71        let t1 = Timestamp::now();
72        std::thread::sleep(Duration::from_millis(2));
73        let t2 = Timestamp::now();
74        assert!(t2 > t1);
75    }
76
77    #[test]
78    fn timestamp_elapsed_since() {
79        let t1 = Timestamp::now();
80        std::thread::sleep(Duration::from_millis(10));
81        let elapsed = t1.elapsed();
82        assert!(elapsed.as_millis() >= 10);
83    }
84
85    #[test]
86    fn timestamp_from_millis_roundtrip() {
87        let ms = 1706500000000_i64;
88        let ts = Timestamp::from_millis(ms);
89        assert_eq!(ts.as_millis(), ms);
90    }
91
92    #[test]
93    fn timestamp_is_expired_after_duration() {
94        let ts = Timestamp::from_millis(chrono::Utc::now().timestamp_millis() - 5000);
95        let ttl = Duration::from_secs(3);
96        assert!(ts.is_expired(ttl));
97    }
98
99    #[test]
100    fn timestamp_is_not_expired_within_duration() {
101        let ts = Timestamp::now();
102        let ttl = Duration::from_secs(3600);
103        assert!(!ts.is_expired(ttl));
104    }
105}