1use 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
76fn 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
101fn 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#[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
119struct Entry {
122 fut: Py<PyAny>,
128 handle: Py<PyHandle>,
131}
132
133#[pyclass(frozen, weakref)]
157pub(crate) struct LoopWake {
158 tx: mpsc::Sender<u64>,
160 rx: Mutex<mpsc::Receiver<u64>>,
164 waker: Arc<Waker>,
168 read_fd: OwnedFd,
172 next_token: AtomicU64,
176 entries: Mutex<HashMap<u64, Entry>>,
179 #[cfg(test)]
181 drain_hooks: Mutex<DrainHooks>,
182}
183
184impl LoopWake {
185 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 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 let current = match registry.call_method1("get", (event_loop, &absent)) {
217 Ok(current) => current,
218 Err(err) if err.is_instance_of::<PyTypeError>(py) => return Ok(None),
221 Err(err) => return Err(err),
222 };
223 if current.is(&absent) {
225 return Self::install(py, ®istry, event_loop);
226 }
227 if current.is_none() {
229 return Ok(None);
230 }
231 match current.cast_into::<PyWeakrefReference>()?.upgrade() {
234 Some(channel) => Ok(Some(channel.cast_into::<LoopWake>()?)),
235 None => Self::install(py, ®istry, event_loop),
236 }
237 }
238
239 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 registry.set_item(event_loop, PyWeakrefReference::new(channel.as_any())?)?;
253 let reader = channel.getattr("on_readable")?;
256 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 Err(err) if err.is_instance_of::<PyNotImplementedError>(py) => {
265 registry.set_item(event_loop, py.None())?;
266 Ok(None)
267 }
268 Err(err) => {
271 registry.del_item(event_loop)?;
272 Err(err)
273 }
274 }
275 }
276
277 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 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 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 if let Err(err) = fut.call_method1("add_done_callback", (&reaper,)) {
317 drop(this.take(token));
318 return Err(err);
319 }
320 Ok(Notifier {
323 tx: this.tx.clone(),
324 waker: Arc::clone(&this.waker),
325 token,
326 })
327 }
328
329 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 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 fn drain_pipe(&self) {
358 let mut buf = [0u8; 64];
360 loop {
361 match retry_eintr(|| read(&self.read_fd, &mut buf)) {
362 Ok(0) | Err(Errno::EAGAIN) => return,
366 Ok(_) => {}
368 Err(err) => {
371 tracing::error!(error = %err, "handle wake: pipe read failed");
372 return;
373 }
374 }
375 }
376 }
377
378 fn deliver(&self, py: Python<'_>, token: u64) {
386 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 Ok(true) => return,
397 Err(err) => {
398 report(fut, "handle wake: done() failed; the await is dropped", err);
399 return;
400 }
401 }
402 let published = match entry.handle.bind(py).borrow().poll_ready() {
404 Ok(Some(value)) => complete_asyncio_future(fut, false, value),
405 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
425fn 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 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 fn on_readable(&self, py: Python<'_>) {
491 self.drain_pipe();
492 #[cfg(test)]
493 self.run_hook(|hooks| hooks.after_pipe_drain.take());
494 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#[pyclass(frozen)]
513struct TokenReaper {
514 channel: Py<PyWeakrefReference>,
519 token: u64,
521}
522
523#[pymethods]
524impl TokenReaper {
525 #[pyo3(signature = (*_args))]
528 fn __call__(&self, py: Python<'_>, _args: &Bound<'_, PyTuple>) {
529 if let Some(channel) = self.channel.bind(py).upgrade()
531 && let Ok(channel) = channel.cast::<LoopWake>()
532 {
533 drop(channel.get().take(self.token));
535 }
536 }
537}
538
539pub(crate) struct Notifier {
544 tx: mpsc::Sender<u64>,
546 waker: Arc<Waker>,
549 token: u64,
551}
552
553impl Notifier {
554 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 #[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 #[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 #[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 #[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 #[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(¬ifier.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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 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 #[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 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 #[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 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 #[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 #[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 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}