Skip to content

Commit 4b159df

Browse files
author
Andrew J Westlake
committed
Updated event loop references section to mention contextvars and how _with_locals variants work
1 parent a039895 commit 4b159df

File tree

2 files changed

+114
-61
lines changed

2 files changed

+114
-61
lines changed

src/generic.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ where
8686
if let Some(locals) = R::get_task_locals() {
8787
Ok(locals)
8888
} else {
89-
Ok(TaskLocals::new(get_running_loop(py)?).copy_context(py)?)
89+
Ok(TaskLocals::with_running_loop(py)?.copy_context(py)?)
9090
}
9191
}
9292

src/lib.rs

Lines changed: 113 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -18,62 +18,94 @@
1818
//! be run on the same loop.
1919
//!
2020
//! 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.
2323
//!
24-
//! ## Python's Event Loop
24+
//! ## Python's Event Loop and the Main Thread
2525
//!
2626
//! Python is very picky about the threads used by the `asyncio` executor. In particular, it needs
2727
//! to have control over the main thread in order to handle signals like CTRL-C correctly. This
2828
//! means that Cargo's default test harness will no longer work since it doesn't provide a method of
2929
//! overriding the main function to add our event loop initialization and finalization.
3030
//!
31-
//! ## Event Loop References
31+
//! ## Event Loop References and ContextVars
3232
//!
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.
3437
//!
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.
3645
//!
3746
//! ### The Main Dilemma
3847
//!
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.
4059
//!
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.
4263
//!
4364
//! ### The Solution
4465
//!
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.
4669
//!
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:
5073
//!
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.
5284
//!
5385
//! ```rust
5486
//! use pyo3::{wrap_pyfunction, prelude::*};
5587
//!
5688
//! # #[cfg(feature = "tokio-runtime")]
5789
//! #[pyfunction]
5890
//! 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)?;
6193
//!
6294
//! // 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 {
6496
//! let py_sleep = Python::with_gil(|py| {
6597
//! // Sometimes we need to call other async Python functions within
6698
//! // this future. In order for this to work, we need to track the
6799
//! // 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,
70102
//! py.import("asyncio")?.call_method1("sleep", (1,))?
71103
//! )
72104
//! })?;
73105
//!
74106
//! py_sleep.await?;
75107
//!
76-
//! Ok(Python::with_gil(|py| py.None()))
108+
//! Ok(())
77109
//! })
78110
//! }
79111
//!
@@ -85,18 +117,35 @@
85117
//! }
86118
//! ```
87119
//!
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.
89127
//!
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.
91131
//!
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_.
93137
//!
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.
95142
//!
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:
97145
//!
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.
100149
//!
101150
//! With these new functions, we can make our previous example more correct:
102151
//!
@@ -107,18 +156,17 @@
107156
//! #[pyfunction]
108157
//! fn sleep(py: Python) -> PyResult<&PyAny> {
109158
//! // 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
118166
//! pyo3_asyncio::tokio::scope(locals.clone(), async move {
119167
//! let py_sleep = Python::with_gil(|py| {
120168
//! 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
122170
//! &pyo3_asyncio::tokio::get_current_locals(py)?,
123171
//! py.import("asyncio")?.call_method1("sleep", (1,))?
124172
//! )
@@ -135,18 +183,19 @@
135183
//! #[pyfunction]
136184
//! fn wrap_sleep(py: Python) -> PyResult<&PyAny> {
137185
//! // 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`
139187
//! let locals = pyo3_asyncio::tokio::get_current_locals(py)?;
140188
//!
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
144193
//! pyo3_asyncio::tokio::scope(locals.clone(), async move {
145194
//! let py_sleep = Python::with_gil(|py| {
146195
//! pyo3_asyncio::into_future_with_locals(
147196
//! &pyo3_asyncio::tokio::get_current_locals(py)?,
148197
//! // 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
150199
//! sleep(py)?
151200
//! )
152201
//! })?;
@@ -167,16 +216,23 @@
167216
//! }
168217
//! ```
169218
//!
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:
171221
//!
172222
//! - `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`)
174225
//! - `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)
176229
//! - `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).
178233
//!
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:
180236
//!
181237
//! ```rust
182238
//! use pyo3::prelude::*;
@@ -220,25 +276,17 @@
220276
//! }
221277
//! ```
222278
//!
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!
236281
//!
237282
//! ## Rust's Event Loop
238283
//!
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)!
240287
//!
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!_
242290
//!
243291
//! ## Features
244292
//!
@@ -457,6 +505,11 @@ impl TaskLocals {
457505
}
458506
}
459507

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+
460513
/// Manually provide the contextvars for the current task.
461514
pub fn with_context(self, context: &PyAny) -> Self {
462515
Self {

0 commit comments

Comments
 (0)