1use std::cell::RefCell;
50use std::collections::VecDeque;
51use std::sync::atomic::AtomicUsize;
52use std::sync::atomic::Ordering;
53
54use hyperactor::Instance;
55use monarch_types::py_global;
56use pyo3::IntoPyObjectExt;
57use pyo3::prelude::*;
58use pyo3::types::PyList;
59use pyo3::types::PyTuple;
60use serde_multipart::Part;
61
62use crate::actor::MeshRef;
63use crate::actor::PyMeshRef;
64use crate::actor::PythonActor;
65use crate::actor::PythonMessage;
66use crate::actor::PythonMessageKind;
67use crate::buffers::Buffer;
68use crate::context::PyInstance;
69use crate::handle::PyHandle;
70use crate::pytokio::PyShared;
71use crate::runtime::GilSite;
72use crate::runtime::monarch_with_gil_blocking;
73
74py_global!(cloudpickle, "cloudpickle", "cloudpickle");
76
77py_global!(_unpickle, "pickle", "loads");
78
79py_global!(
85 pickle_monkeypatch,
86 "monarch._src.actor.pickle",
87 "_function_getstate"
88);
89
90py_global!(maybe_torch_fn, "monarch._src.actor.pickle", "maybe_torch");
93
94py_global!(torch_dump_fn, "monarch._src.actor.pickle", "torch_dump");
97
98py_global!(torch_loads_fn, "monarch._src.actor.pickle", "torch_loads");
101
102py_global!(
104 shared_class,
105 "monarch._rust_bindings.monarch_hyperactor.pytokio",
106 "Shared"
107);
108
109thread_local! {
121 static ACTIVE_PICKLING_STATE: RefCell<Option<ActivePicklingState>> = const { RefCell::new(None) };
122}
123
124static PENDING_RESERVE_COUNT: AtomicUsize = AtomicUsize::new(0);
131static MESH_POP_COUNT: AtomicUsize = AtomicUsize::new(0);
132
133struct ActivePicklingGuard {
138 previous: Option<ActivePicklingState>,
139}
140
141impl ActivePicklingGuard {
142 fn enter(state: ActivePicklingState) -> Self {
144 let previous = ACTIVE_PICKLING_STATE.with(|cell| cell.borrow_mut().replace(state));
145 Self { previous }
146 }
147}
148
149impl Drop for ActivePicklingGuard {
150 fn drop(&mut self) {
151 ACTIVE_PICKLING_STATE.with(|cell| {
152 *cell.borrow_mut() = self.previous.take();
153 });
154 }
155}
156
157thread_local! {
158 static RECEIVER_INSTANCE: RefCell<Option<Instance<PythonActor>>> =
170 const { RefCell::new(None) };
171}
172
173struct ReceiverInstanceGuard {
179 previous: Option<Instance<PythonActor>>,
180}
181
182impl ReceiverInstanceGuard {
183 fn enter(instance: &Instance<PythonActor>) -> Self {
188 let previous =
189 RECEIVER_INSTANCE.with(|cell| cell.borrow_mut().replace(instance.clone_for_py()));
190 Self { previous }
191 }
192}
193
194impl Drop for ReceiverInstanceGuard {
195 fn drop(&mut self) {
196 RECEIVER_INSTANCE.with(|cell| {
197 *cell.borrow_mut() = self.previous.take();
198 });
199 }
200}
201
202fn current_receiver_instance_raw() -> Option<Instance<PythonActor>> {
207 RECEIVER_INSTANCE.with(|cell| cell.borrow().as_ref().map(|i| i.clone_for_py()))
208}
209
210#[pyfunction]
215fn _current_receiver_instance() -> Option<PyInstance> {
216 current_receiver_instance_raw().map(PyInstance::from)
220}
221
222struct ActivePicklingState {
227 tensor_engine_references: VecDeque<Py<PyAny>>,
229 mesh_references: VecDeque<Option<MeshRef>>,
232 pending_mesh_fills: Vec<(usize, Py<PyShared>)>,
235 allow_tensor_engine_references: bool,
237 allow_mesh_references: bool,
239}
240
241impl ActivePicklingState {
242 fn new(allow_tensor_engine_references: bool, allow_mesh_references: bool) -> Self {
244 Self {
245 tensor_engine_references: VecDeque::new(),
246 mesh_references: VecDeque::new(),
247 pending_mesh_fills: Vec::new(),
248 allow_tensor_engine_references,
249 allow_mesh_references,
250 }
251 }
252
253 fn into_pickling_state(self, buffer: Part) -> PicklingStateInner {
255 PicklingStateInner {
256 buffer,
257 tensor_engine_references: self.tensor_engine_references,
258 mesh_references: self.mesh_references,
259 pending_mesh_fills: self.pending_mesh_fills,
260 }
261 }
262}
263
264pub struct PicklingStateInner {
269 buffer: Part,
271 tensor_engine_references: VecDeque<Py<PyAny>>,
273 mesh_references: VecDeque<Option<MeshRef>>,
276 pending_mesh_fills: Vec<(usize, Py<PyShared>)>,
278}
279
280impl PicklingStateInner {
281 pub fn take_buffer(self) -> Part {
283 self.buffer
284 }
285
286 pub fn payload_len(&self) -> usize {
288 self.buffer.len()
289 }
290}
291
292#[pyclass(module = "monarch._rust_bindings.monarch_hyperactor.pickle")]
297pub struct PicklingState {
298 inner: Option<PicklingStateInner>,
299}
300
301impl PicklingState {
302 pub fn take_inner(&mut self) -> PyResult<PicklingStateInner> {
303 self.inner.take().ok_or_else(|| {
304 pyo3::exceptions::PyRuntimeError::new_err("PicklingState has already been consumed")
305 })
306 }
307
308 fn inner_ref(&self) -> PyResult<&PicklingStateInner> {
309 self.inner.as_ref().ok_or_else(|| {
310 pyo3::exceptions::PyRuntimeError::new_err("PicklingState has already been consumed")
311 })
312 }
313
314 fn has_pending_mesh_fills(&self) -> PyResult<bool> {
315 Ok(!self.inner_ref()?.pending_mesh_fills.is_empty())
316 }
317
318 pub(crate) fn from_parts(
323 buffer: Part,
324 tensor_engine_references: VecDeque<Py<PyAny>>,
325 mesh_references: VecDeque<Option<MeshRef>>,
326 ) -> Self {
327 Self {
328 inner: Some(PicklingStateInner {
329 buffer,
330 tensor_engine_references,
331 mesh_references,
332 pending_mesh_fills: Vec::new(),
333 }),
334 }
335 }
336}
337
338#[pymethods]
339impl PicklingState {
340 #[new]
345 #[pyo3(signature = (buffer, tensor_engine_references=None, mesh_references=None))]
346 fn py_new(
347 py: Python<'_>,
348 buffer: PyRef<'_, crate::buffers::FrozenBuffer>,
349 tensor_engine_references: Option<&Bound<'_, PyList>>,
350 mesh_references: Option<Vec<Py<PyMeshRef>>>,
351 ) -> PyResult<Self> {
352 let refs: VecDeque<Py<PyAny>> = tensor_engine_references
353 .map(|list| list.iter().map(|item| item.unbind()).collect())
354 .unwrap_or_default();
355
356 let mesh_refs: VecDeque<Option<MeshRef>> = mesh_references
358 .map(|list| {
359 list.into_iter()
360 .map(|m| Some(m.borrow(py).inner.clone()))
361 .collect()
362 })
363 .unwrap_or_default();
364
365 Ok(Self {
366 inner: Some(PicklingStateInner {
367 buffer: Part::from(buffer.inner.clone()),
368 tensor_engine_references: refs,
369 mesh_references: mesh_refs,
370 pending_mesh_fills: Vec::new(),
371 }),
372 })
373 }
374
375 fn tensor_engine_references(&self, py: Python<'_>) -> PyResult<Py<PyList>> {
379 let inner = self.inner_ref()?;
380 let refs: Vec<Py<PyAny>> = inner
381 .tensor_engine_references
382 .iter()
383 .map(|r| r.clone_ref(py))
384 .collect();
385 Ok(PyList::new(py, refs)?.unbind())
386 }
387
388 fn buffer(&self) -> PyResult<crate::buffers::FrozenBuffer> {
393 let inner = self.inner_ref()?;
394 Ok(crate::buffers::FrozenBuffer {
395 inner: inner.buffer.clone().into_bytes(),
396 })
397 }
398
399 pub(crate) fn unpickle(&mut self, py: Python<'_>) -> PyResult<Py<PyAny>> {
404 let inner = self.take_inner()?;
405
406 let mut active = ActivePicklingState::new(false, false);
409 active.tensor_engine_references = inner.tensor_engine_references;
410 active.mesh_references = inner.mesh_references;
411 active.pending_mesh_fills = inner.pending_mesh_fills;
412
413 let _guard = ActivePicklingGuard::enter(active);
414
415 let frozen = crate::buffers::FrozenBuffer {
416 inner: inner.buffer.into_bytes(),
417 };
418
419 let result = if maybe_torch_fn(py).call0()?.is_truthy()? {
422 torch_loads_fn(py).call1((frozen,))
423 } else {
424 cloudpickle(py).getattr("loads")?.call1((frozen,))
425 };
426
427 result.map(|obj| obj.unbind())
428 }
429}
430
431impl PicklingState {
432 pub(crate) fn unpickle_with_receiver(
439 &mut self,
440 py: Python<'_>,
441 instance: &Instance<PythonActor>,
442 ) -> PyResult<Py<PyAny>> {
443 let _guard = ReceiverInstanceGuard::enter(instance);
444 self.unpickle(py)
445 }
446
447 pub async fn resolve(mut self) -> PyResult<PicklingState> {
453 let fills = std::mem::take(
457 &mut self
458 .inner
459 .as_mut()
460 .ok_or_else(|| {
461 pyo3::exceptions::PyRuntimeError::new_err(
462 "PicklingState has already been consumed",
463 )
464 })?
465 .pending_mesh_fills,
466 );
467
468 for (index, handle) in fills {
469 let mut task =
472 monarch_with_gil_blocking(GilSite::AwaitDrive, |py| handle.borrow(py).task())?;
473 task.take_task()?.await?;
474
475 monarch_with_gil_blocking(GilSite::Convert, |py| -> PyResult<()> {
477 let value = handle.borrow(py).poll()?.ok_or_else(|| {
478 pyo3::exceptions::PyRuntimeError::new_err("pending mesh handle did not resolve")
479 })?;
480 let mesh_ref = crate::actor::mesh_ref_from_pyobject(value.bind(py))?;
481 if let Some(inner) = self.inner.as_mut() {
482 inner.mesh_references[index] = Some(mesh_ref);
483 }
484 Ok(())
485 })?;
486 }
487
488 Ok(self)
489 }
490}
491
492#[pyclass(module = "monarch._rust_bindings.monarch_hyperactor.pickle")]
498pub struct PendingMessage {
499 pub(crate) kind: PythonMessageKind,
500 state: PicklingState,
501}
502
503impl PendingMessage {
504 pub fn new(kind: PythonMessageKind, state: PicklingState) -> Self {
506 Self { kind, state }
507 }
508
509 pub fn take(&mut self) -> PyResult<PendingMessage> {
514 let inner = self.state.take_inner()?;
515 Ok(PendingMessage {
516 kind: std::mem::take(&mut self.kind),
517 state: PicklingState { inner: Some(inner) },
518 })
519 }
520
521 pub(crate) fn payload_len(&self) -> usize {
524 self.state
525 .inner_ref()
526 .expect("should be called before the message is resolved")
527 .payload_len()
528 }
529
530 fn into_python_message(mut self) -> PyResult<PythonMessage> {
531 let inner = self.state.take_inner()?;
532 let refs: Vec<MeshRef> = inner
533 .mesh_references
534 .into_iter()
535 .map(|reference| {
536 reference.ok_or_else(|| {
537 pyo3::exceptions::PyRuntimeError::new_err(
538 "mesh reference slot was never filled",
539 )
540 })
541 })
542 .collect::<PyResult<_>>()?;
543 Ok(PythonMessage::new_from_buf_with_refs(
544 self.kind,
545 inner.buffer,
546 refs,
547 ))
548 }
549
550 pub fn try_resolve_now(self) -> PyResult<Result<PythonMessage, PendingMessage>> {
559 if self.state.has_pending_mesh_fills()? {
560 return Ok(Err(self));
561 }
562
563 Ok(Ok(self.into_python_message()?))
564 }
565
566 pub async fn resolve(self) -> PyResult<PythonMessage> {
572 let Self { kind, state } = self;
573 let state = state.resolve().await?;
574 Self::new(kind, state).into_python_message()
575 }
576}
577
578#[pymethods]
579impl PendingMessage {
580 #[new]
582 pub fn py_new(
583 kind: PythonMessageKind,
584 mut state: PyRefMut<'_, PicklingState>,
585 ) -> PyResult<Self> {
586 let inner = state.take_inner()?;
588 Ok(Self {
589 kind,
590 state: PicklingState { inner: Some(inner) },
591 })
592 }
593
594 #[getter]
596 fn kind(&self) -> PythonMessageKind {
597 self.kind.clone()
598 }
599
600 #[pyo3(name = "try_resolve_now")]
604 fn py_try_resolve_now(&mut self) -> PyResult<Option<PythonMessage>> {
605 if self.state.has_pending_mesh_fills()? {
606 return Ok(None);
607 }
608
609 self.take()?.into_python_message().map(Some)
610 }
611
612 #[pyo3(name = "resolve")]
614 fn py_resolve(&mut self) -> PyResult<PyHandle> {
615 let message = self.take()?;
616 Ok(PyHandle::spawn(async move { message.resolve().await }))
617 }
618}
619
620#[pyfunction]
629fn push_tensor_engine_reference_if_active(obj: Py<PyAny>) -> PyResult<bool> {
630 ACTIVE_PICKLING_STATE.with(|cell| {
631 let mut state = cell.borrow_mut();
632 match state.as_mut() {
633 Some(s) => {
634 if !s.allow_tensor_engine_references {
635 return Err(pyo3::exceptions::PyRuntimeError::new_err(
636 "Tensor engine references are not allowed in the current pickling context",
637 ));
638 }
639 s.tensor_engine_references.push_back(obj);
640 Ok(true)
641 }
642 None => Ok(false),
643 }
644 })
645}
646
647#[pyfunction]
652fn pop_tensor_engine_reference(py: Python<'_>) -> PyResult<Py<PyAny>> {
653 ACTIVE_PICKLING_STATE
654 .with(|cell| {
655 let mut state = cell.borrow_mut();
656 match state.as_mut() {
657 Some(s) => s.tensor_engine_references.pop_front().ok_or_else(|| {
658 pyo3::exceptions::PyRuntimeError::new_err(
659 "No tensor engine references remaining",
660 )
661 }),
662 None => Err(pyo3::exceptions::PyRuntimeError::new_err(
663 "No active pickling state",
664 )),
665 }
666 })
667 .map(|obj| obj.clone_ref(py))
668}
669
670#[pyfunction]
676fn pop_mesh_reference(py: Python<'_>) -> PyResult<Py<PyAny>> {
677 let mesh_ref = ACTIVE_PICKLING_STATE.with(|cell| {
678 let mut state = cell.borrow_mut();
679 match state.as_mut() {
680 Some(s) => s.mesh_references.pop_front().ok_or_else(|| {
681 pyo3::exceptions::PyRuntimeError::new_err("No mesh references remaining")
682 }),
683 None => Err(pyo3::exceptions::PyRuntimeError::new_err(
684 "No active pickling state",
685 )),
686 }
687 })?;
688 let mesh_ref = mesh_ref.ok_or_else(|| {
689 pyo3::exceptions::PyRuntimeError::new_err("mesh reference slot was never filled")
690 })?;
691 MESH_POP_COUNT.fetch_add(1, Ordering::Relaxed);
692 mesh_ref.reconstruct(py)
693}
694
695pub fn push_mesh_reference_if_active(mesh_ref: MeshRef) -> bool {
701 ACTIVE_PICKLING_STATE.with(|cell| {
702 let mut state = cell.borrow_mut();
703 match state.as_mut() {
704 Some(s) if s.allow_mesh_references => {
705 s.mesh_references.push_back(Some(mesh_ref));
706 true
707 }
708 _ => false,
709 }
710 })
711}
712
713pub fn reserve_mesh_reference_if_active(handle: Py<PyShared>) -> bool {
719 ACTIVE_PICKLING_STATE.with(|cell| {
720 let mut state = cell.borrow_mut();
721 match state.as_mut() {
722 Some(s) if s.allow_mesh_references => {
723 let index = s.mesh_references.len();
724 s.mesh_references.push_back(None);
725 s.pending_mesh_fills.push((index, handle));
726 PENDING_RESERVE_COUNT.fetch_add(1, Ordering::Relaxed);
727 true
728 }
729 _ => false,
730 }
731 })
732}
733
734#[pyfunction]
737fn reserve_mesh_reference(handle: Py<PyShared>) -> bool {
738 reserve_mesh_reference_if_active(handle)
739}
740
741pub fn reduce_shared<'py>(
747 py: Python<'py>,
748 py_shared: &Bound<'py, PyShared>,
749) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyTuple>)> {
750 if let Some(value) = py_shared.borrow().poll()? {
752 let from_value = shared_class(py).getattr("from_value")?;
753 let args = PyTuple::new(py, [value])?;
754 return Ok((from_value, args));
755 }
756
757 let value = PyShared::block_on(py_shared.borrow(), py)?;
761 let from_value = shared_class(py).getattr("from_value")?;
762 let args = PyTuple::new(py, [value])?;
763 Ok((from_value, args))
764}
765
766fn pickle_into_buffer(py: Python<'_>, obj: &Py<PyAny>, buffer: &Py<Buffer>) -> PyResult<()> {
771 pickle_monkeypatch(py);
773
774 if maybe_torch_fn(py).call0()?.is_truthy()? {
777 torch_dump_fn(py).call1((obj, buffer.bind(py)))?;
778 } else {
779 let pickler = cloudpickle(py)
780 .getattr("Pickler")?
781 .call1((buffer.bind(py),))?;
782 pickler.call_method1("dump", (obj,))?;
783 }
784
785 Ok(())
786}
787
788pub fn pickle_to_part(py: Python<'_>, obj: &Py<PyAny>) -> PyResult<Part> {
794 let active = ActivePicklingState::new(false, false);
795 let buffer = Py::new(py, Buffer::default())?;
796 let _guard = ActivePicklingGuard::enter(active);
797
798 pickle_into_buffer(py, obj, &buffer)?;
799
800 Ok(buffer.borrow_mut(py).take_part())
801}
802
803#[pyfunction]
816#[pyo3(signature = (obj, allow_tensor_engine_references=true, allow_mesh_references=false))]
817pub fn pickle(
818 py: Python<'_>,
819 obj: Py<PyAny>,
820 allow_tensor_engine_references: bool,
821 allow_mesh_references: bool,
822) -> PyResult<PicklingState> {
823 let active = ActivePicklingState::new(allow_tensor_engine_references, allow_mesh_references);
824 let buffer = Py::new(py, Buffer::default())?;
825 let _guard = ActivePicklingGuard::enter(active);
826
827 pickle_into_buffer(py, &obj, &buffer)?;
828
829 let active = ACTIVE_PICKLING_STATE
832 .with(|cell| cell.borrow_mut().take())
833 .expect("active pickling state should still be set");
834
835 let part = buffer.borrow_mut(py).take_part();
837 let inner = active.into_pickling_state(part);
838 Ok(PicklingState { inner: Some(inner) })
839}
840
841pub(crate) fn unpickle(
842 py: Python<'_>,
843 buffer: crate::buffers::FrozenBuffer,
844) -> PyResult<Bound<'_, PyAny>> {
845 _unpickle(py).call1((buffer.into_py_any(py)?,))
846}
847
848#[pyfunction]
850fn _get_pending_reserve_count() -> usize {
851 PENDING_RESERVE_COUNT.load(Ordering::Relaxed)
852}
853
854#[pyfunction]
856fn _reset_pending_reserve_count() {
857 PENDING_RESERVE_COUNT.store(0, Ordering::Relaxed);
858}
859
860#[pyfunction]
862fn _get_mesh_pop_count() -> usize {
863 MESH_POP_COUNT.load(Ordering::Relaxed)
864}
865
866#[pyfunction]
868fn _reset_mesh_pop_count() {
869 MESH_POP_COUNT.store(0, Ordering::Relaxed);
870}
871
872pub fn register_python_bindings(module: &Bound<'_, PyModule>) -> PyResult<()> {
874 module.add_class::<PicklingState>()?;
875 module.add_class::<PendingMessage>()?;
876 module.add_function(wrap_pyfunction!(pickle, module)?)?;
877 module.add_function(wrap_pyfunction!(
878 push_tensor_engine_reference_if_active,
879 module
880 )?)?;
881 module.add_function(wrap_pyfunction!(pop_tensor_engine_reference, module)?)?;
882 module.add_function(wrap_pyfunction!(pop_mesh_reference, module)?)?;
883 module.add_function(wrap_pyfunction!(_current_receiver_instance, module)?)?;
884 module.add_function(wrap_pyfunction!(reserve_mesh_reference, module)?)?;
885 module.add_function(wrap_pyfunction!(_get_pending_reserve_count, module)?)?;
886 module.add_function(wrap_pyfunction!(_reset_pending_reserve_count, module)?)?;
887 module.add_function(wrap_pyfunction!(_get_mesh_pop_count, module)?)?;
888 module.add_function(wrap_pyfunction!(_reset_mesh_pop_count, module)?)?;
889 Ok(())
890}
891
892#[cfg(test)]
893mod tests {
894 use hyperactor::ActorRef;
895 use hyperactor::ProcAddr;
896 use hyperactor::ProcId;
897 use hyperactor::channel::ChannelAddr;
898 use hyperactor::id::Label;
899 use hyperactor::id::Uid;
900 use hyperactor_mesh::mesh_id::ProcMeshId;
901 use hyperactor_mesh::proc_agent::PROC_AGENT_ACTOR_NAME;
902 use hyperactor_mesh::proc_agent::ProcAgent;
903 use hyperactor_mesh::proc_mesh::ProcMeshRef;
904 use hyperactor_mesh::proc_mesh::ProcRef;
905 use pyo3::IntoPyObjectExt;
906 use pyo3::PyTypeInfo;
907
908 use super::*;
909 use crate::proc_mesh::PyProcMesh;
910
911 fn proc_mesh_ref(instance: u64, mesh: &str) -> ProcMeshRef {
914 let proc_id = ProcId::new(
915 Uid::Instance(instance, None),
916 Some(Label::new("local").expect("test label should be valid")),
917 );
918 let proc_addr = ProcAddr::new(proc_id, ChannelAddr::Local(1).into());
919 let agent: ActorRef<ProcAgent> =
920 ActorRef::attest(proc_addr.actor_addr(PROC_AGENT_ACTOR_NAME));
921 let proc_ref = ProcRef::new(proc_addr, 0, agent);
922 ProcMeshRef::new_singleton(
923 ProcMeshId::singleton(Label::new(mesh).expect("test label should be valid")),
924 proc_ref,
925 )
926 .expect("test proc mesh should be valid")
927 }
928
929 fn resolved_proc_mesh_ref() -> MeshRef {
930 MeshRef::Proc(Box::new(proc_mesh_ref(1, "mesh")))
931 }
932
933 fn completed_shared(py: Python<'_>, value: Py<PyAny>) -> Py<PyShared> {
939 py.get_type::<PyShared>()
940 .call_method1("from_value", (value,))
941 .expect("Shared.from_value should accept any object")
942 .extract()
943 .expect("Shared.from_value should return a Shared")
944 }
945
946 fn mesh_value(py: Python<'_>, mesh: &ProcMeshRef) -> Py<PyAny> {
949 Py::new(py, PyProcMesh::new_ref(mesh.clone()))
950 .expect("PyProcMesh should construct")
951 .into_any()
952 }
953
954 fn expect_error<T>(result: PyResult<T>, context: &str) -> PyErr {
956 match result {
957 Ok(_) => panic!("{context}"),
958 Err(error) => error,
959 }
960 }
961
962 fn pickling_state(
963 buffer: Vec<u8>,
964 mesh_references: Vec<Option<MeshRef>>,
965 pending_mesh_fills: Vec<(usize, Py<PyShared>)>,
966 ) -> PicklingState {
967 PicklingState {
968 inner: Some(PicklingStateInner {
969 buffer: Part::from(buffer),
970 tensor_engine_references: VecDeque::new(),
971 mesh_references: mesh_references.into(),
972 pending_mesh_fills,
973 }),
974 }
975 }
976
977 fn pending_message(
978 kind: PythonMessageKind,
979 buffer: Vec<u8>,
980 refs: Vec<MeshRef>,
981 ) -> PendingMessage {
982 PendingMessage::new(
983 kind,
984 PicklingState {
985 inner: Some(PicklingStateInner {
986 buffer: Part::from(buffer),
987 tensor_engine_references: VecDeque::new(),
988 mesh_references: refs.into_iter().map(Some).collect(),
989 pending_mesh_fills: Vec::new(),
990 }),
991 },
992 )
993 }
994
995 #[test]
998 fn payload_len_counts_pickled_bytes_only() {
999 let kind = PythonMessageKind::Result { rank: Some(7) };
1000 let buffer = vec![0, 1, 2, 3, 127, 128, 254, 255];
1001
1002 for refs in [Vec::new(), vec![resolved_proc_mesh_ref()]] {
1003 assert_eq!(
1004 pending_message(kind.clone(), buffer.clone(), refs).payload_len(),
1005 8,
1006 "payload_len should be the length of the pickled buffer"
1007 );
1008 }
1009 }
1010
1011 #[tokio::test]
1017 async fn receiver_instance_guard_clones_nests_and_restores() {
1018 use std::panic::AssertUnwindSafe;
1019
1020 use hyperactor::Proc;
1021 use hyperactor::channel::ChannelTransport;
1022
1023 fn instance(name: &str) -> Instance<PythonActor> {
1024 Proc::direct(ChannelTransport::Unix.any(), format!("{name}_proc"))
1025 .unwrap()
1026 .actor_instance::<PythonActor>(name)
1027 .unwrap()
1028 .instance
1029 }
1030
1031 let outer = instance("outer");
1032 let inner = instance("inner");
1033 let outer_id = outer.self_addr().clone();
1034 let inner_id = inner.self_addr().clone();
1035
1036 assert!(current_receiver_instance_raw().is_none(), "clean to start");
1037
1038 {
1039 let _g = ReceiverInstanceGuard::enter(&outer);
1040
1041 for _ in 0..3 {
1043 let got = current_receiver_instance_raw().expect("receiver installed");
1044 assert_eq!(got.self_addr(), &outer_id);
1045 }
1046
1047 {
1048 let _nested = ReceiverInstanceGuard::enter(&inner);
1049 assert_eq!(
1050 current_receiver_instance_raw().unwrap().self_addr(),
1051 &inner_id,
1052 );
1053 }
1054 assert_eq!(
1055 current_receiver_instance_raw().unwrap().self_addr(),
1056 &outer_id,
1057 "nested guard must restore its parent",
1058 );
1059
1060 let unwound = std::panic::catch_unwind(AssertUnwindSafe(|| {
1062 let _failing = ReceiverInstanceGuard::enter(&inner);
1063 panic!("decode blew up");
1064 }));
1065 assert!(unwound.is_err());
1066 assert_eq!(
1067 current_receiver_instance_raw().unwrap().self_addr(),
1068 &outer_id,
1069 "a guard dropped while unwinding must restore",
1070 );
1071 }
1072
1073 assert!(
1074 current_receiver_instance_raw().is_none(),
1075 "outermost guard must clear the receiver",
1076 );
1077 }
1078
1079 #[tokio::test]
1080 async fn try_resolve_now_matches_async_resolution_with_resolved_refs() {
1081 let kind = PythonMessageKind::Result { rank: Some(7) };
1082 let buffer = vec![0, 1, 2, 3, 127, 128, 254, 255];
1083
1084 for refs in [Vec::new(), vec![resolved_proc_mesh_ref()]] {
1085 let synchronous = match pending_message(kind.clone(), buffer.clone(), refs.clone())
1086 .try_resolve_now()
1087 .expect("zero-pending probe should succeed")
1088 {
1089 Ok(message) => message,
1090 Err(_) => panic!("zero-pending message should resolve synchronously"),
1091 };
1092 let asynchronous = pending_message(kind.clone(), buffer.clone(), refs)
1093 .resolve()
1094 .await
1095 .expect("zero-pending async resolution should succeed");
1096
1097 assert_eq!(
1098 synchronous, asynchronous,
1099 "sync and async resolution should preserve kind, bytes, and refs"
1100 );
1101 }
1102 }
1103
1104 #[tokio::test]
1111 async fn resolve_fills_nonadjacent_slots_in_place_without_touching_the_payload() {
1112 pyo3::Python::initialize();
1113
1114 let first = proc_mesh_ref(11, "first");
1115 let already = proc_mesh_ref(22, "already");
1116 let second = proc_mesh_ref(33, "second");
1117 assert_ne!(
1118 first, second,
1119 "the two pending meshes must be distinguishable"
1120 );
1121 let payload: Vec<u8> = vec![0, 1, 2, 3, 127, 128, 254, 255];
1122
1123 let state = monarch_with_gil_blocking(GilSite::Test, |py| {
1124 Ok::<_, PyErr>(pickling_state(
1125 payload.clone(),
1126 vec![None, Some(MeshRef::Proc(Box::new(already.clone()))), None],
1127 vec![
1131 (2, completed_shared(py, mesh_value(py, &second))),
1132 (0, completed_shared(py, mesh_value(py, &first))),
1133 ],
1134 ))
1135 })
1136 .expect("building the pending state should succeed");
1137
1138 let resolved = state.resolve().await.expect("resolution should succeed");
1139 let inner = resolved
1140 .inner
1141 .expect("a resolved state should still hold its inner");
1142
1143 assert_eq!(
1144 inner.mesh_references,
1145 VecDeque::from(vec![
1146 Some(MeshRef::Proc(Box::new(first))),
1147 Some(MeshRef::Proc(Box::new(already))),
1148 Some(MeshRef::Proc(Box::new(second))),
1149 ]),
1150 "each pending mesh must land in the slot it reserved"
1151 );
1152 assert_eq!(
1153 &inner.take_buffer().into_bytes()[..],
1154 &payload[..],
1155 "resolution must not re-pickle: the payload bytes are carried through"
1156 );
1157 }
1158
1159 #[tokio::test]
1161 async fn py_resolve_consumes_the_source_before_returning_its_handle() {
1162 pyo3::Python::initialize();
1163
1164 monarch_with_gil_blocking(GilSite::Test, |py| {
1165 let mesh = proc_mesh_ref(41, "consumed");
1166 let mut message = PendingMessage::new(
1167 PythonMessageKind::Result { rank: Some(7) },
1168 pickling_state(
1169 vec![1, 2, 3],
1170 vec![None],
1171 vec![(0, completed_shared(py, mesh_value(py, &mesh)))],
1172 ),
1173 );
1174
1175 let handle = message
1176 .py_resolve()
1177 .expect("Handle construction should succeed");
1178 drop(handle);
1179
1180 let second = expect_error(
1181 message.py_resolve(),
1182 "the source was consumed by the first call",
1183 );
1184 assert_eq!(
1185 second.value(py).to_string(),
1186 "PicklingState has already been consumed",
1187 "a second resolution must report the consumed source"
1188 );
1189 Ok::<_, PyErr>(())
1190 })
1191 .expect("test body should not fail");
1192 }
1193
1194 #[tokio::test]
1197 async fn py_resolve_handle_surfaces_the_fill_error_and_leaves_the_source_consumed() {
1198 pyo3::Python::initialize();
1199
1200 let (handle, mut message) = monarch_with_gil_blocking(GilSite::Test, |py| {
1201 let not_a_mesh = 41i64.into_py_any(py)?;
1202 let mut message = PendingMessage::new(
1203 PythonMessageKind::Result { rank: Some(7) },
1204 pickling_state(
1205 vec![1, 2, 3],
1206 vec![None],
1207 vec![(0, completed_shared(py, not_a_mesh))],
1208 ),
1209 );
1210 let handle = message.py_resolve()?;
1211 Ok::<_, PyErr>((handle, message))
1212 })
1213 .expect("construction should succeed");
1214
1215 let error = tokio::time::timeout(std::time::Duration::from_secs(2), handle.wait_future())
1216 .await
1217 .expect("the Handle must finish within the test deadline")
1218 .expect_err("observing the Handle must surface the fill failure");
1219
1220 monarch_with_gil_blocking(GilSite::Test, |py| {
1221 assert!(
1222 error
1223 .get_type(py)
1224 .is(pyo3::exceptions::PyRuntimeError::type_object(py)),
1225 "the original exception type must survive the Handle boundary exactly, \
1226 not merely as a subclass"
1227 );
1228 assert_eq!(
1229 error.value(py).to_string(),
1230 "pending pickle did not resolve to a mesh reference",
1231 "the original message must survive the Handle boundary"
1232 );
1233
1234 let second = expect_error(
1235 message.py_resolve(),
1236 "a failed observation must not hand the source back",
1237 );
1238 assert_eq!(
1239 second.value(py).to_string(),
1240 "PicklingState has already been consumed",
1241 );
1242 Ok::<_, PyErr>(())
1243 })
1244 .expect("assertions should not fail");
1245 }
1246
1247 #[tokio::test]
1254 async fn py_resolve_handle_surfaces_a_failed_pending_handle_error() {
1255 pyo3::Python::initialize();
1256
1257 let (outer_handle, mut outer) = monarch_with_gil_blocking(GilSite::Test, |py| {
1258 let (sender, pending) = PyShared::pending();
1259 sender
1260 .send(Some(Err(pyo3::exceptions::PyRuntimeError::new_err(
1261 "pending mesh producer failed",
1262 ))))
1263 .expect("the pending Shared should retain its receiver");
1264 let handle = Py::new(py, pending)?;
1265
1266 let mut outer = PendingMessage::new(
1267 PythonMessageKind::Result { rank: Some(7) },
1268 pickling_state(vec![1, 2, 3], vec![None], vec![(0, handle)]),
1269 );
1270 let handle = outer.py_resolve()?;
1271 Ok::<_, PyErr>((handle, outer))
1272 })
1273 .expect("construction should succeed");
1274
1275 let error = tokio::time::timeout(
1276 std::time::Duration::from_secs(2),
1277 outer_handle.wait_future(),
1278 )
1279 .await
1280 .expect("the outer Handle must finish within the test deadline")
1281 .expect_err("the failed producer must fail the outer resolution");
1282
1283 monarch_with_gil_blocking(GilSite::Test, |py| {
1284 assert!(
1285 error
1286 .get_type(py)
1287 .is(pyo3::exceptions::PyRuntimeError::type_object(py)),
1288 "the producer's exception type must survive the Shared and Handle boundaries"
1289 );
1290 assert_eq!(
1291 error.value(py).to_string(),
1292 "pending mesh producer failed",
1293 "the outer Handle must report the producer's own failure, not a \
1294 conversion failure raised after a successful await"
1295 );
1296
1297 let second = expect_error(
1298 outer.py_resolve(),
1299 "a failed producer must not hand the source back",
1300 );
1301 assert_eq!(
1302 second.value(py).to_string(),
1303 "PicklingState has already been consumed",
1304 );
1305 Ok::<_, PyErr>(())
1306 })
1307 .expect("assertions should not fail");
1308 }
1309
1310 #[tokio::test]
1313 async fn py_resolve_handle_surfaces_unfilled_slot_error_and_leaves_source_consumed() {
1314 pyo3::Python::initialize();
1315
1316 let (handle, mut message) = monarch_with_gil_blocking(GilSite::Test, |_py| {
1317 let mut message = PendingMessage::new(
1318 PythonMessageKind::Result { rank: Some(1) },
1319 pickling_state(vec![9], vec![None], vec![]),
1320 );
1321 let handle = message.py_resolve()?;
1322 Ok::<_, PyErr>((handle, message))
1323 })
1324 .expect("construction should succeed");
1325
1326 let error = tokio::time::timeout(std::time::Duration::from_secs(2), handle.wait_future())
1327 .await
1328 .expect("the Handle must finish within the test deadline")
1329 .expect_err("the unfilled slot must fail message assembly");
1330
1331 monarch_with_gil_blocking(GilSite::Test, |py| {
1332 assert!(
1333 error
1334 .get_type(py)
1335 .is(pyo3::exceptions::PyRuntimeError::type_object(py)),
1336 "the assembly error must remain exactly PyRuntimeError"
1337 );
1338 assert_eq!(
1339 error.value(py).to_string(),
1340 "mesh reference slot was never filled",
1341 "an unfilled reserved slot must retain its diagnostic"
1342 );
1343
1344 let second = expect_error(
1345 message.py_resolve(),
1346 "an assembly failure must not hand the source back",
1347 );
1348 assert_eq!(
1349 second.value(py).to_string(),
1350 "PicklingState has already been consumed",
1351 );
1352 Ok::<_, PyErr>(())
1353 })
1354 .expect("assertions should not fail");
1355 }
1356
1357 #[tokio::test]
1365 async fn py_resolve_starts_resolution_and_survives_returned_handle_drop() {
1366 pyo3::Python::initialize();
1367
1368 let (sender, pending_handle, baseline, mut message) =
1369 monarch_with_gil_blocking(GilSite::Test, |py| {
1370 let (sender, pending) = PyShared::pending();
1371 let handle = Py::new(py, pending)?;
1372 let baseline = sender.receiver_count();
1373
1374 let mut message = PendingMessage::new(
1375 PythonMessageKind::Result { rank: Some(7) },
1376 pickling_state(vec![1, 2, 3], vec![None], vec![(0, handle.clone_ref(py))]),
1377 );
1378
1379 let observer = message.py_resolve()?;
1380 drop(observer);
1381 Ok::<_, PyErr>((sender, handle, baseline, message))
1382 })
1383 .expect("construction should succeed");
1384
1385 tokio::time::timeout(std::time::Duration::from_secs(2), async {
1386 while sender.receiver_count() == baseline {
1387 tokio::task::yield_now().await;
1388 }
1389 })
1390 .await
1391 .expect("resolution must start and retain a waiter after its observer is dropped");
1392 assert_eq!(
1393 sender.receiver_count(),
1394 baseline + 1,
1395 "one active resolver must retain exactly one additional receiver"
1396 );
1397
1398 let value = monarch_with_gil_blocking(GilSite::Test, |py| {
1399 Ok::<_, PyErr>(mesh_value(py, &proc_mesh_ref(44, "pending")))
1400 })
1401 .expect("the pending mesh value should construct");
1402 sender
1403 .send(Some(Ok(value)))
1404 .expect("the retained pending handle should receive its value");
1405
1406 tokio::time::timeout(std::time::Duration::from_secs(2), async {
1407 while sender.receiver_count() != baseline {
1408 tokio::task::yield_now().await;
1409 }
1410 })
1411 .await
1412 .expect("the detached resolver must finish and release its waiter");
1413
1414 monarch_with_gil_blocking(GilSite::Test, |py| {
1415 let second = expect_error(
1416 message.py_resolve(),
1417 "the source was consumed by the first call",
1418 );
1419 assert_eq!(
1420 second.value(py).to_string(),
1421 "PicklingState has already been consumed",
1422 );
1423 });
1424 drop(pending_handle);
1425 }
1426}