Skip to main content

monarch_hyperactor/
actor_mesh.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
9use std::future::Future;
10use std::ops::Deref;
11use std::pin::Pin;
12use std::sync::Arc;
13use std::thread;
14use std::time::Duration;
15
16use async_trait::async_trait;
17use futures::future;
18use futures::future::BoxFuture;
19use futures::future::FutureExt;
20use futures::future::Shared;
21use hyperactor::Instance;
22use hyperactor::RemoteEndpoint as _;
23use hyperactor::supervision::ActorSupervisionEvent;
24use hyperactor_mesh::actor_mesh::ActorMesh;
25use hyperactor_mesh::actor_mesh::ActorMeshRef;
26use monarch_types::py_global;
27use monarch_types::py_module_add_function;
28use ndslice::view::Ranked;
29use ndslice::view::RankedSliceable;
30use pyo3::IntoPyObjectExt;
31use pyo3::exceptions::PyNotImplementedError;
32use pyo3::exceptions::PyRuntimeError;
33use pyo3::exceptions::PyValueError;
34use pyo3::prelude::*;
35use pyo3::types::PyBytes;
36use pyo3::types::PyTuple;
37use rand::RngExt as _;
38use tokio::sync::mpsc::UnboundedSender;
39use tokio::sync::mpsc::unbounded_channel;
40use tracing::Instrument;
41
42use crate::actor::PythonActor;
43use crate::actor::PythonMessage;
44use crate::actor::PythonMessageKind;
45use crate::context::PyInstance;
46use crate::handle::PyHandle;
47use crate::pickle::PendingMessage;
48use crate::proc::PyActorAddr;
49use crate::pytokio::PyPythonTask;
50use crate::runtime::GilSite;
51use crate::runtime::get_tokio_runtime;
52use crate::runtime::monarch_with_gil;
53use crate::runtime::monarch_with_gil_blocking;
54use crate::shape::PyRegion;
55use crate::supervision::Supervisable;
56use crate::supervision::SupervisionError;
57
58py_global!(_pickle, "monarch._src.actor.actor_mesh", "_pickle");
59
60py_global!(
61    shared_class,
62    "monarch._rust_bindings.monarch_hyperactor.pytokio",
63    "Shared"
64);
65
66/// Closed actor-mesh selection surface used by public send APIs.
67///
68/// Public Python APIs expose only `"all"` and `"choose"`. Keep that
69/// invariant explicit here so arbitrary `ndslice::Selection` values stay out
70/// of the public mesh-casting surface.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub(crate) enum AllOrChoose {
73    All,
74    Choose,
75}
76
77impl AllOrChoose {
78    pub(crate) fn as_str(self) -> &'static str {
79        match self {
80            Self::All => "all",
81            Self::Choose => "choose",
82        }
83    }
84}
85
86/// Trait defining the common interface for actor mesh, mesh ref and actor mesh implementations.
87/// This corresponds to the Python ActorMeshProtocol ABC.
88pub(crate) trait ActorMeshProtocol: Send + Sync {
89    /// Cast a message to actors selected by the given selection using the specified mailbox.
90    fn cast(
91        &self,
92        message: PythonMessage,
93        selection: AllOrChoose,
94        instance: &Instance<PythonActor>,
95    ) -> PyResult<()>;
96
97    /// Cast a message, merging caller-supplied envelope headers into
98    /// the outbound request. Implementations that reach the real
99    /// envelope emission site override this to thread `caller_headers`
100    /// through `hyperactor_mesh::ActorMeshRef::cast_with_headers`;
101    /// the default collapses to the non-headers path for impls that
102    /// have no envelope access.
103    fn cast_with_headers(
104        &self,
105        message: PythonMessage,
106        selection: AllOrChoose,
107        instance: &Instance<PythonActor>,
108        _caller_headers: hyperactor_config::Flattrs,
109    ) -> PyResult<()> {
110        self.cast(message, selection, instance)
111    }
112
113    /// Cast a pending message (which may contain unresolved async values) to actors.
114    ///
115    /// The default implementation blocks on resolving the message and then calls cast.
116    /// AsyncActorMesh overrides this with an optimized async implementation.
117    fn cast_unresolved(
118        &self,
119        message: PendingMessage,
120        selection: AllOrChoose,
121        instance: &Instance<PythonActor>,
122    ) -> PyResult<()> {
123        let message = get_tokio_runtime().block_on(message.resolve())?;
124        self.cast(message, selection, instance)
125    }
126
127    // HOT PATH: Be mindful of performance when making changes here.
128    // To test how performance is affected by a change, run the RPC benchmarks in
129    // `monarch/python/benches/`.
130    /// Async counterpart of `cast_with_headers`. The default
131    /// resolves the pending message synchronously and delegates;
132    /// `AsyncActorMesh` overrides this to resolve asynchronously
133    /// and route through `cast_with_headers`.
134    fn cast_unresolved_with_headers(
135        &self,
136        message: PendingMessage,
137        selection: AllOrChoose,
138        instance: &Instance<PythonActor>,
139        caller_headers: hyperactor_config::Flattrs,
140    ) -> PyResult<()> {
141        let message = get_tokio_runtime().block_on(message.resolve())?;
142        self.cast_with_headers(message, selection, instance, caller_headers)
143    }
144
145    fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)>;
146
147    /// The serializable reference for this mesh, for the out-of-band `refs`
148    /// table. Required so every impl chooses: hold a resolved ref and return
149    /// it, or (a pending mesh) have none and error explicitly.
150    fn mesh_ref(&self) -> PyResult<ActorMeshRef<PythonActor>>;
151
152    /// Return a future that completes when the mesh has stopped.
153    ///
154    /// The default implementation rejects the call immediately.
155    /// Mesh types that support stopping override this method.
156    fn stop(
157        &self,
158        _instance: &PyInstance,
159        _reason: String,
160    ) -> PyResult<BoxFuture<'static, PyResult<()>>> {
161        Err(PyNotImplementedError::new_err(format!(
162            "stop() is not supported for {}",
163            std::any::type_name::<Self>()
164        )))
165    }
166
167    /// Return a future that completes when the mesh has been created.
168    ///
169    /// The default returns an already-completed future: the mesh exists.
170    /// Pending meshes override this method to wait for creation.
171    /// The completion value is exposed to Python as None.
172    fn initialized(&self) -> BoxFuture<'static, PyResult<Option<()>>> {
173        future::ready(Ok(None)).boxed()
174    }
175
176    /// Return a future that reports the mesh's name once creation has finished.
177    fn name(&self) -> BoxFuture<'static, PyResult<String>>;
178}
179
180pub(crate) trait SupervisableActorMesh: ActorMeshProtocol + Supervisable {
181    fn new_with_region(&self, region: &PyRegion) -> PyResult<Box<dyn SupervisableActorMesh>>;
182}
183
184/// This just forwards to the rust trait that can implement these bindings
185#[pyclass(
186    name = "PythonActorMesh",
187    module = "monarch._rust_bindings.monarch_hyperactor.actor_mesh"
188)]
189#[derive(Clone)]
190pub(crate) struct PythonActorMesh {
191    inner: Arc<dyn SupervisableActorMesh>,
192}
193
194impl PythonActorMesh {
195    pub(crate) fn new<F>(f: F, supervised: bool) -> Self
196    where
197        F: Future<Output = PyResult<Box<dyn SupervisableActorMesh>>> + Send + 'static,
198    {
199        let f = async move { Ok(Arc::from(f.await?)) }.boxed().shared();
200        PythonActorMesh {
201            inner: Arc::new(AsyncActorMesh::new_queue(f, supervised)),
202        }
203    }
204
205    pub(crate) fn from_impl(inner: Arc<dyn SupervisableActorMesh>) -> Self {
206        PythonActorMesh { inner }
207    }
208
209    pub(crate) fn get_inner(&self) -> Arc<dyn SupervisableActorMesh> {
210        self.inner.clone()
211    }
212}
213
214pub(crate) fn to_all_or_choose(selection: &str) -> PyResult<AllOrChoose> {
215    match selection {
216        "choose" => Ok(AllOrChoose::Choose),
217        "all" => Ok(AllOrChoose::All),
218        _ => Err(PyErr::new::<PyValueError, _>(format!(
219            "Invalid selection: {}",
220            selection
221        ))),
222    }
223}
224
225#[pymethods]
226impl PythonActorMesh {
227    #[tracing::instrument(level = "debug", skip_all)]
228    #[pyo3(name = "cast")]
229    fn py_cast(
230        &self,
231        message: &PythonMessage,
232        selection: &str,
233        instance: &PyInstance,
234    ) -> PyResult<()> {
235        let sel = to_all_or_choose(selection)?;
236        self.inner.cast(message.clone(), sel, instance.deref())
237    }
238
239    #[hyperactor::instrument]
240    pub(crate) fn cast_unresolved(
241        &self,
242        message: &mut PendingMessage,
243        selection: &str,
244        instance: &PyInstance,
245    ) -> PyResult<()> {
246        let sel = to_all_or_choose(selection)?;
247        let message = message.take()?;
248        self.inner.cast_unresolved(message, sel, instance)
249    }
250
251    fn new_with_region(&self, region: &PyRegion) -> PyResult<PythonActorMesh> {
252        let inner = self.inner.new_with_region(region)?;
253        Ok(PythonActorMesh {
254            inner: Arc::from(inner),
255        })
256    }
257
258    /// Start stopping the mesh and return a Handle that reports the outcome.
259    /// A mesh type that does not support stopping may reject the call before
260    /// returning a Handle. Dropping the Handle does not cancel an accepted stop.
261    fn stop(&self, instance: &PyInstance, reason: String) -> PyResult<PyHandle> {
262        self.inner.stop(instance, reason).map(PyHandle::spawn)
263    }
264
265    /// Start waiting for mesh creation and return a Handle that completes
266    /// with Python None. Dropping the Handle does not cancel creation.
267    fn initialized(&self) -> PyHandle {
268        PyHandle::spawn(self.inner.initialized())
269    }
270
271    /// Return a Handle for the mesh's name, waiting for creation if necessary.
272    /// The wait starts with this call; dropping the Handle does not cancel it.
273    fn name(&self) -> PyHandle {
274        PyHandle::spawn(self.inner.name())
275    }
276
277    fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> {
278        self.inner.__reduce__(py)
279    }
280}
281
282#[derive(Debug)]
283pub(crate) struct ClonePyErr {
284    inner: PyErr,
285}
286
287impl From<ClonePyErr> for PyErr {
288    fn from(value: ClonePyErr) -> PyErr {
289        value.inner
290    }
291}
292impl From<PyErr> for ClonePyErr {
293    fn from(inner: PyErr) -> ClonePyErr {
294        ClonePyErr { inner }
295    }
296}
297
298impl Clone for ClonePyErr {
299    fn clone(&self) -> Self {
300        monarch_with_gil_blocking(GilSite::Convert, |py| self.inner.clone_ref(py).into())
301    }
302}
303
304type ActorMeshResult = Result<Arc<dyn SupervisableActorMesh>, ClonePyErr>;
305type ActorMeshFut = Shared<Pin<Box<dyn Future<Output = ActorMeshResult> + Send + 'static>>>;
306
307pub(crate) struct AsyncActorMesh {
308    mesh: ActorMeshFut,
309    queue: UnboundedSender<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>,
310    supervised: bool,
311}
312
313impl AsyncActorMesh {
314    pub(crate) fn new_queue(f: ActorMeshFut, supervised: bool) -> AsyncActorMesh {
315        let (queue, mut recv) = unbounded_channel();
316
317        get_tokio_runtime().spawn(async move {
318            loop {
319                let r = recv.recv().await;
320                if let Some(r) = r {
321                    r.await;
322                } else {
323                    return;
324                }
325            }
326        });
327
328        let mesh = AsyncActorMesh::new(queue, supervised, f);
329        // Eagerly trigger the mesh initialization by pushing an init task onto
330        // the queue. This ensures actors are spawned immediately rather than
331        // waiting for the first endpoint call, which is critical for:
332        // 1. Tests/code that wait for supervision events from actor __init__
333        //    failures without making any endpoint calls.
334        // 2. Ensuring all meshes on a proc are spawned before any errors occur,
335        //    preventing spawn rejections due to stale supervision events.
336        let f = mesh.mesh.clone();
337        mesh.push(async move {
338            let _ = f.await;
339        });
340        mesh
341    }
342
343    fn new(
344        queue: UnboundedSender<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>,
345        supervised: bool,
346        f: ActorMeshFut,
347    ) -> AsyncActorMesh {
348        AsyncActorMesh {
349            mesh: f,
350            queue,
351            supervised,
352        }
353    }
354
355    fn push<F>(&self, f: F)
356    where
357        F: Future<Output = ()> + Send + 'static,
358    {
359        self.queue.send(f.boxed()).unwrap();
360    }
361
362    pub(crate) fn from_impl(mesh: Arc<dyn SupervisableActorMesh>) -> Self {
363        let fut = future::ready(Ok::<Arc<dyn SupervisableActorMesh>, ClonePyErr>(mesh))
364            .boxed()
365            .shared();
366        // Poll the future so that its result can be observed without blocking the tokio runtime.
367        let _ = futures::executor::block_on(fut.clone());
368        Self::new_queue(fut, true)
369    }
370}
371
372impl ActorMeshProtocol for AsyncActorMesh {
373    fn cast(
374        &self,
375        _message: PythonMessage,
376        _selection: AllOrChoose,
377        _instance: &Instance<PythonActor>,
378    ) -> PyResult<()> {
379        panic!("not implemented")
380    }
381
382    fn cast_unresolved(
383        &self,
384        message: PendingMessage,
385        selection: AllOrChoose,
386        instance: &Instance<PythonActor>,
387    ) -> PyResult<()> {
388        self.cast_unresolved_with_headers(
389            message,
390            selection,
391            instance,
392            hyperactor_config::Flattrs::new(),
393        )
394    }
395
396    // HOT PATH: Be mindful of performance when making changes here.
397    // To test how performance is affected by a change, run the RPC benchmarks in
398    // `monarch/python/benches/`.
399    fn cast_unresolved_with_headers(
400        &self,
401        message: PendingMessage,
402        selection: AllOrChoose,
403        instance: &Instance<PythonActor>,
404        caller_headers: hyperactor_config::Flattrs,
405    ) -> PyResult<()> {
406        let mesh = self.mesh.clone();
407        let instance = instance.clone_for_py();
408        let port = match &message.kind {
409            PythonMessageKind::CallMethod { response_port, .. } => response_port.clone(),
410            _ => None,
411        };
412        self.push(
413            async move {
414                let result = async {
415                    let resolved = match message.try_resolve_now()? {
416                        Ok(resolved) => resolved,
417                        Err(message) => message.resolve().await?,
418                    };
419                    mesh.await?
420                        .cast_with_headers(resolved, selection, &instance, caller_headers)
421                }
422                .await;
423                if let (Some(mut port_ref), Err(pyerr)) = (port, result) {
424                    let _ = monarch_with_gil(GilSite::Traceback, |py: Python<'_>| {
425                        let exception_str = crate::logging::format_traceback(py, &pyerr);
426                        tracing::error!(
427                            actor_id = instance.self_addr().to_string(),
428                            "error occurred during cast unresolved: {}",
429                            exception_str
430                        );
431
432                        // Endpoint calls create a response port: the
433                        // PortRef is sent to the remote worker (to send
434                        // results back), and collect_valuemesh owns the
435                        // PortReceiver. If mesh.cast() fails here, we try
436                        // to send the exception back to the caller via
437                        // the PortRef ourselves. But a supervision event
438                        // can cause collect_valuemesh to drop the
439                        // PortReceiver (removing the port from the
440                        // mailbox) before we get here. Disable
441                        // return-undeliverable so a delivery failure
442                        // doesn't bounce back and crash the root client.
443                        //
444                        // TODO: Tie the lifetime of this queued work to
445                        // the PortReceiver (e.g. a cancellation token set
446                        // on drop) so we can distinguish
447                        // supervision-caused failures — where the caller
448                        // already knows — from other cast errors where
449                        // the caller actually needs this exception.
450
451                        port_ref.set_return_undeliverable(false);
452
453                        let mut state = crate::pickle::pickle(
454                            py,
455                            pyerr.into_value(py).into_any(),
456                            false,
457                            false,
458                        )?;
459                        let _ = port_ref.post(
460                            &instance,
461                            PythonMessage::new_from_buf(
462                                PythonMessageKind::Exception { rank: Some(0) },
463                                state.take_inner()?.take_buffer(),
464                            ),
465                        );
466
467                        Ok::<_, PyErr>(())
468                    })
469                    .await;
470                }
471            }
472            .instrument(tracing::debug_span!("AsyncActorMesh::cast")),
473        );
474        Ok(())
475    }
476
477    fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> {
478        let fut = self.mesh.clone();
479        match fut.peek().cloned() {
480            Some(mesh) => mesh?.__reduce__(py),
481            None => {
482                let observer = async move { Ok(PythonActorMesh::from_impl(fut.await?)) };
483                // Test-only: consumes a caller-thread probe, if one is armed, and
484                // moves it into the spawned observer. Adds no field, branch,
485                // callback or synchronization point to the production build.
486                #[cfg(test)]
487                let observer = tests::probe_reduction_observer(observer);
488                let shared = PyPythonTask::new(observer)?.spawn_abortable()?;
489                let shared = Py::new(py, shared)?;
490                if crate::pickle::reserve_mesh_reference_if_active(shared.clone_ref(py)) {
491                    let pop_fn = py
492                        .import("monarch._rust_bindings.monarch_hyperactor.pickle")?
493                        .getattr("pop_mesh_reference")?;
494                    return Ok((pop_fn, PyTuple::empty(py).into_any()));
495                }
496                // Get Shared.block_on as an unbound method
497                let block_on = shared_class(py).getattr("block_on")?;
498                let args = PyTuple::new(py, [shared])?;
499                Ok((block_on, args.into_any()))
500            }
501        }
502    }
503
504    fn mesh_ref(&self) -> PyResult<ActorMeshRef<PythonActor>> {
505        // A pending mesh has no serializable ref of its own; the reserve/fill
506        // slot carries it out-of-band. This is a backstop: the happy path never
507        // asks a pending mesh for a ref.
508        Err(pyo3::exceptions::PyRuntimeError::new_err(
509            "pending actor mesh has no serializable ref; it is carried via the reserve/fill slot",
510        ))
511    }
512
513    /// Queue the stop after work already queued for this mesh, and return a
514    /// future for its result.
515    ///
516    /// The queued work waits for mesh creation, then stops the mesh. Dropping
517    /// the returned future does not cancel that work.
518    fn stop(
519        &self,
520        instance: &PyInstance,
521        reason: String,
522    ) -> PyResult<BoxFuture<'static, PyResult<()>>> {
523        // The queued work owns these clones so it can outlive this call.
524        let mesh = self.mesh.clone();
525        let instance = monarch_with_gil_blocking(GilSite::Stop, |_py| instance.clone());
526        // The queue runs the stop; this channel reports its outcome to the
527        // caller's future. Observing that future does not drive the stop.
528        let (tx, rx) = tokio::sync::oneshot::channel();
529        self.push(async move {
530            // If a step fails, `?` exits this inner async block with an error.
531            // Awaiting the block binds either Ok or Err to `result`, after which
532            // the outer block calls tx.send(result). No additional task is spawned.
533            let result = async move {
534                // Wait for mesh creation; a creation failure becomes the result.
535                let mesh = mesh.await?;
536                // Request the stop. A mesh reference rejects it here; an owned
537                // mesh returns a future that performs the stop.
538                let completion = mesh.stop(&instance, reason)?;
539                // Run that future until stopping succeeds or fails.
540                completion.await
541            }
542            .await;
543            // The future returned by stop owns rx. If that future has been
544            // dropped, tx.send fails. Ignore the error rather than panic and
545            // prevent later queued work from running.
546            let _ = tx.send(result);
547        });
548        Ok(async move { rx.await.map_err(anyhow::Error::from)? }.boxed())
549    }
550
551    /// Return a future that waits for mesh creation without adding a queue item.
552    /// Creation errors fail the future; success is exposed to Python as None.
553    fn initialized(&self) -> BoxFuture<'static, PyResult<Option<()>>> {
554        let mesh = self.mesh.clone();
555        async move {
556            // Wait for creation. If it fails, the future returned by initialized
557            // completes with that error.
558            mesh.await?;
559            Ok(None::<()>)
560        }
561        .boxed()
562    }
563
564    /// Return a future that waits for mesh creation, then reads its name.
565    /// This does not add a queue item; creation or name errors fail the future.
566    fn name(&self) -> BoxFuture<'static, PyResult<String>> {
567        let mesh = self.mesh.clone();
568        async move { mesh.await?.name().await }.boxed()
569    }
570}
571
572#[async_trait]
573impl Supervisable for AsyncActorMesh {
574    async fn supervision_event(&self, instance: &Instance<PythonActor>) -> Option<PyErr> {
575        if !self.supervised {
576            return None;
577        }
578        let mesh = self.mesh.clone();
579        match mesh.await {
580            Ok(mesh) => mesh.supervision_event(instance).await,
581            Err(e) => Some(e.into()),
582        }
583    }
584}
585
586impl SupervisableActorMesh for AsyncActorMesh {
587    fn new_with_region(&self, region: &PyRegion) -> PyResult<Box<dyn SupervisableActorMesh>> {
588        let mesh = self.mesh.clone();
589        let region = region.clone();
590        Ok(Box::new(AsyncActorMesh::new(
591            self.queue.clone(),
592            self.supervised,
593            async move { Ok(Arc::from(mesh.await?.new_with_region(&region)?)) }
594                .boxed()
595                .shared(),
596        )))
597    }
598}
599
600#[derive(Debug, Clone)]
601#[pyclass(
602    name = "PyActorMesh",
603    module = "monarch._rust_bindings.monarch_hyperactor.actor_mesh"
604)]
605pub(crate) struct PyActorMesh {
606    mesh: ActorMesh<PythonActor>,
607}
608
609#[derive(Debug, Clone)]
610#[pyclass(
611    name = "PyActorMeshRef",
612    module = "monarch._rust_bindings.monarch_hyperactor.actor_mesh"
613)]
614pub(crate) struct PyActorMeshRef {
615    mesh: ActorMeshRef<PythonActor>,
616}
617
618#[derive(Debug, Clone)]
619#[pyclass(
620    name = "PythonActorMeshImpl",
621    module = "monarch._rust_bindings.monarch_hyperactor.actor_mesh"
622)]
623pub(crate) enum PythonActorMeshImpl {
624    Owned(PyActorMesh),
625    Ref(PyActorMeshRef),
626}
627
628impl PythonActorMeshImpl {
629    /// Get a new owned [`PythonActorMeshImpl`].
630    pub(crate) fn new_owned(inner: ActorMesh<PythonActor>) -> Self {
631        PythonActorMeshImpl::Owned(PyActorMesh { mesh: inner })
632    }
633
634    /// Get a new ref-based [`PythonActorMeshImpl`].
635    pub(crate) fn new_ref(inner: ActorMeshRef<PythonActor>) -> Self {
636        PythonActorMeshImpl::Ref(PyActorMeshRef { mesh: inner })
637    }
638
639    fn mesh_ref(&self) -> &ActorMeshRef<PythonActor> {
640        match self {
641            PythonActorMeshImpl::Owned(inner) => &inner.mesh,
642            PythonActorMeshImpl::Ref(inner) => &inner.mesh,
643        }
644    }
645}
646
647#[async_trait]
648impl Supervisable for PythonActorMeshImpl {
649    async fn supervision_event(&self, instance: &Instance<PythonActor>) -> Option<PyErr> {
650        let mesh = self.mesh_ref();
651        match mesh.next_supervision_event(instance).await {
652            Ok(supervision_failure) => Some(SupervisionError::new_err_from(supervision_failure)),
653            Err(e) => Some(PyValueError::new_err(e.to_string())),
654        }
655    }
656}
657
658impl ActorMeshProtocol for PythonActorMeshImpl {
659    fn cast(
660        &self,
661        message: PythonMessage,
662        selection: AllOrChoose,
663        instance: &Instance<PythonActor>,
664    ) -> PyResult<()> {
665        <ActorMeshRef<PythonActor> as ActorMeshProtocol>::cast(
666            self.mesh_ref(),
667            message,
668            selection,
669            instance,
670        )
671    }
672
673    fn cast_with_headers(
674        &self,
675        message: PythonMessage,
676        selection: AllOrChoose,
677        instance: &Instance<PythonActor>,
678        caller_headers: hyperactor_config::Flattrs,
679    ) -> PyResult<()> {
680        <ActorMeshRef<PythonActor> as ActorMeshProtocol>::cast_with_headers(
681            self.mesh_ref(),
682            message,
683            selection,
684            instance,
685            caller_headers,
686        )
687    }
688
689    /// Return a future that stops an owned mesh.
690    ///
691    /// A mesh reference is rejected immediately with NotImplementedError.
692    /// Native stop failures make the returned future fail with ValueError.
693    fn stop(
694        &self,
695        instance: &PyInstance,
696        reason: String,
697    ) -> PyResult<BoxFuture<'static, PyResult<()>>> {
698        let (slf, instance) =
699            monarch_with_gil_blocking(GilSite::Stop, |_py| (self.clone(), instance.clone()));
700        match slf {
701            PythonActorMeshImpl::Owned(mut mesh) => Ok(async move {
702                mesh.mesh
703                    .stop(instance.deref(), reason)
704                    .await
705                    .map_err(|err| PyValueError::new_err(err.to_string()))
706            }
707            .boxed()),
708            PythonActorMeshImpl::Ref(_) => Err(PyNotImplementedError::new_err(
709                "Cannot call stop on an ActorMeshRef, requires an owned ActorMesh",
710            )),
711        }
712    }
713
714    fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> {
715        self.mesh_ref().__reduce__(py)
716    }
717
718    fn mesh_ref(&self) -> PyResult<ActorMeshRef<PythonActor>> {
719        // `self.mesh_ref()` resolves to the inherent borrow-returning method.
720        Ok(PythonActorMeshImpl::mesh_ref(self).clone())
721    }
722
723    /// Return the mesh reference's ID as its name, for both owned and
724    /// reference meshes. The delegated method reads the ID immediately
725    /// and returns an already-completed future.
726    fn name(&self) -> BoxFuture<'static, PyResult<String>> {
727        <ActorMeshRef<PythonActor> as ActorMeshProtocol>::name(self.mesh_ref())
728    }
729}
730
731impl SupervisableActorMesh for PythonActorMeshImpl {
732    fn new_with_region(&self, region: &PyRegion) -> PyResult<Box<dyn SupervisableActorMesh>> {
733        assert!(region.as_inner().is_subset(self.mesh_ref().region()));
734        Ok(Box::new(PythonActorMeshImpl::new_ref(
735            self.mesh_ref().sliced(region.as_inner().clone()),
736        )))
737    }
738}
739
740fn cast_error_to_py_error(err: hyperactor_mesh::Error) -> PyErr {
741    PyRuntimeError::new_err(err.to_string())
742}
743
744impl ActorMeshProtocol for ActorMeshRef<PythonActor> {
745    fn cast(
746        &self,
747        message: PythonMessage,
748        selection: AllOrChoose,
749        instance: &Instance<PythonActor>,
750    ) -> PyResult<()> {
751        <Self as ActorMeshProtocol>::cast_with_headers(
752            self,
753            message,
754            selection,
755            instance,
756            hyperactor_config::Flattrs::new(),
757        )
758    }
759
760    fn cast_with_headers(
761        &self,
762        message: PythonMessage,
763        selection: AllOrChoose,
764        instance: &Instance<PythonActor>,
765        caller_headers: hyperactor_config::Flattrs,
766    ) -> PyResult<()> {
767        match selection {
768            AllOrChoose::All => ActorMeshRef::<PythonActor>::cast_with_headers(
769                self,
770                instance,
771                &caller_headers,
772                message,
773            )
774            .map_err(cast_error_to_py_error),
775            AllOrChoose::Choose => match self {
776                ActorMeshRef::Managed(mesh) => mesh
777                    .cast_choose_with_headers(instance, &caller_headers, message)
778                    .map_err(cast_error_to_py_error),
779                ActorMeshRef::Data(mesh) => {
780                    let num_ranks = mesh.region().num_ranks();
781                    if num_ranks > 0 {
782                        let actor = mesh
783                            .get(rand::rng().random_range(0..num_ranks))
784                            .expect("selected rank should exist in dense data mesh");
785                        actor.post_with_headers(instance, caller_headers, message);
786                    }
787                    Ok(())
788                }
789            },
790        }
791    }
792
793    /// Reject the call immediately with NotImplementedError: stopping
794    /// requires an owned mesh, not a reference. No future is returned.
795    fn stop(
796        &self,
797        _instance: &PyInstance,
798        _reason: String,
799    ) -> PyResult<BoxFuture<'static, PyResult<()>>> {
800        Err(PyNotImplementedError::new_err(
801            "This cannot be used on ActorMeshRef, only on owned ActorMesh",
802        ))
803    }
804
805    fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> {
806        if crate::pickle::push_mesh_reference_if_active(crate::actor::MeshRef::Actor(Box::new(
807            self.clone(),
808        ))) {
809            let pop_fn = py
810                .import("monarch._rust_bindings.monarch_hyperactor.pickle")?
811                .getattr("pop_mesh_reference")?;
812            return Ok((pop_fn, pyo3::types::PyTuple::empty(py).into_any()));
813        }
814        let bytes = bincode::serde::encode_to_vec(self, bincode::config::legacy())
815            .map_err(|e| PyValueError::new_err(e.to_string()))?;
816        let py_bytes = (PyBytes::new(py, &bytes),).into_bound_py_any(py).unwrap();
817        let module = py
818            .import("monarch._rust_bindings.monarch_hyperactor.actor_mesh")
819            .unwrap();
820        let from_bytes = module.getattr("py_actor_mesh_from_bytes").unwrap();
821        Ok((from_bytes, py_bytes))
822    }
823
824    fn mesh_ref(&self) -> PyResult<ActorMeshRef<PythonActor>> {
825        Ok(self.clone())
826    }
827
828    /// Read the mesh ID immediately and return it as the name in an
829    /// already-completed future. No creation wait or queue item is needed.
830    fn name(&self) -> BoxFuture<'static, PyResult<String>> {
831        let name = self.id().to_string();
832        future::ready(Ok(name)).boxed()
833    }
834}
835
836#[pymethods]
837impl PythonActorMeshImpl {
838    fn get(&self, rank: usize) -> PyResult<Option<PyActorAddr>> {
839        Ok(self
840            .mesh_ref()
841            .get(rank)
842            .map(|r| hyperactor::ActorRef::into_actor_addr(r.clone()))
843            .map(PyActorAddr::from))
844    }
845
846    fn __repr__(&self) -> String {
847        format!("PythonActorMeshImpl({:?})", self.mesh_ref())
848    }
849}
850
851#[pyfunction]
852fn py_actor_mesh_from_bytes(bytes: &Bound<'_, PyBytes>) -> PyResult<PythonActorMesh> {
853    let r: PyResult<ActorMeshRef<PythonActor>> =
854        bincode::serde::decode_from_slice(bytes.as_bytes(), bincode::config::legacy())
855            .map(|(v, _)| v)
856            .map_err(|e| PyValueError::new_err(e.to_string()));
857    r.map(|r| AsyncActorMesh::from_impl(Arc::new(PythonActorMeshImpl::new_ref(r))))
858        .map(|r| PythonActorMesh::from_impl(Arc::from(r)))
859}
860
861#[pyclass(
862    name = "ActorSupervisionEvent",
863    module = "monarch._rust_bindings.monarch_hyperactor.actor_mesh"
864)]
865#[derive(Debug)]
866pub struct PyActorSupervisionEvent {
867    inner: ActorSupervisionEvent,
868}
869
870#[pymethods]
871impl PyActorSupervisionEvent {
872    pub(crate) fn __repr__(&self) -> PyResult<String> {
873        Ok(format!("<PyActorSupervisionEvent: {}>", self.inner))
874    }
875
876    #[getter]
877    pub(crate) fn actor_id(&self) -> PyResult<PyActorAddr> {
878        Ok(PyActorAddr::from(self.inner.actor_id.clone()))
879    }
880
881    #[getter]
882    pub(crate) fn actor_status(&self) -> PyResult<String> {
883        Ok(self.inner.actor_status.to_string())
884    }
885}
886
887impl From<ActorSupervisionEvent> for PyActorSupervisionEvent {
888    fn from(event: ActorSupervisionEvent) -> Self {
889        PyActorSupervisionEvent { inner: event }
890    }
891}
892
893#[pyfunction]
894fn py_identity(obj: Py<PyAny>) -> PyResult<Py<PyAny>> {
895    Ok(obj)
896}
897
898/// Holds the GIL for the specified number of seconds without releasing it.
899///
900/// This is a test utility function that spawns a background thread which
901/// acquires the GIL using Rust's Python::attach and holds it for the
902/// specified duration using thread::sleep. Unlike Python code which
903/// periodically releases the GIL, this function holds it continuously.
904///
905/// We intentionally use `std::thread::sleep` here (not `Clock::sleep` or async sleep)
906/// because the purpose is to simulate a blocking operation that holds the GIL without
907/// releasing it. Using an async sleep would release the GIL periodically, defeating
908/// the purpose of this test utility.
909///
910/// Args:
911///     delay_secs: Seconds to wait before acquiring the GIL
912///     hold_secs: Seconds to hold the GIL
913#[pyfunction]
914#[pyo3(name = "hold_gil_for_test", signature = (delay_secs, hold_secs))]
915pub fn hold_gil_for_test(delay_secs: f64, hold_secs: f64) {
916    thread::spawn(move || {
917        // Wait before grabbing the GIL (blocking sleep is fine here, we're in a spawned thread)
918        thread::sleep(Duration::from_secs_f64(delay_secs));
919        // Acquire and hold the GIL - MUST use blocking sleep to keep GIL held
920        monarch_with_gil_blocking(GilSite::Test, |_py| {
921            tracing::info!("start holding the gil...");
922            thread::sleep(Duration::from_secs_f64(hold_secs));
923            tracing::info!("end holding the gil...");
924        });
925    });
926}
927
928pub fn register_python_bindings(hyperactor_mod: &Bound<'_, PyModule>) -> PyResult<()> {
929    py_module_add_function!(
930        hyperactor_mod,
931        "monarch._rust_bindings.monarch_hyperactor.actor_mesh",
932        py_identity
933    );
934    py_module_add_function!(
935        hyperactor_mod,
936        "monarch._rust_bindings.monarch_hyperactor.actor_mesh",
937        py_actor_mesh_from_bytes
938    );
939    py_module_add_function!(
940        hyperactor_mod,
941        "monarch._rust_bindings.monarch_hyperactor.actor_mesh",
942        hold_gil_for_test
943    );
944    hyperactor_mod.add_class::<PythonActorMesh>()?;
945    hyperactor_mod.add_class::<PythonActorMeshImpl>()?;
946    hyperactor_mod.add_class::<PyActorSupervisionEvent>()?;
947    Ok(())
948}
949
950#[cfg(test)]
951mod tests {
952    use std::cell::RefCell;
953    use std::sync::atomic::AtomicU8;
954    use std::sync::atomic::AtomicUsize;
955    use std::sync::atomic::Ordering;
956    use std::sync::mpsc::Receiver;
957    use std::sync::mpsc::SyncSender;
958    use std::task::Context;
959    use std::task::Poll;
960    use std::time::Duration;
961
962    use hyperactor::ActorRef;
963    use hyperactor::Proc;
964    use hyperactor::ProcAddr;
965    use hyperactor::ProcId;
966    use hyperactor::channel::ChannelAddr;
967    use hyperactor::id::Label;
968    use hyperactor::id::Uid;
969    use hyperactor_mesh::host_mesh::HostMeshRef;
970    use hyperactor_mesh::mesh_id::ActorMeshId;
971    use hyperactor_mesh::mesh_id::HostMeshId;
972    use ndslice::extent;
973    use pyo3::PyTypeInfo;
974    use tokio::sync::watch;
975
976    use super::*;
977
978    /// A mesh id no implementation could return by accident: `instance` draws a
979    /// fresh uid, so a hard-coded name cannot match it.
980    fn fixture_mesh_id() -> ActorMeshId {
981        ActorMeshId::instance(Label::new("mesh_name_fixture").expect("test label should be valid"))
982    }
983
984    /// A managed `ActorMeshRef<PythonActor>` carrying `id`, obtained through the
985    /// public deserializer.
986    ///
987    /// `ActorMeshRef::new_managed` is not public outside `hyperactor_mesh`.
988    /// Starting a real mesh merely to obtain this value would introduce runtime
989    /// resources, so this fixture instead uses the public serde representation.
990    /// `HostMeshRef::from_hosts` is a synchronous constructor whose embedded
991    /// host-agent mesh is managed with no controller. The actor type occurs in
992    /// the managed wire form only in that absent controller field, which makes
993    /// the serialized value safe to decode as `ActorMeshRef<PythonActor>`. No
994    /// host is contacted and the local address is never connected.
995    ///
996    /// `ActorMeshRefRepr::Managed` is externally tagged and serializes as the
997    /// four-element tuple `(id, proc_mesh_id, controller, cast_domain)`. The
998    /// `HostMeshRef` constructor supplies a valid value for every element, but
999    /// always uses the fixed `host_agent` id. The fixture replaces tuple element
1000    /// zero only; otherwise a name implementation incorrectly hard-coded to
1001    /// `host_agent` would satisfy the test.
1002    ///
1003    /// The field lookup, variant lookup, and arity assertion below deliberately
1004    /// make this dependency on the wire format explicit. A field, tag, or tuple
1005    /// layout change fails before mutation, and the post-decode assertion proves
1006    /// that the public deserializer retained the caller-selected id.
1007    fn managed_mesh_ref(id: &ActorMeshId) -> ActorMeshRef<PythonActor> {
1008        let host_mesh = HostMeshRef::from_hosts(
1009            HostMeshId::singleton(Label::strip("managed_name_fixture")),
1010            vec![ChannelAddr::Local(1)],
1011        );
1012        let mut wire = serde_json::to_value(&host_mesh).expect("HostMeshRef should serialize");
1013        let payload = wire
1014            .get_mut("host_agent_mesh")
1015            .and_then(|mesh| mesh.get_mut("Managed"))
1016            .and_then(serde_json::Value::as_array_mut)
1017            .expect("the host agent mesh should encode as the managed variant");
1018        assert_eq!(
1019            payload.len(),
1020            4,
1021            "the managed payload should be the (id, proc mesh, controller, cast domain) tuple"
1022        );
1023        payload[0] = serde_json::to_value(id).expect("ActorMeshId should serialize");
1024
1025        let mesh_ref: ActorMeshRef<PythonActor> =
1026            serde_json::from_value(wire["host_agent_mesh"].clone())
1027                .expect("the host agent mesh should decode as an ActorMeshRef");
1028        assert_eq!(
1029            mesh_ref
1030                .as_managed()
1031                .expect("the fixture must decode as the managed variant")
1032                .id(),
1033            id,
1034            "the fixture must carry the caller-selected id, not the constructor default"
1035        );
1036        mesh_ref
1037    }
1038
1039    /// A one-rank data `ActorMeshRef<PythonActor>` carrying `id`. Its single
1040    /// member is never messaged.
1041    fn data_mesh_ref(id: &ActorMeshId) -> ActorMeshRef<PythonActor> {
1042        let proc_addr = ProcAddr::new(
1043            ProcId::new(
1044                Uid::Instance(1, None),
1045                Some(Label::new("local").expect("test label should be valid")),
1046            ),
1047            ChannelAddr::Local(1).into(),
1048        );
1049        let member: ActorRef<PythonActor> = ActorRef::attest(proc_addr.actor_addr("member"));
1050        ActorMeshRef::try_new_data(id.clone(), extent!(members = 1).into(), vec![member])
1051            .expect("a one-rank data mesh should be valid")
1052    }
1053
1054    /// Drive the native name future to completion.
1055    fn drive_name(future: BoxFuture<'static, PyResult<String>>) -> String {
1056        get_tokio_runtime()
1057            .block_on(future)
1058            .expect("the name future should resolve")
1059    }
1060
1061    // Both managed name wrappers yield the mesh id, and discarding a wrapper
1062    // consumes nothing: a later wrapper reports the same id.
1063    //
1064    // What these assertions establish and what they do not. They prove the
1065    // value is repeatable across a discarded observation. They do NOT prove
1066    // dynamically that the id is read at call time rather than inside the
1067    // returned future; an implementation that cloned the mesh and read the id
1068    // when driven would pass too. Call-time computation is source-grounded --
1069    // both bodies compute the string and return a ready Rust future. The
1070    // Python binding converts the result separately through `PyHandle::spawn`.
1071    #[test]
1072    fn discarded_managed_name_wrappers_leave_exact_name_repeatable() {
1073        pyo3::Python::initialize();
1074        let id = fixture_mesh_id();
1075        let mesh_ref = managed_mesh_ref(&id);
1076        let mesh_impl = PythonActorMeshImpl::new_ref(mesh_ref.clone());
1077        let expected = id.to_string();
1078
1079        drop(<ActorMeshRef<PythonActor> as ActorMeshProtocol>::name(
1080            &mesh_ref,
1081        ));
1082        drop(<PythonActorMeshImpl as ActorMeshProtocol>::name(&mesh_impl));
1083
1084        assert_eq!(
1085            drive_name(<ActorMeshRef<PythonActor> as ActorMeshProtocol>::name(
1086                &mesh_ref
1087            )),
1088            expected,
1089            "discarding a ref-wrapper task must not disturb the next one"
1090        );
1091        assert_eq!(
1092            drive_name(<PythonActorMeshImpl as ActorMeshProtocol>::name(&mesh_impl)),
1093            expected,
1094            "discarding an impl-wrapper task must not disturb the next one"
1095        );
1096    }
1097
1098    // Both data name wrappers yield the mesh id, as on the managed path.
1099    #[test]
1100    fn data_name_wrappers_report_mesh_id() {
1101        pyo3::Python::initialize();
1102        let id = fixture_mesh_id();
1103        let mesh_ref = data_mesh_ref(&id);
1104        let mesh_impl = PythonActorMeshImpl::new_ref(mesh_ref.clone());
1105        let expected = id.to_string();
1106
1107        assert_eq!(
1108            drive_name(<ActorMeshRef<PythonActor> as ActorMeshProtocol>::name(
1109                &mesh_ref
1110            )),
1111            expected,
1112            "the ref wrapper must report the data mesh id"
1113        );
1114        assert_eq!(
1115            drive_name(<PythonActorMeshImpl as ActorMeshProtocol>::name(&mesh_impl)),
1116            expected,
1117            "the impl wrapper must report the data mesh id"
1118        );
1119    }
1120
1121    // The trait-default `initialized` wrapper resolves to None and leaves the
1122    // mesh usable, on both implementors that inherit it.
1123    //
1124    // What this establishes and what it does not. The absence of domain work is
1125    // a property of the default's source body, `future::ready(Ok(None))`, not
1126    // something these assertions measure: driving through `block_on` would also
1127    // pass if the future yielded or did unrelated non-mutating work first, and
1128    // `PyHandle::spawn` converts its result through `monarch_with_gil`, which
1129    // may wait on the process-global GIL lock. So this is not a first-poll-ready
1130    // claim. Re-reading the name afterwards shows the mesh is still usable; it
1131    // is not a work counter.
1132    //
1133    // No production caller reaches this default today. Normal `PythonActorMesh`
1134    // construction and reconstruction use `AsyncActorMesh`, which overrides it.
1135    //
1136    // One alternate route exists. When the mesh is still pending, and only on
1137    // the non-reserved branch, `AsyncActorMesh::__reduce__` returns
1138    // `(Shared.block_on, (shared,))` over an observer that builds a wrapper
1139    // directly around the resolved implementation. Ordinary pickle reduces that
1140    // value again into an async-wrapped reference, but a caller that invokes
1141    // `Shared.block_on` itself can retain the bare wrapper and make
1142    // `PythonActorMesh::initialized` dispatch here. Under active Monarch
1143    // pickling the callable is `pop_mesh_reference` instead, which reconstructs
1144    // async-wrapped, so that branch cannot expose the wrapper at all.
1145    //
1146    // The route is source-grounded rather than exercised below, and it does not
1147    // depend on which arm resolved: neither `PythonActorMeshImpl` nor
1148    // `ActorMeshRef` overrides `initialized`. Both inheriting implementors are
1149    // exercised directly because no production caller was found.
1150    #[test]
1151    fn default_initialized_wrappers_return_none_and_leave_mesh_usable() {
1152        pyo3::Python::initialize();
1153        let id = fixture_mesh_id();
1154        let mesh_ref = managed_mesh_ref(&id);
1155        let mesh_impl = PythonActorMeshImpl::new_ref(mesh_ref.clone());
1156
1157        drop(<ActorMeshRef<PythonActor> as ActorMeshProtocol>::initialized(&mesh_ref));
1158        drop(<PythonActorMeshImpl as ActorMeshProtocol>::initialized(
1159            &mesh_impl,
1160        ));
1161
1162        for (future, which) in [
1163            (
1164                <ActorMeshRef<PythonActor> as ActorMeshProtocol>::initialized(&mesh_ref),
1165                "ref",
1166            ),
1167            (
1168                <PythonActorMeshImpl as ActorMeshProtocol>::initialized(&mesh_impl),
1169                "impl",
1170            ),
1171        ] {
1172            let handle = PyHandle::spawn(future);
1173            let value = get_tokio_runtime()
1174                .block_on(handle.wait_future())
1175                .expect("the default initialized Handle should resolve");
1176            assert!(
1177                monarch_with_gil_blocking(GilSite::Test, |py| value.is_none(py)),
1178                "the default {which} initialized wrapper must resolve to None"
1179            );
1180        }
1181
1182        assert_eq!(
1183            drive_name(<PythonActorMeshImpl as ActorMeshProtocol>::name(&mesh_impl)),
1184            id.to_string(),
1185            "observing the default initialized wrapper must leave the mesh usable"
1186        );
1187    }
1188
1189    // Controlled actor-mesh fixture
1190
1191    /// What `ControlledActorMesh::__reduce__` reconstructs to. `str` is a real
1192    /// builtin, so the reduce tuple survives a genuine `pickle.dumps`/`loads`
1193    /// round trip; `py_identity` could not, since its declared module is not
1194    /// installed in a Rust unit binary.
1195    const CONTROLLED_MARKER: &str = "controlled-actor-mesh-reconstructed";
1196
1197    /// Signals for one reduction observer: the future built inside
1198    /// `AsyncActorMesh::__reduce__`.
1199    struct ReductionProbe {
1200        entered: SyncSender<()>,
1201        dropped: SyncSender<()>,
1202    }
1203
1204    thread_local! {
1205        /// Armed by a test immediately before it calls `__reduce__`, and
1206        /// consumed there. Caller-thread scoped: a process-global slot would be
1207        /// unusable in a binary that runs tests in parallel.
1208        static REDUCTION_PROBE: RefCell<Option<ReductionProbe>> = const { RefCell::new(None) };
1209    }
1210
1211    /// Restores the previous probe when dropped, so a run that takes the ready
1212    /// branch -- or fails before `__reduce__` -- cannot leak an armed probe into
1213    /// the next test on this thread. Serial test mode puts every test on the
1214    /// main thread, so the leak is reachable without the guard.
1215    struct ArmedProbe {
1216        previous: Option<ReductionProbe>,
1217    }
1218
1219    impl Drop for ArmedProbe {
1220        fn drop(&mut self) {
1221            let previous = self.previous.take();
1222            REDUCTION_PROBE.with(|slot| *slot.borrow_mut() = previous);
1223        }
1224    }
1225
1226    #[must_use]
1227    fn arm_reduction_probe(probe: ReductionProbe) -> ArmedProbe {
1228        let previous = REDUCTION_PROBE.with(|slot| slot.borrow_mut().replace(probe));
1229        ArmedProbe { previous }
1230    }
1231
1232    /// Wraps the reduction observer, reporting its first poll and its
1233    /// destruction. With no probe armed it is a transparent pass-through.
1234    pub(super) struct ProbedObserver {
1235        inner: Option<Pin<Box<dyn Future<Output = PyResult<PythonActorMesh>> + Send>>>,
1236        probe: Option<ReductionProbe>,
1237        entered: bool,
1238        completed: bool,
1239    }
1240
1241    pub(super) fn probe_reduction_observer<F>(inner: F) -> ProbedObserver
1242    where
1243        F: Future<Output = PyResult<PythonActorMesh>> + Send + 'static,
1244    {
1245        ProbedObserver {
1246            inner: Some(Box::pin(inner)),
1247            probe: REDUCTION_PROBE.with(|slot| slot.borrow_mut().take()),
1248            entered: false,
1249            completed: false,
1250        }
1251    }
1252
1253    impl Future for ProbedObserver {
1254        type Output = PyResult<PythonActorMesh>;
1255
1256        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1257            let this = self.get_mut();
1258            let polled = match this.inner.as_mut() {
1259                Some(inner) => inner.as_mut().poll(cx),
1260                None => Poll::Pending,
1261            };
1262            if polled.is_ready() {
1263                this.completed = true;
1264            }
1265            // Signalled after the inner poll returns, so the entry signal
1266            // witnesses an actual poll of the observer rather than mere entry
1267            // into this wrapper.
1268            if !this.entered {
1269                this.entered = true;
1270                if let Some(probe) = &this.probe {
1271                    let _ = probe.entered.try_send(());
1272                }
1273            }
1274            polled
1275        }
1276    }
1277
1278    impl Drop for ProbedObserver {
1279        fn drop(&mut self) {
1280            // Destroy the wrapped future -- and with it the `Shared` clone it
1281            // captured -- before acknowledging. `Drop::drop` runs ahead of field
1282            // destruction, so signalling here without the explicit take would
1283            // leave open the cancellation race this probe exists to close.
1284            drop(self.inner.take());
1285            // Only a destruction while still pending is cancellation. Firing on
1286            // normal completion too would let an observer that simply finished
1287            // satisfy an assertion about abandonment.
1288            if let Some(probe) = self.probe.take()
1289                && !self.completed
1290            {
1291                let _ = probe.dropped.try_send(());
1292            }
1293        }
1294    }
1295
1296    /// Release gate for a controlled root initialization.
1297    struct Gate {
1298        release: watch::Sender<bool>,
1299        entered: SyncSender<()>,
1300    }
1301
1302    impl Gate {
1303        fn new(entered: SyncSender<()>) -> (Arc<Self>, watch::Receiver<bool>) {
1304            let (release, rx) = watch::channel(false);
1305            (Arc::new(Self { release, entered }), rx)
1306        }
1307
1308        fn open(&self) {
1309            let _ = self.release.send(true);
1310        }
1311    }
1312
1313    /// The fixed result a controlled operation yields once it is released.
1314    #[derive(Default)]
1315    enum OpOutcome {
1316        #[default]
1317        Succeed,
1318        /// Resolves to a `PyValueError` carrying exactly this message.
1319        Fail(&'static str),
1320    }
1321
1322    /// Opt-in control over one `ControlledActorMesh` operation.
1323    ///
1324    /// `Default` is no entry signal, no gate, immediate success. It is not
1325    /// literally inert: `run` always increments `completed`, so a defaulted
1326    /// operation still performs one sequentially consistent atomic increment
1327    /// that nothing reads. Every externally visible property -- value, error, and timing --
1328    /// matches returning the value directly, which is what lets a caller that
1329    /// does not care about ordering ignore the control entirely.
1330    #[derive(Default)]
1331    struct OpControl {
1332        /// `None` leaves the operation ungated.
1333        release: Option<watch::Receiver<bool>>,
1334        /// `None` emits no entry acknowledgement.
1335        entered: Option<SyncSender<()>>,
1336        /// Incremented once per released run, after the wait and before the
1337        /// outcome is produced.
1338        completed: Arc<AtomicUsize>,
1339        outcome: OpOutcome,
1340    }
1341
1342    impl OpControl {
1343        /// Gated: acknowledges entry, then waits for `gate` before completing.
1344        fn gated(gate: &Arc<Gate>, release: watch::Receiver<bool>, outcome: OpOutcome) -> Self {
1345            Self {
1346                release: Some(release),
1347                entered: Some(gate.entered.clone()),
1348                completed: Arc::new(AtomicUsize::new(0)),
1349                outcome,
1350            }
1351        }
1352
1353        /// Ungated but still counted, for a subcase that needs a fixed outcome
1354        /// without an ordering rendezvous.
1355        fn immediate(outcome: OpOutcome) -> Self {
1356            Self {
1357                outcome,
1358                ..Self::default()
1359            }
1360        }
1361
1362        fn completions(&self) -> usize {
1363            self.completed.load(Ordering::SeqCst)
1364        }
1365
1366        /// Acknowledge entry, wait for release, count the completion, then
1367        /// yield the fixed outcome.
1368        ///
1369        /// The acknowledgement is emitted before the wait, so an observer of it
1370        /// learns that the caller reached this operation -- not that it
1371        /// finished. The counter is incremented after the wait, so a reader
1372        /// that has not synchronized with completion can legitimately see zero.
1373        async fn run(&self) -> PyResult<()> {
1374            if let Some(entered) = &self.entered {
1375                let _ = entered.try_send(());
1376            }
1377            if let Some(release) = &self.release {
1378                let mut release = release.clone();
1379                while !*release.borrow() {
1380                    if release.changed().await.is_err() {
1381                        break;
1382                    }
1383                }
1384            }
1385            self.completed.fetch_add(1, Ordering::SeqCst);
1386            match self.outcome {
1387                OpOutcome::Succeed => Ok(()),
1388                OpOutcome::Fail(message) => Err(PyValueError::new_err(message)),
1389            }
1390        }
1391    }
1392
1393    /// The per-operation controls a controlled mesh carries. Both default to no
1394    /// gate, no entry signal and immediate success -- externally
1395    /// indistinguishable from returning the value directly, though `run` still
1396    /// performs its unread completion increment. Both are shared with every
1397    /// slice derived from the mesh.
1398    #[derive(Default, Clone)]
1399    struct ControlledOps {
1400        name: Arc<OpControl>,
1401        stop: Arc<OpControl>,
1402    }
1403
1404    /// A closed `SupervisableActorMesh`: fixed results and internal gates only,
1405    /// never Python callables, coroutines or caller-supplied work.
1406    struct ControlledActorMesh {
1407        name: String,
1408        slices: Arc<AtomicUsize>,
1409        ops: ControlledOps,
1410    }
1411
1412    impl ActorMeshProtocol for ControlledActorMesh {
1413        fn cast(
1414            &self,
1415            _message: PythonMessage,
1416            _selection: AllOrChoose,
1417            _instance: &Instance<PythonActor>,
1418        ) -> PyResult<()> {
1419            unreachable!("the controlled mesh has no cast path")
1420        }
1421
1422        fn __reduce__<'py>(
1423            &self,
1424            py: Python<'py>,
1425        ) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> {
1426            // Reached by the bare-pickle test: `reduce_shared` resolves the
1427            // observer to a `PythonActorMesh` over this implementation and
1428            // pickle then recurses into it. Deliberately not production's
1429            // `self.mesh_ref().__reduce__(py)`, which would need a managed ref
1430            // and exercise bincode to no purpose here.
1431            let ctor = py.get_type::<pyo3::types::PyString>().into_any();
1432            let args = PyTuple::new(py, [CONTROLLED_MARKER])?;
1433            Ok((ctor, args.into_any()))
1434        }
1435
1436        fn mesh_ref(&self) -> PyResult<ActorMeshRef<PythonActor>> {
1437            // Mirrors the backstop `AsyncActorMesh::mesh_ref` uses for a mesh
1438            // with no serializable ref of its own. Nothing here needs it.
1439            Err(PyRuntimeError::new_err(
1440                "the controlled mesh has no serializable reference",
1441            ))
1442        }
1443
1444        fn name(&self) -> BoxFuture<'static, PyResult<String>> {
1445            let name = self.name.clone();
1446            let ops = self.ops.name.clone();
1447            async move {
1448                ops.run().await?;
1449                Ok(name)
1450            }
1451            .boxed()
1452        }
1453
1454        /// Resolves to `()`, mirroring the owned production arm, whose
1455        /// `ActorMesh::stop` returns `Result<()>`. On the Python side that is
1456        /// an empty tuple, not `None`: pyo3 defines `IntoPyObject for ()` as
1457        /// `PyTuple::empty`. Without this method the trait default returns
1458        /// `PyNotImplementedError` synchronously, and a queued caller's `?`
1459        /// would short-circuit before any inner work ran.
1460        fn stop(
1461            &self,
1462            _instance: &PyInstance,
1463            _reason: String,
1464        ) -> PyResult<BoxFuture<'static, PyResult<()>>> {
1465            let ops = self.ops.stop.clone();
1466            Ok(async move {
1467                ops.run().await?;
1468                Ok(())
1469            }
1470            .boxed())
1471        }
1472    }
1473
1474    #[async_trait]
1475    impl Supervisable for ControlledActorMesh {
1476        async fn supervision_event(&self, _instance: &Instance<PythonActor>) -> Option<PyErr> {
1477            std::future::pending().await
1478        }
1479    }
1480
1481    impl SupervisableActorMesh for ControlledActorMesh {
1482        fn new_with_region(&self, _region: &PyRegion) -> PyResult<Box<dyn SupervisableActorMesh>> {
1483            self.slices.fetch_add(1, Ordering::SeqCst);
1484            Ok(Box::new(ControlledActorMesh {
1485                name: format!("{}-slice", self.name),
1486                slices: self.slices.clone(),
1487                // Shared, not defaulted: a slice that reverted to the default
1488                // control would have no gate and no entry signal, making any
1489                // ordering measurement taken through it vacuous.
1490                ops: self.ops.clone(),
1491            }))
1492        }
1493    }
1494
1495    /// The controlled root future: reports entry, waits for the gate, then
1496    /// resolves to a `ControlledActorMesh`.
1497    fn controlled_root(
1498        gate: &Arc<Gate>,
1499        mut release: watch::Receiver<bool>,
1500        slices: Option<Arc<AtomicUsize>>,
1501        ops: ControlledOps,
1502    ) -> ActorMeshFut {
1503        let entered = gate.entered.clone();
1504        let slices = slices.unwrap_or_default();
1505        async move {
1506            let _ = entered.try_send(());
1507            while !*release.borrow() {
1508                if release.changed().await.is_err() {
1509                    break;
1510                }
1511            }
1512            Ok::<Arc<dyn SupervisableActorMesh>, ClonePyErr>(Arc::new(ControlledActorMesh {
1513                name: "controlled".to_string(),
1514                slices,
1515                ops,
1516            }))
1517        }
1518        .boxed()
1519        .shared()
1520    }
1521
1522    /// The production queue loop, with its `JoinHandle` retained.
1523    ///
1524    /// `AsyncActorMesh::new_queue` spawns exactly this loop and discards the
1525    /// handle; keeping it is the only difference, and production is not
1526    /// modified.
1527    fn test_queue() -> (
1528        UnboundedSender<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>,
1529        tokio::task::JoinHandle<()>,
1530    ) {
1531        let (queue, mut recv) = unbounded_channel();
1532        let driver = get_tokio_runtime().spawn(async move {
1533            loop {
1534                let r = recv.recv().await;
1535                if let Some(r) = r {
1536                    r.await;
1537                } else {
1538                    return;
1539                }
1540            }
1541        });
1542        (queue, driver)
1543    }
1544
1545    /// Neither release permission nor a `dumps` return has claimed the phase.
1546    const PHASE_RUNNING: u8 = 0;
1547    /// The helper observed the blocking acknowledgement first.
1548    const PHASE_RELEASE_PERMITTED: u8 = 1;
1549    /// `pickle.dumps` returned first, which the bare-pickle test rejects.
1550    const PHASE_DUMPS_RETURNED: u8 = 2;
1551
1552    /// Bound shared by every wait here: long enough that a healthy run never
1553    /// reaches it, short enough that a stranded one fails instead of hanging.
1554    const WAIT: Duration = Duration::from_secs(30);
1555
1556    fn signal() -> (SyncSender<()>, Receiver<()>) {
1557        std::sync::mpsc::sync_channel(1)
1558    }
1559
1560    /// Bounded wait; returns false on timeout so a test can clean up before
1561    /// asserting rather than unwinding with gates still shut.
1562    fn awaited(rx: &Receiver<()>) -> bool {
1563        rx.recv_timeout(WAIT).is_ok()
1564    }
1565
1566    /// Drive a native future under a bound. Returns `None` on timeout so a
1567    /// stranded `Shared` fails a named assertion rather than blocking forever
1568    /// and taking the whole test binary with it.
1569    fn drive_bounded<T>(future: BoxFuture<'static, PyResult<T>>) -> Option<T> {
1570        get_tokio_runtime()
1571            .block_on(async { tokio::time::timeout(WAIT, future).await })
1572            .ok()
1573            .map(|resolved| resolved.expect("the future should resolve"))
1574    }
1575
1576    fn bounded_name(future: BoxFuture<'static, PyResult<String>>) -> Option<String> {
1577        drive_bounded(future)
1578    }
1579
1580    /// Owns every release gate and the retained queue driver for one test.
1581    ///
1582    /// The success path calls `join`, which drops the driver handle and waits
1583    /// for the loop to exit once its senders close. `Drop` is the failure path
1584    /// only: on unwind it opens every gate still shut and aborts a driver still
1585    /// held, so an assertion failure cannot leave the runtime wedged. `Drop`
1586    /// cannot await, which is why it aborts rather than joins -- `abort()` only
1587    /// requests cancellation, so the guarantee is a bounded return, not that no
1588    /// task outlives the test.
1589    ///
1590    /// Ownership of the operation gates is what makes that promise reachable: a
1591    /// gate held only by the test body stays shut on an unwinding path, and the
1592    /// future waiting on it never wakes.
1593    struct Teardown {
1594        gate: Arc<Gate>,
1595        op_gates: Vec<Arc<Gate>>,
1596        driver: Option<tokio::task::JoinHandle<()>>,
1597    }
1598
1599    impl Teardown {
1600        fn new(gate: Arc<Gate>, driver: tokio::task::JoinHandle<()>) -> Self {
1601            Self {
1602                gate,
1603                op_gates: Vec::new(),
1604                driver: Some(driver),
1605            }
1606        }
1607
1608        /// Hand an operation gate to the teardown so every exit path opens it.
1609        #[must_use]
1610        fn with_op_gate(mut self, gate: Arc<Gate>) -> Self {
1611            self.op_gates.push(gate);
1612            self
1613        }
1614
1615        /// Open the root gate and every operation gate. Idempotent.
1616        fn open_all(&self) {
1617            self.gate.open();
1618            for gate in &self.op_gates {
1619                gate.open();
1620            }
1621        }
1622
1623        /// True only if the loop returned normally. A panicked or aborted task
1624        /// yields `Ok(Err(JoinError))`, which the outer `is_ok()` alone would
1625        /// accept.
1626        fn join(&mut self) -> bool {
1627            let Some(driver) = self.driver.as_mut() else {
1628                return false;
1629            };
1630            let outcome =
1631                get_tokio_runtime().block_on(async { tokio::time::timeout(WAIT, driver).await });
1632            match outcome {
1633                // Only a normal return counts. A panicked or aborted task
1634                // yields `Ok(Err(JoinError))`, which an outer `is_ok()` alone
1635                // would accept.
1636                Ok(result) => {
1637                    self.driver.take();
1638                    result.is_ok()
1639                }
1640                // Wedged. Request cancellation and wait once more rather than
1641                // dropping the handle, which would only detach. The second wait
1642                // can itself elapse, so this attempts a bounded reap; it does
1643                // not establish one.
1644                Err(_) => {
1645                    if let Some(driver) = self.driver.take() {
1646                        driver.abort();
1647                        let _ = get_tokio_runtime()
1648                            .block_on(async { tokio::time::timeout(WAIT, driver).await });
1649                    }
1650                    false
1651                }
1652            }
1653        }
1654
1655        /// `join` for a caller already inside a runtime.
1656        async fn join_async(&mut self) -> bool {
1657            let Some(driver) = self.driver.as_mut() else {
1658                return false;
1659            };
1660            match tokio::time::timeout(WAIT, driver).await {
1661                Ok(result) => {
1662                    self.driver.take();
1663                    result.is_ok()
1664                }
1665                Err(_) => {
1666                    if let Some(driver) = self.driver.take() {
1667                        driver.abort();
1668                        let _ = tokio::time::timeout(WAIT, driver).await;
1669                    }
1670                    false
1671                }
1672            }
1673        }
1674    }
1675
1676    impl Drop for Teardown {
1677        fn drop(&mut self) {
1678            self.open_all();
1679            if let Some(driver) = self.driver.take() {
1680                driver.abort();
1681            }
1682        }
1683    }
1684
1685    // Ordinary bare pickle of a pending mesh blocks until initialization
1686    // resolves.
1687    //
1688    // The release is driven by an acknowledgement emitted inside
1689    // `PyShared::block_on` once it has committed to blocking, not by the
1690    // controlled future's first poll: `__reduce__` calls `spawn_abortable()`
1691    // before returning, so that first poll happens while pickle is still on the
1692    // outer reducer, and releasing there would let the producer resolve in time
1693    // for `reduce_shared`'s ready fast path -- passing this test without ever
1694    // blocking.
1695    //
1696    // A plain `#[test]`: `PyShared::block_on` enters `signal_safe_block_on` for
1697    // a pending value, and calling that from inside a Tokio runtime
1698    // characterizes nested-runtime failure instead of bare-pickle behavior.
1699    #[test]
1700    fn pending_bare_actor_mesh_pickle_waits_for_release() {
1701        pyo3::Python::initialize();
1702        monarch_with_gil_blocking(GilSite::Test, |py| {
1703            PyPythonTask::install_test_module(py).expect("the pytokio module should install")
1704        });
1705
1706        let (entered_tx, entered_rx) = signal();
1707        let (gate, release) = Gate::new(entered_tx);
1708        let (queue, driver) = test_queue();
1709        // No init item is pushed: the reduction observer is the only driver.
1710        let mesh = AsyncActorMesh::new(
1711            queue,
1712            true,
1713            controlled_root(&gate, release, None, ControlledOps::default()),
1714        );
1715        let mut teardown = Teardown::new(gate.clone(), driver);
1716        let mesh = monarch_with_gil_blocking(GilSite::Test, |py| {
1717            Py::new(py, PythonActorMesh::from_impl(Arc::new(mesh)))
1718                .expect("the mesh wrapper should construct")
1719        });
1720
1721        // A CAS rather than a sampled flag: exactly one of "release was
1722        // permitted" and "dumps returned" claims the phase, so a return racing
1723        // the read cannot satisfy the assertion below. This guards that `dumps`
1724        // did not return before release permission; it does not show where the
1725        // acknowledgement hook sits. That the hook fires after `block_on`'s own
1726        // pending poll and before `signal_safe_block_on` is source-grounded,
1727        // not established here.
1728        let phase = Arc::new(AtomicU8::new(PHASE_RUNNING));
1729        let (ack_tx, ack_rx) = signal();
1730        let helper_gate = gate.clone();
1731        let helper_phase = phase.clone();
1732        let helper = std::thread::spawn(move || {
1733            let acknowledged = ack_rx.recv_timeout(WAIT).is_ok();
1734            let release_won = helper_phase
1735                .compare_exchange(
1736                    PHASE_RUNNING,
1737                    PHASE_RELEASE_PERMITTED,
1738                    Ordering::SeqCst,
1739                    Ordering::SeqCst,
1740                )
1741                .is_ok();
1742            // Opened on every path, timeout included, so the pickling call can
1743            // never be stranded.
1744            helper_gate.open();
1745            (acknowledged, release_won)
1746        });
1747
1748        let blob = monarch_with_gil_blocking(GilSite::Test, |py| {
1749            let _routing = PyPythonTask::route_pending_block_on(ack_tx);
1750            let pickle = py.import("pickle").expect("pickle should import");
1751            let blob = pickle
1752                .call_method1("dumps", (mesh.clone_ref(py),))
1753                .expect("pickling a pending mesh should succeed once released")
1754                .unbind();
1755            let _ = phase.compare_exchange(
1756                PHASE_RUNNING,
1757                PHASE_DUMPS_RETURNED,
1758                Ordering::SeqCst,
1759                Ordering::SeqCst,
1760            );
1761            blob
1762        });
1763
1764        let (acknowledged, release_won) =
1765            helper.join().expect("the helper thread should not panic");
1766
1767        let restored = monarch_with_gil_blocking(GilSite::Test, |py| {
1768            let pickle = py.import("pickle").expect("pickle should import");
1769            pickle
1770                .call_method1("loads", (blob.bind(py),))
1771                .expect("the pickled mesh should load")
1772                .extract::<String>()
1773                .expect("the fixture reconstructs to its marker")
1774        });
1775
1776        // Dropped with the GIL held: a `Py<_>` released without it only queues
1777        // the decref, so the wrapper -- and the queue sender inside it -- would
1778        // stay alive and the driver would never see its senders close.
1779        monarch_with_gil_blocking(GilSite::Test, move |_py| {
1780            drop(mesh);
1781            drop(blob);
1782        });
1783        let joined = teardown.join();
1784
1785        assert!(
1786            awaited(&entered_rx),
1787            "the controlled root must have been polled by the reduction observer"
1788        );
1789        assert!(
1790            acknowledged,
1791            "pickling must reach the pending branch of PyShared::block_on"
1792        );
1793        assert!(
1794            release_won,
1795            "pickle.dumps must not return before release permission is granted"
1796        );
1797        assert_eq!(
1798            restored, CONTROLLED_MARKER,
1799            "the recursive controlled reducer must have run after the blocking branch"
1800        );
1801        assert!(
1802            joined,
1803            "the queue driver must return once its senders close"
1804        );
1805    }
1806
1807    /// Call the pending reducer and prove the pending branch exactly: the real
1808    /// `Shared.block_on` descriptor plus exactly one real `Shared` argument.
1809    fn pending_reduce(py: Python<'_>, mesh: &dyn ActorMeshProtocol) -> Py<PyAny> {
1810        let (callable, args) = mesh.__reduce__(py).expect("__reduce__ should succeed");
1811        let expected = shared_class(py)
1812            .getattr("block_on")
1813            .expect("Shared.block_on should resolve");
1814        assert!(
1815            callable.is(&expected),
1816            "a pending mesh must reduce through the real Shared.block_on descriptor"
1817        );
1818        let args = args
1819            .cast_into::<PyTuple>()
1820            .expect("the reducer arguments should be a tuple");
1821        assert_eq!(args.len(), 1, "exactly one observer argument is expected");
1822        let observer = args.get_item(0).expect("the tuple has one item");
1823        assert!(
1824            observer
1825                .is_instance(&shared_class(py))
1826                .expect("instance check should succeed"),
1827            "the reducer argument must be a real Shared"
1828        );
1829        args.into_any().unbind()
1830    }
1831
1832    // Dropping a transient reduction observer does not cancel initialization.
1833    // The root keeps its own queued initializer, so
1834    // it completes on its own and stays usable.
1835    //
1836    // The completion signal is emitted by the queued initializer *after* its
1837    // `f.await` returns, and is awaited before any replacement observer is
1838    // created. Without that ordering the fresh `initialized()` could be what
1839    // drove initialization and the test would prove nothing.
1840    #[test]
1841    fn dropped_root_observers_do_not_cancel_initialization() {
1842        pyo3::Python::initialize();
1843        monarch_with_gil_blocking(GilSite::Test, |py| {
1844            PyPythonTask::install_test_module(py).expect("the pytokio module should install")
1845        });
1846
1847        let (entered_tx, _entered_rx) = signal();
1848        let (gate, release) = Gate::new(entered_tx);
1849        let (queue, driver) = test_queue();
1850        let mesh = AsyncActorMesh::new(
1851            queue,
1852            true,
1853            controlled_root(&gate, release, None, ControlledOps::default()),
1854        );
1855        let mut teardown = Teardown::new(gate.clone(), driver);
1856
1857        // The independent root initializer, equivalent to the one `new_queue`
1858        // pushes, plus the completion signal after `f.await` returns. Enqueued
1859        // here rather than taken from `new_queue` because the test needs the
1860        // driver's `JoinHandle`, which `new_queue` discards.
1861        //
1862        // Evidence boundary: this pins that abandonment does not cancel an
1863        // already-enqueued initializer. That production supplies that item is
1864        // source-grounded -- `new_queue` pushes `async { let _ = f.await; }` --
1865        // and is not what these assertions establish.
1866        let (completed_tx, completed_rx) = signal();
1867        let root = mesh.mesh.clone();
1868        mesh.push(async move {
1869            let _ = root.await;
1870            let _ = completed_tx.try_send(());
1871        });
1872
1873        let (probe_entered_tx, probe_entered_rx) = signal();
1874        let (probe_dropped_tx, probe_dropped_rx) = signal();
1875        let _probe = arm_reduction_probe(ReductionProbe {
1876            entered: probe_entered_tx,
1877            dropped: probe_dropped_tx,
1878        });
1879        let tuple = monarch_with_gil_blocking(GilSite::Test, |py| pending_reduce(py, &mesh));
1880
1881        let observed = awaited(&probe_entered_rx);
1882        // Dropped with the GIL held. Releasing a `Py<_>` without it only queues
1883        // the decref, so the tuple's `Shared` would not be freed, its
1884        // abort-on-drop would not fire, and the acknowledgement below would wait
1885        // on a future that is still alive.
1886        monarch_with_gil_blocking(GilSite::Test, move |_py| drop(tuple));
1887        let acknowledged = awaited(&probe_dropped_rx);
1888
1889        gate.open();
1890        let completed = awaited(&completed_rx);
1891
1892        // Only drive replacements once the prerequisite holds. Falling through
1893        // on a failed prerequisite would block on a `Shared` that can never
1894        // resolve, turning the regression this test exists to catch into a hang
1895        // that kills the whole binary.
1896        let readiness = completed.then(|| drive_bounded(mesh.initialized()));
1897        let name = completed.then(|| bounded_name(mesh.name()));
1898
1899        drop(mesh);
1900        let joined = teardown.join();
1901
1902        assert!(
1903            observed,
1904            "the reduction observer must be polled before it is dropped"
1905        );
1906        assert!(
1907            acknowledged,
1908            "the reduction observer must be destroyed while still pending"
1909        );
1910        assert!(
1911            completed,
1912            "the queue-held root initializer must complete on its own after release"
1913        );
1914        let readiness = readiness.flatten().expect("readiness must resolve in time");
1915        assert!(
1916            readiness.is_none(),
1917            "a replacement readiness observer resolves to None"
1918        );
1919        assert_eq!(
1920            name.flatten().as_deref(),
1921            Some("controlled"),
1922            "the resolved implementation stays usable"
1923        );
1924        assert!(
1925            joined,
1926            "the queue driver must return once its senders close"
1927        );
1928    }
1929
1930    // A slice holds a derived computation with no queue item of its own, so
1931    // discarding its observers leaves it dormant and re-observable.
1932    //
1933    // Boundary of this claim: `new_with_region` enqueues no independent
1934    // initializer, but a later `stop` or cast enqueues an await, and name,
1935    // readiness and `supervision_event` await the derived `Shared` directly. These assertions
1936    // therefore prove dormancy only after the named observers are discarded and
1937    // while no other mesh operation occurs. They do not prove that an
1938    // `initialized` observer is the only thing that can ever drive a slice.
1939    #[test]
1940    fn dropped_slice_observers_leave_the_slice_reobservable() {
1941        pyo3::Python::initialize();
1942        monarch_with_gil_blocking(GilSite::Test, |py| {
1943            PyPythonTask::install_test_module(py).expect("the pytokio module should install")
1944        });
1945
1946        let slices = Arc::new(AtomicUsize::new(0));
1947        let (entered_tx, _entered_rx) = signal();
1948        let (gate, release) = Gate::new(entered_tx);
1949        let (queue, driver) = test_queue();
1950        let mesh = AsyncActorMesh::new(
1951            queue,
1952            true,
1953            controlled_root(
1954                &gate,
1955                release,
1956                Some(slices.clone()),
1957                ControlledOps::default(),
1958            ),
1959        );
1960        let mut teardown = Teardown::new(gate.clone(), driver);
1961
1962        // The independent root initializer, equivalent to the one `new_queue`
1963        // pushes, plus the completion signal after `f.await` returns. Enqueued
1964        // here rather than taken from `new_queue` because the test needs the
1965        // driver's `JoinHandle`, which `new_queue` discards.
1966        //
1967        // Evidence boundary: this pins that abandonment does not cancel an
1968        // already-enqueued initializer. That production supplies that item is
1969        // source-grounded -- `new_queue` pushes `async { let _ = f.await; }` --
1970        // and is not what these assertions establish.
1971        let (completed_tx, completed_rx) = signal();
1972        let root = mesh.mesh.clone();
1973        mesh.push(async move {
1974            let _ = root.await;
1975            let _ = completed_tx.try_send(());
1976        });
1977
1978        let region: PyRegion = ndslice::Region::from(extent!(slices = 1)).into();
1979        let slice = mesh
1980            .new_with_region(&region)
1981            .expect("slicing a pending mesh should succeed");
1982
1983        let (probe_entered_tx, probe_entered_rx) = signal();
1984        let (probe_dropped_tx, probe_dropped_rx) = signal();
1985        let _probe = arm_reduction_probe(ReductionProbe {
1986            entered: probe_entered_tx,
1987            dropped: probe_dropped_tx,
1988        });
1989        let tuple =
1990            monarch_with_gil_blocking(GilSite::Test, |py| pending_reduce(py, slice.as_ref()));
1991
1992        let observed = awaited(&probe_entered_rx);
1993        monarch_with_gil_blocking(GilSite::Test, move |_py| drop(tuple));
1994        let acknowledged = awaited(&probe_dropped_rx);
1995
1996        gate.open();
1997        let completed = awaited(&completed_rx);
1998        let dormant = slices.load(Ordering::SeqCst);
1999
2000        let readiness = completed.then(|| drive_bounded(slice.initialized()));
2001        let driven = slices.load(Ordering::SeqCst);
2002        let name = completed.then(|| bounded_name(slice.name()));
2003
2004        drop(slice);
2005        drop(mesh);
2006        let joined = teardown.join();
2007
2008        assert!(
2009            observed,
2010            "the slice reduction observer must be polled before it is dropped"
2011        );
2012        assert!(
2013            acknowledged,
2014            "the slice reduction observer must be destroyed while still pending"
2015        );
2016        assert!(completed, "the root must complete on its own after release");
2017        assert_eq!(
2018            dormant, 0,
2019            "the slice has no queue item, so discarding its observers leaves it undriven"
2020        );
2021        let readiness = readiness.flatten().expect("readiness must resolve in time");
2022        assert!(
2023            readiness.is_none(),
2024            "a replacement slice observer resolves to None"
2025        );
2026        assert_eq!(
2027            driven, 1,
2028            "the replacement observer drives the slice exactly once"
2029        );
2030        assert_eq!(
2031            name.flatten().as_deref(),
2032            Some("controlled-slice"),
2033            "the retained slice stays usable"
2034        );
2035        assert!(
2036            joined,
2037            "the queue driver must return once its senders close"
2038        );
2039    }
2040
2041    // Name and stop completion and abandonment
2042
2043    /// The fixed message a controlled operation fails with, so a test can
2044    /// assert the exact error text rather than merely that an error arrived.
2045    const CONTROLLED_FAILURE: &str = "controlled operation failed";
2046
2047    /// The message the `Ref` arm rejects with. Pinned here so a wording change
2048    /// in production fails this test rather than silently weakening it.
2049    const REF_STOP_REJECTION: &str =
2050        "Cannot call stop on an ActorMeshRef, requires an owned ActorMesh";
2051
2052    /// A real `PyInstance` over an isolated proc.
2053    ///
2054    /// `actor_instance` spawns detached introspect tasks, so every caller must
2055    /// be a `#[tokio::test]`: cleanup is the per-test runtime being dropped, and
2056    /// building this on the shared runtime would leak those tasks for the life
2057    /// of the test binary.
2058    fn isolated_py_instance(name: &str) -> PyInstance {
2059        let instance = Proc::isolated()
2060            .actor_instance::<PythonActor>(name)
2061            .expect("an isolated proc should host a client instance")
2062            .instance;
2063        PyInstance::from(instance)
2064    }
2065
2066    /// A controlled mesh over a retained queue driver.
2067    ///
2068    /// The teardown owns the root gate, every operation gate passed here, and
2069    /// the driver, so an unwinding assertion cannot strand a future behind a
2070    /// gate that only the test body held.
2071    fn controlled_fixture(
2072        ops: ControlledOps,
2073        op_gates: &[Arc<Gate>],
2074    ) -> (AsyncActorMesh, Arc<Gate>, Receiver<()>, Teardown) {
2075        let (root_entered_tx, root_entered_rx) = signal();
2076        let (root_gate, release) = Gate::new(root_entered_tx);
2077        let (queue, driver) = test_queue();
2078        let mesh =
2079            AsyncActorMesh::new(queue, true, controlled_root(&root_gate, release, None, ops));
2080        let mut teardown = Teardown::new(root_gate.clone(), driver);
2081        for gate in op_gates {
2082            teardown = teardown.with_op_gate(gate.clone());
2083        }
2084        (mesh, root_gate, root_entered_rx, teardown)
2085    }
2086
2087    /// Drive the producer as far as the inner operation, reporting what
2088    /// was observed instead of asserting it.
2089    ///
2090    /// Returns `(root entered, operation entered)`. Both waits are bounded, and
2091    /// neither is asserted here: a failed wait must not unwind while the gates
2092    /// are shut and the driver is live, because `Teardown::Drop` only requests
2093    /// cancellation. Callers collect this evidence, run their cleanup, and
2094    /// assert afterwards.
2095    ///
2096    /// The order is fixed and load-bearing. This seam builds the mesh with
2097    /// `AsyncActorMesh::new`, which enqueues no initializer, so nothing polls
2098    /// the root future until a queued stop or a name Handle awaits it. With the
2099    /// root gate still shut the producer parks there and never reaches the inner
2100    /// method, so the operation acknowledgement would never arrive. Opening the
2101    /// root gate only after the root acknowledgement is what makes the
2102    /// operation acknowledgement mean "entered the inner method".
2103    fn reach_inner_operation(
2104        root_entered: &Receiver<()>,
2105        root_gate: &Arc<Gate>,
2106        op_entered: &Receiver<()>,
2107    ) -> (bool, bool) {
2108        let reached_root = awaited(root_entered);
2109        root_gate.open();
2110        (reached_root, awaited(op_entered))
2111    }
2112
2113    /// Drop the returned completion future or Handle. For a queued stop,
2114    /// dropping the Rust future immediately closes its oneshot receiver.
2115    fn discard_observer<T>(observer: PyResult<T>) -> PyResult<()> {
2116        observer.map(drop)
2117    }
2118
2119    /// Everything observing a Handle can produce, as data rather than as a
2120    /// panic. Nothing here asserts: a subcase collects an outcome, runs its
2121    /// cleanup, and only then decides whether it is the expected one.
2122    enum DriveOutcome {
2123        /// The call returned an error instead of an observer.
2124        NotConstructed(PyErr),
2125        /// The observer did not resolve within `WAIT`.
2126        TimedOut,
2127        Resolved(PyResult<Py<PyAny>>),
2128    }
2129
2130    impl DriveOutcome {
2131        /// The resolved result, or a description of why there is not one.
2132        fn resolved(self, context: &str) -> PyResult<Py<PyAny>> {
2133            match self {
2134                DriveOutcome::Resolved(result) => result,
2135                DriveOutcome::NotConstructed(err) => {
2136                    panic!("{context}: the wrapper returned no observer: {err:?}")
2137                }
2138                DriveOutcome::TimedOut => panic!("{context}: the observer did not resolve"),
2139            }
2140        }
2141    }
2142
2143    /// Observe a Handle under a bound without asserting before cleanup.
2144    fn drive_evidence(handle: PyResult<PyHandle>) -> DriveOutcome {
2145        get_tokio_runtime().block_on(drive_evidence_async(handle))
2146    }
2147
2148    /// `drive_evidence` for a caller already inside a runtime.
2149    async fn drive_evidence_async(handle: PyResult<PyHandle>) -> DriveOutcome {
2150        let handle = match handle {
2151            Ok(handle) => handle,
2152            Err(err) => return DriveOutcome::NotConstructed(err),
2153        };
2154        match tokio::time::timeout(WAIT, handle.wait_future()).await {
2155            Ok(resolved) => DriveOutcome::Resolved(resolved),
2156            Err(_) => DriveOutcome::TimedOut,
2157        }
2158    }
2159
2160    /// Enqueue a barrier behind the work already queued and wait for it.
2161    ///
2162    /// The queue runs one item at a time to completion, so observing the
2163    /// barrier proves the preceding closure finished -- including its
2164    /// `tx.send`. That is a rendezvous with the send, not a delay.
2165    ///
2166    /// Sends on the queue directly rather than through `push`, whose
2167    /// `send(..).unwrap()` would panic before the caller's cleanup if the
2168    /// driver had already died. A closed queue is reported, not raised.
2169    fn queue_barrier(mesh: &AsyncActorMesh) -> bool {
2170        let (passed_tx, passed_rx) = signal();
2171        let queued = mesh
2172            .queue
2173            .send(
2174                async move {
2175                    let _ = passed_tx.try_send(());
2176                }
2177                .boxed(),
2178            )
2179            .is_ok();
2180        queued && awaited(&passed_rx)
2181    }
2182
2183    /// Require an error to be exactly `E`, by type identity rather than by
2184    /// subclass or message, and to carry exactly `message`.
2185    fn assert_exact_error<E: PyTypeInfo>(err: &PyErr, message: &str, context: &str) {
2186        monarch_with_gil_blocking(GilSite::Test, |py| {
2187            assert!(
2188                err.get_type(py).is(py.get_type::<E>()),
2189                "{context}: expected exactly {}, got {}",
2190                std::any::type_name::<E>(),
2191                err.get_type(py),
2192            );
2193            assert_eq!(
2194                err.value(py).to_string(),
2195                message,
2196                "{context}: unexpected error message"
2197            );
2198        });
2199    }
2200
2201    // Name waits directly on the mesh, outside the cast/stop queue. A native
2202    // Handle drives that wait even when its last observer is dropped. A
2203    // retained observer receives the inner value or error verbatim.
2204    //
2205    // Every subcase collects evidence, opens its gates, closes the queue and
2206    // joins the driver before asserting. Name completion is acknowledged
2207    // separately: a queue barrier cannot prove off-queue work has finished.
2208    //
2209    // A plain `#[test]`: no subcase needs a `PyInstance`, so nothing here
2210    // requires a per-test runtime and the sync helpers apply.
2211    #[test]
2212    fn async_name_survives_a_dropped_observer_and_leaves_queue_usable() {
2213        pyo3::Python::initialize();
2214
2215        // Retained observer, success: the inner value reaches the caller.
2216        {
2217            let name_ops = Arc::new(OpControl::immediate(OpOutcome::Succeed));
2218            let (mesh, root_gate, root_entered, mut teardown) = controlled_fixture(
2219                ControlledOps {
2220                    name: name_ops.clone(),
2221                    stop: Arc::default(),
2222                },
2223                &[],
2224            );
2225
2226            let handle = PyHandle::spawn(<AsyncActorMesh as ActorMeshProtocol>::name(&mesh));
2227            let reached_root = awaited(&root_entered);
2228            root_gate.open();
2229            let outcome = drive_evidence(Ok(handle));
2230
2231            teardown.open_all();
2232            drop(mesh);
2233            let joined = teardown.join();
2234
2235            assert!(
2236                reached_root,
2237                "the name producer should reach root initialization"
2238            );
2239            let value = outcome
2240                .resolved("retained name success")
2241                .expect("the controlled name should succeed");
2242            let observed = monarch_with_gil_blocking(GilSite::Test, move |py| {
2243                value
2244                    .extract::<String>(py)
2245                    .expect("name() should resolve to a string")
2246            });
2247            assert_eq!(
2248                observed, "controlled",
2249                "a retained observer must receive the inner name"
2250            );
2251            assert_eq!(
2252                name_ops.completions(),
2253                1,
2254                "the inner operation must run exactly once"
2255            );
2256            assert!(joined, "the driver must return once its senders close");
2257        }
2258
2259        // Retained observer, error: the inner error reaches the caller intact.
2260        {
2261            let name_ops = Arc::new(OpControl::immediate(OpOutcome::Fail(CONTROLLED_FAILURE)));
2262            let (mesh, root_gate, root_entered, mut teardown) = controlled_fixture(
2263                ControlledOps {
2264                    name: name_ops.clone(),
2265                    stop: Arc::default(),
2266                },
2267                &[],
2268            );
2269
2270            let handle = PyHandle::spawn(<AsyncActorMesh as ActorMeshProtocol>::name(&mesh));
2271            let reached_root = awaited(&root_entered);
2272            root_gate.open();
2273            let outcome = drive_evidence(Ok(handle));
2274
2275            teardown.open_all();
2276            drop(mesh);
2277            let joined = teardown.join();
2278
2279            assert!(
2280                reached_root,
2281                "the name producer should reach root initialization"
2282            );
2283            let err = outcome
2284                .resolved("retained name error")
2285                .expect_err("the fixed-error control must surface as an error");
2286            assert_exact_error::<PyValueError>(&err, CONTROLLED_FAILURE, "retained name error");
2287            assert_eq!(
2288                name_ops.completions(),
2289                1,
2290                "the failing inner operation must still run exactly once"
2291            );
2292            assert!(joined, "the driver must return once its senders close");
2293        }
2294
2295        // Last Handle dropped after completion: the result need not be read.
2296        {
2297            let name_ops = Arc::new(OpControl::immediate(OpOutcome::Succeed));
2298            let (mesh, root_gate, root_entered, mut teardown) = controlled_fixture(
2299                ControlledOps {
2300                    name: name_ops.clone(),
2301                    stop: Arc::default(),
2302                },
2303                &[],
2304            );
2305            let (completed_tx, completed_rx) = signal();
2306            let future = mesh.name();
2307            let handle = PyHandle::spawn(async move {
2308                let result = future.await;
2309                let _ = completed_tx.try_send(());
2310                result
2311            });
2312            let reached_root = awaited(&root_entered);
2313            root_gate.open();
2314            let completed = awaited(&completed_rx);
2315            drop(handle);
2316            let usable = queue_barrier(&mesh);
2317            teardown.open_all();
2318            drop(mesh);
2319            let joined = teardown.join();
2320
2321            assert!(reached_root, "the name producer must reach initialization");
2322            assert!(completed, "name must complete before its Handle is dropped");
2323            assert_eq!(name_ops.completions(), 1);
2324            assert!(usable, "later queue work must still run");
2325            assert!(joined, "the queue driver must return normally");
2326        }
2327
2328        // Last Handle dropped while name is pending. The queue can serve work
2329        // before the name gate opens, and name still completes after release.
2330        {
2331            let (op_entered_tx, op_entered) = signal();
2332            let (op_gate, op_release) = Gate::new(op_entered_tx);
2333            let name_ops = Arc::new(OpControl::gated(&op_gate, op_release, OpOutcome::Succeed));
2334            let (mesh, root_gate, root_entered, mut teardown) = controlled_fixture(
2335                ControlledOps {
2336                    name: name_ops.clone(),
2337                    stop: Arc::default(),
2338                },
2339                std::slice::from_ref(&op_gate),
2340            );
2341            let (completed_tx, completed_rx) = signal();
2342            let future = mesh.name();
2343            let handle = PyHandle::spawn(async move {
2344                let result = future.await;
2345                let _ = completed_tx.try_send(());
2346                result
2347            });
2348            let (reached_root, entered_op) =
2349                reach_inner_operation(&root_entered, &root_gate, &op_entered);
2350            drop(handle);
2351            let usable = queue_barrier(&mesh);
2352            teardown.open_all();
2353            let completed = awaited(&completed_rx);
2354            drop(mesh);
2355            let joined = teardown.join();
2356
2357            assert!(reached_root, "the name producer must reach initialization");
2358            assert!(
2359                entered_op,
2360                "name must be pending when its Handle is dropped"
2361            );
2362            assert!(usable, "name must not hold up the cast/stop queue");
2363            assert!(completed, "unobserved name must complete after release");
2364            assert_eq!(name_ops.completions(), 1);
2365            assert!(joined, "the queue driver must return normally");
2366        }
2367    }
2368
2369    // Stop keeps its queue position and its pre-enqueue instance clone under
2370    // `GilSite::Stop`. A retained Handle receives the inner error verbatim or
2371    // the current owned-stop result: an empty tuple, since pyo3 converts Rust
2372    // `()` through `PyTuple::empty`.
2373    //
2374    // Dropping the returned Rust future closes the oneshot receiver, but the
2375    // queued stop must still finish and permit later queue work. Test that
2376    // inner boundary directly; a dropped Handle alone would leave its producer
2377    // holding the receiver and would not exercise the failed `tx.send`.
2378    //
2379    // A `#[tokio::test]`: `stop` needs a real `PyInstance`, and `actor_instance`
2380    // spawns detached introspect tasks that the per-test runtime reaps. That in
2381    // turn rules out the `block_on` helpers, so this test uses the async
2382    // siblings throughout.
2383    #[tokio::test]
2384    async fn async_stop_survives_a_dropped_observer_and_leaves_queue_usable() {
2385        pyo3::Python::initialize();
2386        let instance = isolated_py_instance("stop_characterization_client");
2387
2388        // Retained observer, success: the caller sees the unit conversion.
2389        {
2390            let stop_ops = Arc::new(OpControl::immediate(OpOutcome::Succeed));
2391            let (mesh, root_gate, root_entered, mut teardown) = controlled_fixture(
2392                ControlledOps {
2393                    name: Arc::default(),
2394                    stop: stop_ops.clone(),
2395                },
2396                &[],
2397            );
2398
2399            let task = <AsyncActorMesh as ActorMeshProtocol>::stop(
2400                &mesh,
2401                &instance,
2402                "characterization".to_string(),
2403            );
2404            let reached_root = awaited(&root_entered);
2405            root_gate.open();
2406            let outcome = drive_evidence_async(task.map(PyHandle::spawn)).await;
2407
2408            teardown.open_all();
2409            drop(mesh);
2410            let joined = teardown.join_async().await;
2411
2412            assert!(
2413                reached_root,
2414                "the queued closure should reach root initialization"
2415            );
2416            let value = outcome
2417                .resolved("retained stop success")
2418                .expect("the controlled stop should succeed");
2419            monarch_with_gil_blocking(GilSite::Test, move |py| {
2420                // An empty tuple, not None: pyo3 converts the unit type with
2421                // `IntoPyObject for () -> PyTuple::empty`, and the fixture
2422                // mirrors production's `Result<()>` exactly.
2423                let bound = value.bind(py);
2424                let tuple = bound
2425                    .cast::<PyTuple>()
2426                    .expect("the unit result should convert to a tuple");
2427                assert!(
2428                    tuple.is_empty(),
2429                    "a successful owned stop resolves to the unit conversion, the empty tuple"
2430                );
2431            });
2432            assert_eq!(
2433                stop_ops.completions(),
2434                1,
2435                "the inner operation must run exactly once"
2436            );
2437            assert!(joined, "the driver must return once its senders close");
2438        }
2439
2440        // Retained observer, error: the inner error reaches the caller intact.
2441        {
2442            let stop_ops = Arc::new(OpControl::immediate(OpOutcome::Fail(CONTROLLED_FAILURE)));
2443            let (mesh, root_gate, root_entered, mut teardown) = controlled_fixture(
2444                ControlledOps {
2445                    name: Arc::default(),
2446                    stop: stop_ops.clone(),
2447                },
2448                &[],
2449            );
2450
2451            let task = <AsyncActorMesh as ActorMeshProtocol>::stop(
2452                &mesh,
2453                &instance,
2454                "characterization".to_string(),
2455            );
2456            let reached_root = awaited(&root_entered);
2457            root_gate.open();
2458            let outcome = drive_evidence_async(task.map(PyHandle::spawn)).await;
2459
2460            teardown.open_all();
2461            drop(mesh);
2462            let joined = teardown.join_async().await;
2463
2464            assert!(
2465                reached_root,
2466                "the queued closure should reach root initialization"
2467            );
2468            let err = outcome
2469                .resolved("retained stop error")
2470                .expect_err("the fixed-error control must surface as an error");
2471            assert_exact_error::<PyValueError>(&err, CONTROLLED_FAILURE, "retained stop error");
2472            assert_eq!(
2473                stop_ops.completions(),
2474                1,
2475                "the failing inner operation must still run exactly once"
2476            );
2477            assert!(joined, "the driver must return once its senders close");
2478        }
2479
2480        // Completion future dropped after the send. The serial queue barrier
2481        // proves the stop closure, including its send, has finished.
2482        {
2483            let stop_ops = Arc::new(OpControl::immediate(OpOutcome::Succeed));
2484            let (mesh, root_gate, root_entered, mut teardown) = controlled_fixture(
2485                ControlledOps {
2486                    name: Arc::default(),
2487                    stop: stop_ops.clone(),
2488                },
2489                &[],
2490            );
2491
2492            let task = <AsyncActorMesh as ActorMeshProtocol>::stop(
2493                &mesh,
2494                &instance,
2495                "characterization".to_string(),
2496            );
2497            let reached_root = awaited(&root_entered);
2498            root_gate.open();
2499            let sent = queue_barrier(&mesh);
2500            let construction = discard_observer(task);
2501
2502            teardown.open_all();
2503            drop(mesh);
2504            let joined = teardown.join_async().await;
2505
2506            assert!(
2507                reached_root,
2508                "the queued closure should reach root initialization"
2509            );
2510            assert!(sent, "the barrier should prove the stop closure completed");
2511            assert!(
2512                construction.is_ok(),
2513                "the outer observer must have been constructed: {:?}",
2514                construction.err()
2515            );
2516            assert_eq!(
2517                stop_ops.completions(),
2518                1,
2519                "the inner operation must run exactly once"
2520            );
2521            assert!(
2522                joined,
2523                "destroying an unpolled observer after a successful send must not panic the driver"
2524            );
2525        }
2526
2527        // Observer destroyed while the operation is still pending.
2528        {
2529            let (op_entered_tx, op_entered) = signal();
2530            let (op_gate, op_release) = Gate::new(op_entered_tx);
2531            let stop_ops = Arc::new(OpControl::gated(&op_gate, op_release, OpOutcome::Succeed));
2532            let (mesh, root_gate, root_entered, mut teardown) = controlled_fixture(
2533                ControlledOps {
2534                    name: Arc::default(),
2535                    stop: stop_ops.clone(),
2536                },
2537                std::slice::from_ref(&op_gate),
2538            );
2539
2540            let task = <AsyncActorMesh as ActorMeshProtocol>::stop(
2541                &mesh,
2542                &instance,
2543                "characterization".to_string(),
2544            );
2545            let (reached_root, entered_op) =
2546                reach_inner_operation(&root_entered, &root_gate, &op_entered);
2547            let construction = discard_observer(task);
2548
2549            teardown.open_all();
2550            let sent = queue_barrier(&mesh);
2551            drop(mesh);
2552            let joined = teardown.join_async().await;
2553
2554            assert!(
2555                reached_root,
2556                "the queued closure should reach root initialization"
2557            );
2558            assert!(
2559                entered_op,
2560                "the queued closure should enter the inner operation"
2561            );
2562            assert!(
2563                construction.is_ok(),
2564                "the outer observer must have been constructed: {:?}",
2565                construction.err()
2566            );
2567            assert!(
2568                sent,
2569                "later queue work must run after the closed-receiver send"
2570            );
2571            assert!(joined, "the queue driver must return normally");
2572            assert_eq!(
2573                stop_ops.completions(),
2574                1,
2575                "the inner stop must finish exactly once despite losing its observer"
2576            );
2577        }
2578    }
2579
2580    // Asking a `PythonActorMeshImpl::Ref` to stop is rejected by the inner arm
2581    // itself, with no future returned: there is nothing to observe, nothing to
2582    // drop, and no queue involved.
2583    //
2584    // The assertions cover what is returned. "No future was returned" is read
2585    // off the source -- the `Ref` arm returns before constructing its future --
2586    // and is not proven here, because an `Err` return
2587    // cannot distinguish "never constructed" from "constructed and dropped".
2588    // The arm is also not side-effect free: `stop` clones `self` and the
2589    // instance under `GilSite::Stop` before the match, so this rejection still
2590    // takes the GIL once.
2591    //
2592    // This is the inner arm only. A deserialized public ref is wrapped in
2593    // `AsyncActorMesh`, so `ActorMesh.stop()` on one still enqueues and surfaces
2594    // this error through an outer observer instead of at the call.
2595    //
2596    // A `#[tokio::test]` for the same reason as the queued stop test: the
2597    // signature takes a `&PyInstance`.
2598    #[tokio::test]
2599    async fn actor_mesh_impl_ref_stop_rejects_before_returning_a_future() {
2600        pyo3::Python::initialize();
2601        // Built outside the assertion boundary, so a fixture failure cannot
2602        // satisfy the rejection assertion below.
2603        let mesh_impl = PythonActorMeshImpl::new_ref(data_mesh_ref(&fixture_mesh_id()));
2604        let instance = isolated_py_instance("ref_stop_characterization_client");
2605
2606        let rejected = <PythonActorMeshImpl as ActorMeshProtocol>::stop(
2607            &mesh_impl,
2608            &instance,
2609            "characterization".to_string(),
2610        );
2611
2612        let Err(err) = rejected else {
2613            panic!("the Ref arm must reject rather than return a future");
2614        };
2615        assert_exact_error::<PyNotImplementedError>(&err, REF_STOP_REJECTION, "Ref stop rejection");
2616    }
2617}