1use std::{
19 num::NonZeroU64,
20 sync::{
21 Arc,
22 atomic::{self, AtomicU64},
23 },
24};
25
26use nautilus_core::{
27 UUID4, UnixNanos,
28 correctness::{FAILED, check_valid_string_utf8},
29 datetime::floor_to_nearest_microsecond,
30 time::get_atomic_clock_realtime,
31};
32use tokio::{
33 task::JoinHandle,
34 time::{Duration, Instant},
35};
36use ustr::Ustr;
37
38use super::runtime::get_runtime;
39use crate::{
40 runner::TimeEventSender,
41 timer::{TimeEvent, TimeEventCallback, TimeEventHandler, Timer},
42};
43
44#[derive(Debug)]
53pub struct LiveTimer {
54 pub name: Ustr,
56 pub interval_ns: NonZeroU64,
58 pub start_time_ns: UnixNanos,
60 pub stop_time_ns: Option<UnixNanos>,
62 pub fire_immediately: bool,
64 next_time_ns: Arc<AtomicU64>,
65 callback: TimeEventCallback,
66 task_handle: Option<JoinHandle<()>>,
67 canceled: bool,
68 sender: Option<Arc<dyn TimeEventSender>>,
69}
70
71impl LiveTimer {
72 #[must_use]
78 pub fn new(
79 name: Ustr,
80 interval_ns: NonZeroU64,
81 start_time_ns: UnixNanos,
82 stop_time_ns: Option<UnixNanos>,
83 callback: TimeEventCallback,
84 fire_immediately: bool,
85 sender: Option<Arc<dyn TimeEventSender>>,
86 ) -> Self {
87 check_valid_string_utf8(name, stringify!(name)).expect(FAILED);
88
89 let next_time_ns = if fire_immediately {
90 start_time_ns.as_u64()
91 } else {
92 start_time_ns.as_u64() + interval_ns.get()
93 };
94
95 log::trace!("Creating timer '{name}'");
96
97 Self {
98 name,
99 interval_ns,
100 start_time_ns,
101 stop_time_ns,
102 fire_immediately,
103 next_time_ns: Arc::new(AtomicU64::new(next_time_ns)),
104 callback,
105 task_handle: None,
106 canceled: false,
107 sender,
108 }
109 }
110
111 #[must_use]
115 pub fn next_time_ns(&self) -> UnixNanos {
116 UnixNanos::from(self.next_time_ns.load(atomic::Ordering::SeqCst))
117 }
118
119 #[must_use]
124 pub fn is_expired(&self) -> bool {
125 self.canceled
126 || self
127 .task_handle
128 .as_ref()
129 .is_some_and(tokio::task::JoinHandle::is_finished)
130 }
131
132 #[allow(unused_variables)]
141 pub fn start(&mut self) {
142 let event_name = self.name;
143 let stop_time_ns = self.stop_time_ns;
144 let interval_ns = self.interval_ns.get();
145
146 if self.callback.is_local() {
147 log::debug!(
148 "Timer '{event_name}' uses a RustLocal callback on a live Tokio timer; \
149 callback registry dispatch is needed to avoid cloning Rc on worker threads"
150 );
151 }
152
153 let callback = self.callback.clone();
154
155 let clock = get_atomic_clock_realtime();
157 let now_ns = clock.get_time_ns();
158
159 let now_raw = now_ns.as_u64();
161 let mut observed_next = self.next_time_ns.load(atomic::Ordering::SeqCst);
162
163 if observed_next <= now_raw {
164 loop {
165 match self.next_time_ns.compare_exchange(
166 observed_next,
167 now_raw,
168 atomic::Ordering::SeqCst,
169 atomic::Ordering::SeqCst,
170 ) {
171 Ok(_) => {
172 if observed_next < now_raw {
173 let original = UnixNanos::from(observed_next);
174 log::warn!(
175 "Timer '{event_name}' alert time {} was in the past, adjusted to current time for immediate fire",
176 original.to_rfc3339(),
177 );
178 }
179 observed_next = now_raw;
180 break;
181 }
182 Err(actual) => {
183 observed_next = actual;
184 if observed_next > now_raw {
185 break;
186 }
187 }
188 }
189 }
190 }
191
192 let mut next_time_ns = UnixNanos::from(floor_to_nearest_microsecond(observed_next));
194 let next_time_atomic = self.next_time_ns.clone();
195 next_time_atomic.store(next_time_ns.as_u64(), atomic::Ordering::SeqCst);
196
197 let sender = self.sender.clone();
198
199 let rt = get_runtime();
200 let handle = rt.spawn(async move {
201 let clock = get_atomic_clock_realtime();
202
203 let overhead = Duration::from_millis(1);
205 let delay_ns = next_time_ns.saturating_sub(now_ns.as_u64());
206 let mut delay = Duration::from_nanos(delay_ns);
207
208 if delay > overhead {
210 delay -= overhead;
211 } else {
212 delay = Duration::from_nanos(0);
213 }
214
215 let start = Instant::now() + delay;
216
217 let mut timer = tokio::time::interval_at(start, Duration::from_nanos(interval_ns));
218
219 loop {
220 timer.tick().await;
223 let now_ns = clock.get_time_ns();
224
225 let event = TimeEvent::new(event_name, UUID4::new(), next_time_ns, now_ns);
226
227 if let Some(sender) = sender.as_ref() {
228 let handler = TimeEventHandler::new(event, callback.clone());
231 sender.send(handler);
232 } else {
233 #[cfg(feature = "python")]
234 if matches!(&callback, TimeEventCallback::Python(_)) {
235 callback.call(event);
236 } else {
237 panic!("timer event sender was unset for Rust callback system");
238 }
239
240 #[cfg(not(feature = "python"))]
241 {
242 panic!("timer event sender was unset for Rust callback system");
243 }
244 }
245
246 next_time_ns += interval_ns;
248 next_time_atomic.store(next_time_ns.as_u64(), atomic::Ordering::SeqCst);
249
250 if let Some(stop_time_ns) = stop_time_ns
252 && std::cmp::max(next_time_ns, now_ns) >= stop_time_ns
253 {
254 break; }
256 }
257 });
258
259 self.task_handle = Some(handle);
260 self.canceled = false;
261 }
262
263 pub fn cancel(&mut self) {
267 log::trace!("Cancel timer '{}'", self.name);
268
269 if let Some(handle) = self.task_handle.take() {
270 handle.abort();
271 }
272 self.canceled = true;
273 }
274}
275
276impl Timer for LiveTimer {
277 fn is_expired(&self) -> bool {
278 Self::is_expired(self)
279 }
280
281 fn cancel(&mut self) {
282 Self::cancel(self);
283 }
284}
285
286impl Drop for LiveTimer {
287 fn drop(&mut self) {
288 if let Some(handle) = self.task_handle.take() {
289 handle.abort();
290 }
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use std::{num::NonZeroU64, sync::Arc};
297 #[cfg(feature = "python")]
298 use std::{
299 sync::{Mutex, mpsc},
300 time::Duration as StdDuration,
301 };
302
303 use nautilus_core::{
304 UnixNanos, datetime::floor_to_nearest_microsecond, time::get_atomic_clock_realtime,
305 };
306 #[cfg(feature = "python")]
307 use pyo3::{
308 Python,
309 types::{PyAnyMethods, PyList, PyListMethods},
310 };
311 use rstest::*;
312 use ustr::Ustr;
313
314 use super::LiveTimer;
315 use crate::{
316 runner::TimeEventSender,
317 timer::{TimeEventCallback, TimeEventHandler},
318 };
319
320 #[rstest]
321 fn test_live_timer_fire_immediately_field() {
322 let timer = LiveTimer::new(
323 Ustr::from("TEST_TIMER"),
324 NonZeroU64::new(1000).unwrap(),
325 UnixNanos::from(100),
326 None,
327 TimeEventCallback::from(|_| {}),
328 true, None, );
331
332 assert!(timer.fire_immediately);
334
335 assert_eq!(timer.next_time_ns(), UnixNanos::from(100));
337 }
338
339 #[rstest]
340 fn test_live_timer_fire_immediately_false_field() {
341 let timer = LiveTimer::new(
342 Ustr::from("TEST_TIMER"),
343 NonZeroU64::new(1000).unwrap(),
344 UnixNanos::from(100),
345 None,
346 TimeEventCallback::from(|_| {}),
347 false, None, );
350
351 assert!(!timer.fire_immediately);
353
354 assert_eq!(timer.next_time_ns(), UnixNanos::from(1100));
356 }
357
358 #[rstest]
359 fn test_live_timer_adjusts_past_due_start_time() {
360 #[derive(Debug)]
361 struct NoopSender;
362
363 impl TimeEventSender for NoopSender {
364 fn send(&self, _handler: TimeEventHandler) {}
365 }
366
367 let sender = Arc::new(NoopSender);
368 let mut timer = LiveTimer::new(
369 Ustr::from("PAST_TIMER"),
370 NonZeroU64::new(1).unwrap(),
371 UnixNanos::from(0),
372 None,
373 TimeEventCallback::from(|_| {}),
374 true,
375 Some(sender),
376 );
377
378 let before = get_atomic_clock_realtime().get_time_ns();
379
380 timer.start();
381
382 let before_floored = UnixNanos::from(floor_to_nearest_microsecond(before.as_u64()));
385 assert!(timer.next_time_ns() >= before_floored);
386
387 timer.cancel();
388 }
389
390 #[cfg(feature = "python")]
391 #[rstest]
392 fn test_live_timer_with_sender_defers_python_callback_to_handler() {
393 #[derive(Debug)]
394 struct ChannelSender {
395 tx: Mutex<mpsc::Sender<TimeEventHandler>>,
396 }
397
398 impl TimeEventSender for ChannelSender {
399 fn send(&self, handler: TimeEventHandler) {
400 self.tx
401 .lock()
402 .expect("sender mutex should lock")
403 .send(handler)
404 .expect("handler should send");
405 }
406 }
407
408 Python::initialize();
409
410 Python::attach(|py| {
411 let py_list = PyList::empty(py);
412 let py_append = py_list
413 .getattr("append")
414 .expect("append should exist")
415 .unbind();
416 let callback = TimeEventCallback::from(py_append);
417 let (tx, rx) = mpsc::channel();
418 let sender = Arc::new(ChannelSender { tx: Mutex::new(tx) });
419 let now = get_atomic_clock_realtime().get_time_ns();
420
421 let mut timer = LiveTimer::new(
422 Ustr::from("PY_TIMER"),
423 NonZeroU64::new(1_000_000).unwrap(),
424 now,
425 Some(UnixNanos::from(now.as_u64() + 2_000_000)),
426 callback,
427 true,
428 Some(sender),
429 );
430
431 timer.start();
432 let handler = rx
433 .recv_timeout(StdDuration::from_secs(1))
434 .expect("timer handler should arrive without acquiring the GIL on the worker");
435 timer.cancel();
436
437 assert_eq!(py_list.len(), 0);
438 handler.run();
439 assert_eq!(py_list.len(), 1);
440 });
441 }
442}