Expand description
Handle: an observe-only handle to a background Tokio task.
A Handle wraps a watch channel that a producer fulfills exactly once with
the task’s PyResult<Py<PyAny>>. You observe its eventual result:
synchronously via get(), on an asyncio loop via as_asyncio()/await, or
without blocking via poll(). The channel is multi-observer, so a resolved
value stays observable by any number of later observers.
The watch-channel mechanics live in [HandleCore].
§Handle invariants (HDL-*)
Code that relies on or enforces an invariant is tagged // HDL-N, and each
#[cfg(test)] test that attests one names it; read the matching entry below
before changing a tagged site.
Assumed of the shared core and producer (not enforced here):
- HDL-1 (single terminal completion). The watch value goes
None->Some(_)exactly once and is never reset (Some->Some,Some->None). Relied on by [HandleCore::wait_future] (wait loop +expect) and [HandleCore::poll] (re-borrow across the GIL). Held by convention: every producer sends once, alwaysSome(_). A send-once sender (a newtype consuming thewatch::Sender) could make it structural, hardeningpoll’s re-borrow. - HDL-2 (value encoding).
Noneis pending,Some(Ok(_))success,Some(Err(_))a producer error.
Guaranteed by this module:
- HDL-3 (non-consuming reads). Every read clones the value out
(
clone_ref) rather than moving it, so any number of observers see the same result. Enforced by construction: no read path moves the value. - HDL-4 (drop does not cancel; drop never panics). Dropping a
Handlenever cancels its producer; aHandleobserves, it does not own the producer’s lifecycle. Enforced by construction: aHandle’s core hasabort_on_drop: None, andHandleCore::Dropaborts only whenabort_on_dropis set. The abort is wrapped incatch_unwind, so anabort()that panics at interpreter shutdown (the Tokio runtime already gone) cannot escapeDropand abort the process. - HDL-5 (GIL discipline). No Python object is touched without the GIL,
and every acquisition goes through
monarch_with_gil{,_blocking}. Enforced by the crate’s#![deny(clippy::disallowed_methods)]ban on rawPython::with_gil/attach– a hard compile error, not adebug_assert. - HDL-6 (
WouldBlockRuntimeisget()’s alone). Onlyget()raises it, and only in a Tokio runtime context;as_asyncio()/__await__off a loop raise the nativeRuntimeErrorinstead. - HDL-7 (
as_asynciopublish). The observer waits borrow-first (viawait_ready, neverchanged()-first) and sets a result only on a non-cancelled future, swallowingInvalidStateError; anyStopIteration(including subclasses) is wrapped inRuntimeErrorfirst, sinceset_exceptionrejectsStopIteration(PEP 479); if the loop has closed it logs rather than panics. - HDL-8 (
get()loop-warning is call-pattern, not outcome).get()on a running asyncio loop warns regardless of whether the value is ready: it flags the blocking-call-from-a-loop anti-pattern, not whether this call happens to block. A ready value returns after warning; under a warnings-as-errors filter the warning escalates to an error. - HDL-9 (GIL/watch borrow discipline). A
watchborrow is never held across a GIL acquisition or an.await.polldrops its read guard before taking the GIL and re-borrows under it (GIL-then-watch order, never the inverse against the producer’s write-locksend); thewait_ready/wait_futureloops drop the temporaryRefbefore each.await; and their returned futures own a cloned receiver (Send + 'static, borrowing nothing from&self), which is what letsget()drop(slf)+ release the GIL before blocking and letsas_asynciospawnthe observer. Enforced partly by the compiler (Sendbounds, the borrow checker ondrop(slf)) and partly by construction; exercised by the multi-observer/blocking tests. - HDL-10 (dropped producer surfaces, never hangs). If every producer drops
its sender without sending, the wait loop’s
changed().await?yields aRecvErrorturned into a Python exception (to_py_error) on both the sync (get/wait_future) and async (as_asyncio) paths, so an observer raises rather than hanging forever. - HDL-11 (
Handleis not constructible from Python). The pyclass has no#[new]and the onlyfrom_valueconstructor is#[cfg(test)]-gated, so a liveHandlealways wraps a producer-supplied core (upholding HDL-1/HDL-2); exposing a Python constructor would let user code mint an unresolvable core. - HDL-12 (
get()timeout contract).get()validates the timeout up front (rejecting negative/NaN/non-finite asValueErrorviatry_from_secs_f64, never panicking) and before the ready fast path, so an invalid timeout raises deterministically even for a ready handle. A timeout is non-cancelling: it raisesTimeoutErrorwhile leaving the producer and every observer untouched, so a laterpoll()/get()/awaitstill observes completion.
Structs§
- PyHandle
- The observe-only handle to a background Tokio task.
- Would
Block Runtime - raised when Handle.get() is called from a Tokio runtime context