1use std::{
22 collections::VecDeque,
23 future::Future,
24 pin::Pin,
25 sync::Arc,
26 task::{Context, Poll, Waker},
27 thread::{self, ThreadId},
28};
29
30use ahash::AHashMap;
31use nautilus_core::python::{to_pyruntime_err, to_pytype_err};
32use parking_lot::Mutex;
33use pyo3::{
34 prelude::*,
35 sync::PyOnceLock,
36 types::{PyCFunction, PyDict, PyTuple},
37};
38
39const COMMAND_CAPACITY: usize = 1024;
40
41#[pyclass(
43 module = "nautilus_trader.live",
44 name = "_ClientRuntime",
45 frozen,
46 weakref
47)]
48#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")]
49#[derive(Debug)]
50pub struct ClientRuntime {
51 owner: ThreadId,
52 client: Py<PyAny>,
53 name: String,
54 logger: Py<PyAny>,
55 state: Mutex<RuntimeState>,
56}
57
58#[derive(Debug, Default)]
61struct RuntimeState {
62 event_loop: Option<Py<PyAny>>,
63 capacity: usize,
64 queue: VecDeque<(String, Py<PyTuple>)>,
65 tasks: AHashMap<usize, TaskEntry>,
66 worker: Option<Py<PyAny>>,
67 client_active: Option<Py<PyAny>>,
68 accepting: bool,
69 connected: bool,
70 disposed: bool,
71}
72
73#[derive(Debug)]
74struct TaskEntry {
75 task: Py<PyAny>,
76 operation: String,
77 driver: Py<RuntimeOperation>,
78 awaited: bool,
79 cancellation_requested: bool,
80}
81
82#[pyo3_stub_gen::derive::gen_stub_pymethods]
83#[pymethods]
84impl ClientRuntime {
85 #[new]
86 #[pyo3(signature = (client, capacity=COMMAND_CAPACITY))]
87 fn py_new(py: Python<'_>, client: &Bound<'_, PyAny>, capacity: usize) -> PyResult<Self> {
88 let name = client.getattr("client_id")?.str()?.to_string();
89 let logger = py
90 .import("nautilus_trader.common")?
91 .getattr("Logger")?
92 .call1((&name,))?
93 .unbind();
94 let client = py
95 .import("weakref")?
96 .getattr("ref")?
97 .call1((client,))?
98 .unbind();
99 Ok(Self {
100 owner: thread::current().id(),
101 client,
102 name,
103 logger,
104 state: Mutex::new(RuntimeState {
105 capacity,
106 ..RuntimeState::default()
107 }),
108 })
109 }
110
111 #[getter]
112 fn connected(&self) -> bool {
113 self.state.lock().connected
114 }
115
116 #[getter]
117 fn complete(&self) -> bool {
118 let state = self.state.lock();
119 state.tasks.is_empty() && state.queue.is_empty()
120 }
121
122 #[getter]
123 fn event_loop(&self, py: Python<'_>) -> Option<Py<PyAny>> {
124 self.state
125 .lock()
126 .event_loop
127 .as_ref()
128 .map(|value| value.clone_ref(py))
129 }
130
131 fn bind(&self, py: Python<'_>, event_loop: Py<PyAny>) -> PyResult<()> {
132 self.check_thread()?;
133 {
134 let state = self.state.lock();
135 if state.event_loop.is_some() || state.disposed {
136 return Err(to_pyruntime_err(format!(
137 "Client {} can only run once",
138 self.name
139 )));
140 }
141 }
142
143 if !event_loop
144 .bind(py)
145 .is(&py.import("asyncio")?.call_method0("get_running_loop")?)
146 {
147 return Err(to_pyruntime_err(
148 "Client must bind to the running owner loop",
149 ));
150 }
151
152 let mut state = self.state.lock();
153 state.event_loop = Some(event_loop);
154 state.accepting = true;
155 Ok(())
156 }
157
158 #[pyo3(signature = (operation, args=None))]
159 fn call(
160 slf: &Bound<'_, Self>,
161 operation: String,
162 args: Option<Py<PyTuple>>,
163 ) -> PyResult<Py<PyAny>> {
164 Self::invoke_task(slf, operation, args, false)
165 }
166
167 #[pyo3(signature = (operation, args=None))]
168 fn call_awaited(
169 slf: &Bound<'_, Self>,
170 operation: String,
171 args: Option<Py<PyTuple>>,
172 ) -> PyResult<Py<PyAny>> {
173 Self::invoke_task(slf, operation, args, true)
174 }
175
176 #[pyo3(signature = (operation, args=None))]
177 fn admit(slf: &Bound<'_, Self>, operation: String, args: Option<Py<PyTuple>>) -> PyResult<()> {
178 let py = slf.py();
179 slf.get().check_bound(py)?;
180 let args = args.unwrap_or_else(|| PyTuple::empty(py).unbind());
181
182 let needs_worker = {
183 let mut state = slf.get().state.lock();
184 if !state.accepting {
185 return Err(slf.get().shutting_down());
186 }
187
188 if state.queue.len() >= state.capacity {
189 return Err(to_pyruntime_err(format!(
190 "Client {} command queue is full",
191 slf.get().name
192 )));
193 }
194
195 state.queue.push_back((operation, args));
196 state.worker.is_none()
197 };
198
199 if needs_worker {
200 match Self::schedule(slf, OperationKind::Dispatch, "commands".into(), false) {
201 Ok(task) => slf.get().state.lock().worker = Some(task),
202 Err(e) => {
203 let rejected = slf.get().state.lock().queue.pop_back();
204 drop(rejected);
205 return Err(e);
206 }
207 }
208 }
209
210 Ok(())
211 }
212
213 #[pyo3(signature = (coroutine, operation="background"))]
214 fn create_task(
215 slf: &Bound<'_, Self>,
216 coroutine: Py<PyAny>,
217 operation: &str,
218 ) -> PyResult<Py<PyAny>> {
219 Self::schedule(
220 slf,
221 OperationKind::Coroutine(coroutine),
222 operation.into(),
223 false,
224 )
225 }
226
227 fn lifecycle(slf: &Bound<'_, Self>, operation: &str) -> PyResult<Py<PyAny>> {
228 let kind = match operation {
229 "connect" => OperationKind::Connect,
230 "disconnect" => OperationKind::Disconnect,
231 _ => return Err(to_pytype_err("Expected connect or disconnect")),
232 };
233
234 Self::schedule(slf, kind, operation.into(), true)
235 }
236
237 fn connect(slf: &Bound<'_, Self>) -> PyResult<Py<PyAny>> {
238 Self::coroutine(slf, OperationKind::Connect)
239 }
240
241 fn abandon(&self, py: Python<'_>, task: &Bound<'_, PyAny>) -> PyResult<()> {
242 self.check_thread()?;
243
244 let owned = {
245 let mut state = self.state.lock();
246 if let Some(entry) = state.tasks.get_mut(&(task.as_ptr() as usize)) {
247 entry.awaited = false;
248 true
249 } else {
250 false
251 }
252 };
253
254 if task.call_method0("done")?.extract::<bool>()? {
255 if owned {
256 self.completed(py, task)?;
257 } else {
258 self.retrieve(
259 py,
260 task,
261 &task.call_method0("get_name")?.extract::<String>()?,
262 true,
263 )?;
264 }
265 } else if owned && !self.loop_closed(py)? {
266 self.cancel_task(task)?;
267 }
268
269 Ok(())
270 }
271
272 fn dispose(&self, py: Python<'_>) -> PyResult<()> {
273 self.check_thread()?;
274
275 let (already_disposed, tasks) = {
276 let mut state = self.state.lock();
277 let disposed = state.disposed;
278 state.disposed = true;
279 state.accepting = false;
280 (
281 disposed,
282 state
283 .tasks
284 .values()
285 .map(|entry| entry.task.clone_ref(py))
286 .collect::<Vec<_>>(),
287 )
288 };
289
290 self.discard_commands(py)?;
291 for task in tasks {
292 if task.call_method0(py, "done")?.extract::<bool>(py)? {
293 self.completed(py, task.bind(py))?;
294 } else if !already_disposed && !self.loop_closed(py)? {
295 self.cancel_task(task.bind(py))?;
296 }
297 }
298
299 if !already_disposed && !self.complete() {
300 let count = self.state.lock().tasks.len();
301 self.log(
302 py,
303 &format!("Client {} cleanup is incomplete ({count} tasks)", self.name),
304 )?;
305 }
306
307 Ok(())
308 }
309}
310
311impl ClientRuntime {
312 fn invoke_task(
313 slf: &Bound<'_, Self>,
314 operation: String,
315 args: Option<Py<PyTuple>>,
316 awaited: bool,
317 ) -> PyResult<Py<PyAny>> {
318 let args = args.unwrap_or_else(|| PyTuple::empty(slf.py()).unbind());
319 let name = operation.clone();
320 Self::schedule(slf, OperationKind::Invoke(operation, args), name, awaited)
321 }
322
323 fn schedule(
324 slf: &Bound<'_, Self>,
325 kind: OperationKind,
326 operation: String,
327 awaited: bool,
328 ) -> PyResult<Py<PyAny>> {
329 let py = slf.py();
330
331 let validation = (|| {
332 slf.get().check_bound(py)?;
333 if !slf.get().state.lock().accepting {
334 return Err(slf.get().shutting_down());
335 }
336
337 if let OperationKind::Coroutine(ref coroutine) = kind
338 && !py
339 .import("inspect")?
340 .call_method1("iscoroutine", (coroutine,))?
341 .extract::<bool>()?
342 {
343 return Err(to_pytype_err("Expected a coroutine"));
344 }
345
346 slf.get().get_client(py)
347 })();
348
349 let client = match validation {
350 Ok(client) => client,
351 Err(e) => {
352 if let OperationKind::Coroutine(ref coroutine) = kind
353 && py
354 .import("inspect")?
355 .call_method1("iscoroutine", (coroutine,))?
356 .extract::<bool>()?
357 {
358 coroutine.call_method0(py, "close")?;
359 }
360
361 return Err(e);
362 }
363 };
364
365 let driver = Py::new(py, RuntimeOperation::new(slf.clone().unbind(), kind))?;
366 let wrapper = coroutine_driver(py)?.call1(py, (driver.clone_ref(py),))?;
367 let kwargs = PyDict::new(py);
368 kwargs.set_item("name", format!("{}:{operation}", slf.get().name))?;
369 let event_loop = slf.get().event_loop(py).expect("validated event loop");
370
371 let task = match event_loop.call_method(py, "create_task", (&wrapper,), Some(&kwargs)) {
372 Ok(task) => task,
373 Err(e) => {
374 wrapper.call_method0(py, "close")?;
375 driver.borrow_mut(py).close(py)?;
376 return Err(e);
377 }
378 };
379
380 {
381 let mut state = slf.get().state.lock();
382 state.tasks.insert(
383 task.as_ptr() as usize,
384 TaskEntry {
385 task: task.clone_ref(py),
386 operation,
387 driver,
388 awaited,
389 cancellation_requested: false,
390 },
391 );
392
393 state.client_active = Some(client);
394 }
395
396 Self::track_cancellation(slf, task.bind(py))?;
397
398 let runtime = Mutex::new(Some(slf.clone().unbind()));
400
401 let callback = pyo3::types::PyCFunction::new_closure(
402 py,
403 None,
404 None,
405 move |args: &Bound<'_, PyTuple>, _kwargs: Option<&Bound<'_, PyDict>>| {
406 let owner = runtime.lock().take();
407 if let Some(owner) = owner {
408 owner.get().completed(args.py(), &args.get_item(0)?)?;
409 }
410
411 Ok::<(), PyErr>(())
412 },
413 )?;
414
415 task.call_method1(py, "add_done_callback", (callback,))?;
416 Ok(task)
417 }
418
419 fn track_cancellation(slf: &Bound<'_, Self>, task: &Bound<'_, PyAny>) -> PyResult<()> {
420 let py = slf.py();
421 let owner = py
422 .import("weakref")?
423 .getattr("ref")?
424 .call1((slf,))?
425 .unbind();
426 let task_id = task.as_ptr() as usize;
427
428 let notify = PyCFunction::new_closure(
429 py,
430 None,
431 None,
432 move |args: &Bound<'_, PyTuple>, _kwargs: Option<&Bound<'_, PyDict>>| {
433 let runtime = owner.call0(args.py())?;
434 if !runtime.is_none(args.py()) {
435 let runtime = runtime.bind(args.py()).cast::<Self>()?;
436 if let Some(entry) = runtime.get().state.lock().tasks.get_mut(&task_id) {
437 if args.get_item(0)?.extract::<bool>()? && entry.cancellation_requested {
438 return Ok(false);
439 }
440
441 entry.cancellation_requested = true;
442 }
443 }
444
445 Ok::<bool, PyErr>(true)
446 },
447 )?;
448
449 py.import("nautilus_trader.live._coroutine")?
450 .call_method1("track_cancellation", (task, notify))?;
451 Ok(())
452 }
453
454 fn cancel_task(&self, task: &Bound<'_, PyAny>) -> PyResult<()> {
455 let request = self
456 .state
457 .lock()
458 .tasks
459 .get(&(task.as_ptr() as usize))
460 .is_some_and(|entry| !entry.cancellation_requested);
461
462 if request && !task.call_method0("done")?.extract::<bool>()? {
463 task.py()
464 .import("nautilus_trader.live._coroutine")?
465 .call_method1("cancel_supervised", (task,))?;
466 }
467
468 Ok(())
469 }
470
471 fn coroutine(slf: &Bound<'_, Self>, kind: OperationKind) -> PyResult<Py<PyAny>> {
472 let py = slf.py();
473 let operation = Py::new(py, RuntimeOperation::new(slf.clone().unbind(), kind))?;
474 coroutine_driver(py)?.call1(py, (operation,))
475 }
476
477 fn completed(&self, py: Python<'_>, task: &Bound<'_, PyAny>) -> PyResult<()> {
478 let entry = self.state.lock().tasks.remove(&(task.as_ptr() as usize));
479 if let Some(entry) = entry {
480 entry.driver.borrow_mut(py).close(py)?;
481 self.retrieve(py, task, &entry.operation, !entry.awaited)?;
482
483 let released = {
485 let mut state = self.state.lock();
486 if state.tasks.is_empty() {
487 state.client_active.take()
488 } else {
489 None
490 }
491 };
492
493 drop(released);
494 }
495
496 Ok(())
497 }
498
499 fn retrieve(
500 &self,
501 py: Python<'_>,
502 task: &Bound<'_, PyAny>,
503 operation: &str,
504 log_failure: bool,
505 ) -> PyResult<()> {
506 if task.call_method0("cancelled")?.extract::<bool>()? {
508 return Ok(());
509 }
510
511 let exception = task.call_method0("exception")?;
513
514 if log_failure && !exception.is_none() {
515 self.log_error(py, operation, &PyErr::from_value(exception))?;
516 }
517
518 Ok(())
519 }
520
521 fn log_error(&self, py: Python<'_>, operation: &str, e: &PyErr) -> PyResult<()> {
522 let traceback = py
523 .import("traceback")?
524 .call_method1(
525 "format_exception",
526 (e.get_type(py), e.value(py), e.traceback(py)),
527 )?
528 .extract::<Vec<String>>()?
529 .concat();
530 self.log(
531 py,
532 &format!(
533 "Client {} operation {operation} failed\n{traceback}",
534 self.name
535 ),
536 )
537 }
538
539 fn log(&self, py: Python<'_>, message: &str) -> PyResult<()> {
540 self.logger.call_method1(py, "error", (message,))?;
541 Ok(())
542 }
543
544 fn discard_commands(&self, py: Python<'_>) -> PyResult<()> {
545 let queue = std::mem::take(&mut self.state.lock().queue);
546 for (operation, _) in queue {
547 self.log(
548 py,
549 &format!(
550 "Client {} abandoned queued operation {operation}",
551 self.name
552 ),
553 )?;
554 }
555
556 Ok(())
557 }
558
559 fn get_client(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
560 let client = self.client.call0(py)?;
561 if client.is_none(py) {
562 return Err(to_pyruntime_err(format!(
563 "Client {} no longer exists",
564 self.name
565 )));
566 }
567
568 Ok(client)
569 }
570
571 fn invoke(
572 &self,
573 py: Python<'_>,
574 operation: &str,
575 args: &Bound<'_, PyTuple>,
576 ) -> PyResult<Py<PyAny>> {
577 self.get_client(py)?.call_method1(py, operation, args)
578 }
579
580 fn shutting_down(&self) -> PyErr {
581 to_pyruntime_err(format!("Client {} is shutting down", self.name))
582 }
583
584 fn check_bound(&self, py: Python<'_>) -> PyResult<()> {
585 self.check_thread()?;
586
587 let event_loop = {
588 let state = self.state.lock();
589 if state.event_loop.is_none() || state.disposed {
590 return Err(to_pyruntime_err(format!(
591 "Client {} is not bound to an active node",
592 self.name
593 )));
594 }
595
596 state.event_loop.as_ref().expect("bound loop").clone_ref(py)
597 };
598
599 if event_loop
600 .call_method0(py, "is_closed")?
601 .extract::<bool>(py)?
602 || !event_loop
603 .bind(py)
604 .is(&py.import("asyncio")?.call_method0("get_running_loop")?)
605 {
606 return Err(to_pyruntime_err(format!(
607 "Client {} must use its bound event loop",
608 self.name
609 )));
610 }
611
612 Ok(())
613 }
614
615 fn check_thread(&self) -> PyResult<()> {
616 if self.owner != thread::current().id() {
617 return Err(to_pyruntime_err(format!(
618 "Client {} must be used on its owner thread",
619 self.name
620 )));
621 }
622
623 Ok(())
624 }
625
626 fn loop_closed(&self, py: Python<'_>) -> PyResult<bool> {
627 match self.event_loop(py) {
628 Some(event_loop) => event_loop.call_method0(py, "is_closed")?.extract(py),
629 None => Ok(true),
630 }
631 }
632}
633
634enum OperationKind {
635 Coroutine(Py<PyAny>),
636 Invoke(String, Py<PyTuple>),
637 Dispatch,
638 Connect,
639 Disconnect,
640}
641
642#[derive(Clone, Copy)]
643enum Phase {
644 Start,
645 Awaiting,
646 Draining,
647 Done,
648}
649
650#[pyclass(module = "nautilus_trader.live", name = "_ClientOperation")]
651struct RuntimeOperation {
652 runtime: Py<ClientRuntime>,
653 kind: OperationKind,
654 phase: Phase,
655 current: Option<Py<PyAny>>,
656 pending_error: Option<PyErr>,
657 command: Option<String>,
658 closed: bool,
659}
660
661#[pymethods]
662impl RuntimeOperation {
663 #[pyo3(signature = (value=None, error=None))]
664 fn advance(
665 &mut self,
666 py: Python<'_>,
667 value: Option<Py<PyAny>>,
668 error: Option<Py<PyAny>>,
669 ) -> PyResult<(bool, Py<PyAny>)> {
670 self.runtime.get().check_thread()?;
671
672 let result = match error {
673 Some(e) => Err(PyErr::from_value(e.into_bound(py))),
674 None => Ok(value.unwrap_or_else(|| py.None())),
675 };
676
677 if matches!(self.phase, Phase::Start) {
678 self.phase = Phase::Awaiting;
679 let runtime = self.runtime.get();
680
681 let next = match &self.kind {
682 OperationKind::Coroutine(coroutine) => Ok(coroutine.clone_ref(py)),
683 OperationKind::Invoke(name, args) => runtime.invoke(py, name, args.bind(py)),
684 OperationKind::Connect => runtime.invoke(py, "_connect", &PyTuple::empty(py)),
685 OperationKind::Disconnect => {
686 let worker = {
687 let mut state = runtime.state.lock();
688 state.accepting = false;
689 state.worker.as_ref().map(|task| task.clone_ref(py))
690 };
691
692 runtime.discard_commands(py)?;
693 if let Some(worker) = worker {
694 runtime.cancel_task(worker.bind(py))?;
695 }
696
697 runtime.invoke(py, "_disconnect", &PyTuple::empty(py))
698 }
699 OperationKind::Dispatch => return self.next_command(py),
700 };
701
702 return match next {
703 Ok(next) => Ok(self.suspend(py, next)),
704 Err(e) => self.finish_step(py, Err(e)),
705 };
706 }
707
708 self.current.take();
709 self.finish_step(py, result)
710 }
711
712 fn close(&mut self, py: Python<'_>) -> PyResult<()> {
713 if self.closed {
714 return Ok(());
715 }
716
717 self.closed = true;
718 self.pending_error.take();
719
720 if let Some(current) = self.current.take()
721 && current.bind(py).hasattr("close")?
722 {
723 current.call_method0(py, "close")?;
724 }
725
726 if let OperationKind::Coroutine(coroutine) = &self.kind {
727 coroutine.call_method0(py, "close")?;
728 }
729
730 if matches!(self.kind, OperationKind::Dispatch) {
731 let worker = self.runtime.get().state.lock().worker.take();
732 drop(worker);
733 }
734
735 self.phase = Phase::Done;
736 Ok(())
737 }
738}
739
740impl RuntimeOperation {
741 fn new(runtime: Py<ClientRuntime>, kind: OperationKind) -> Self {
742 Self {
743 runtime,
744 kind,
745 phase: Phase::Start,
746 current: None,
747 pending_error: None,
748 command: None,
749 closed: false,
750 }
751 }
752
753 fn finish_step(
754 &mut self,
755 py: Python<'_>,
756 result: PyResult<Py<PyAny>>,
757 ) -> PyResult<(bool, Py<PyAny>)> {
758 let runtime = self.runtime.get();
759
760 match self.kind {
761 OperationKind::Dispatch => {
762 if let Err(e) = result {
763 self.command_failed(py, e)?;
764 }
765
766 self.next_command(py)
767 }
768 OperationKind::Connect => {
769 result?;
770 let mut state = runtime.state.lock();
771 if !state.accepting || state.disposed {
772 return Err(to_pyruntime_err(format!(
773 "Client {} connect finished after shutdown",
774 runtime.name
775 )));
776 }
777
778 state.connected = true;
779 self.phase = Phase::Done;
780 Ok((true, py.None()))
781 }
782 OperationKind::Disconnect if !matches!(self.phase, Phase::Draining) => {
783 self.pending_error = result.err();
784 self.phase = Phase::Draining;
785 let asyncio = py.import("asyncio")?;
786 let current = asyncio.call_method0("current_task")?;
787 let tasks = runtime
788 .state
789 .lock()
790 .tasks
791 .values()
792 .filter(|entry| !entry.task.bind(py).is(¤t))
793 .map(|entry| entry.task.clone_ref(py))
794 .collect::<Vec<_>>();
795
796 for task in &tasks {
797 runtime.cancel_task(task.bind(py))?;
798 }
799
800 if tasks.is_empty() {
801 return self.finish_step(py, Ok(py.None()));
802 }
803
804 let kwargs = PyDict::new(py);
805 kwargs.set_item("return_exceptions", true)?;
806 let shielded = tasks
808 .iter()
809 .map(|task| asyncio.call_method1("shield", (task,)))
810 .collect::<PyResult<Vec<_>>>()?;
811 let gathered = asyncio
812 .call_method("gather", PyTuple::new(py, shielded)?, Some(&kwargs))?
813 .unbind();
814 Ok(self.suspend(py, gathered))
815 }
816 OperationKind::Disconnect => {
817 result?;
818
819 if let Some(e) = self.pending_error.take() {
820 return Err(e);
821 }
822
823 runtime.state.lock().connected = false;
824 self.phase = Phase::Done;
825 Ok((true, py.None()))
826 }
827 _ => {
828 self.phase = Phase::Done;
829 result.map(|value| (true, value))
830 }
831 }
832 }
833
834 fn next_command(&mut self, py: Python<'_>) -> PyResult<(bool, Py<PyAny>)> {
835 loop {
836 let task = py.import("asyncio")?.call_method0("current_task")?;
838 while task.call_method0("cancelling")?.extract::<usize>()? != 0 {
839 task.call_method0("uncancel")?;
840 }
841
842 if let Some(entry) = self
843 .runtime
844 .get()
845 .state
846 .lock()
847 .tasks
848 .get_mut(&(task.as_ptr() as usize))
849 {
850 entry.cancellation_requested = false;
851 }
852
853 let entry = self.runtime.get().state.lock().queue.pop_front();
854
855 let Some((name, args)) = entry else {
856 self.phase = Phase::Done;
857 return Ok((true, py.None()));
858 };
859
860 self.command = Some(name.clone());
861 match self.runtime.get().invoke(py, &name, args.bind(py)) {
862 Ok(coroutine) => return Ok(self.suspend(py, coroutine)),
863 Err(e) => {
864 self.command_failed(py, e)?;
865 }
866 }
867 }
868 }
869
870 fn command_failed(&self, py: Python<'_>, e: PyErr) -> PyResult<()> {
871 if is_cancelled(py, &e)? {
872 if !self.runtime.get().state.lock().accepting {
873 return Err(e);
874 }
875 } else if !e.is_instance_of::<pyo3::exceptions::PyException>(py) {
876 return Err(e);
877 }
878
879 self.runtime
880 .get()
881 .log_error(py, self.command.as_deref().unwrap_or("commands"), &e)
882 }
883
884 fn suspend(&mut self, py: Python<'_>, value: Py<PyAny>) -> (bool, Py<PyAny>) {
885 self.current = Some(value.clone_ref(py));
886 (false, value)
887 }
888}
889
890pub(crate) struct PythonOperation {
891 runtime: Py<PyAny>,
892 task: Py<PyAny>,
893 waker: Arc<Mutex<Option<Waker>>>,
894 complete: bool,
895}
896
897impl PythonOperation {
898 pub(crate) fn new(py: Python<'_>, task: Py<PyAny>, runtime: Py<PyAny>) -> PyResult<Self> {
899 let waker: Arc<Mutex<Option<Waker>>> = Arc::default();
900 let callback_waker = waker.clone();
901
902 let callback = PyCFunction::new_closure(
903 py,
904 None,
905 None,
906 move |_args: &Bound<'_, PyTuple>, _kwargs: Option<&Bound<'_, PyDict>>| {
907 if let Some(waker) = callback_waker.lock().take() {
908 waker.wake();
909 }
910
911 Ok::<(), PyErr>(())
912 },
913 )?;
914
915 task.call_method1(py, "add_done_callback", (callback,))?;
916 Ok(Self {
917 runtime,
918 task,
919 waker,
920 complete: false,
921 })
922 }
923}
924
925impl Future for PythonOperation {
926 type Output = PyResult<Py<PyAny>>;
927 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
928 *self.waker.lock() = Some(cx.waker().clone());
929 Python::attach(|py| {
930 match self
931 .task
932 .call_method0(py, "done")
933 .and_then(|done| done.extract::<bool>(py))
934 {
935 Ok(false) => Poll::Pending,
936 Ok(true) => {
937 self.complete = true;
938 Poll::Ready(self.task.call_method0(py, "result"))
939 }
940 Err(e) => Poll::Ready(Err(e)),
941 }
942 })
943 }
944}
945
946impl Drop for PythonOperation {
947 fn drop(&mut self) {
948 if !self.complete {
949 Python::attach(|py| {
950 if let Err(e) = self
951 .runtime
952 .call_method1(py, "abandon", (self.task.clone_ref(py),))
953 {
954 log::error!("Failed to request Python operation cancellation: {e}");
955 }
956 });
957 }
958 }
959}
960
961fn is_cancelled(py: Python<'_>, e: &PyErr) -> PyResult<bool> {
962 e.value(py)
963 .is_instance(&py.import("asyncio")?.getattr("CancelledError")?)
964}
965
966fn coroutine_driver(py: Python<'_>) -> PyResult<&Py<PyAny>> {
967 static DRIVER: PyOnceLock<Py<PyAny>> = PyOnceLock::new();
968 DRIVER.get_or_try_init(py, || {
969 let module = py.import("nautilus_trader.live._coroutine")?;
970 Ok(module.getattr("drive")?.unbind())
971 })
972}