1use std::future::Future;
93
94use monarch_types::py_global;
95use pyo3::exceptions::PyRuntimeError;
96use pyo3::exceptions::PyStopIteration;
97use pyo3::exceptions::PyTimeoutError;
98use pyo3::exceptions::PyUserWarning;
99use pyo3::exceptions::PyValueError;
100use pyo3::prelude::*;
101use pyo3::sync::PyOnceLock;
102use pyo3::types::PyCFunction;
103use pyo3::types::PyType;
104use tokio::sync::watch;
105use tokio::task::AbortHandle;
106
107use crate::pytokio::is_tokio_thread;
108use crate::pytokio::to_py_error;
109use crate::runtime::GilSite;
110use crate::runtime::get_tokio_runtime;
111use crate::runtime::monarch_with_gil;
112use crate::runtime::monarch_with_gil_blocking;
113use crate::runtime::signal_safe_block_on;
114
115py_global!(invalid_state_error, "asyncio", "InvalidStateError");
119
120pyo3::create_exception!(
121 pytokio,
122 WouldBlockRuntime,
123 pyo3::exceptions::PyRuntimeError,
124 "raised when Handle.get() is called from a Tokio runtime context"
125);
126
127pub(crate) struct HandleCore {
133 rx: watch::Receiver<Option<PyResult<Py<PyAny>>>>,
135
136 abort_on_drop: Option<AbortHandle>,
141
142 traceback: Option<Py<PyAny>>,
144}
145
146impl HandleCore {
147 pub(crate) fn new(
149 rx: watch::Receiver<Option<PyResult<Py<PyAny>>>>,
150 abort_on_drop: Option<AbortHandle>,
151 traceback: Option<Py<PyAny>>,
152 ) -> Self {
153 Self {
154 rx,
155 abort_on_drop,
156 traceback,
157 }
158 }
159
160 pub(crate) fn from_value(value: Py<PyAny>) -> PyResult<Self> {
165 let (tx, rx) = watch::channel(None);
166 tx.send(Some(Ok(value))).map_err(to_py_error)?;
167 Ok(Self {
168 rx,
169 abort_on_drop: None,
170 traceback: None,
171 })
172 }
173
174 pub(crate) fn poll(&self) -> PyResult<Option<Py<PyAny>>> {
179 if self.rx.borrow().is_none() {
184 return Ok(None);
185 }
186 monarch_with_gil_blocking(GilSite::Convert, |py| {
187 match self
188 .rx
189 .borrow()
190 .as_ref()
191 .expect("HDL-1: value is Some after the is_none check")
192 {
193 Ok(v) => Ok(Some(v.clone_ref(py))),
194 Err(err) => Err(err.clone_ref(py)),
195 }
196 })
197 }
198
199 pub(crate) fn wait_future(&self) -> impl Future<Output = PyResult<Py<PyAny>>> + Send + 'static {
206 let ready = self.wait_ready();
209 async move {
210 let rx = ready.await.map_err(to_py_error)?;
212 monarch_with_gil(GilSite::Convert, |py| {
213 match rx
214 .borrow()
215 .as_ref()
216 .expect("HDL-1: value is Some after wait_ready")
217 {
218 Ok(v) => Ok(v.clone_ref(py)),
219 Err(err) => Err(err.clone_ref(py)),
220 }
221 })
222 .await
223 }
224 }
225
226 pub(crate) fn wait_ready(
234 &self,
235 ) -> impl Future<
236 Output = Result<watch::Receiver<Option<PyResult<Py<PyAny>>>>, watch::error::RecvError>,
237 > + Send
238 + 'static {
239 let mut rx = self.rx.clone();
243 async move {
244 while rx.borrow().is_none() {
249 rx.changed().await?;
250 }
251 Ok(rx)
252 }
253 }
254
255 pub(crate) fn traceback_clone(&self) -> Option<Py<PyAny>> {
257 self.traceback
258 .as_ref()
259 .map(|t| monarch_with_gil_blocking(GilSite::Traceback, |py| t.clone_ref(py)))
260 }
261}
262
263impl Drop for HandleCore {
270 fn drop(&mut self) {
271 if let Some(abort_handle) = self.abort_on_drop.as_ref() {
272 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
276 abort_handle.abort();
277 }));
278 }
279 }
280}
281
282#[pyclass(
289 name = "Handle",
290 module = "monarch._rust_bindings.monarch_hyperactor.pytokio"
291)]
292pub struct PyHandle {
293 core: HandleCore,
294}
295
296impl PyHandle {
297 pub(crate) fn from_core(core: HandleCore) -> Self {
303 Self { core }
304 }
305}
306
307#[cfg(test)]
308impl PyHandle {
309 pub(crate) fn from_value(value: Py<PyAny>) -> PyResult<Self> {
314 Ok(Self {
315 core: HandleCore::from_value(value)?,
316 })
317 }
318}
319
320#[pymethods]
321impl PyHandle {
322 #[pyo3(signature = (timeout = None))]
339 fn get(slf: PyRef<'_, Self>, py: Python<'_>, timeout: Option<f64>) -> PyResult<Py<PyAny>> {
340 if is_tokio_thread() {
345 return Err(WouldBlockRuntime::new_err(
346 "get() cannot be called from a Tokio runtime context; use poll() or as_asyncio()",
347 ));
348 }
349
350 let duration = timeout
355 .map(|seconds| {
356 std::time::Duration::try_from_secs_f64(seconds)
357 .map_err(|e| PyValueError::new_err(format!("invalid timeout {seconds}: {e}")))
358 })
359 .transpose()?;
360
361 if pyo3_async_runtimes::get_running_loop(py).is_ok() {
367 let category = py.get_type::<PyUserWarning>();
368 PyErr::warn(
369 py,
370 &category,
371 c"Blocking get() was called from a running asyncio event loop. It is a synchronous, blocking call that can freeze the loop and deadlock; use as_asyncio() (or await) instead.",
372 1,
373 )?;
374 }
375
376 if let Some(value) = slf.core.poll()? {
378 return Ok(value);
379 }
380
381 let wait = slf.core.wait_future();
382 drop(slf);
386
387 match duration {
391 Some(duration) => signal_safe_block_on(py, async move {
392 tokio::time::timeout(duration, wait)
393 .await
394 .map_err(|_| PyTimeoutError::new_err(()))?
395 })?,
396 None => signal_safe_block_on(py, wait)?,
397 }
398 }
399
400 fn poll(&self) -> PyResult<Option<Py<PyAny>>> {
405 self.core.poll()
406 }
407
408 fn as_asyncio<'py>(slf: PyRef<'_, Self>, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
425 let event_loop = pyo3_async_runtimes::get_running_loop(py)?;
427 let fut = event_loop.call_method0("create_future")?;
428
429 let loop_handle = event_loop.clone().unbind();
430 let fut_handle = fut.clone().unbind();
431 let wait = slf.core.wait_ready();
432
433 get_tokio_runtime().spawn(async move {
434 let recv = wait.await;
435 let scheduled = monarch_with_gil(GilSite::Convert, move |py| {
436 let result: PyResult<Py<PyAny>> = match recv {
441 Ok(rx) => match rx
442 .borrow()
443 .as_ref()
444 .expect("HDL-1: value is Some after wait_ready")
445 {
446 Ok(v) => Ok(v.clone_ref(py)),
447 Err(err) => Err(err.clone_ref(py)),
448 },
449 Err(e) => Err(to_py_error(e)),
451 };
452 schedule_completion(py, loop_handle.bind(py), fut_handle.bind(py), result)
453 })
454 .await;
455 if let Err(err) = scheduled {
456 tracing::warn!("as_asyncio: failed to schedule completion on the loop: {err}");
457 }
458 });
459
460 Ok(fut)
461 }
462
463 fn __await__<'py>(slf: PyRef<'_, Self>, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
465 Self::as_asyncio(slf, py)?.call_method0("__await__")
466 }
467
468 #[classmethod]
470 fn __class_getitem__(cls: &Bound<'_, PyType>, _arg: Py<PyAny>) -> Py<PyAny> {
471 cls.clone().unbind().into()
472 }
473}
474
475fn cached_completer(py: Python<'_>) -> PyResult<Bound<'_, PyCFunction>> {
480 static COMPLETER: PyOnceLock<Py<PyCFunction>> = PyOnceLock::new();
481 COMPLETER
482 .get_or_try_init(py, || {
483 Ok(wrap_pyfunction!(complete_asyncio_future, py)?.unbind())
484 })
485 .map(|f| f.bind(py).clone())
486}
487
488fn schedule_completion(
496 py: Python<'_>,
497 event_loop: &Bound<'_, PyAny>,
498 fut: &Bound<'_, PyAny>,
499 result: PyResult<Py<PyAny>>,
500) -> PyResult<()> {
501 let completer = cached_completer(py)?;
502 let (is_exc, value) = match result {
503 Ok(v) => (false, v),
504 Err(e) => (true, e.into_value(py).into_any()),
505 };
506 event_loop.call_method1("call_soon_threadsafe", (completer, fut, is_exc, value))?;
507 Ok(())
508}
509
510#[pyfunction]
515fn complete_asyncio_future(fut: &Bound<'_, PyAny>, is_exc: bool, value: Py<PyAny>) -> PyResult<()> {
516 if fut.call_method0("cancelled")?.is_truthy()? {
517 return Ok(());
518 }
519 let py = fut.py();
520 let (method, value) = if is_exc {
521 let value = if value.bind(py).is_instance_of::<PyStopIteration>() {
527 PyRuntimeError::new_err("coroutine raised StopIteration")
528 .into_value(py)
529 .into_any()
530 } else {
531 value
532 };
533 ("set_exception", value)
534 } else {
535 ("set_result", value)
536 };
537 match fut.call_method1(method, (value,)) {
541 Ok(_) => Ok(()),
542 Err(e) => {
543 if e.is_instance(py, &invalid_state_error(py)) {
544 Ok(())
545 } else {
546 Err(e)
547 }
548 }
549 }
550}
551
552#[cfg(test)]
553mod tests {
554 use pyo3::IntoPyObjectExt;
555 use pyo3::exceptions::PyRuntimeError;
556 use pyo3::exceptions::PyValueError;
557 use pyo3::types::PyModule;
558 use pyo3::types::PyTuple;
559
560 use super::*;
561 use crate::pytokio::ensure_python;
562
563 fn pending_handle() -> (watch::Sender<Option<PyResult<Py<PyAny>>>>, PyHandle) {
566 let (tx, rx) = watch::channel(None);
567 let handle = PyHandle {
568 core: HandleCore::new(rx, None, None),
569 };
570 (tx, handle)
571 }
572
573 fn loop_helper(py: Python<'_>) -> Bound<'_, PyModule> {
575 let code = cr#"
576import asyncio
577import warnings
578
579async def _await(h):
580 return await h
581
582def run_await(h):
583 loop = asyncio.new_event_loop()
584 try:
585 return loop.run_until_complete(_await(h))
586 finally:
587 loop.close()
588
589async def _get_on_loop(h):
590 with warnings.catch_warnings(record=True) as caught:
591 warnings.simplefilter("always")
592 value = h.get()
593 return (value, any(issubclass(w.category, UserWarning) for w in caught))
594
595def run_get_on_loop(h):
596 loop = asyncio.new_event_loop()
597 try:
598 return loop.run_until_complete(_get_on_loop(h))
599 finally:
600 loop.close()
601
602async def _get_on_loop_strict(h):
603 # get() under a warnings-as-errors filter: a ready value must still return
604 # (no-warn fast path), not raise the UserWarning as an error.
605 with warnings.catch_warnings():
606 warnings.simplefilter("error")
607 return h.get()
608
609def run_get_on_loop_strict(h):
610 loop = asyncio.new_event_loop()
611 try:
612 return loop.run_until_complete(_get_on_loop_strict(h))
613 finally:
614 loop.close()
615
616async def _cancel(h):
617 f = h.as_asyncio()
618 f.cancel()
619 await asyncio.sleep(0.05)
620 return (f.cancelled(), h.poll())
621
622def run_cancel(h):
623 loop = asyncio.new_event_loop()
624 try:
625 return loop.run_until_complete(_cancel(h))
626 finally:
627 loop.close()
628
629async def _cancel_then_await(h):
630 # cancel the first observer, then observe again while a live producer runs
631 f1 = h.as_asyncio()
632 f1.cancel()
633 result = await h.as_asyncio()
634 # let the cancelled observer's completion callback drain (it no-ops)
635 await asyncio.sleep(0.05)
636 return (f1.cancelled(), result, h.poll())
637
638def run_cancel_then_await(h):
639 loop = asyncio.new_event_loop()
640 try:
641 return loop.run_until_complete(_cancel_then_await(h))
642 finally:
643 loop.close()
644
645async def _two(h):
646 a = h.as_asyncio()
647 b = h.as_asyncio()
648 # gather returns a list; the Rust side downcasts a tuple
649 return tuple(await asyncio.gather(a, b))
650
651def run_two(h):
652 loop = asyncio.new_event_loop()
653 try:
654 return loop.run_until_complete(_two(h))
655 finally:
656 loop.close()
657"#;
658 PyModule::from_code(py, code, c"handle_test_helper.py", c"handle_test_helper").unwrap()
659 }
660
661 #[test]
664 fn get_returns_ready_value() {
665 ensure_python();
666 let got = monarch_with_gil_blocking(GilSite::Test, |py| -> i64 {
667 let value = 42i64.into_py_any(py).unwrap();
668 let handle = PyHandle::from_value(value).unwrap();
669 let r = Py::new(py, handle).unwrap();
670 let out = PyHandle::get(r.borrow(py), py, None).unwrap();
671 out.extract::<i64>(py).unwrap()
672 });
673 assert_eq!(got, 42, "get() should return the resolved value");
674 }
675
676 #[test]
679 fn get_blocks_then_returns() {
680 ensure_python();
681 let value = monarch_with_gil_blocking(GilSite::Test, |py| 7i64.into_py_any(py).unwrap());
682 let (tx, handle) = pending_handle();
683 get_tokio_runtime().spawn(async move {
684 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
685 let _ = tx.send(Some(Ok(value)));
686 });
687 let got = monarch_with_gil_blocking(GilSite::Test, |py| -> i64 {
688 let r = Py::new(py, handle).unwrap();
689 let out = PyHandle::get(r.borrow(py), py, None).unwrap();
690 out.extract::<i64>(py).unwrap()
691 });
692 assert_eq!(got, 7, "get() should block then return the produced value");
693 }
694
695 #[test]
698 fn poll_transitions_pending_to_ready() {
699 ensure_python();
700 let (tx, rx) = watch::channel(None);
701 let core = HandleCore::new(rx, None, None);
702 assert!(
703 core.poll().unwrap().is_none(),
704 "poll() should be None while pending"
705 );
706 let value = monarch_with_gil_blocking(GilSite::Test, |py| 1i64.into_py_any(py).unwrap());
707 tx.send(Some(Ok(value))).unwrap();
708 let got = core.poll().unwrap();
709 let extracted =
710 monarch_with_gil_blocking(GilSite::Test, |py| got.unwrap().extract::<i64>(py).unwrap());
711 assert_eq!(extracted, 1, "poll() should observe the produced value");
712 }
713
714 #[test]
718 fn repeated_observation_after_poll() {
719 ensure_python();
720 monarch_with_gil_blocking(GilSite::Test, |py| {
721 let value = 5i64.into_py_any(py).unwrap();
722 let handle = PyHandle::from_value(value).unwrap();
723 let r = Py::new(py, handle).unwrap();
724
725 let first = r.borrow(py).poll().unwrap().unwrap();
726 assert_eq!(first.extract::<i64>(py).unwrap(), 5);
727
728 let via_get = PyHandle::get(r.borrow(py), py, None).unwrap();
729 assert_eq!(via_get.extract::<i64>(py).unwrap(), 5);
730
731 let again = r.borrow(py).poll().unwrap().unwrap();
732 assert_eq!(again.extract::<i64>(py).unwrap(), 5);
733 });
734 }
735
736 #[test]
740 fn multiple_observers_resolve() {
741 ensure_python();
742 let (tx, rx) = watch::channel(None);
743 let core = HandleCore::new(rx, None, None);
744 let f1 = core.wait_future();
745 let f2 = core.wait_future();
746 let value = monarch_with_gil_blocking(GilSite::Test, |py| 3i64.into_py_any(py).unwrap());
747 tx.send(Some(Ok(value))).unwrap();
748 let (r1, r2) = get_tokio_runtime().block_on(async move { tokio::join!(f1, f2) });
749 monarch_with_gil_blocking(GilSite::Test, |py| {
750 assert_eq!(r1.unwrap().extract::<i64>(py).unwrap(), 3);
751 assert_eq!(r2.unwrap().extract::<i64>(py).unwrap(), 3);
752 });
753 }
754
755 #[test]
758 fn get_pending_in_tokio_raises_would_block() {
759 ensure_python();
760 let (_tx, handle) = pending_handle();
761 get_tokio_runtime().block_on(async {
762 monarch_with_gil(GilSite::Test, |py| {
763 let r = Py::new(py, handle).unwrap();
764 let err = PyHandle::get(r.borrow(py), py, None).unwrap_err();
765 assert!(
766 err.is_instance_of::<WouldBlockRuntime>(py),
767 "pending get() in a tokio context should raise WouldBlockRuntime"
768 );
769 })
770 .await
771 });
772 }
773
774 #[test]
778 fn get_ready_in_tokio_also_raises() {
779 ensure_python();
780 get_tokio_runtime().block_on(async {
781 monarch_with_gil(GilSite::Test, |py| {
782 let value = 9i64.into_py_any(py).unwrap();
783 let handle = PyHandle::from_value(value).unwrap();
784 let r = Py::new(py, handle).unwrap();
785 let err = PyHandle::get(r.borrow(py), py, None).unwrap_err();
786 assert!(
787 err.is_instance_of::<WouldBlockRuntime>(py),
788 "get() in a tokio context must raise even for a ready value"
789 );
790 })
791 .await
792 });
793 }
794
795 #[test]
799 fn get_timeout_raises_and_leaves_pending() {
800 ensure_python();
801 let (tx, handle) = pending_handle();
802 let r = monarch_with_gil_blocking(GilSite::Test, |py| Py::new(py, handle).unwrap());
803
804 let is_timeout = monarch_with_gil_blocking(GilSite::Test, |py| {
805 let err = PyHandle::get(r.borrow(py), py, Some(0.05)).unwrap_err();
806 err.is_instance_of::<PyTimeoutError>(py)
807 });
808 assert!(is_timeout, "get(timeout) should raise TimeoutError");
809
810 let still_pending =
811 monarch_with_gil_blocking(GilSite::Test, |py| r.borrow(py).poll().unwrap().is_none());
812 assert!(
813 still_pending,
814 "a timed-out get() must not resolve the handle"
815 );
816
817 let value = monarch_with_gil_blocking(GilSite::Test, |py| 4i64.into_py_any(py).unwrap());
818 tx.send(Some(Ok(value))).unwrap();
819 let observed = monarch_with_gil_blocking(GilSite::Test, |py| {
820 r.borrow(py)
821 .poll()
822 .unwrap()
823 .unwrap()
824 .extract::<i64>(py)
825 .unwrap()
826 });
827 assert_eq!(
828 observed, 4,
829 "completion is still observable after a timeout"
830 );
831 }
832
833 #[test]
837 fn as_asyncio_off_loop_raises_runtime_error() {
838 ensure_python();
839 monarch_with_gil_blocking(GilSite::Test, |py| {
840 let handle = PyHandle::from_value(py.None()).unwrap();
841 let r = Py::new(py, handle).unwrap();
842 let err = PyHandle::as_asyncio(r.borrow(py), py).unwrap_err();
843 assert!(
844 err.is_instance_of::<PyRuntimeError>(py),
845 "off a loop as_asyncio() should raise RuntimeError"
846 );
847 assert!(
848 !err.is_instance_of::<WouldBlockRuntime>(py),
849 "the off-loop error must be the native RuntimeError, not WouldBlockRuntime"
850 );
851 });
852 }
853
854 #[test]
857 fn await_off_loop_raises_runtime_error() {
858 ensure_python();
859 monarch_with_gil_blocking(GilSite::Test, |py| {
860 let handle = PyHandle::from_value(py.None()).unwrap();
861 let r = Py::new(py, handle).unwrap();
862 let err = PyHandle::__await__(r.borrow(py), py).unwrap_err();
863 assert!(
864 err.is_instance_of::<PyRuntimeError>(py),
865 "off a loop __await__ should raise RuntimeError"
866 );
867 assert!(
868 !err.is_instance_of::<WouldBlockRuntime>(py),
869 "__await__ must not raise WouldBlockRuntime"
870 );
871 });
872 }
873
874 #[test]
878 fn await_resolves_ok_on_loop() {
879 ensure_python();
880 let got = monarch_with_gil_blocking(GilSite::Test, |py| -> i64 {
881 let value = 42i64.into_py_any(py).unwrap();
882 let handle = PyHandle::from_value(value).unwrap();
883 let h = Py::new(py, handle).unwrap();
884 let helper = loop_helper(py);
885 helper
886 .getattr("run_await")
887 .unwrap()
888 .call1((h,))
889 .unwrap()
890 .extract::<i64>()
891 .unwrap()
892 });
893 assert_eq!(got, 42, "await should resolve to the value on a real loop");
894 }
895
896 #[test]
899 fn await_resolves_err_on_loop() {
900 ensure_python();
901 let (tx, rx) = watch::channel(None);
902 tx.send(Some(Err(PyValueError::new_err("boom")))).unwrap();
903 let is_value_error = monarch_with_gil_blocking(GilSite::Test, |py| {
904 let handle = PyHandle {
905 core: HandleCore::new(rx, None, None),
906 };
907 let h = Py::new(py, handle).unwrap();
908 let helper = loop_helper(py);
909 let err = helper
910 .getattr("run_await")
911 .unwrap()
912 .call1((h,))
913 .unwrap_err();
914 err.is_instance_of::<PyValueError>(py)
915 });
916 assert!(is_value_error, "await should raise the stored exception");
917 }
918
919 #[test]
923 fn await_resolves_pending_on_loop() {
924 ensure_python();
925 let value = monarch_with_gil_blocking(GilSite::Test, |py| 11i64.into_py_any(py).unwrap());
926 let (tx, handle) = pending_handle();
927 get_tokio_runtime().spawn(async move {
928 tokio::time::sleep(std::time::Duration::from_millis(30)).await;
929 let _ = tx.send(Some(Ok(value)));
930 });
931 let got = monarch_with_gil_blocking(GilSite::Test, |py| -> i64 {
932 let h = Py::new(py, handle).unwrap();
933 let helper = loop_helper(py);
934 helper
935 .getattr("run_await")
936 .unwrap()
937 .call1((h,))
938 .unwrap()
939 .extract::<i64>()
940 .unwrap()
941 });
942 assert_eq!(got, 11, "await should resolve once the producer completes");
943 }
944
945 #[test]
952 fn cancel_asyncio_future_does_not_cancel_handle() {
953 ensure_python();
954 let (cancelled, handle_resolved) =
955 monarch_with_gil_blocking(GilSite::Test, |py| -> (bool, bool) {
956 let value = 42i64.into_py_any(py).unwrap();
957 let handle = PyHandle::from_value(value).unwrap();
958 let h = Py::new(py, handle).unwrap();
959 let helper = loop_helper(py);
960 let res = helper.getattr("run_cancel").unwrap().call1((h,)).unwrap();
961 let tup = res.downcast::<PyTuple>().unwrap();
962 let cancelled = tup.get_item(0).unwrap().extract::<bool>().unwrap();
963 let poll_val = tup.get_item(1).unwrap();
964 (cancelled, !poll_val.is_none())
965 });
966 assert!(cancelled, "the asyncio future should be cancelled");
967 assert!(
968 handle_resolved,
969 "cancelling the future must not cancel the handle"
970 );
971 }
972
973 #[test]
981 fn cancel_asyncio_future_does_not_stop_producer_or_observers() {
982 ensure_python();
983 let value = monarch_with_gil_blocking(GilSite::Test, |py| 42i64.into_py_any(py).unwrap());
984 let (tx, handle) = pending_handle();
985 get_tokio_runtime().spawn(async move {
986 tokio::time::sleep(std::time::Duration::from_millis(30)).await;
987 let _ = tx.send(Some(Ok(value)));
988 });
989 let (cancelled, awaited, polled) =
990 monarch_with_gil_blocking(GilSite::Test, |py| -> (bool, i64, i64) {
991 let h = Py::new(py, handle).unwrap();
992 let helper = loop_helper(py);
993 let res = helper
994 .getattr("run_cancel_then_await")
995 .unwrap()
996 .call1((h,))
997 .unwrap();
998 let tup = res.downcast::<PyTuple>().unwrap();
999 (
1000 tup.get_item(0).unwrap().extract::<bool>().unwrap(),
1001 tup.get_item(1).unwrap().extract::<i64>().unwrap(),
1002 tup.get_item(2).unwrap().extract::<i64>().unwrap(),
1003 )
1004 });
1005 assert!(cancelled, "the cancelled future should stay cancelled");
1006 assert_eq!(
1007 awaited, 42,
1008 "a second observer resolves even after the first future is cancelled"
1009 );
1010 assert_eq!(
1011 polled, 42,
1012 "the handle stays observable after one observer is cancelled"
1013 );
1014 }
1015
1016 #[test]
1019 fn two_asyncio_observers_resolve_on_loop() {
1020 ensure_python();
1021 let (a, b) = monarch_with_gil_blocking(GilSite::Test, |py| -> (i64, i64) {
1022 let value = 8i64.into_py_any(py).unwrap();
1023 let handle = PyHandle::from_value(value).unwrap();
1024 let h = Py::new(py, handle).unwrap();
1025 let helper = loop_helper(py);
1026 let res = helper.getattr("run_two").unwrap().call1((h,)).unwrap();
1027 let tup = res.downcast::<PyTuple>().unwrap();
1028 (
1029 tup.get_item(0).unwrap().extract::<i64>().unwrap(),
1030 tup.get_item(1).unwrap().extract::<i64>().unwrap(),
1031 )
1032 });
1033 assert_eq!((a, b), (8, 8), "both observers should resolve to the value");
1034 }
1035
1036 #[test]
1040 fn schedule_on_closed_loop_errs_not_panics() {
1041 ensure_python();
1042 monarch_with_gil_blocking(GilSite::Test, |py| {
1043 let asyncio = py.import("asyncio").unwrap();
1044 let event_loop = asyncio.call_method0("new_event_loop").unwrap();
1045 let fut = event_loop.call_method0("create_future").unwrap();
1046 event_loop.call_method0("close").unwrap();
1047 let result = schedule_completion(py, &event_loop, &fut, Ok(py.None()));
1048 assert!(
1049 result.is_err(),
1050 "call_soon_threadsafe on a closed loop should error, not panic"
1051 );
1052 });
1053 }
1054
1055 #[test]
1059 fn get_invalid_timeout_raises_value_error() {
1060 ensure_python();
1061 let (_tx, handle) = pending_handle();
1062 let r = monarch_with_gil_blocking(GilSite::Test, |py| Py::new(py, handle).unwrap());
1063 monarch_with_gil_blocking(GilSite::Test, |py| {
1064 for bad in [-1.0, f64::NAN, f64::INFINITY] {
1065 let err = PyHandle::get(r.borrow(py), py, Some(bad)).unwrap_err();
1066 assert!(
1067 err.is_instance_of::<PyValueError>(py),
1068 "get(timeout={bad}) should raise ValueError, not panic"
1069 );
1070 }
1071 });
1072 }
1073
1074 #[test]
1079 fn get_on_asyncio_loop_warns() {
1080 ensure_python();
1081 let (value, warned) = monarch_with_gil_blocking(GilSite::Test, |py| -> (i64, bool) {
1082 let v = 5i64.into_py_any(py).unwrap();
1083 let handle = PyHandle::from_value(v).unwrap();
1084 let h = Py::new(py, handle).unwrap();
1085 let helper = loop_helper(py);
1086 let res = helper
1087 .getattr("run_get_on_loop")
1088 .unwrap()
1089 .call1((h,))
1090 .unwrap();
1091 let tup = res.downcast::<PyTuple>().unwrap();
1092 (
1093 tup.get_item(0).unwrap().extract::<i64>().unwrap(),
1094 tup.get_item(1).unwrap().extract::<bool>().unwrap(),
1095 )
1096 });
1097 assert_eq!(value, 5, "a ready value still returns after warning");
1098 assert!(
1099 warned,
1100 "get() on a running asyncio loop warns regardless of readiness"
1101 );
1102 }
1103
1104 #[test]
1109 fn poll_and_get_surface_producer_error() {
1110 ensure_python();
1111 let (tx, handle) = pending_handle();
1112 tx.send(Some(Err(PyValueError::new_err("boom")))).unwrap();
1113 monarch_with_gil_blocking(GilSite::Test, |py| {
1114 let r = Py::new(py, handle).unwrap();
1115 let poll_err = r.borrow(py).poll().unwrap_err();
1116 assert!(
1117 poll_err.is_instance_of::<PyValueError>(py),
1118 "poll() should surface the producer error"
1119 );
1120 let get_err = PyHandle::get(r.borrow(py), py, None).unwrap_err();
1121 assert!(
1122 get_err.is_instance_of::<PyValueError>(py),
1123 "get() should raise the producer error"
1124 );
1125 });
1126 }
1127
1128 #[test]
1133 fn dropped_producer_surfaces_error() {
1134 ensure_python();
1135 let (tx, handle) = pending_handle();
1136 drop(tx);
1137 monarch_with_gil_blocking(GilSite::Test, |py| {
1138 let r = Py::new(py, handle).unwrap();
1139 let err = PyHandle::get(r.borrow(py), py, None).unwrap_err();
1140 assert!(
1141 err.is_instance_of::<pyo3::exceptions::PyException>(py),
1142 "a dropped producer should surface an exception, not hang"
1143 );
1144 });
1145 }
1146
1147 #[test]
1151 fn complete_asyncio_future_swallows_and_propagates() {
1152 ensure_python();
1153 monarch_with_gil_blocking(GilSite::Test, |py| {
1154 let event_loop = py
1155 .import("asyncio")
1156 .unwrap()
1157 .call_method0("new_event_loop")
1158 .unwrap();
1159 let settled = event_loop.call_method0("create_future").unwrap();
1160 settled.call_method1("set_result", (1i64,)).unwrap();
1161 assert!(
1162 complete_asyncio_future(&settled, false, py.None()).is_ok(),
1163 "InvalidStateError from a settled future should be swallowed"
1164 );
1165 event_loop.call_method0("close").unwrap();
1166
1167 let helper = PyModule::from_code(
1170 py,
1171 cr#"
1172class RaisingFuture:
1173 def cancelled(self):
1174 return False
1175
1176 def set_result(self, value):
1177 raise ValueError("nope")
1178"#,
1179 c"raising_future.py",
1180 c"raising_future",
1181 )
1182 .unwrap();
1183 let fake = helper.getattr("RaisingFuture").unwrap().call0().unwrap();
1184 let err = complete_asyncio_future(&fake, false, py.None()).unwrap_err();
1185 assert!(
1186 err.is_instance_of::<PyValueError>(py),
1187 "a non-InvalidStateError setter error should propagate"
1188 );
1189 });
1190 }
1191
1192 #[test]
1197 fn get_timeout_returns_value_before_deadline() {
1198 ensure_python();
1199 let value = monarch_with_gil_blocking(GilSite::Test, |py| 13i64.into_py_any(py).unwrap());
1200 let (tx, handle) = pending_handle();
1201 get_tokio_runtime().spawn(async move {
1202 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
1203 let _ = tx.send(Some(Ok(value)));
1204 });
1205 let got = monarch_with_gil_blocking(GilSite::Test, |py| -> i64 {
1206 let r = Py::new(py, handle).unwrap();
1207 let out = PyHandle::get(r.borrow(py), py, Some(5.0)).unwrap();
1208 out.extract::<i64>(py).unwrap()
1209 });
1210 assert_eq!(got, 13, "a generous timeout returns the produced value");
1211 }
1212
1213 #[test]
1218 fn get_on_loop_under_warnings_as_errors_raises() {
1219 ensure_python();
1220 let raised = monarch_with_gil_blocking(GilSite::Test, |py| -> bool {
1221 let v = 5i64.into_py_any(py).unwrap();
1222 let handle = PyHandle::from_value(v).unwrap();
1223 let h = Py::new(py, handle).unwrap();
1224 let helper = loop_helper(py);
1225 helper
1226 .getattr("run_get_on_loop_strict")
1227 .unwrap()
1228 .call1((h,))
1229 .is_err()
1230 });
1231 assert!(
1232 raised,
1233 "get() on a loop under warnings-as-errors should raise the escalated warning"
1234 );
1235 }
1236
1237 #[test]
1242 fn get_invalid_timeout_on_ready_raises() {
1243 ensure_python();
1244 monarch_with_gil_blocking(GilSite::Test, |py| {
1245 let value = 7i64.into_py_any(py).unwrap();
1246 let handle = PyHandle::from_value(value).unwrap();
1247 let r = Py::new(py, handle).unwrap();
1248 let err = PyHandle::get(r.borrow(py), py, Some(f64::NAN)).unwrap_err();
1249 assert!(
1250 err.is_instance_of::<PyValueError>(py),
1251 "an invalid timeout must raise ValueError even for a ready handle"
1252 );
1253 });
1254 }
1255
1256 #[test]
1261 fn as_asyncio_dropped_producer_raises_on_loop() {
1262 ensure_python();
1263 let (tx, handle) = pending_handle();
1264 drop(tx);
1265 let raised = monarch_with_gil_blocking(GilSite::Test, |py| -> bool {
1266 let h = Py::new(py, handle).unwrap();
1267 let helper = loop_helper(py);
1268 helper.getattr("run_await").unwrap().call1((h,)).is_err()
1269 });
1270 assert!(
1271 raised,
1272 "awaiting a handle whose producer dropped its sender should raise, not hang"
1273 );
1274 }
1275
1276 #[test]
1281 fn complete_asyncio_future_wraps_stop_iteration() {
1282 ensure_python();
1283 monarch_with_gil_blocking(GilSite::Test, |py| {
1284 let event_loop = py
1285 .import("asyncio")
1286 .unwrap()
1287 .call_method0("new_event_loop")
1288 .unwrap();
1289 let fut = event_loop.call_method0("create_future").unwrap();
1290 let stop_iteration = py.get_type::<PyStopIteration>().call0().unwrap().unbind();
1291 complete_asyncio_future(&fut, true, stop_iteration).unwrap();
1293 let exc = fut.call_method0("exception").unwrap();
1294 assert!(
1295 exc.is_instance_of::<PyRuntimeError>(),
1296 "a StopIteration producer error should be wrapped in RuntimeError"
1297 );
1298 assert!(
1299 !exc.is_instance_of::<PyStopIteration>(),
1300 "the wrapped error must not remain a StopIteration"
1301 );
1302 event_loop.call_method0("close").unwrap();
1303 });
1304 }
1305
1306 #[test]
1311 fn complete_asyncio_future_wraps_stop_iteration_subclass() {
1312 ensure_python();
1313 monarch_with_gil_blocking(GilSite::Test, |py| {
1314 let event_loop = py
1315 .import("asyncio")
1316 .unwrap()
1317 .call_method0("new_event_loop")
1318 .unwrap();
1319 let fut = event_loop.call_method0("create_future").unwrap();
1320 let helper = PyModule::from_code(
1321 py,
1322 cr#"
1323class MyStop(StopIteration):
1324 pass
1325"#,
1326 c"my_stop.py",
1327 c"my_stop",
1328 )
1329 .unwrap();
1330 let subclass_exc = helper.getattr("MyStop").unwrap().call0().unwrap().unbind();
1331 complete_asyncio_future(&fut, true, subclass_exc).unwrap();
1332 let exc = fut.call_method0("exception").unwrap();
1333 assert!(
1334 exc.is_instance_of::<PyRuntimeError>(),
1335 "a StopIteration subclass should be wrapped in RuntimeError"
1336 );
1337 assert!(
1338 !exc.is_instance_of::<PyStopIteration>(),
1339 "the wrapped error must not remain a StopIteration"
1340 );
1341 event_loop.call_method0("close").unwrap();
1342 });
1343 }
1344
1345 #[test]
1350 fn drop_aborts_producer_only_when_abort_set() {
1351 use std::sync::Arc;
1352 use std::sync::atomic::AtomicBool;
1353 use std::sync::atomic::Ordering;
1354 use std::time::Duration;
1355
1356 for (abort, expect_completed) in [(true, false), (false, true)] {
1357 let completed = Arc::new(AtomicBool::new(false));
1358 let completed_in_task = Arc::clone(&completed);
1359 let (_tx, rx) = watch::channel::<Option<PyResult<Py<PyAny>>>>(None);
1360 let jh = get_tokio_runtime().spawn(async move {
1361 tokio::time::sleep(Duration::from_millis(50)).await;
1362 completed_in_task.store(true, Ordering::SeqCst);
1363 });
1364 let core = HandleCore::new(rx, abort.then(|| jh.abort_handle()), None);
1365 drop(core);
1366 std::thread::sleep(Duration::from_millis(250));
1367 assert_eq!(
1368 completed.load(Ordering::SeqCst),
1369 expect_completed,
1370 "abort={abort}: producer completion after drop should be {expect_completed}"
1371 );
1372 }
1373 }
1374
1375 #[test]
1379 fn handle_not_constructible_from_python() {
1380 ensure_python();
1381 monarch_with_gil_blocking(GilSite::Test, |py| {
1382 let handle_type = py.get_type::<PyHandle>();
1383 let err = handle_type.call0().unwrap_err();
1384 assert!(
1385 err.is_instance_of::<pyo3::exceptions::PyTypeError>(py),
1386 "constructing Handle() from Python should raise TypeError (no __new__)"
1387 );
1388 });
1389 }
1390}