|
18 | 18 | //! be run on the same loop.
|
19 | 19 | //!
|
20 | 20 | //! It's not immediately clear that this would provide worthwhile performance wins either, so in the
|
21 |
| -//! interest of keeping things simple, this crate creates and manages the Python event loop and |
22 |
| -//! handles the communication between separate Rust event loops. |
| 21 | +//! interest of getting something simple out there to facilitate these conversions, this crate |
| 22 | +//! handles the communication between _separate_ Python and Rust event loops. |
23 | 23 | //!
|
24 |
| -//! ## Python's Event Loop |
| 24 | +//! ## Python's Event Loop and the Main Thread |
25 | 25 | //!
|
26 | 26 | //! Python is very picky about the threads used by the `asyncio` executor. In particular, it needs
|
27 | 27 | //! to have control over the main thread in order to handle signals like CTRL-C correctly. This
|
28 | 28 | //! means that Cargo's default test harness will no longer work since it doesn't provide a method of
|
29 | 29 | //! overriding the main function to add our event loop initialization and finalization.
|
30 | 30 | //!
|
31 |
| -//! ## Event Loop References |
| 31 | +//! ## Event Loop References and ContextVars |
32 | 32 | //!
|
33 |
| -//! One problem that arises when interacting with Python's asyncio library is that the functions we use to get a reference to the Python event loop can only be called in certain contexts. Since PyO3 Asyncio needs to interact with Python's event loop during conversions, the context of these conversions can matter a lot. |
| 33 | +//! One problem that arises when interacting with Python's asyncio library is that the functions we |
| 34 | +//! use to get a reference to the Python event loop can only be called in certain contexts. Since |
| 35 | +//! PyO3 Asyncio needs to interact with Python's event loop during conversions, the context of these |
| 36 | +//! conversions can matter a lot. |
34 | 37 | //!
|
35 |
| -//! > The core conversions we've mentioned so far in this guide should insulate you from these concerns in most cases, but in the event that they don't, this section should provide you with the information you need to solve these problems. |
| 38 | +//! Likewise, Python's `contextvars` library can require some special treatment. Python functions |
| 39 | +//! and coroutines can rely on the context of outer coroutines to function correctly, so this |
| 40 | +//! library needs to be able to preserve `contextvars` during conversions. |
| 41 | +//! |
| 42 | +//! > The core conversions we've mentioned so far in the README should insulate you from these |
| 43 | +//! concerns in most cases. For the edge cases where they don't, this section should provide you |
| 44 | +//! with the information you need to solve these problems. |
36 | 45 | //!
|
37 | 46 | //! ### The Main Dilemma
|
38 | 47 | //!
|
39 |
| -//! Python programs can have many independent event loop instances throughout the lifetime of the application (`asyncio.run` for example creates its own event loop each time it's called for instance), and they can even run concurrent with other event loops. For this reason, the most correct method of obtaining a reference to the Python event loop is via `asyncio.get_running_loop`. |
| 48 | +//! Python programs can have many independent event loop instances throughout the lifetime of the |
| 49 | +//! application (`asyncio.run` for example creates its own event loop each time it's called for |
| 50 | +//! instance), and they can even run concurrent with other event loops. For this reason, the most |
| 51 | +//! correct method of obtaining a reference to the Python event loop is via |
| 52 | +//! `asyncio.get_running_loop`. |
| 53 | +//! |
| 54 | +//! `asyncio.get_running_loop` returns the event loop associated with the current OS thread. It can |
| 55 | +//! be used inside Python coroutines to spawn concurrent tasks, interact with timers, or in our case |
| 56 | +//! signal between Rust and Python. This is all well and good when we are operating on a Python |
| 57 | +//! thread, but since Rust threads are not associated with a Python event loop, |
| 58 | +//! `asyncio.get_running_loop` will fail when called on a Rust runtime. |
40 | 59 | //!
|
41 |
| -//! `asyncio.get_running_loop` returns the event loop associated with the current OS thread. It can be used inside Python coroutines to spawn concurrent tasks, interact with timers, or in our case signal between Rust and Python. This is all well and good when we are operating on a Python thread, but since Rust threads are not associated with a Python event loop, `asyncio.get_running_loop` will fail when called on a Rust runtime. |
| 60 | +//! `contextvars` operates in a similar way, though the current context is not always associated |
| 61 | +//! with the current OS thread. Different contexts can be associated with different coroutines even |
| 62 | +//! if they run on the same OS thread. |
42 | 63 | //!
|
43 | 64 | //! ### The Solution
|
44 | 65 | //!
|
45 |
| -//! A really straightforward way of dealing with this problem is to pass a reference to the associated Python event loop for every conversion. That's why in `v0.14`, we introduced a new set of conversion functions that do just that: |
| 66 | +//! A really straightforward way of dealing with this problem is to pass references to the |
| 67 | +//! associated Python event loop and context for every conversion. That's why we have a structure |
| 68 | +//! called `TaskLocals` and a set of conversions that accept it. |
46 | 69 | //!
|
47 |
| -//! - `pyo3_asyncio::into_future_with_loop` - Convert a Python awaitable into a Rust future with the given asyncio event loop. |
48 |
| -//! - `pyo3_asyncio::<runtime>::future_into_py_with_loop` - Convert a Rust future into a Python awaitable with the given asyncio event loop. |
49 |
| -//! - `pyo3_asyncio::<runtime>::local_future_into_py_with_loop` - Convert a `!Send` Rust future into a Python awaitable with the given asyncio event loop. |
| 70 | +//! `TaskLocals` stores the current event loop, and allows the user to copy the current Python |
| 71 | +//! context if necessary. The following conversions will use these references to perform the |
| 72 | +//! necessary conversions and restore Python context when needed: |
50 | 73 | //!
|
51 |
| -//! One clear disadvantage to this approach (aside from the verbose naming) is that the Rust application has to explicitly track its references to the Python event loop. In native libraries, we can't make any assumptions about the underlying event loop, so the only reliable way to make sure our conversions work properly is to store a reference to the current event loop at the callsite to use later on. |
| 74 | +//! - `pyo3_asyncio::into_future_with_locals` - Convert a Python awaitable into a Rust future. |
| 75 | +//! - `pyo3_asyncio::<runtime>::future_into_py_with_locals` - Convert a Rust future into a Python |
| 76 | +//! awaitable. |
| 77 | +//! - `pyo3_asyncio::<runtime>::local_future_into_py_with_locals` - Convert a `!Send` Rust future |
| 78 | +//! into a Python awaitable. |
| 79 | +//! |
| 80 | +//! One clear disadvantage to this approach is that the Rust application has to explicitly track |
| 81 | +//! these references. In native libraries, we can't make any assumptions about the underlying event |
| 82 | +//! loop, so the only reliable way to make sure our conversions work properly is to store these |
| 83 | +//! references at the callsite to use later on. |
52 | 84 | //!
|
53 | 85 | //! ```rust
|
54 | 86 | //! use pyo3::{wrap_pyfunction, prelude::*};
|
55 | 87 | //!
|
56 | 88 | //! # #[cfg(feature = "tokio-runtime")]
|
57 | 89 | //! #[pyfunction]
|
58 | 90 | //! fn sleep(py: Python) -> PyResult<&PyAny> {
|
59 |
| -//! let current_loop = pyo3_asyncio::get_running_loop(py)?; |
60 |
| -//! let loop_ref = PyObject::from(current_loop); |
| 91 | +//! // Construct the task locals structure with the current running loop and context |
| 92 | +//! let locals = pyo3_asyncio::TaskLocals::with_running_loop(py)?.copy_context(py)?; |
61 | 93 | //!
|
62 | 94 | //! // Convert the async move { } block to a Python awaitable
|
63 |
| -//! pyo3_asyncio::tokio::future_into_py_with_loop(current_loop, async move { |
| 95 | +//! pyo3_asyncio::tokio::future_into_py_with_locals(py, locals.clone(), async move { |
64 | 96 | //! let py_sleep = Python::with_gil(|py| {
|
65 | 97 | //! // Sometimes we need to call other async Python functions within
|
66 | 98 | //! // this future. In order for this to work, we need to track the
|
67 | 99 | //! // event loop from earlier.
|
68 |
| -//! pyo3_asyncio::into_future_with_loop( |
69 |
| -//! loop_ref.as_ref(py), |
| 100 | +//! pyo3_asyncio::into_future_with_locals( |
| 101 | +//! &locals, |
70 | 102 | //! py.import("asyncio")?.call_method1("sleep", (1,))?
|
71 | 103 | //! )
|
72 | 104 | //! })?;
|
73 | 105 | //!
|
74 | 106 | //! py_sleep.await?;
|
75 | 107 | //!
|
76 |
| -//! Ok(Python::with_gil(|py| py.None())) |
| 108 | +//! Ok(()) |
77 | 109 | //! })
|
78 | 110 | //! }
|
79 | 111 | //!
|
|
85 | 117 | //! }
|
86 | 118 | //! ```
|
87 | 119 | //!
|
88 |
| -//! > A naive solution to this tracking problem would be to cache a global reference to the asyncio event loop that all PyO3 Asyncio conversions can use. In fact this is what we did in PyO3 Asyncio `v0.13`. This works well for applications, but it soon became clear that this is not so ideal for libraries. Libraries usually have no direct control over how the event loop is managed, they're just expected to work with any event loop at any point in the application. This problem is compounded further when multiple event loops are used in the application since the global reference will only point to one. |
| 120 | +//! > A naive solution to this tracking problem would be to cache a global reference to the asyncio |
| 121 | +//! event loop that all PyO3 Asyncio conversions can use. In fact this is what we did in PyO3 |
| 122 | +//! Asyncio `v0.13`. This works well for applications, but it soon became clear that this is not |
| 123 | +//! so ideal for libraries. Libraries usually have no direct control over how the event loop is |
| 124 | +//! managed, they're just expected to work with any event loop at any point in the application. |
| 125 | +//! This problem is compounded further when multiple event loops are used in the application since |
| 126 | +//! the global reference will only point to one. |
89 | 127 | //!
|
90 |
| -//! Another disadvantage to this explicit approach that is less obvious is that we can no longer call our `#[pyfunction] fn sleep` on a Rust runtime since `asyncio.get_running_loop` only works on Python threads! It's clear that we need a slightly more flexible approach. |
| 128 | +//! Another disadvantage to this explicit approach that is less obvious is that we can no longer |
| 129 | +//! call our `#[pyfunction] fn sleep` on a Rust runtime since `asyncio.get_running_loop` only works |
| 130 | +//! on Python threads! It's clear that we need a slightly more flexible approach. |
91 | 131 | //!
|
92 |
| -//! In order to detect the Python event loop at the callsite, we need something like `asyncio.get_running_loop` that works for _both Python and Rust_. In Python, `asyncio.get_running_loop` uses thread-local data to retrieve the event loop associated with the current thread. What we need in Rust is something that can retrieve the Python event loop associated with the current _task_. |
| 132 | +//! In order to detect the Python event loop at the callsite, we need something like |
| 133 | +//! `asyncio.get_running_loop` and `contextvars.copy_context` that works for _both Python and Rust_. |
| 134 | +//! In Python, `asyncio.get_running_loop` uses thread-local data to retrieve the event loop |
| 135 | +//! associated with the current thread. What we need in Rust is something that can retrieve the |
| 136 | +//! Python event loop and contextvars associated with the current Rust _task_. |
93 | 137 | //!
|
94 |
| -//! Enter `pyo3_asyncio::<runtime>::get_current_loop`. This function first checks task-local data for a Python event loop, then falls back on `asyncio.get_running_loop` if no task-local event loop is found. This way both bases are covered. |
| 138 | +//! Enter `pyo3_asyncio::<runtime>::get_current_locals`. This function first checks task-local data |
| 139 | +//! for the `TaskLocals`, then falls back on `asyncio.get_running_loop` and |
| 140 | +//! `contextvars.copy_context` if no task locals are found. This way both bases are |
| 141 | +//! covered. |
95 | 142 | //!
|
96 |
| -//! Now, all we need is a way to store the event loop in task-local data. Since this is a runtime-specific feature, you can find the following functions in each runtime module: |
| 143 | +//! Now, all we need is a way to store the `TaskLocals` for the Rust future. Since this is a |
| 144 | +//! runtime-specific feature, you can find the following functions in each runtime module: |
97 | 145 | //!
|
98 |
| -//! - `pyo3_asyncio::<runtime>::scope` - Store the event loop in task-local data when executing the given Future. |
99 |
| -//! - `pyo3_asyncio::<runtime>::scope_local` - Store the event loop in task-local data when executing the given `!Send` Future. |
| 146 | +//! - `pyo3_asyncio::<runtime>::scope` - Store the task-local data when executing the given Future. |
| 147 | +//! - `pyo3_asyncio::<runtime>::scope_local` - Store the task-local data when executing the given |
| 148 | +//! `!Send` Future. |
100 | 149 | //!
|
101 | 150 | //! With these new functions, we can make our previous example more correct:
|
102 | 151 | //!
|
|
107 | 156 | //! #[pyfunction]
|
108 | 157 | //! fn sleep(py: Python) -> PyResult<&PyAny> {
|
109 | 158 | //! // get the current event loop through task-local data
|
110 |
| -//! // OR `asyncio.get_running_loop` |
111 |
| -//! let locals = Python::with_gil(|py| { |
112 |
| -//! pyo3_asyncio::tokio::get_current_locals(py).unwrap() |
113 |
| -//! }); |
114 |
| -//! |
115 |
| -//! pyo3_asyncio::tokio::future_into_py_with_loop( |
116 |
| -//! locals.event_loop(py), |
117 |
| -//! // Store the current loop in task-local data |
| 159 | +//! // OR `asyncio.get_running_loop` and `contextvars.copy_context` |
| 160 | +//! let locals = pyo3_asyncio::tokio::get_current_locals(py)?; |
| 161 | +//! |
| 162 | +//! pyo3_asyncio::tokio::future_into_py_with_locals( |
| 163 | +//! py, |
| 164 | +//! locals.clone(), |
| 165 | +//! // Store the current locals in task-local data |
118 | 166 | //! pyo3_asyncio::tokio::scope(locals.clone(), async move {
|
119 | 167 | //! let py_sleep = Python::with_gil(|py| {
|
120 | 168 | //! pyo3_asyncio::into_future_with_locals(
|
121 |
| -//! // Now we can get the current loop through task-local data |
| 169 | +//! // Now we can get the current locals through task-local data |
122 | 170 | //! &pyo3_asyncio::tokio::get_current_locals(py)?,
|
123 | 171 | //! py.import("asyncio")?.call_method1("sleep", (1,))?
|
124 | 172 | //! )
|
|
135 | 183 | //! #[pyfunction]
|
136 | 184 | //! fn wrap_sleep(py: Python) -> PyResult<&PyAny> {
|
137 | 185 | //! // get the current event loop through task-local data
|
138 |
| -//! // OR `asyncio.get_running_loop` |
| 186 | +//! // OR `asyncio.get_running_loop` and `contextvars.copy_context` |
139 | 187 | //! let locals = pyo3_asyncio::tokio::get_current_locals(py)?;
|
140 | 188 | //!
|
141 |
| -//! pyo3_asyncio::tokio::future_into_py_with_loop( |
142 |
| -//! locals.event_loop(py), |
143 |
| -//! // Store the current loop in task-local data |
| 189 | +//! pyo3_asyncio::tokio::future_into_py_with_locals( |
| 190 | +//! py, |
| 191 | +//! locals.clone(), |
| 192 | +//! // Store the current locals in task-local data |
144 | 193 | //! pyo3_asyncio::tokio::scope(locals.clone(), async move {
|
145 | 194 | //! let py_sleep = Python::with_gil(|py| {
|
146 | 195 | //! pyo3_asyncio::into_future_with_locals(
|
147 | 196 | //! &pyo3_asyncio::tokio::get_current_locals(py)?,
|
148 | 197 | //! // We can also call sleep within a Rust task since the
|
149 |
| -//! // event loop is stored in task local data |
| 198 | +//! // locals are stored in task local data |
150 | 199 | //! sleep(py)?
|
151 | 200 | //! )
|
152 | 201 | //! })?;
|
|
167 | 216 | //! }
|
168 | 217 | //! ```
|
169 | 218 | //!
|
170 |
| -//! Even though this is more correct, it's clearly not more ergonomic. That's why we introduced a new set of functions with this functionality baked in: |
| 219 | +//! Even though this is more correct, it's clearly not more ergonomic. That's why we introduced a |
| 220 | +//! set of functions with this functionality baked in: |
171 | 221 | //!
|
172 | 222 | //! - `pyo3_asyncio::<runtime>::into_future`
|
173 |
| -//! > Convert a Python awaitable into a Rust future (using `pyo3_asyncio::<runtime>::get_current_loop`) |
| 223 | +//! > Convert a Python awaitable into a Rust future (using |
| 224 | +//! `pyo3_asyncio::<runtime>::get_current_locals`) |
174 | 225 | //! - `pyo3_asyncio::<runtime>::future_into_py`
|
175 |
| -//! > Convert a Rust future into a Python awaitable (using `pyo3_asyncio::<runtime>::get_current_loop` and `pyo3_asyncio::<runtime>::scope` to set the task-local event loop for the given Rust future) |
| 226 | +//! > Convert a Rust future into a Python awaitable (using |
| 227 | +//! `pyo3_asyncio::<runtime>::get_current_locals` and `pyo3_asyncio::<runtime>::scope` to set the |
| 228 | +//! task-local event loop for the given Rust future) |
176 | 229 | //! - `pyo3_asyncio::<runtime>::local_future_into_py`
|
177 |
| -//! > Convert a `!Send` Rust future into a Python awaitable (using `pyo3_asyncio::<runtime>::get_current_loop` and `pyo3_asyncio::<runtime>::scope_local` to set the task-local event loop for the given Rust future). |
| 230 | +//! > Convert a `!Send` Rust future into a Python awaitable (using |
| 231 | +//! `pyo3_asyncio::<runtime>::get_current_locals` and `pyo3_asyncio::<runtime>::scope_local` to |
| 232 | +//! set the task-local event loop for the given Rust future). |
178 | 233 | //!
|
179 |
| -//! __These are the functions that we recommend using__. With these functions, the previous example can be rewritten to be more compact: |
| 234 | +//! __These are the functions that we recommend using__. With these functions, the previous example |
| 235 | +//! can be rewritten to be more compact: |
180 | 236 | //!
|
181 | 237 | //! ```rust
|
182 | 238 | //! use pyo3::prelude::*;
|
|
220 | 276 | //! }
|
221 | 277 | //! ```
|
222 | 278 | //!
|
223 |
| -//! ## A Note for `v0.13` Users |
224 |
| -//! |
225 |
| -//! Hey guys, I realize that these are pretty major changes for `v0.14`, and I apologize in advance for having to modify the public API so much. I hope |
226 |
| -//! the explanation above gives some much needed context and justification for all the breaking changes. |
227 |
| -//! |
228 |
| -//! Part of the reason why it's taken so long to push out a `v0.14` release is because I wanted to make sure we got this release right. There were a lot of issues with the `v0.13` release that I hadn't anticipated, and it's thanks to your feedback and patience that we've worked through these issues to get a more correct, more flexible version out there! |
229 |
| -//! |
230 |
| -//! This new release should address most the core issues that users have reported in the `v0.13` release, so I think we can expect more stability going forward. |
231 |
| -//! |
232 |
| -//! Also, a special thanks to [@ShadowJonathan](https://github.com/ShadowJonathan) for helping with the design and review |
233 |
| -//! of these changes! |
234 |
| -//! |
235 |
| -//! - [@awestlake87](https://github.com/awestlake87) |
| 279 | +//! > A special thanks to [@ShadowJonathan](https://github.com/ShadowJonathan) for helping with the |
| 280 | +//! design and review of these changes! |
236 | 281 | //!
|
237 | 282 | //! ## Rust's Event Loop
|
238 | 283 | //!
|
239 |
| -//! Currently only the async-std and Tokio runtimes are supported by this crate. |
| 284 | +//! Currently only the Async-Std and Tokio runtimes are supported by this crate. If you need support |
| 285 | +//! for another runtime, feel free to make a request on GitHub (or attempt to add support yourself |
| 286 | +//! with the [`generic`] module)! |
240 | 287 | //!
|
241 |
| -//! > _In the future, more runtimes may be supported for Rust._ |
| 288 | +//! > _In the future, we may implement first class support for more Rust runtimes. Contributions are |
| 289 | +//! welcome as well!_ |
242 | 290 | //!
|
243 | 291 | //! ## Features
|
244 | 292 | //!
|
@@ -457,6 +505,11 @@ impl TaskLocals {
|
457 | 505 | }
|
458 | 506 | }
|
459 | 507 |
|
| 508 | + /// Construct TaskLocals with the event loop returned by `get_running_loop` |
| 509 | + pub fn with_running_loop(py: Python) -> PyResult<Self> { |
| 510 | + Ok(Self::new(get_running_loop(py)?)) |
| 511 | + } |
| 512 | + |
460 | 513 | /// Manually provide the contextvars for the current task.
|
461 | 514 | pub fn with_context(self, context: &PyAny) -> Self {
|
462 | 515 | Self {
|
|
0 commit comments