sim_time/
lib.rs

1#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
2#![cfg_attr(feature = "fail-on-warnings", deny(clippy::all))]
3#![forbid(unsafe_code)]
4
5mod config;
6
7use chrono::{DateTime, Utc};
8pub use config::*;
9use std::{
10    sync::{
11        Arc, OnceLock,
12        atomic::{AtomicU64, Ordering},
13    },
14    time::Duration,
15};
16
17static INSTANCE: OnceLock<Time> = OnceLock::new();
18
19#[derive(Clone)]
20struct Time {
21    config: TimeConfig,
22    elapsed_ms: Arc<AtomicU64>,
23    ticker_task: Arc<OnceLock<()>>,
24}
25
26impl Time {
27    fn new(config: TimeConfig) -> Self {
28        let time = Self {
29            config,
30            elapsed_ms: Arc::new(AtomicU64::new(0)),
31            ticker_task: Arc::new(OnceLock::new()),
32        };
33        if !time.config.realtime {
34            time.spawn_ticker();
35        }
36        time
37    }
38
39    fn spawn_ticker(&self) {
40        let elapsed_ms = self.elapsed_ms.clone();
41        let sim_config = self
42            .config
43            .sim_time
44            .as_ref()
45            .expect("sim_time required when realtime is false");
46        let tick_interval_ms = sim_config.tick_interval_ms;
47        let tick_duration = sim_config.tick_duration_secs;
48        self.ticker_task.get_or_init(|| {
49            tokio::spawn(async move {
50                let mut interval =
51                    tokio::time::interval(tokio::time::Duration::from_millis(tick_interval_ms));
52                loop {
53                    interval.tick().await;
54                    elapsed_ms.fetch_add(tick_duration.as_millis() as u64, Ordering::Relaxed);
55                }
56            });
57        });
58    }
59
60    fn now(&self) -> DateTime<Utc> {
61        if self.config.realtime {
62            Utc::now()
63        } else {
64            let sim_config = self
65                .config
66                .sim_time
67                .as_ref()
68                .expect("sim_time required when realtime is false");
69            let elapsed_ms = self.elapsed_ms.load(Ordering::Relaxed);
70
71            let simulated_time =
72                sim_config.start_at + chrono::Duration::milliseconds(elapsed_ms as i64);
73
74            if sim_config.transform_to_realtime && simulated_time >= Utc::now() {
75                Utc::now()
76            } else {
77                simulated_time
78            }
79        }
80    }
81
82    fn real_ms(&self, duration: Duration) -> Duration {
83        if self.config.realtime {
84            duration
85        } else {
86            let sim_config = self
87                .config
88                .sim_time
89                .as_ref()
90                .expect("sim_time required when realtime is false");
91
92            let current_time = self.now();
93            let real_now = Utc::now();
94
95            if sim_config.transform_to_realtime && current_time >= real_now {
96                return duration;
97            }
98
99            let sim_ms_per_real_ms = sim_config.tick_duration_secs.as_millis() as f64
100                / sim_config.tick_interval_ms as f64;
101
102            Duration::from_millis((duration.as_millis() as f64 / sim_ms_per_real_ms).ceil() as u64)
103        }
104    }
105
106    fn sleep(&self, duration: Duration) -> tokio::time::Sleep {
107        tokio::time::sleep(self.real_ms(duration))
108    }
109
110    fn timeout<F>(&self, duration: Duration, future: F) -> tokio::time::Timeout<F::IntoFuture>
111    where
112        F: core::future::IntoFuture,
113    {
114        tokio::time::timeout(self.real_ms(duration), future)
115    }
116
117    pub async fn wait_until_realtime(&self) {
118        if self.config.realtime {
119            return;
120        }
121
122        let current = self.now();
123        let real_now = Utc::now();
124
125        if current >= real_now {
126            return;
127        }
128
129        let wait_duration =
130            std::time::Duration::from_millis((real_now - current).num_milliseconds() as u64);
131
132        self.sleep(wait_duration).await;
133    }
134}
135
136pub async fn wait_until_realtime() {
137    INSTANCE
138        .get_or_init(|| Time::new(TimeConfig::default()))
139        .wait_until_realtime()
140        .await
141}
142
143pub fn init(config: TimeConfig) {
144    INSTANCE.get_or_init(|| Time::new(config));
145}
146
147pub fn now() -> DateTime<Utc> {
148    INSTANCE
149        .get_or_init(|| Time::new(TimeConfig::default()))
150        .now()
151}
152
153pub fn sleep(duration: Duration) -> tokio::time::Sleep {
154    INSTANCE
155        .get_or_init(|| Time::new(TimeConfig::default()))
156        .sleep(duration)
157}
158
159pub fn timeout<F>(duration: Duration, future: F) -> tokio::time::Timeout<F::IntoFuture>
160where
161    F: core::future::IntoFuture,
162{
163    INSTANCE
164        .get_or_init(|| Time::new(TimeConfig::default()))
165        .timeout(duration, future)
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use chrono::Duration as ChronoDuration;
172    use std::time::Duration as StdDuration;
173
174    #[tokio::test]
175    async fn test_simulated_time() {
176        // Configure time where 10ms = 10 days of simulated time
177        let config = TimeConfig {
178            realtime: false,
179            sim_time: Some(SimTimeConfig {
180                start_at: Utc::now(),
181                tick_interval_ms: 10,
182                tick_duration_secs: StdDuration::from_secs(10 * 24 * 60 * 60), // 10 days in seconds
183                transform_to_realtime: false,
184            }),
185        };
186
187        init(config);
188        let start = now();
189        tokio::time::sleep(tokio::time::Duration::from_millis(20)).await;
190        let end = now();
191        let elapsed = end - start;
192
193        assert!(
194            elapsed >= ChronoDuration::days(19) && elapsed <= ChronoDuration::days(21),
195            "Expected ~20 days to pass, but got {} days",
196            elapsed.num_days()
197        );
198    }
199
200    #[test]
201    fn test_default_realtime() {
202        let t1 = now();
203        let t2 = Utc::now();
204        assert!(t2 - t1 < ChronoDuration::seconds(1));
205    }
206}