Skip to main content

nautilus_live/python/
runtime.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
16//! Owns Python adapter operations on the node's asyncio event loop.
17//!
18//! Admission, command ordering, lifecycle transitions, and terminal task ownership live here.
19//! Asyncio executes coroutines through a small driver; it does not own client lifecycle policy.
20
21use 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/// Supervises a Python client's operations until their terminal results are retrieved.
42#[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// These references deliberately root pending work across Python garbage collection. Completion
59// removes each task record and releases the active client, breaking the cycle without a global set.
60#[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        // Release ownership while attached, before the callback capsule is destroyed
399        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            // Release Python references outside the lock: finalizers can re-enter the runtime
484            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        // Task.exception() consumes the cancellation message needed by a subsequent awaiter
507        if task.call_method0("cancelled")?.extract::<bool>()? {
508            return Ok(());
509        }
510
511        // Inspect without re-raising: repeated Task.result() calls can replace the traceback
512        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(&current))
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                // Shield each owned task so cancelling the drain cannot interrupt its cleanup
807                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            // A reused dispatcher starts the next command with no consumed cancellation requests
837            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}