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
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        // Release ownership while attached, before the callback capsule is destroyed
400        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            // Release Python references outside the lock: finalizers can re-enter the runtime
485            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        // Task.exception() consumes the cancellation message needed by a subsequent awaiter
508        if task.call_method0("cancelled")?.extract::<bool>()? {
509            return Ok(());
510        }
511
512        // Inspect without re-raising: repeated Task.result() calls can replace the traceback
513        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(&current))
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                // Shield each owned task so cancelling the drain cannot interrupt its cleanup
809                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            // A reused dispatcher starts the next command with no consumed cancellation requests
839            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}