Skip to main content

monarch_hyperactor/
handle_wake.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! GIL-free completion wake for `Handle.as_asyncio` (HDL-17 to HDL-21 in
10//! [`crate::handle`]).
11//!
12//! Each asyncio loop that can watch a file descriptor (`loop.add_reader`,
13//! which every selector-based loop, including all of Monarch's, provides) gets
14//! one [`LoopWake`]: a queue of `u64` tokens and a non-blocking pipe whose read
15//! end the loop watches. A loop without `add_reader` keeps the
16//! `call_soon_threadsafe` path (HDL-21).
17//!
18//! `waker` and `read_fd` are the two ends of the pipe; `tx` and `rx` are the
19//! two ends of the token queue. When a `Handle` completes, its Tokio observer
20//! pushes its token and writes one byte to the pipe ([`Notifier::notify`]),
21//! without the GIL. The byte only wakes the loop; the queue says which awaits
22//! completed. The loop's reader drains the pipe, then the queue, and settles
23//! the future of each completed await.
24//!
25//! The loop's selector is the channel's only strong owner, through its
26//! `on_readable` callback. The registry holds the loop as a weak key and the
27//! channel through a weak reference. Each entry holds its pending await's
28//! future strongly, because that reference is the waiting task's only owner
29//! outside the task's own cycle; asyncio holds tasks weakly. Closing the loop
30//! therefore releases the channel, its read end, its entries and their
31//! futures; the write end lasts until every notifier is done (HDL-20).
32//! `__traverse__` lets the garbage collector free an unclosed loop (HDL-19,
33//! which also records the case that is not freed):
34//!
35//! ```text
36//! registry:         weak(loop) ──▶ weak(LoopWake)
37//! loop's selector:  read_fd    ──▶ LoopWake.on_readable
38//!
39//! LoopWake
40//!   ├─ tx / rx    queue of u64 tokens
41//!   ├─ waker      pipe write end, shared with every Notifier
42//!   ├─ read_fd    pipe read end, registered with `add_reader`
43//!   └─ entries    token → (asyncio future, Handle)
44//! ```
45
46use std::collections::HashMap;
47use std::iter;
48use std::os::fd::AsRawFd;
49use std::os::fd::OwnedFd;
50use std::sync::Arc;
51use std::sync::Mutex;
52use std::sync::atomic::AtomicU64;
53use std::sync::atomic::Ordering;
54use std::sync::mpsc;
55
56use monarch_types::MapPyErr;
57use nix::errno::Errno;
58use nix::unistd::read;
59use pyo3::PyTraverseError;
60use pyo3::PyVisit;
61use pyo3::exceptions::PyNotImplementedError;
62use pyo3::exceptions::PyRuntimeError;
63use pyo3::exceptions::PyTypeError;
64use pyo3::prelude::*;
65use pyo3::sync::PyOnceLock;
66use pyo3::types::PyDict;
67use pyo3::types::PyTuple;
68use pyo3::types::PyWeakrefReference;
69
70use crate::handle::PyHandle;
71use crate::handle::complete_asyncio_future;
72use crate::pywaker;
73use crate::pywaker::Waker;
74use crate::pywaker::retry_eintr;
75
76/// The process-wide `weakref.WeakKeyDictionary` from each asyncio loop to its
77/// wake state:
78///
79/// - no entry: no `Handle` has been awaited on the loop yet;
80/// - a weak reference to a [`LoopWake`]: the loop's channel, installed by its
81///   first await and owned by the loop's selector;
82/// - `None`: the loop's `add_reader` raised `NotImplementedError`, so its
83///   awaits use the `call_soon_threadsafe` fallback (HDL-21).
84///
85/// Keys are weak and channels are held weakly, so the registry keeps neither a
86/// loop nor anything its channel holds alive (HDL-19). A Rust map keyed by the
87/// loop's `id` would outlive the loop, and a later loop could reuse that `id`.
88fn registry(py: Python<'_>) -> PyResult<Bound<'_, PyAny>> {
89    static REGISTRY: PyOnceLock<Py<PyAny>> = PyOnceLock::new();
90    REGISTRY
91        .get_or_try_init(py, || -> PyResult<Py<PyAny>> {
92            Ok(py
93                .import("weakref")?
94                .getattr("WeakKeyDictionary")?
95                .call0()?
96                .unbind())
97        })
98        .map(|registry| registry.bind(py).clone())
99}
100
101/// The lookup default that tells "no entry" apart from a cached `None`.
102fn absent(py: Python<'_>) -> PyResult<Bound<'_, PyAny>> {
103    static ABSENT: PyOnceLock<Py<PyAny>> = PyOnceLock::new();
104    ABSENT
105        .get_or_try_init(py, || -> PyResult<Py<PyAny>> {
106            Ok(py.import("builtins")?.getattr("object")?.call0()?.unbind())
107        })
108        .map(|absent| absent.bind(py).clone())
109}
110
111/// One-shot hooks run at the reader's two drain boundaries (HDL-18).
112#[cfg(test)]
113#[derive(Default)]
114struct DrainHooks {
115    after_pipe_drain: Option<Box<dyn FnOnce(&LoopWake) + Send>>,
116    after_queue_drain: Option<Box<dyn FnOnce(&LoopWake) + Send>>,
117}
118
119/// A pending await: the `asyncio.Future` it is suspended on, and the `Handle`
120/// it is waiting for.
121struct Entry {
122    /// The awaiting `asyncio.Future`, held strongly until delivery,
123    /// cancellation or loop close. A suspended task is referenced only through
124    /// the future it awaits, so without this reference a task whose other
125    /// references form a cycle would be collected while it waits.
126    /// `LoopWake.__traverse__` exposes it to the garbage collector (HDL-19).
127    fut: Py<PyAny>,
128    /// The awaited `Handle`. Its value is read when the entry's token arrives,
129    /// and set as the result of `fut`.
130    handle: Py<PyHandle>,
131}
132
133/// The completion-wake channel of one asyncio loop: a token queue, and a pipe
134/// the loop watches.
135///
136/// [`LoopWake::for_loop`] creates it on the loop's first `Handle` await and
137/// registers [`on_readable`](LoopWake::on_readable) with the loop's
138/// `add_reader`. From then on the loop's selector, which holds that callback,
139/// is its only strong owner, so it lives until the loop is closed or freed.
140/// The registry refers to it weakly (HDL-19).
141///
142/// Two sides use it:
143/// - the loop thread, holding the GIL: [`LoopWake::register`] adds an entry
144///   for each await, and `on_readable` delivers the completed ones;
145/// - Tokio, without the GIL: each [`Notifier`] holds clones of `tx` and
146///   `waker`, and nothing else.
147///
148/// The `pyclass` attributes:
149/// - `pyclass`: `add_reader` needs a Python callable, the bound
150///   `on_readable`, and the registry is a Python dictionary;
151/// - `frozen`: nothing needs `&mut self`. Every field is reached through
152///   `&self`, from whichever thread holds the GIL, so the fields that change
153///   are a `Mutex` or an atomic, and pyo3 keeps no runtime borrow flag;
154/// - `weakref`: a `TokenReaper` refers to its channel weakly, so no reference
155///   cycle runs through Rust fields the garbage collector cannot see (HDL-19).
156#[pyclass(frozen, weakref)]
157pub(crate) struct LoopWake {
158    /// The sending half of the token queue, cloned into each [`Notifier`].
159    tx: mpsc::Sender<u64>,
160    /// The receiving half of the token queue. Only `on_readable` drains it, on
161    /// the loop thread, so the `Mutex` is never contended; it is there because
162    /// `frozen` requires the struct to be `Sync`.
163    rx: Mutex<mpsc::Receiver<u64>>,
164    /// The pipe's write end, shared through the `Arc` with every [`Notifier`].
165    /// It stays open while any notifier lives, so a late write can never reach
166    /// a closed or reused descriptor (HDL-20).
167    waker: Arc<Waker>,
168    /// The pipe's read end, watched by the loop through `add_reader`. It is
169    /// closed when the channel is dropped, after which a write gets `EPIPE`
170    /// (HDL-20).
171    read_fd: OwnedFd,
172    /// The next token. Tokens are never reused, so a token that arrives after
173    /// its entry was removed finds nothing, instead of resolving a later
174    /// await.
175    next_token: AtomicU64,
176    /// Maps each token to its pending await. The Tokio side holds only the
177    /// token, never the entry's Python objects (HDL-17).
178    entries: Mutex<HashMap<u64, Entry>>,
179    /// Hooks that tests run inside `on_readable`; see [`DrainHooks`].
180    #[cfg(test)]
181    drain_hooks: Mutex<DrainHooks>,
182}
183
184impl LoopWake {
185    /// Create a channel with an empty token queue, a new non-blocking pipe, and
186    /// no entries. It is not yet watched by any loop; [`LoopWake::install`]
187    /// registers it. Fails only if the pipe cannot be created or made
188    /// non-blocking, for example when the process is out of file descriptors.
189    fn try_new() -> Result<Self, nix::Error> {
190        let (waker, read_fd) = pywaker::pipe()?;
191        let (tx, rx) = mpsc::channel();
192        Ok(Self {
193            tx,
194            rx: Mutex::new(rx),
195            waker: Arc::new(waker),
196            read_fd,
197            next_token: AtomicU64::new(0),
198            entries: Mutex::new(HashMap::new()),
199            #[cfg(test)]
200            drain_hooks: Mutex::new(DrainHooks::default()),
201        })
202    }
203
204    /// Return `event_loop`'s channel, installing one on first use, or `None`
205    /// when the loop uses the `call_soon_threadsafe` fallback (HDL-21).
206    ///
207    /// Call on the thread running `event_loop`: installing calls `add_reader`.
208    pub(crate) fn for_loop<'py>(
209        py: Python<'py>,
210        event_loop: &Bound<'py, PyAny>,
211    ) -> PyResult<Option<Bound<'py, LoopWake>>> {
212        let registry = registry(py)?;
213        let absent = absent(py)?;
214        // `registry.get(loop, absent)`: the loop's state, or `absent` if it has
215        // no entry.
216        let current = match registry.call_method1("get", (event_loop, &absent)) {
217            Ok(current) => current,
218            // HDL-21: `get` builds `weakref.ref(loop)`, which raises
219            // `TypeError` for a loop that cannot be weakly referenced.
220            Err(err) if err.is_instance_of::<PyTypeError>(py) => return Ok(None),
221            Err(err) => return Err(err),
222        };
223        // No entry: this is the loop's first `Handle` await.
224        if current.is(&absent) {
225            return Self::install(py, &registry, event_loop);
226        }
227        // Cached as unsupported (HDL-21).
228        if current.is_none() {
229            return Ok(None);
230        }
231        // A weak reference to the loop's channel. It is dead only once the
232        // loop's selector has dropped the channel, when the loop was closed.
233        match current.cast_into::<PyWeakrefReference>()?.upgrade() {
234            Some(channel) => Ok(Some(channel.cast_into::<LoopWake>()?)),
235            None => Self::install(py, &registry, event_loop),
236        }
237    }
238
239    /// Install a channel for `event_loop` as one transaction: every
240    /// `add_reader` failure removes it, and only `NotImplementedError` is
241    /// cached (HDL-21).
242    fn install<'py>(
243        py: Python<'py>,
244        registry: &Bound<'py, PyAny>,
245        event_loop: &Bound<'py, PyAny>,
246    ) -> PyResult<Option<Bound<'py, LoopWake>>> {
247        let channel = Bound::new(py, Self::try_new().map_pyerr()?)?;
248        // Provisional: inserted before `add_reader` so that every failure below
249        // can be undone, leaving no reader registered for a channel the
250        // registry does not know. Weak, so the registry never keeps the
251        // channel, or anything it holds, alive (HDL-19).
252        registry.set_item(event_loop, PyWeakrefReference::new(channel.as_any())?)?;
253        // The bound method holds the channel. Once the loop's selector holds
254        // the method, it is the channel's only strong owner (HDL-19).
255        let reader = channel.getattr("on_readable")?;
256        // The channel keeps ownership of the descriptor; the loop only watches
257        // it.
258        let read_fd = channel.get().read_fd.as_raw_fd();
259        match event_loop.call_method1("add_reader", (read_fd, reader)) {
260            Ok(_) => Ok(Some(channel)),
261            // HDL-21: the loop cannot watch descriptors. Replacing the entry
262            // lets the channel and its pipe be freed, and later awaits skip
263            // `install`.
264            Err(err) if err.is_instance_of::<PyNotImplementedError>(py) => {
265                registry.set_item(event_loop, py.None())?;
266                Ok(None)
267            }
268            // Any other failure: undo the insert and propagate, so a later
269            // await tries again.
270            Err(err) => {
271                registry.del_item(event_loop)?;
272                Err(err)
273            }
274        }
275    }
276
277    /// Record a pending await, `fut` waiting on `handle`, under a new token,
278    /// and return the [`Notifier`] that its Tokio observer uses to report that
279    /// `handle` completed.
280    ///
281    /// Takes the bound `channel` rather than `&self` because the reaper needs a
282    /// weak reference to the Python object the registry holds, and a `&self`
283    /// cannot provide one (HDL-19). On error, nothing stays registered.
284    pub(crate) fn register(
285        channel: &Bound<'_, LoopWake>,
286        fut: &Bound<'_, PyAny>,
287        handle: Py<PyHandle>,
288    ) -> PyResult<Notifier> {
289        let py = channel.py();
290        let this = channel.get();
291        // Checked, so a token is never reused.
292        let token = this
293            .next_token
294            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |token| {
295                token.checked_add(1)
296            })
297            .map_err(|_| PyRuntimeError::new_err("handle wake tokens exhausted"))?;
298        // Removes this entry once `fut` is cancelled or settled.
299        let reaper = Bound::new(
300            py,
301            TokenReaper {
302                channel: PyWeakrefReference::new(channel.as_any())?.unbind(),
303                token,
304            },
305        )?;
306        this.insert(
307            token,
308            Entry {
309                fut: fut.clone().unbind(),
310                handle,
311            },
312        );
313        // Runs the reaper when `fut` is cancelled or completed. If the future
314        // rejects the callback, undo the insert. Removal is idempotent, since a
315        // custom future may already have run the reaper.
316        if let Err(err) = fut.call_method1("add_done_callback", (&reaper,)) {
317            drop(this.take(token));
318            return Err(err);
319        }
320        // All that the Tokio side gets: clones of the queue's sender and the
321        // pipe's write end, and the token (HDL-17).
322        Ok(Notifier {
323            tx: this.tx.clone(),
324            waker: Arc::clone(&this.waker),
325            token,
326        })
327    }
328
329    /// Add `entry` under `token`. Tokens are never reused, so the slot is
330    /// always empty. Any replaced entry would be dropped after the lock is
331    /// released, as [`LoopWake::take`] requires.
332    fn insert(&self, token: u64, entry: Entry) {
333        let previous = self
334            .entries
335            .lock()
336            .expect("entries lock should not be poisoned")
337            .insert(token, entry);
338        debug_assert!(previous.is_none(), "tokens are never reused");
339    }
340
341    /// Remove `token`'s entry. The caller drops or uses it after the lock is
342    /// released: a decref under the lock can run a finalizer that calls
343    /// `register`, which takes the same lock.
344    fn take(&self, token: u64) -> Option<Entry> {
345        self.entries
346            .lock()
347            .expect("entries lock should not be poisoned")
348            .remove(&token)
349    }
350
351    /// Read and discard every byte in the pipe, so the loop stops seeing it as
352    /// readable, and return once the pipe is empty. The bytes carry no
353    /// information: each only says "wake up", and the queue says which awaits
354    /// completed. [`LoopWake::on_readable`] drains the queue next whatever
355    /// happens here, so a token pushed after this drain still has its byte
356    /// pending (HDL-18).
357    fn drain_pipe(&self) {
358        // Discarded; its size only sets how many bytes one `read` takes.
359        let mut buf = [0u8; 64];
360        loop {
361            match retry_eintr(|| read(&self.read_fd, &mut buf)) {
362                // `EAGAIN`: the pipe is empty. `Ok(0)`: end of file, which
363                // needs every write end closed and cannot happen while the
364                // channel holds its own `waker`.
365                Ok(0) | Err(Errno::EAGAIN) => return,
366                // More may remain.
367                Ok(_) => {}
368                // Unexpected; `EINTR` was already retried. Stop, and let the
369                // caller drain the queue.
370                Err(err) => {
371                    tracing::error!(error = %err, "handle wake: pipe read failed");
372                    return;
373                }
374            }
375        }
376    }
377
378    /// Settle the pending await named by `token`: read its `Handle`'s value or
379    /// error and set it as the result of its `asyncio.Future` (HDL-7).
380    ///
381    /// Does nothing when there is no longer anyone to settle: the entry is
382    /// gone or the future is already done. Never raises:
383    /// a failure is reported through the future's loop, as asyncio reports an
384    /// exception from a callback, and the reader goes on to the next token.
385    fn deliver(&self, py: Python<'_>, token: u64) {
386        // A stale token: the reaper removed the entry because the await was
387        // cancelled or settled. The entry is dropped after `take` releases
388        // the lock.
389        let Some(entry) = self.take(token) else {
390            return;
391        };
392        let fut = entry.fut.bind(py);
393        match fut.call_method0("done").and_then(|done| done.is_truthy()) {
394            Ok(false) => {}
395            // Already settled, for example cancelled but not yet reaped.
396            Ok(true) => return,
397            Err(err) => {
398                report(fut, "handle wake: done() failed; the await is dropped", err);
399                return;
400            }
401        }
402        // A non-consuming read (HDL-3).
403        let published = match entry.handle.bind(py).borrow().poll_ready() {
404            Ok(Some(value)) => complete_asyncio_future(fut, false, value),
405            // Includes HDL-10's "producer ended without publishing a result".
406            Err(err) => complete_asyncio_future(fut, true, err.into_value(py).into_any()),
407            Ok(None) => {
408                debug_assert!(
409                    false,
410                    "HDL-1: a token is pushed only after its Handle is ready"
411                );
412                return;
413            }
414        };
415        if let Err(err) = published {
416            report(
417                fut,
418                "handle wake: publishing to an asyncio future failed",
419                err,
420            );
421        }
422    }
423}
424
425/// Report a reader failure to `fut`'s loop through `call_exception_handler`,
426/// the way asyncio reports an exception raised by a callback. Falls back to
427/// logging if the loop cannot be reached.
428fn report(fut: &Bound<'_, PyAny>, message: &str, err: PyErr) {
429    let py = fut.py();
430    let reported = (|| -> PyResult<()> {
431        let context = PyDict::new(py);
432        context.set_item("message", message)?;
433        context.set_item("exception", err.value(py))?;
434        context.set_item("future", fut)?;
435        fut.call_method0("get_loop")?
436            .call_method1("call_exception_handler", (context,))?;
437        Ok(())
438    })();
439    if let Err(report_err) = reported {
440        tracing::error!(error = %err, report_error = %report_err, "{message}");
441    }
442}
443
444#[cfg(test)]
445impl LoopWake {
446    fn run_hook(
447        &self,
448        take: impl FnOnce(&mut DrainHooks) -> Option<Box<dyn FnOnce(&LoopWake) + Send>>,
449    ) {
450        let hook = take(
451            &mut self
452                .drain_hooks
453                .lock()
454                .expect("drain hooks lock should not be poisoned"),
455        );
456        if let Some(hook) = hook {
457            hook(self);
458        }
459    }
460
461    fn entry_count(&self) -> usize {
462        self.entries
463            .lock()
464            .expect("entries lock should not be poisoned")
465            .len()
466    }
467}
468
469#[pymethods]
470impl LoopWake {
471    /// Expose each entry's future and `Handle` object to the garbage collector,
472    /// so a loop dropped without being closed while an await is registered is
473    /// still freed (HDL-19). Every holder of `entries` holds the GIL and
474    /// allocates no Python object while holding it, so the collector never runs
475    /// inside it and `try_lock` succeeds here. A lock missed in only one of the
476    /// collector's passes could free a live future, so no holder may break that
477    /// rule.
478    fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
479        if let Ok(entries) = self.entries.try_lock() {
480            for entry in entries.values() {
481                visit.call(&entry.fut)?;
482                visit.call(&entry.handle)?;
483            }
484        }
485        Ok(())
486    }
487
488    /// The `add_reader` callback: drain the pipe, then the queue, then deliver
489    /// (HDL-18). Never raises.
490    fn on_readable(&self, py: Python<'_>) {
491        self.drain_pipe();
492        #[cfg(test)]
493        self.run_hook(|hooks| hooks.after_pipe_drain.take());
494        // Collected so `rx` is released before any Python runs.
495        let tokens: Vec<u64> = {
496            let rx = self.rx.lock().expect("rx lock should not be poisoned");
497            iter::from_fn(|| rx.try_recv().ok()).collect()
498        };
499        #[cfg(test)]
500        self.run_hook(|hooks| hooks.after_queue_drain.take());
501        tokens.into_iter().for_each(|token| self.deliver(py, token));
502    }
503}
504
505/// Removes a pending await's entry once its future is cancelled or settled, so
506/// an abandoned await does not keep its future and `Handle` alive.
507///
508/// [`LoopWake::register`] attaches it as the future's done callback. It may run
509/// after delivery has already removed the entry, so removal is idempotent. A
510/// `pyclass` because it is a Python callback; `frozen` because it never
511/// changes.
512#[pyclass(frozen)]
513struct TokenReaper {
514    /// A weak reference to the channel holding the entry. The future's done
515    /// callbacks hold this reaper and the entry holds the future, so a strong
516    /// reference would close a cycle through the reaper, which the garbage
517    /// collector cannot traverse (HDL-19).
518    channel: Py<PyWeakrefReference>,
519    /// The entry to remove.
520    token: u64,
521}
522
523#[pymethods]
524impl TokenReaper {
525    /// Remove the entry, if the channel still exists. Accepts any arguments; a
526    /// done callback is passed the future.
527    #[pyo3(signature = (*_args))]
528    fn __call__(&self, py: Python<'_>, _args: &Bound<'_, PyTuple>) {
529        // No channel means its loop, and every entry with it, is already gone.
530        if let Some(channel) = self.channel.bind(py).upgrade()
531            && let Ok(channel) = channel.cast::<LoopWake>()
532        {
533            // Dropped after `take` releases the lock.
534            drop(channel.get().take(self.token));
535        }
536    }
537}
538
539/// Everything a `Handle`'s Tokio observer needs to report that one pending
540/// await's `Handle` completed: the await's token, a sender into the channel's
541/// token queue, and the pipe's write end. It is plain Rust, with no Python
542/// object, so the observer uses it without the GIL (HDL-17).
543pub(crate) struct Notifier {
544    /// A clone of the channel's `tx`. Sending fails once the channel is freed.
545    tx: mpsc::Sender<u64>,
546    /// The pipe's write end, shared with the channel and every other notifier.
547    /// It stays open while this notifier lives (HDL-20).
548    waker: Arc<Waker>,
549    /// The token of the await this notifier reports.
550    token: u64,
551}
552
553impl Notifier {
554    /// Push the token, then write the pipe (HDL-18). A channel that is gone
555    /// drops the token (HDL-20).
556    pub(crate) fn notify(self) {
557        if self.tx.send(self.token).is_err() {
558            return;
559        }
560        if let Err(err) = self.waker.wake() {
561            tracing::error!(error = %err, "handle wake: pipe write failed");
562        }
563    }
564}
565
566#[cfg(test)]
567mod tests {
568    use std::ffi::CStr;
569    use std::os::fd::AsFd;
570    use std::time::Duration;
571
572    use nix::poll::PollFd;
573    use nix::poll::PollFlags;
574    use nix::poll::poll;
575    use pyo3::IntoPyObjectExt;
576    use pyo3::exceptions::PyOSError;
577    use pyo3::types::IntoPyDict;
578    use pyo3::types::PyModule;
579    use tokio::sync::watch;
580
581    use super::*;
582    use crate::handle::HandleCore;
583    use crate::runtime::GilSite;
584    use crate::runtime::ensure_python;
585    use crate::runtime::monarch_with_gil_blocking;
586
587    const HELPER: &CStr = cr#"
588import asyncio
589
590
591class Probe:
592    pass
593
594
595def start_observer(loop, h):
596    async def start():
597        return h.as_asyncio()
598
599    return loop.run_until_complete(start())
600
601
602def run_until_done(loop, fut):
603    return loop.run_until_complete(asyncio.wait_for(fut, 5))
604
605
606def run_await(loop, h):
607    async def wait():
608        return await asyncio.wait_for(h.as_asyncio(), 5)
609
610    return loop.run_until_complete(wait())
611
612
613def try_observe(loop, h):
614    async def start():
615        try:
616            h.as_asyncio()
617        except Exception as e:
618            return e
619        return None
620
621    return loop.run_until_complete(start())
622
623
624def await_in_task(loop, h):
625    async def waiter():
626        await h
627
628    task = loop.create_task(waiter())
629    loop.run_until_complete(asyncio.sleep(0))
630    return task
631
632
633async def _await(observer):
634    await observer
635
636
637def await_observer_in_task(loop, h):
638    # The task holds only the observer future, not `h`: a task that held `h`
639    # would, once published as `h`'s value, form a cycle through the Handle.
640    async def start():
641        return loop.create_task(_await(h.as_asyncio()))
642
643    task = loop.run_until_complete(start())
644    loop.run_until_complete(asyncio.sleep(0))
645    return task
646
647
648def start_and_cancel(loop, h):
649    errors = []
650    loop.set_exception_handler(lambda _loop, context: errors.append(context))
651
652    async def start():
653        h.as_asyncio().cancel()
654
655    loop.run_until_complete(start())
656    return errors
657
658
659def observe_after_cancel(loop, h):
660    async def observe():
661        value = await asyncio.wait_for(h.as_asyncio(), 5)
662        await asyncio.sleep(0.05)
663        return value
664
665    return loop.run_until_complete(observe())
666
667
668class NoReaderLoop(asyncio.SelectorEventLoop):
669    def __init__(self):
670        super().__init__()
671        self.add_reader_calls = 0
672
673    def add_reader(self, fd, callback, *args):
674        self.add_reader_calls += 1
675        raise NotImplementedError
676
677
678class FlakyReaderLoop(asyncio.SelectorEventLoop):
679    def __init__(self):
680        super().__init__()
681        self.add_reader_calls = 0
682
683    def add_reader(self, fd, callback, *args):
684        self.add_reader_calls += 1
685        if self.add_reader_calls == 1:
686            raise OSError("transient add_reader failure")
687        return super().add_reader(fd, callback, *args)
688
689
690class RejectingFuture(asyncio.Future):
691    def add_done_callback(self, fn, *, context=None):
692        raise RuntimeError("add_done_callback rejected")
693
694
695class RejectingLoop(asyncio.SelectorEventLoop):
696    def create_future(self):
697        return RejectingFuture(loop=self)
698
699
700class TypeRejectingFuture(asyncio.Future):
701    def add_done_callback(self, fn, *, context=None):
702        raise TypeError("add_done_callback rejected")
703
704
705class TypeRejectingLoop(asyncio.SelectorEventLoop):
706    def create_future(self):
707        return TypeRejectingFuture(loop=self)
708
709
710class NoWeakFuture:
711    __slots__ = ("_inner", "_asyncio_future_blocking")
712
713    def __init__(self, loop):
714        self._inner = asyncio.Future(loop=loop)
715        self._asyncio_future_blocking = False
716
717    def __getattr__(self, name):
718        return getattr(self._inner, name)
719
720    def __await__(self):
721        return self._inner.__await__()
722
723    def add_done_callback(self, fn, *, context=None):
724        self._inner.add_done_callback(lambda _: fn(self), context=context)
725
726
727class NoWeakLoop(asyncio.SelectorEventLoop):
728    def create_future(self):
729        return NoWeakFuture(self)
730
731
732class BrokenDoneFuture(asyncio.Future):
733    def done(self):
734        raise RuntimeError("done() is broken")
735
736
737def capture_errors(loop):
738    errors = []
739    loop.set_exception_handler(lambda _loop, context: errors.append(context))
740    return errors
741
742
743def await_unreferenced(loop, h):
744    # The task is not kept: only the future it awaits can keep it alive.
745    results = []
746
747    async def waiter():
748        results.append(await h)
749
750    loop.create_task(waiter())
751    loop.run_until_complete(asyncio.sleep(0))
752    return results
753
754
755def run_until_nonempty(loop, items):
756    async def wait():
757        while not items:
758            await asyncio.sleep(0.001)
759
760    loop.run_until_complete(asyncio.wait_for(wait(), 5))
761
762
763def base_done(fut):
764    return asyncio.Future.done(fut)
765"#;
766
767    type Sender = watch::Sender<Option<PyResult<Py<PyAny>>>>;
768
769    fn helper(py: Python<'_>) -> Bound<'_, PyModule> {
770        PyModule::from_code(
771            py,
772            HELPER,
773            c"handle_wake_test_helper.py",
774            c"handle_wake_test_helper",
775        )
776        .unwrap()
777    }
778
779    fn new_loop(py: Python<'_>) -> Bound<'_, PyAny> {
780        py.import("asyncio")
781            .unwrap()
782            .call_method0("new_event_loop")
783            .unwrap()
784    }
785
786    fn pending_handle(py: Python<'_>) -> (Sender, Py<PyHandle>) {
787        let (tx, rx) = watch::channel(None);
788        let handle = Py::new(py, PyHandle::from_core(HandleCore::new(rx, None, None))).unwrap();
789        (tx, handle)
790    }
791
792    fn ready_handle(py: Python<'_>, value: i64) -> Py<PyHandle> {
793        Py::new(
794            py,
795            PyHandle::from_value(value.into_py_any(py).unwrap()).unwrap(),
796        )
797        .unwrap()
798    }
799
800    fn collect_garbage(py: Python<'_>) {
801        py.import("gc").unwrap().call_method0("collect").unwrap();
802    }
803
804    fn is_done(fut: &Bound<'_, PyAny>) -> bool {
805        fut.call_method0("done").unwrap().is_truthy().unwrap()
806    }
807
808    fn readable_within(channel: &LoopWake, timeout: Duration) -> bool {
809        let mut fds = [PollFd::new(channel.read_fd.as_fd(), PollFlags::POLLIN)];
810        let millis = u16::try_from(timeout.as_millis()).unwrap_or(u16::MAX);
811        poll(&mut fds, millis).unwrap() == 1
812    }
813
814    // HDL-17: Tokio writes the wake pipe while this thread holds the GIL.
815    #[test]
816    fn wake_is_written_while_test_holds_gil() {
817        ensure_python();
818        monarch_with_gil_blocking(GilSite::Test, |py| {
819            let helper = helper(py);
820            let event_loop = new_loop(py);
821            let (tx, handle) = pending_handle(py);
822            let fut = helper
823                .call_method1("start_observer", (&event_loop, &handle))
824                .unwrap();
825            let channel = LoopWake::for_loop(py, &event_loop)
826                .unwrap()
827                .expect("a selector loop should get a wake channel");
828
829            tx.send(Some(Ok(42i64.into_py_any(py).unwrap()))).unwrap();
830            assert!(
831                readable_within(channel.get(), Duration::from_secs(5)),
832                "the observer should write the pipe without the GIL, which this thread holds"
833            );
834
835            let value: i64 = helper
836                .call_method1("run_until_done", (&event_loop, &fut))
837                .unwrap()
838                .extract()
839                .unwrap();
840            assert_eq!(value, 42, "the reader should set the Handle's value");
841            event_loop.call_method0("close").unwrap();
842        });
843    }
844
845    // HDL-19: a dropped observer's future stays registered until its loop
846    // closes, and closing the loop releases the entry, its Handle and the
847    // published value.
848    #[test]
849    fn dropped_observer_is_retained_until_loop_close() {
850        ensure_python();
851        let value_ref = monarch_with_gil_blocking(GilSite::Test, |py| {
852            let helper = helper(py);
853            let event_loop = new_loop(py);
854            let (tx, handle) = pending_handle(py);
855            let fut = helper
856                .call_method1("start_observer", (&event_loop, &handle))
857                .unwrap();
858            let channel = LoopWake::for_loop(py, &event_loop).unwrap().unwrap();
859            drop(fut);
860            drop(handle);
861            collect_garbage(py);
862            assert_eq!(
863                channel.get().entry_count(),
864                1,
865                "the entry should hold the dropped observer's future"
866            );
867
868            let value = helper.getattr("Probe").unwrap().call0().unwrap();
869            let value_ref = PyWeakrefReference::new(&value).unwrap().unbind();
870            tx.send(Some(Ok(value.unbind()))).unwrap();
871            drop(tx);
872            assert!(
873                readable_within(channel.get(), Duration::from_secs(5)),
874                "the observer should notify the channel"
875            );
876            event_loop.call_method0("close").unwrap();
877            value_ref
878        });
879        monarch_with_gil_blocking(GilSite::Test, |py| {
880            collect_garbage(py);
881            assert!(
882                value_ref.bind(py).upgrade().is_none(),
883                "closing the loop should release the entry, its Handle and the value"
884            );
885        });
886    }
887
888    // HDL-19: a task that nothing else references survives a collection while
889    // it awaits a pending Handle, because its entry holds the future.
890    #[test]
891    fn unreferenced_waiting_task_survives_collection() {
892        ensure_python();
893        monarch_with_gil_blocking(GilSite::Test, |py| {
894            let helper = helper(py);
895            let event_loop = new_loop(py);
896            let errors = helper
897                .call_method1("capture_errors", (&event_loop,))
898                .unwrap();
899            let (tx, handle) = pending_handle(py);
900            let results = helper
901                .call_method1("await_unreferenced", (&event_loop, &handle))
902                .unwrap();
903            drop(handle);
904            collect_garbage(py);
905            tx.send(Some(Ok(7i64.into_py_any(py).unwrap()))).unwrap();
906            helper
907                .call_method1("run_until_nonempty", (&event_loop, &results))
908                .unwrap();
909            assert_eq!(results.extract::<Vec<i64>>().unwrap(), vec![7]);
910            assert_eq!(
911                errors.len().unwrap(),
912                0,
913                "no pending task should have been destroyed"
914            );
915            event_loop.call_method0("close").unwrap();
916        });
917    }
918
919    // HDL-19: a loop dropped without being closed, while an await on a pending
920    // Handle with no value is registered, is freed by a collection.
921    #[test]
922    fn unclosed_loop_with_a_pending_await_is_collected() {
923        ensure_python();
924        monarch_with_gil_blocking(GilSite::Test, |py| {
925            let helper = helper(py);
926            let event_loop = new_loop(py);
927            let (tx, handle) = pending_handle(py);
928            let task = helper
929                .call_method1("await_in_task", (&event_loop, &handle))
930                .unwrap();
931            let loop_ref = PyWeakrefReference::new(&event_loop).unwrap();
932            drop((task, event_loop, handle));
933            collect_garbage(py);
934            assert!(
935                loop_ref.upgrade().is_none(),
936                "the collector should free the unclosed loop"
937            );
938            drop(tx);
939        });
940    }
941
942    // HDL-20: a notify after the channel is gone is dropped.
943    #[test]
944    fn late_notify_after_channel_dropped_is_ignored() {
945        ensure_python();
946        monarch_with_gil_blocking(GilSite::Test, |py| {
947            let channel = Bound::new(py, LoopWake::try_new().unwrap()).unwrap();
948            let notifier = Notifier {
949                tx: channel.get().tx.clone(),
950                waker: Arc::clone(&channel.get().waker),
951                token: 0,
952            };
953            let waker = Arc::clone(&notifier.waker);
954            drop(channel);
955
956            notifier.notify();
957            assert!(
958                !waker.wake().unwrap(),
959                "a write after the read end closed should report EPIPE"
960            );
961        });
962    }
963
964    // HDL-19: closing a loop frees it and its channel, even with an await
965    // pending.
966    #[test]
967    fn channel_does_not_retain_loop() {
968        ensure_python();
969        monarch_with_gil_blocking(GilSite::Test, |py| {
970            let helper = helper(py);
971            let event_loop = new_loop(py);
972            let (tx, handle) = pending_handle(py);
973            let task = helper
974                .call_method1("await_in_task", (&event_loop, &handle))
975                .unwrap();
976            let channel = LoopWake::for_loop(py, &event_loop).unwrap().unwrap();
977            assert_eq!(
978                channel.get().entry_count(),
979                1,
980                "the awaiting task should be registered"
981            );
982            let loop_ref = PyWeakrefReference::new(&event_loop).unwrap();
983            let channel_ref = PyWeakrefReference::new(channel.as_any()).unwrap();
984
985            event_loop.call_method0("close").unwrap();
986            drop((task, channel, event_loop));
987            collect_garbage(py);
988            assert!(loop_ref.upgrade().is_none(), "the loop should be freed");
989            assert!(
990                channel_ref.upgrade().is_none(),
991                "the loop's channel, with its token, should be freed with it"
992            );
993
994            tx.send(Some(Ok(py.None()))).unwrap();
995        });
996    }
997
998    // HDL-19: closing the loop releases the channel-owned graph, so a
999    // completed, undelivered Handle value that references the loop no longer
1000    // keeps it alive. The value is the observer future itself, or a task that
1001    // awaits that future without holding the Handle.
1002    #[test]
1003    fn closing_loop_releases_values_that_reference_it() {
1004        ensure_python();
1005        for variant in ["observer future", "awaiting task"] {
1006            monarch_with_gil_blocking(GilSite::Test, |py| {
1007                let helper = helper(py);
1008                let event_loop = new_loop(py);
1009                let (tx, handle) = pending_handle(py);
1010                let fixture = match variant {
1011                    "observer future" => "start_observer",
1012                    _ => "await_observer_in_task",
1013                };
1014                let value = helper
1015                    .call_method1(fixture, (&event_loop, &handle))
1016                    .unwrap();
1017                let channel = LoopWake::for_loop(py, &event_loop).unwrap().unwrap();
1018                let waker = Arc::clone(&channel.get().waker);
1019                let loop_ref = PyWeakrefReference::new(&event_loop).unwrap();
1020                let value_ref = PyWeakrefReference::new(&value).unwrap();
1021                let channel_ref = PyWeakrefReference::new(channel.as_any()).unwrap();
1022
1023                // The loop never runs again, so the value is never delivered.
1024                tx.send(Some(Ok(value.clone().unbind()))).unwrap();
1025                drop(tx);
1026                assert!(
1027                    readable_within(channel.get(), Duration::from_secs(5)),
1028                    "{variant}: the observer should finish, releasing its receiver"
1029                );
1030
1031                event_loop.call_method0("close").unwrap();
1032                drop((value, handle, channel, event_loop));
1033                collect_garbage(py);
1034                assert!(
1035                    loop_ref.upgrade().is_none(),
1036                    "{variant}: the loop should be freed"
1037                );
1038                assert!(
1039                    value_ref.upgrade().is_none(),
1040                    "{variant}: the Handle value should be freed"
1041                );
1042                assert!(
1043                    channel_ref.upgrade().is_none(),
1044                    "{variant}: the channel, with its entry, should be freed"
1045                );
1046                assert!(
1047                    !waker.wake().unwrap(),
1048                    "{variant}: the pipe's read end should be closed"
1049                );
1050            });
1051        }
1052    }
1053
1054    // HDL-7: a future whose `done()` raises is reported to its loop, and the
1055    // other token in the same drain is still delivered.
1056    #[test]
1057    fn done_failure_is_reported_without_collateral_damage() {
1058        ensure_python();
1059        monarch_with_gil_blocking(GilSite::Test, |py| {
1060            let helper = helper(py);
1061            let event_loop = new_loop(py);
1062            let errors = helper
1063                .call_method1("capture_errors", (&event_loop,))
1064                .unwrap();
1065            let channel = Bound::new(py, LoopWake::try_new().unwrap()).unwrap();
1066            let broken = helper
1067                .getattr("BrokenDoneFuture")
1068                .unwrap()
1069                .call((), Some(&[("loop", &event_loop)].into_py_dict(py).unwrap()))
1070                .unwrap();
1071            let healthy = event_loop.call_method0("create_future").unwrap();
1072            for (fut, value) in [(&broken, 1i64), (&healthy, 2)] {
1073                LoopWake::register(&channel, fut, ready_handle(py, value))
1074                    .unwrap()
1075                    .notify();
1076            }
1077
1078            channel.get().on_readable(py);
1079            assert!(is_done(&healthy), "the healthy token should be delivered");
1080            let base_done: bool = helper
1081                .call_method1("base_done", (&broken,))
1082                .unwrap()
1083                .extract()
1084                .unwrap();
1085            assert!(!base_done, "the broken future should be left pending");
1086            assert_eq!(
1087                channel.get().entry_count(),
1088                0,
1089                "both entries should be released"
1090            );
1091            assert_eq!(
1092                errors.len().unwrap(),
1093                1,
1094                "one failure should be reported: {errors}"
1095            );
1096            let context = errors.get_item(0).unwrap();
1097            let message: String = context.get_item("message").unwrap().extract().unwrap();
1098            assert!(
1099                message.contains("done() failed"),
1100                "unexpected message: {message}"
1101            );
1102            assert!(
1103                context
1104                    .get_item("exception")
1105                    .unwrap()
1106                    .is_instance_of::<PyRuntimeError>(),
1107                "the report should carry done()'s exception"
1108            );
1109            event_loop.call_method0("close").unwrap();
1110        });
1111    }
1112
1113    // HDL-7: completing after a cancelled observer raises nothing into the loop.
1114    #[test]
1115    fn cancelled_observer_then_completion_is_silent() {
1116        ensure_python();
1117        monarch_with_gil_blocking(GilSite::Test, |py| {
1118            let helper = helper(py);
1119            let event_loop = new_loop(py);
1120            let (tx, handle) = pending_handle(py);
1121            let errors = helper
1122                .call_method1("start_and_cancel", (&event_loop, &handle))
1123                .unwrap();
1124
1125            tx.send(Some(Ok(7i64.into_py_any(py).unwrap()))).unwrap();
1126            let value: i64 = helper
1127                .call_method1("observe_after_cancel", (&event_loop, &handle))
1128                .unwrap()
1129                .extract()
1130                .unwrap();
1131            assert_eq!(value, 7, "a later observer should resolve");
1132            assert_eq!(
1133                errors.len().unwrap(),
1134                0,
1135                "the reader should raise nothing into the loop: {errors}"
1136            );
1137            event_loop.call_method0("close").unwrap();
1138        });
1139    }
1140
1141    // HDL-18: a token pushed at either drain boundary is delivered.
1142    #[test]
1143    fn token_pushed_between_drains_is_delivered() {
1144        ensure_python();
1145        monarch_with_gil_blocking(GilSite::Test, |py| {
1146            let event_loop = new_loop(py);
1147            let channel = Bound::new(py, LoopWake::try_new().unwrap()).unwrap();
1148            let register_ready = |value: i64| {
1149                let fut = event_loop.call_method0("create_future").unwrap();
1150                let notifier = LoopWake::register(&channel, &fut, ready_handle(py, value)).unwrap();
1151                (fut, notifier)
1152            };
1153
1154            let queued: Vec<_> = (0..8)
1155                .map(|value| {
1156                    let (fut, notifier) = register_ready(value);
1157                    notifier.notify();
1158                    fut
1159                })
1160                .collect();
1161            let (between, notifier) = register_ready(100);
1162            channel.get().drain_hooks.lock().unwrap().after_pipe_drain =
1163                Some(Box::new(move |_| notifier.notify()));
1164            channel.get().on_readable(py);
1165            assert!(
1166                queued.iter().all(is_done),
1167                "queued tokens should be delivered"
1168            );
1169            assert!(
1170                is_done(&between),
1171                "a token pushed between the drains should be delivered in the same callback"
1172            );
1173
1174            let (after, notifier) = register_ready(200);
1175            channel.get().drain_hooks.lock().unwrap().after_queue_drain =
1176                Some(Box::new(move |_| notifier.notify()));
1177            channel.get().on_readable(py);
1178            assert!(
1179                !is_done(&after),
1180                "a token pushed after the queue drain should wait for the next callback"
1181            );
1182            assert!(
1183                readable_within(channel.get(), Duration::ZERO),
1184                "its pipe byte should still be pending"
1185            );
1186            channel.get().on_readable(py);
1187            assert!(is_done(&after), "the pending byte should deliver it");
1188            event_loop.call_method0("close").unwrap();
1189        });
1190    }
1191
1192    // HDL-18: `EINTR` is retried, and any other result returns at once.
1193    #[test]
1194    fn retry_eintr_repeats_until_a_non_eintr_result() {
1195        let mut calls = 0;
1196        let result = retry_eintr(|| {
1197            calls += 1;
1198            if calls < 3 { Err(Errno::EINTR) } else { Ok(1) }
1199        });
1200        assert_eq!(
1201            (result, calls),
1202            (Ok(1), 3),
1203            "two EINTRs should be retried before the success"
1204        );
1205
1206        let mut calls = 0;
1207        let result: nix::Result<i32> = retry_eintr(|| {
1208            calls += 1;
1209            Err(Errno::EAGAIN)
1210        });
1211        assert_eq!(
1212            (result, calls),
1213            (Err(Errno::EAGAIN), 1),
1214            "EAGAIN should return without a retry"
1215        );
1216    }
1217
1218    // HDL-21: a loop without `add_reader` falls back and is not retried.
1219    #[test]
1220    fn loop_without_add_reader_falls_back() {
1221        ensure_python();
1222        monarch_with_gil_blocking(GilSite::Test, |py| {
1223            let helper = helper(py);
1224            let event_loop = helper.getattr("NoReaderLoop").unwrap().call0().unwrap();
1225            for value in [1i64, 2] {
1226                // Pending, so the observation reaches wake-channel installation.
1227                let (tx, handle) = pending_handle(py);
1228                let fut = helper
1229                    .call_method1("start_observer", (&event_loop, &handle))
1230                    .unwrap();
1231                tx.send(Some(Ok(value.into_py_any(py).unwrap()))).unwrap();
1232                let got: i64 = helper
1233                    .call_method1("run_until_done", (&event_loop, &fut))
1234                    .unwrap()
1235                    .extract()
1236                    .unwrap();
1237                assert_eq!(got, value, "the fallback should resolve the await");
1238            }
1239            let calls: i64 = event_loop
1240                .getattr("add_reader_calls")
1241                .unwrap()
1242                .extract()
1243                .unwrap();
1244            assert_eq!(
1245                calls, 1,
1246                "a loop cached as unsupported should not be retried"
1247            );
1248            assert!(LoopWake::for_loop(py, &event_loop).unwrap().is_none());
1249            event_loop.call_method0("close").unwrap();
1250        });
1251    }
1252
1253    // HDL-21: a failed `add_reader` is not cached, and a later await installs.
1254    #[test]
1255    fn failed_add_reader_does_not_poison_registry() {
1256        ensure_python();
1257        monarch_with_gil_blocking(GilSite::Test, |py| {
1258            let helper = helper(py);
1259            let event_loop = helper.getattr("FlakyReaderLoop").unwrap().call0().unwrap();
1260            // Pending, with the producer held, so each observation reaches
1261            // wake-channel installation.
1262            let (_first_tx, first) = pending_handle(py);
1263            let err = helper
1264                .call_method1("try_observe", (&event_loop, &first))
1265                .unwrap();
1266            assert!(
1267                err.is_instance_of::<PyOSError>(),
1268                "the first as_asyncio should raise the add_reader failure: {err}"
1269            );
1270
1271            let (tx, second) = pending_handle(py);
1272            let fut = helper
1273                .call_method1("start_observer", (&event_loop, &second))
1274                .unwrap();
1275            tx.send(Some(Ok(2i64.into_py_any(py).unwrap()))).unwrap();
1276            let got: i64 = helper
1277                .call_method1("run_until_done", (&event_loop, &fut))
1278                .unwrap()
1279                .extract()
1280                .unwrap();
1281            assert_eq!(got, 2, "the second await should resolve through the wake");
1282            let calls: i64 = event_loop
1283                .getattr("add_reader_calls")
1284                .unwrap()
1285                .extract()
1286                .unwrap();
1287            assert_eq!(calls, 2, "the failed install should have been retried");
1288            assert!(LoopWake::for_loop(py, &event_loop).unwrap().is_some());
1289            event_loop.call_method0("close").unwrap();
1290        });
1291    }
1292
1293    // HDL-19: a failed registration leaves no entry holding its Handle.
1294    #[test]
1295    fn failed_registration_leaves_no_entry() {
1296        ensure_python();
1297        monarch_with_gil_blocking(GilSite::Test, |py| {
1298            let helper = helper(py);
1299            let event_loop = helper.getattr("RejectingLoop").unwrap().call0().unwrap();
1300            // Pending, with the producer held, so the observation reaches
1301            // `LoopWake::register`.
1302            let (_tx, handle) = pending_handle(py);
1303            let err = helper
1304                .call_method1("try_observe", (&event_loop, &handle))
1305                .unwrap();
1306            assert!(
1307                err.is_instance_of::<PyRuntimeError>(),
1308                "as_asyncio should raise the add_done_callback failure: {err}"
1309            );
1310
1311            let channel = LoopWake::for_loop(py, &event_loop)
1312                .unwrap()
1313                .expect("the channel is installed before registration");
1314            assert_eq!(
1315                channel.get().entry_count(),
1316                0,
1317                "the failed registration should remove its entry"
1318            );
1319            event_loop.call_method0("close").unwrap();
1320        });
1321    }
1322
1323    // A future that cannot be weakly referenced uses the channel.
1324    #[test]
1325    fn future_without_weakref_uses_the_channel() {
1326        ensure_python();
1327        monarch_with_gil_blocking(GilSite::Test, |py| {
1328            let helper = helper(py);
1329            let event_loop = helper.getattr("NoWeakLoop").unwrap().call0().unwrap();
1330            let (tx, handle) = pending_handle(py);
1331            let fut = helper
1332                .call_method1("start_observer", (&event_loop, handle))
1333                .unwrap();
1334            let channel = LoopWake::for_loop(py, &event_loop)
1335                .unwrap()
1336                .expect("the loop supports the channel");
1337            assert_eq!(
1338                channel.get().entry_count(),
1339                1,
1340                "the await should be registered with the channel"
1341            );
1342            tx.send(Some(Ok(7i64.into_py_any(py).unwrap()))).unwrap();
1343            let got: i64 = helper
1344                .call_method1("run_until_done", (&event_loop, &fut))
1345                .unwrap()
1346                .extract()
1347                .unwrap();
1348            assert_eq!(got, 7, "the channel should resolve the await");
1349            assert_eq!(
1350                channel.get().entry_count(),
1351                0,
1352                "the delivered await should leave no entry"
1353            );
1354            event_loop.call_method0("close").unwrap();
1355        });
1356    }
1357
1358    // A `TypeError` from the future's own `add_done_callback` propagates.
1359    #[test]
1360    fn add_done_callback_type_error_propagates() {
1361        ensure_python();
1362        monarch_with_gil_blocking(GilSite::Test, |py| {
1363            let helper = helper(py);
1364            let event_loop = helper
1365                .getattr("TypeRejectingLoop")
1366                .unwrap()
1367                .call0()
1368                .unwrap();
1369            // Pending, with the producer held, so the observation reaches
1370            // `LoopWake::register`.
1371            let (_tx, handle) = pending_handle(py);
1372            let err = helper
1373                .call_method1("try_observe", (&event_loop, &handle))
1374                .unwrap();
1375            assert!(
1376                err.is_instance_of::<PyTypeError>(),
1377                "as_asyncio should raise the add_done_callback TypeError, not fall back: {err}"
1378            );
1379            let channel = LoopWake::for_loop(py, &event_loop)
1380                .unwrap()
1381                .expect("the channel is installed before registration");
1382            assert_eq!(
1383                channel.get().entry_count(),
1384                0,
1385                "the failed registration should remove its entry"
1386            );
1387            event_loop.call_method0("close").unwrap();
1388        });
1389    }
1390}