Skip to main content

nautilus_common/python/
timer.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use std::str::FromStr;
17
18use nautilus_core::{
19    UUID4, UnixNanos,
20    python::{IntoPyObjectNautilusExt, to_pyvalue_err},
21};
22use pyo3::{
23    IntoPyObjectExt,
24    basic::CompareOp,
25    prelude::*,
26    types::{PyInt, PyString, PyTuple},
27};
28use ustr::Ustr;
29
30use crate::timer::TimeEvent;
31
32#[pymethods]
33#[pyo3_stub_gen::derive::gen_stub_pymethods]
34impl TimeEvent {
35    /// Represents a named timer event.
36    ///
37    /// `ts_event` records the scheduled event time, while `ts_init` records
38    /// when the event instance was initialized.
39    #[new]
40    fn py_new(name: &str, event_id: UUID4, ts_event: u64, ts_init: u64) -> Self {
41        Self::new(Ustr::from(name), event_id, ts_event.into(), ts_init.into())
42    }
43
44    fn __setstate__(&mut self, state: &Bound<'_, PyAny>) -> PyResult<()> {
45        let py_tuple: &Bound<'_, PyTuple> = state.cast::<PyTuple>()?;
46
47        let ts_event = py_tuple.get_item(2)?.cast::<PyInt>()?.extract::<u64>()?;
48        let ts_init: u64 = py_tuple.get_item(3)?.cast::<PyInt>()?.extract::<u64>()?;
49
50        self.name = Ustr::from(
51            py_tuple
52                .get_item(0)?
53                .cast::<PyString>()?
54                .extract::<&str>()?,
55        );
56        self.event_id = UUID4::from_str(
57            py_tuple
58                .get_item(1)?
59                .cast::<PyString>()?
60                .extract::<&str>()?,
61        )
62        .map_err(to_pyvalue_err)?;
63        self.ts_event = ts_event.into();
64        self.ts_init = ts_init.into();
65
66        Ok(())
67    }
68
69    fn __getstate__(&self, py: Python) -> PyResult<Py<PyAny>> {
70        (
71            self.name.to_string(),
72            self.event_id.to_string(),
73            self.ts_event.as_u64(),
74            self.ts_init.as_u64(),
75        )
76            .into_py_any(py)
77    }
78
79    fn __reduce__(&self, py: Python) -> PyResult<Py<PyAny>> {
80        let safe_constructor = py.get_type::<Self>().getattr("_safe_constructor")?;
81        let state = self.__getstate__(py)?;
82        (safe_constructor, PyTuple::empty(py), state).into_py_any(py)
83    }
84
85    #[staticmethod]
86    fn _safe_constructor() -> Self {
87        Self::new(
88            Ustr::from("NULL"),
89            UUID4::new(),
90            UnixNanos::default(),
91            UnixNanos::default(),
92        )
93    }
94
95    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
96        match op {
97            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
98            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
99            _ => py.NotImplemented(),
100        }
101    }
102
103    fn __repr__(&self) -> String {
104        self.to_string()
105    }
106
107    #[getter]
108    #[pyo3(name = "name")]
109    fn py_name(&self) -> String {
110        self.name.to_string()
111    }
112
113    #[getter]
114    #[pyo3(name = "event_id")]
115    const fn py_event_id(&self) -> UUID4 {
116        self.event_id
117    }
118
119    #[getter]
120    #[pyo3(name = "ts_event")]
121    const fn py_ts_event(&self) -> u64 {
122        self.ts_event.as_u64()
123    }
124
125    #[getter]
126    #[pyo3(name = "ts_init")]
127    const fn py_ts_init(&self) -> u64 {
128        self.ts_init.as_u64()
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use std::{num::NonZeroU64, sync::Arc, time::Duration};
135
136    use nautilus_core::{
137        UnixNanos, datetime::NANOSECONDS_IN_MILLISECOND, python::IntoPyObjectNautilusExt,
138        time::get_atomic_clock_realtime,
139    };
140    use pyo3::prelude::*;
141
142    use crate::{
143        live::timer::LiveTimer,
144        runner::{TimeEventMessage, TimeEventSender, set_time_event_sender},
145        testing::wait_until,
146        timer::{TimeEvent, TimeEventCallback},
147    };
148
149    #[pyfunction]
150    const fn receive_event(_py: Python, _event: TimeEvent) {
151        // TODO: Assert the length of a handler vec
152    }
153
154    #[derive(Debug)]
155    struct TestTimeEventSender;
156
157    impl TimeEventSender for TestTimeEventSender {
158        fn send(&self, _message: TimeEventMessage) {
159            // Test implementation - just ignore the events
160        }
161    }
162
163    #[tokio::test]
164    async fn test_live_timer_starts_and_stops() {
165        set_time_event_sender(Arc::new(TestTimeEventSender));
166
167        Python::initialize();
168        let callback = Python::attach(|py| {
169            let callable = wrap_pyfunction!(receive_event, py).unwrap();
170            let callable = callable.into_py_any_unwrap(py);
171            TimeEventCallback::from(callable)
172        });
173
174        // Create a new LiveTimer with no stop time
175        let clock = get_atomic_clock_realtime();
176        let start_time = clock.get_time_ns();
177        let interval_ns = NonZeroU64::new(100 * NANOSECONDS_IN_MILLISECOND).unwrap();
178
179        let test_sender = Arc::new(TestTimeEventSender);
180        let mut timer = LiveTimer::new(
181            "TEST_TIMER".into(),
182            interval_ns,
183            start_time,
184            None,
185            callback,
186            false,
187            Some(test_sender),
188        );
189
190        let next_time_ns = timer.next_time_ns();
191        timer.start();
192
193        // Wait for timer to run
194        tokio::time::sleep(Duration::from_millis(300)).await;
195
196        timer.cancel();
197        assert!(timer.is_expired(), "Timer should be expired after cancel");
198        assert!(timer.next_time_ns() > next_time_ns);
199    }
200
201    #[tokio::test]
202    async fn test_live_timer_with_stop_time() {
203        set_time_event_sender(Arc::new(TestTimeEventSender));
204
205        Python::initialize();
206        let callback = Python::attach(|py| {
207            let callable = wrap_pyfunction!(receive_event, py).unwrap();
208            let callable = callable.into_py_any_unwrap(py);
209            TimeEventCallback::from(callable)
210        });
211
212        // Create a new LiveTimer with a stop time
213        let clock = get_atomic_clock_realtime();
214        let start_time = clock.get_time_ns();
215        let interval_ns = NonZeroU64::new(100 * NANOSECONDS_IN_MILLISECOND).unwrap();
216        let stop_time = start_time + 500 * NANOSECONDS_IN_MILLISECOND;
217
218        let test_sender = Arc::new(TestTimeEventSender);
219        let mut timer = LiveTimer::new(
220            "TEST_TIMER".into(),
221            interval_ns,
222            start_time,
223            Some(stop_time),
224            callback,
225            false,
226            Some(test_sender),
227        );
228
229        let next_time_ns = timer.next_time_ns();
230        timer.start();
231
232        // Wait for a longer time than the stop time
233        tokio::time::sleep(Duration::from_secs(1)).await;
234
235        wait_until(|| timer.is_expired(), Duration::from_secs(2));
236        assert!(timer.next_time_ns() > next_time_ns);
237    }
238
239    #[tokio::test]
240    async fn test_live_timer_with_zero_interval_and_immediate_stop_time() {
241        set_time_event_sender(Arc::new(TestTimeEventSender));
242
243        Python::initialize();
244        let callback = Python::attach(|py| {
245            let callable = wrap_pyfunction!(receive_event, py).unwrap();
246            let callable = callable.into_py_any_unwrap(py);
247            TimeEventCallback::from(callable)
248        });
249
250        // Create a new LiveTimer with a stop time
251        let clock = get_atomic_clock_realtime();
252        let start_time = UnixNanos::default();
253        let interval_ns = NonZeroU64::new(1).unwrap();
254        let stop_time = clock.get_time_ns();
255
256        let test_sender = Arc::new(TestTimeEventSender);
257        let mut timer = LiveTimer::new(
258            "TEST_TIMER".into(),
259            interval_ns,
260            start_time,
261            Some(stop_time),
262            callback,
263            false,
264            Some(test_sender),
265        );
266
267        timer.start();
268
269        wait_until(|| timer.is_expired(), Duration::from_secs(2));
270    }
271}