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
292 for task in tasks {
293 if task.call_method0(py, "done")?.extract::<bool>(py)? {
294 self.completed(py, task.bind(py))?;
295 } else if !already_disposed && !self.loop_closed(py)? {
296 self.cancel_task(task.bind(py))?;
297 }
298 }
299
300 if !already_disposed && !self.complete() {
301 let count = self.state.lock().tasks.len();
302 self.log(
303 py,
304 &format!("Client {} cleanup is incomplete ({count} tasks)", self.name),
305 )?;
306 }
307
308 Ok(())
309 }
310}
311
312impl ClientRuntime {
313 fn invoke_task(
314 slf: &Bound<'_, Self>,
315 operation: String,
316 args: Option<Py<PyTuple>>,
317 awaited: bool,
318 ) -> PyResult<Py<PyAny>> {
319 let args = args.unwrap_or_else(|| PyTuple::empty(slf.py()).unbind());
320 let name = operation.clone();
321 Self::schedule(slf, OperationKind::Invoke(operation, args), name, awaited)
322 }
323
324 fn schedule(
325 slf: &Bound<'_, Self>,
326 kind: OperationKind,
327 operation: String,
328 awaited: bool,
329 ) -> PyResult<Py<PyAny>> {
330 let py = slf.py();
331
332 let validation = (|| {
333 slf.get().check_bound(py)?;
334 if !slf.get().state.lock().accepting {
335 return Err(slf.get().shutting_down());
336 }
337
338 if let OperationKind::Coroutine(ref coroutine) = kind
339 && !py
340 .import("inspect")?
341 .call_method1("iscoroutine", (coroutine,))?
342 .extract::<bool>()?
343 {
344 return Err(to_pytype_err("Expected a coroutine"));
345 }
346
347 slf.get().get_client(py)
348 })();
349
350 let client = match validation {
351 Ok(client) => client,
352 Err(e) => {
353 if let OperationKind::Coroutine(ref coroutine) = kind
354 && py
355 .import("inspect")?
356 .call_method1("iscoroutine", (coroutine,))?
357 .extract::<bool>()?
358 {
359 coroutine.call_method0(py, "close")?;
360 }
361
362 return Err(e);
363 }
364 };
365
366 let driver = Py::new(py, RuntimeOperation::new(slf.clone().unbind(), kind))?;
367 let wrapper = coroutine_driver(py)?.call1(py, (driver.clone_ref(py),))?;
368 let kwargs = PyDict::new(py);
369 kwargs.set_item("name", format!("{}:{operation}", slf.get().name))?;
370 let event_loop = slf.get().event_loop(py).expect("validated event loop");
371
372 let task = match event_loop.call_method(py, "create_task", (&wrapper,), Some(&kwargs)) {
373 Ok(task) => task,
374 Err(e) => {
375 wrapper.call_method0(py, "close")?;
376 driver.borrow_mut(py).close(py)?;
377 return Err(e);
378 }
379 };
380
381 {
382 let mut state = slf.get().state.lock();
383 state.tasks.insert(
384 task.as_ptr() as usize,
385 TaskEntry {
386 task: task.clone_ref(py),
387 operation,
388 driver,
389 awaited,
390 cancellation_requested: false,
391 },
392 );
393
394 state.client_active = Some(client);
395 }
396
397 Self::track_cancellation(slf, task.bind(py))?;
398
399 let runtime = Mutex::new(Some(slf.clone().unbind()));
401
402 let callback = pyo3::types::PyCFunction::new_closure(
403 py,
404 None,
405 None,
406 move |args: &Bound<'_, PyTuple>, _kwargs: Option<&Bound<'_, PyDict>>| {
407 let owner = runtime.lock().take();
408 if let Some(owner) = owner {
409 owner.get().completed(args.py(), &args.get_item(0)?)?;
410 }
411
412 Ok::<(), PyErr>(())
413 },
414 )?;
415
416 task.call_method1(py, "add_done_callback", (callback,))?;
417 Ok(task)
418 }
419
420 fn track_cancellation(slf: &Bound<'_, Self>, task: &Bound<'_, PyAny>) -> PyResult<()> {
421 let py = slf.py();
422 let owner = py
423 .import("weakref")?
424 .getattr("ref")?
425 .call1((slf,))?
426 .unbind();
427 let task_id = task.as_ptr() as usize;
428
429 let notify = PyCFunction::new_closure(
430 py,
431 None,
432 None,
433 move |args: &Bound<'_, PyTuple>, _kwargs: Option<&Bound<'_, PyDict>>| {
434 let runtime = owner.call0(args.py())?;
435 if !runtime.is_none(args.py()) {
436 let runtime = runtime.bind(args.py()).cast::<Self>()?;
437 if let Some(entry) = runtime.get().state.lock().tasks.get_mut(&task_id) {
438 if args.get_item(0)?.extract::<bool>()? && entry.cancellation_requested {
439 return Ok(false);
440 }
441
442 entry.cancellation_requested = true;
443 }
444 }
445
446 Ok::<bool, PyErr>(true)
447 },
448 )?;
449
450 py.import("nautilus_trader.live._coroutine")?
451 .call_method1("track_cancellation", (task, notify))?;
452 Ok(())
453 }
454
455 fn cancel_task(&self, task: &Bound<'_, PyAny>) -> PyResult<()> {
456 let request = self
457 .state
458 .lock()
459 .tasks
460 .get(&(task.as_ptr() as usize))
461 .is_some_and(|entry| !entry.cancellation_requested);
462
463 if request && !task.call_method0("done")?.extract::<bool>()? {
464 task.py()
465 .import("nautilus_trader.live._coroutine")?
466 .call_method1("cancel_supervised", (task,))?;
467 }
468
469 Ok(())
470 }
471
472 fn coroutine(slf: &Bound<'_, Self>, kind: OperationKind) -> PyResult<Py<PyAny>> {
473 let py = slf.py();
474 let operation = Py::new(py, RuntimeOperation::new(slf.clone().unbind(), kind))?;
475 coroutine_driver(py)?.call1(py, (operation,))
476 }
477
478 fn completed(&self, py: Python<'_>, task: &Bound<'_, PyAny>) -> PyResult<()> {
479 let entry = self.state.lock().tasks.remove(&(task.as_ptr() as usize));
480 if let Some(entry) = entry {
481 entry.driver.borrow_mut(py).close(py)?;
482 self.retrieve(py, task, &entry.operation, !entry.awaited)?;
483
484 let released = {
486 let mut state = self.state.lock();
487 if state.tasks.is_empty() {
488 state.client_active.take()
489 } else {
490 None
491 }
492 };
493
494 drop(released);
495 }
496
497 Ok(())
498 }
499
500 fn retrieve(
501 &self,
502 py: Python<'_>,
503 task: &Bound<'_, PyAny>,
504 operation: &str,
505 log_failure: bool,
506 ) -> PyResult<()> {
507 if task.call_method0("cancelled")?.extract::<bool>()? {
509 return Ok(());
510 }
511
512 let exception = task.call_method0("exception")?;
514
515 if log_failure && !exception.is_none() {
516 self.log_error(py, operation, &PyErr::from_value(exception))?;
517 }
518
519 Ok(())
520 }
521
522 fn log_error(&self, py: Python<'_>, operation: &str, e: &PyErr) -> PyResult<()> {
523 let traceback = py
524 .import("traceback")?
525 .call_method1(
526 "format_exception",
527 (e.get_type(py), e.value(py), e.traceback(py)),
528 )?
529 .extract::<Vec<String>>()?
530 .concat();
531 self.log(
532 py,
533 &format!(
534 "Client {} operation {operation} failed\n{traceback}",
535 self.name
536 ),
537 )
538 }
539
540 fn log(&self, py: Python<'_>, message: &str) -> PyResult<()> {
541 self.logger.call_method1(py, "error", (message,))?;
542 Ok(())
543 }
544
545 fn discard_commands(&self, py: Python<'_>) -> PyResult<()> {
546 let queue = std::mem::take(&mut self.state.lock().queue);
547 for (operation, _) in queue {
548 self.log(
549 py,
550 &format!(
551 "Client {} abandoned queued operation {operation}",
552 self.name
553 ),
554 )?;
555 }
556
557 Ok(())
558 }
559
560 fn get_client(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
561 let client = self.client.call0(py)?;
562 if client.is_none(py) {
563 return Err(to_pyruntime_err(format!(
564 "Client {} no longer exists",
565 self.name
566 )));
567 }
568
569 Ok(client)
570 }
571
572 fn invoke(
573 &self,
574 py: Python<'_>,
575 operation: &str,
576 args: &Bound<'_, PyTuple>,
577 ) -> PyResult<Py<PyAny>> {
578 self.get_client(py)?.call_method1(py, operation, args)
579 }
580
581 fn shutting_down(&self) -> PyErr {
582 to_pyruntime_err(format!("Client {} is shutting down", self.name))
583 }
584
585 fn check_bound(&self, py: Python<'_>) -> PyResult<()> {
586 self.check_thread()?;
587
588 let event_loop = {
589 let state = self.state.lock();
590 if state.event_loop.is_none() || state.disposed {
591 return Err(to_pyruntime_err(format!(
592 "Client {} is not bound to an active node",
593 self.name
594 )));
595 }
596
597 state.event_loop.as_ref().expect("bound loop").clone_ref(py)
598 };
599
600 if event_loop
601 .call_method0(py, "is_closed")?
602 .extract::<bool>(py)?
603 || !event_loop
604 .bind(py)
605 .is(&py.import("asyncio")?.call_method0("get_running_loop")?)
606 {
607 return Err(to_pyruntime_err(format!(
608 "Client {} must use its bound event loop",
609 self.name
610 )));
611 }
612
613 Ok(())
614 }
615
616 fn check_thread(&self) -> PyResult<()> {
617 if self.owner != thread::current().id() {
618 return Err(to_pyruntime_err(format!(
619 "Client {} must be used on its owner thread",
620 self.name
621 )));
622 }
623
624 Ok(())
625 }
626
627 fn loop_closed(&self, py: Python<'_>) -> PyResult<bool> {
628 match self.event_loop(py) {
629 Some(event_loop) => event_loop.call_method0(py, "is_closed")?.extract(py),
630 None => Ok(true),
631 }
632 }
633}
634
635enum OperationKind {
636 Coroutine(Py<PyAny>),
637 Invoke(String, Py<PyTuple>),
638 Dispatch,
639 Connect,
640 Disconnect,
641}
642
643#[derive(Clone, Copy)]
644enum Phase {
645 Start,
646 Awaiting,
647 Draining,
648 Done,
649}
650
651#[pyclass(module = "nautilus_trader.live", name = "_ClientOperation")]
652struct RuntimeOperation {
653 runtime: Py<ClientRuntime>,
654 kind: OperationKind,
655 phase: Phase,
656 current: Option<Py<PyAny>>,
657 pending_error: Option<PyErr>,
658 command: Option<String>,
659 closed: bool,
660}
661
662#[pymethods]
663impl RuntimeOperation {
664 #[pyo3(signature = (value=None, error=None))]
665 fn advance(
666 &mut self,
667 py: Python<'_>,
668 value: Option<Py<PyAny>>,
669 error: Option<Py<PyAny>>,
670 ) -> PyResult<(bool, Py<PyAny>)> {
671 self.runtime.get().check_thread()?;
672
673 let result = match error {
674 Some(e) => Err(PyErr::from_value(e.into_bound(py))),
675 None => Ok(value.unwrap_or_else(|| py.None())),
676 };
677
678 if matches!(self.phase, Phase::Start) {
679 self.phase = Phase::Awaiting;
680 let runtime = self.runtime.get();
681
682 let next = match &self.kind {
683 OperationKind::Coroutine(coroutine) => Ok(coroutine.clone_ref(py)),
684 OperationKind::Invoke(name, args) => runtime.invoke(py, name, args.bind(py)),
685 OperationKind::Connect => runtime.invoke(py, "_connect", &PyTuple::empty(py)),
686 OperationKind::Disconnect => {
687 let worker = {
688 let mut state = runtime.state.lock();
689 state.accepting = false;
690 state.worker.as_ref().map(|task| task.clone_ref(py))
691 };
692
693 runtime.discard_commands(py)?;
694
695 if let Some(worker) = worker {
696 runtime.cancel_task(worker.bind(py))?;
697 }
698
699 runtime.invoke(py, "_disconnect", &PyTuple::empty(py))
700 }
701 OperationKind::Dispatch => return self.next_command(py),
702 };
703
704 return match next {
705 Ok(next) => Ok(self.suspend(py, next)),
706 Err(e) => self.finish_step(py, Err(e)),
707 };
708 }
709
710 self.current.take();
711 self.finish_step(py, result)
712 }
713
714 fn close(&mut self, py: Python<'_>) -> PyResult<()> {
715 if self.closed {
716 return Ok(());
717 }
718
719 self.closed = true;
720 self.pending_error.take();
721
722 if let Some(current) = self.current.take()
723 && current.bind(py).hasattr("close")?
724 {
725 current.call_method0(py, "close")?;
726 }
727
728 if let OperationKind::Coroutine(coroutine) = &self.kind {
729 coroutine.call_method0(py, "close")?;
730 }
731
732 if matches!(self.kind, OperationKind::Dispatch) {
733 let worker = self.runtime.get().state.lock().worker.take();
734 drop(worker);
735 }
736
737 self.phase = Phase::Done;
738 Ok(())
739 }
740}
741
742impl RuntimeOperation {
743 fn new(runtime: Py<ClientRuntime>, kind: OperationKind) -> Self {
744 Self {
745 runtime,
746 kind,
747 phase: Phase::Start,
748 current: None,
749 pending_error: None,
750 command: None,
751 closed: false,
752 }
753 }
754
755 fn finish_step(
756 &mut self,
757 py: Python<'_>,
758 result: PyResult<Py<PyAny>>,
759 ) -> PyResult<(bool, Py<PyAny>)> {
760 let runtime = self.runtime.get();
761
762 match self.kind {
763 OperationKind::Dispatch => {
764 if let Err(e) = result {
765 self.command_failed(py, e)?;
766 }
767
768 self.next_command(py)
769 }
770 OperationKind::Connect => {
771 result?;
772 let mut state = runtime.state.lock();
773 if !state.accepting || state.disposed {
774 return Err(to_pyruntime_err(format!(
775 "Client {} connect finished after shutdown",
776 runtime.name
777 )));
778 }
779
780 state.connected = true;
781 self.phase = Phase::Done;
782 Ok((true, py.None()))
783 }
784 OperationKind::Disconnect if !matches!(self.phase, Phase::Draining) => {
785 self.pending_error = result.err();
786 self.phase = Phase::Draining;
787 let asyncio = py.import("asyncio")?;
788 let current = asyncio.call_method0("current_task")?;
789 let tasks = runtime
790 .state
791 .lock()
792 .tasks
793 .values()
794 .filter(|entry| !entry.task.bind(py).is(¤t))
795 .map(|entry| entry.task.clone_ref(py))
796 .collect::<Vec<_>>();
797
798 for task in &tasks {
799 runtime.cancel_task(task.bind(py))?;
800 }
801
802 if tasks.is_empty() {
803 return self.finish_step(py, Ok(py.None()));
804 }
805
806 let kwargs = PyDict::new(py);
807 kwargs.set_item("return_exceptions", true)?;
808 let shielded = tasks
810 .iter()
811 .map(|task| asyncio.call_method1("shield", (task,)))
812 .collect::<PyResult<Vec<_>>>()?;
813 let gathered = asyncio
814 .call_method("gather", PyTuple::new(py, shielded)?, Some(&kwargs))?
815 .unbind();
816 Ok(self.suspend(py, gathered))
817 }
818 OperationKind::Disconnect => {
819 result?;
820
821 if let Some(e) = self.pending_error.take() {
822 return Err(e);
823 }
824
825 runtime.state.lock().connected = false;
826 self.phase = Phase::Done;
827 Ok((true, py.None()))
828 }
829 _ => {
830 self.phase = Phase::Done;
831 result.map(|value| (true, value))
832 }
833 }
834 }
835
836 fn next_command(&mut self, py: Python<'_>) -> PyResult<(bool, Py<PyAny>)> {
837 loop {
838 let task = py.import("asyncio")?.call_method0("current_task")?;
840 while task.call_method0("cancelling")?.extract::<usize>()? != 0 {
841 task.call_method0("uncancel")?;
842 }
843
844 if let Some(entry) = self
845 .runtime
846 .get()
847 .state
848 .lock()
849 .tasks
850 .get_mut(&(task.as_ptr() as usize))
851 {
852 entry.cancellation_requested = false;
853 }
854
855 let entry = self.runtime.get().state.lock().queue.pop_front();
856
857 let Some((name, args)) = entry else {
858 self.phase = Phase::Done;
859 return Ok((true, py.None()));
860 };
861
862 self.command = Some(name.clone());
863 match self.runtime.get().invoke(py, &name, args.bind(py)) {
864 Ok(coroutine) => return Ok(self.suspend(py, coroutine)),
865 Err(e) => {
866 self.command_failed(py, e)?;
867 }
868 }
869 }
870 }
871
872 fn command_failed(&self, py: Python<'_>, e: PyErr) -> PyResult<()> {
873 if is_cancelled(py, &e)? {
874 if !self.runtime.get().state.lock().accepting {
875 return Err(e);
876 }
877 } else if !e.is_instance_of::<pyo3::exceptions::PyException>(py) {
878 return Err(e);
879 }
880
881 self.runtime
882 .get()
883 .log_error(py, self.command.as_deref().unwrap_or("commands"), &e)
884 }
885
886 fn suspend(&mut self, py: Python<'_>, value: Py<PyAny>) -> (bool, Py<PyAny>) {
887 self.current = Some(value.clone_ref(py));
888 (false, value)
889 }
890}
891
892pub(crate) struct PythonOperation {
893 runtime: Py<PyAny>,
894 task: Py<PyAny>,
895 waker: Arc<Mutex<Option<Waker>>>,
896 complete: bool,
897}
898
899impl PythonOperation {
900 pub(crate) fn new(py: Python<'_>, task: Py<PyAny>, runtime: Py<PyAny>) -> PyResult<Self> {
901 let waker: Arc<Mutex<Option<Waker>>> = Arc::default();
902 let callback_waker = waker.clone();
903
904 let callback = PyCFunction::new_closure(
905 py,
906 None,
907 None,
908 move |_args: &Bound<'_, PyTuple>, _kwargs: Option<&Bound<'_, PyDict>>| {
909 if let Some(waker) = callback_waker.lock().take() {
910 waker.wake();
911 }
912
913 Ok::<(), PyErr>(())
914 },
915 )?;
916
917 task.call_method1(py, "add_done_callback", (callback,))?;
918 Ok(Self {
919 runtime,
920 task,
921 waker,
922 complete: false,
923 })
924 }
925}
926
927impl Future for PythonOperation {
928 type Output = PyResult<Py<PyAny>>;
929 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
930 *self.waker.lock() = Some(cx.waker().clone());
931 Python::attach(|py| {
932 match self
933 .task
934 .call_method0(py, "done")
935 .and_then(|done| done.extract::<bool>(py))
936 {
937 Ok(false) => Poll::Pending,
938 Ok(true) => {
939 self.complete = true;
940 Poll::Ready(self.task.call_method0(py, "result"))
941 }
942 Err(e) => Poll::Ready(Err(e)),
943 }
944 })
945 }
946}
947
948impl Drop for PythonOperation {
949 fn drop(&mut self) {
950 if !self.complete {
951 Python::attach(|py| {
952 if let Err(e) = self
953 .runtime
954 .call_method1(py, "abandon", (self.task.clone_ref(py),))
955 {
956 log::error!("Failed to request Python operation cancellation: {e}");
957 }
958 });
959 }
960 }
961}
962
963fn is_cancelled(py: Python<'_>, e: &PyErr) -> PyResult<bool> {
964 e.value(py)
965 .is_instance(&py.import("asyncio")?.getattr("CancelledError")?)
966}
967
968fn coroutine_driver(py: Python<'_>) -> PyResult<&Py<PyAny>> {
969 static DRIVER: PyOnceLock<Py<PyAny>> = PyOnceLock::new();
970 DRIVER.get_or_try_init(py, || {
971 let module = py.import("nautilus_trader.live._coroutine")?;
972 Ok(module.getattr("drive")?.unbind())
973 })
974}