1use std::any::Any;
101use std::any::TypeId;
102use std::collections::BTreeMap;
103use std::collections::HashMap;
104use std::fmt;
105use std::future::Future;
106use std::ops::Deref;
107use std::panic;
108use std::panic::AssertUnwindSafe;
109use std::panic::Location as PanicLocation;
110use std::pin::Pin;
111use std::sync::Arc;
112use std::sync::Condvar;
113use std::sync::Mutex;
114use std::sync::OnceLock;
115use std::sync::RwLock;
116use std::sync::Weak;
117use std::sync::atomic::AtomicBool;
118use std::sync::atomic::AtomicU64;
119use std::sync::atomic::AtomicUsize;
120use std::sync::atomic::Ordering;
121use std::time::Duration;
122use std::time::Instant;
123use std::time::SystemTime;
124
125use async_trait::async_trait;
126use dashmap::DashMap;
127use dashmap::DashSet;
128use dashmap::mapref::entry::Entry;
129use dashmap::mapref::multiple::RefMulti;
130use futures::FutureExt;
131use hyperactor_config::Flattrs;
132use hyperactor_telemetry::ActorStatusEvent;
133use hyperactor_telemetry::generate_actor_status_event_id;
134use hyperactor_telemetry::hash_to_u64;
135use hyperactor_telemetry::notify_actor_status_changed;
136use hyperactor_telemetry::notify_message;
137use hyperactor_telemetry::notify_message_status;
138use hyperactor_telemetry::recorder::Recording;
139use serde::Deserialize;
140use serde::Serialize;
141use tokio::sync::Notify;
142use tokio::sync::mpsc;
143use tokio::sync::oneshot;
144use tokio::sync::watch;
145use tokio::task::JoinHandle;
146use tracing::Instrument;
147use tracing::Span;
148use typeuri::Named;
149use uuid::Uuid;
150use wirevalue::TypeInfo;
151
152use crate as hyperactor;
153use crate::Actor;
154use crate::ActorAddr;
155use crate::ActorRef;
156use crate::Addr;
157use crate::Data;
158use crate::Handler;
159use crate::Location;
160use crate::Message;
161use crate::PortAddr;
162use crate::PortRef;
163use crate::ProcAddr;
164use crate::ProcId;
165use crate::RemoteMessage;
166use crate::actor::ActorError;
167use crate::actor::ActorErrorKind;
168use crate::actor::ActorHandle;
169use crate::actor::ActorStatus;
170use crate::actor::ActorStoppingReason;
171use crate::actor::AnyActorHandle;
172use crate::actor::Binds;
173use crate::actor::HandlerInfo;
174use crate::actor::Referable;
175use crate::actor::RemoteHandles;
176use crate::actor::Signal;
177use crate::actor::StopMode;
178use crate::actor_local::ActorLocalStorage;
179use crate::channel;
180use crate::channel::ChannelAddr;
181use crate::channel::ChannelError;
182use crate::client::Client;
183use crate::client::ClientActor;
184use crate::config;
185use crate::context;
186use crate::context::Mailbox as _;
187use crate::endpoint::Endpoint as _;
188use crate::gateway::Gateway;
189use crate::id::ActorId;
190use crate::id::Label;
191use crate::id::Uid;
192use crate::introspect::IntrospectMessage;
193use crate::introspect::IntrospectResult;
194use crate::mailbox::BoxedMailboxSender;
195use crate::mailbox::DeliveryFailure;
196use crate::mailbox::DialMailboxRouter;
197use crate::mailbox::IntoBoxedMailboxSender as _;
198use crate::mailbox::Mailbox;
199use crate::mailbox::MailboxMuxer;
200use crate::mailbox::MailboxSender;
201use crate::mailbox::MessageEnvelope;
202use crate::mailbox::OncePortHandle;
203use crate::mailbox::OncePortReceiver;
204use crate::mailbox::PortGone;
205use crate::mailbox::PortHandle;
206use crate::mailbox::PortReceiver;
207use crate::mailbox::PortSender as _;
208use crate::mailbox::TransportFailure;
209use crate::mailbox::TransportFailureReason;
210use crate::mailbox::Undeliverable;
211use crate::mailbox::UndeliverableReason;
212use crate::metrics::ACTOR_MESSAGE_HANDLER_DURATION;
213use crate::metrics::ACTOR_MESSAGE_QUEUE_SIZE;
214use crate::metrics::ACTOR_MESSAGES_RECEIVED;
215use crate::port::Port;
216use crate::subject::AsSubject as _;
217
218tokio::task_local! {
219 static CURRENT_TASK_PROC: Proc;
220}
221
222pub const LEGACY_LOCAL_PROC_NAME: &str = "local";
227
228pub const LEGACY_SERVICE_PROC_NAME: &str = "service";
233
234fn wall_clock_epoch_ms() -> u64 {
238 std::time::SystemTime::now()
239 .duration_since(std::time::UNIX_EPOCH)
240 .unwrap_or_default()
241 .as_millis() as u64
242}
243
244pub(crate) struct ProcQueueStats {
251 running_total: AtomicU64,
255 high_water_mark: AtomicU64,
257 last_nonzero_epoch_ms: AtomicU64,
263 clock: fn() -> u64,
266}
267
268impl ProcQueueStats {
269 fn new() -> Self {
270 Self {
271 running_total: AtomicU64::new(0),
272 high_water_mark: AtomicU64::new(0),
273 last_nonzero_epoch_ms: AtomicU64::new(0),
274 clock: wall_clock_epoch_ms,
275 }
276 }
277
278 #[cfg(test)]
280 fn with_clock(clock: fn() -> u64) -> Self {
281 Self {
282 running_total: AtomicU64::new(0),
283 high_water_mark: AtomicU64::new(0),
284 last_nonzero_epoch_ms: AtomicU64::new(0),
285 clock,
286 }
287 }
288
289 fn now_ms(&self) -> u64 {
291 (self.clock)()
292 }
293
294 pub(crate) fn running_total(&self) -> u64 {
296 self.running_total.load(Ordering::Relaxed)
297 }
298
299 pub(crate) fn high_water_mark(&self) -> u64 {
301 self.high_water_mark.load(Ordering::Relaxed)
302 }
303
304 pub(crate) fn last_nonzero_age_ms(&self) -> Option<u64> {
309 let ts = self.last_nonzero_epoch_ms.load(Ordering::Relaxed);
310 if ts == 0 {
311 return None;
312 }
313 Some(self.now_ms().saturating_sub(ts))
314 }
315}
316
317fn account_enqueue(queue_depth: &AtomicU64, proc_stats: &ProcQueueStats, actor_id: &str) {
324 queue_depth.fetch_add(1, Ordering::Relaxed);
325 let new_total = proc_stats.running_total.fetch_add(1, Ordering::Relaxed) + 1;
326 proc_stats
328 .high_water_mark
329 .fetch_max(new_total, Ordering::Relaxed);
330 proc_stats
332 .last_nonzero_epoch_ms
333 .store(proc_stats.now_ms(), Ordering::Relaxed);
334 ACTOR_MESSAGE_QUEUE_SIZE.add(
335 1,
336 hyperactor_telemetry::kv_pairs!("actor_id" => actor_id.to_owned()),
337 );
338}
339
340fn account_dequeue(queue_depth: &AtomicU64, proc_stats: &ProcQueueStats, actor_id: &str) {
347 queue_depth.fetch_sub(1, Ordering::Relaxed);
348 let prev_total = proc_stats.running_total.fetch_sub(1, Ordering::Relaxed);
349 if prev_total > 1 {
353 proc_stats
354 .last_nonzero_epoch_ms
355 .store(proc_stats.now_ms(), Ordering::Relaxed);
356 }
357 ACTOR_MESSAGE_QUEUE_SIZE.add(
358 -1,
359 hyperactor_telemetry::kv_pairs!("actor_id" => actor_id.to_owned()),
360 );
361}
362
363fn account_cancel_enqueue(queue_depth: &AtomicU64, proc_stats: &ProcQueueStats, actor_id: &str) {
373 queue_depth.fetch_sub(1, Ordering::Relaxed);
374 proc_stats.running_total.fetch_sub(1, Ordering::Relaxed);
375 ACTOR_MESSAGE_QUEUE_SIZE.add(
376 -1,
377 hyperactor_telemetry::kv_pairs!("actor_id" => actor_id.to_owned()),
378 );
379}
380
381use crate::ordering::SEQ_INFO;
382use crate::ordering::SeqInfo;
383use crate::ordering::Sequencer;
384use crate::panic_handler;
385use crate::sequenced::SequencedEnvelope;
386use crate::sequenced::SequencedReceiver;
387use crate::sequenced::sequenced_unbounded_with_buffering;
388use crate::supervision::ActorSupervisionEvent;
389
390#[derive(Clone)]
397pub struct Proc {
398 inner: Arc<ProcState>,
399}
400
401impl fmt::Debug for Proc {
402 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403 f.debug_struct("Proc")
404 .field("proc_id", &self.inner.proc_id)
405 .finish()
406 }
407}
408
409struct ProcState {
410 proc_id: ProcId,
413
414 gateway: Gateway,
416
417 proc_muxer: MailboxMuxer,
420
421 reserved_roots: DashSet<crate::id::Uid>,
425
426 reserved_child_uids: DashSet<crate::id::Uid>,
429
430 instances: DashMap<ActorId, WeakInstanceCell>,
432
433 root_actors: DashSet<ActorId>,
435
436 queue_stats: Arc<ProcQueueStats>,
441
442 terminated_snapshots: DashMap<ActorId, TerminatedSnapshot>,
447
448 actor_tombstones: DashMap<ActorId, ActorStatus>,
453
454 supervision_coordinator_port: OnceLock<PortRef<ActorSupervisionEvent>>,
459
460 supervision_coordinator_actor_id: OnceLock<ActorAddr>,
463
464 mailbox_server_handle: std::sync::Mutex<Option<crate::mailbox::MailboxServerHandle>>,
469
470 _attached_proc_guard: OnceLock<crate::gateway::AttachedProcGuard>,
478}
479
480struct TerminatedSnapshot {
481 actor_addr: ActorAddr,
482 payload: crate::introspect::IntrospectResult,
483}
484
485#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Named)]
487pub enum StatusMessage {
488 GetStatus {
490 reply: crate::OncePortRef<Option<ActorStatus>>,
493 },
494}
495wirevalue::register_type!(StatusMessage);
496
497struct StatusSender(WeakProc);
498
499impl StatusSender {
500 fn new(weak_proc: WeakProc) -> Self {
501 Self(weak_proc)
502 }
503}
504
505#[async_trait]
506impl MailboxSender for StatusSender {
507 fn post_unchecked(
508 &self,
509 envelope: MessageEnvelope,
510 return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
511 ) {
512 let reply = match envelope.deserialized() {
513 Ok(StatusMessage::GetStatus { reply }) => reply,
514 Err(err) => {
515 let target = envelope.dest().clone();
516 let failure =
517 DeliveryFailure::new(UndeliverableReason::Transport(TransportFailure::new(
518 target,
519 TransportFailureReason::LinkUnavailable(format!(
520 "status message deserialization failed: {err}"
521 )),
522 )));
523 envelope.undeliverable(failure, return_handle);
524 return;
525 }
526 };
527
528 let Some(proc) = self.0.upgrade() else {
529 let failure = DeliveryFailure::new(UndeliverableReason::PortGone(PortGone::new(
530 envelope.dest().clone(),
531 envelope.data().typename().map(str::to_string),
532 )));
533 envelope.undeliverable(failure, return_handle);
534 return;
535 };
536
537 let actor_id = envelope.dest().actor_id().clone();
538 let status = proc.status_for_actor(&actor_id);
539
540 if let Err(err) =
541 proc.serialize_and_send_once(reply, status, crate::mailbox::monitored_return_handle())
542 {
543 tracing::error!("status reply failed: {err}");
544 }
545 }
546}
547
548impl Drop for ProcState {
549 fn drop(&mut self) {
550 let proc_addr = self.proc_addr();
554 tracing::info!(
555 subject = %proc_addr.subject(),
556 name = "ProcStatus",
557 status = "Dropped"
558 );
559 }
560}
561
562impl ProcState {
563 fn default_location(&self) -> Location {
565 self.gateway.default_location()
566 }
567
568 fn set_default_location(&self, location: Location) {
569 self.gateway.set_default_location(location)
570 }
571
572 fn proc_addr(&self) -> ProcAddr {
573 ProcAddr::new(self.proc_id.clone(), self.gateway.default_location())
574 }
575}
576
577pub struct ActorInstance<A: Actor> {
582 pub instance: Instance<A>,
584 pub handle: ActorHandle<A>,
586 pub supervision: mpsc::UnboundedReceiver<ActorSupervisionEvent>,
588 pub signal: mpsc::UnboundedReceiver<Signal>,
590 pub work: ActorWorkReceiver<A>,
592}
593
594pub struct ActorWorkReceiver<A: Actor> {
596 inner: SequencedReceiver<SequencedEnvelope<WorkCell<A>>>,
597}
598
599impl<A: Actor> fmt::Debug for ActorWorkReceiver<A> {
600 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
601 f.debug_struct("ActorWorkReceiver").finish_non_exhaustive()
602 }
603}
604
605impl<A: Actor> ActorWorkReceiver<A> {
606 fn new(inner: SequencedReceiver<SequencedEnvelope<WorkCell<A>>>) -> Self {
607 Self { inner }
608 }
609
610 pub async fn recv(&mut self) -> Option<WorkCell<A>> {
612 self.inner.recv().await
613 }
614
615 pub fn try_recv(&mut self) -> Result<WorkCell<A>, mpsc::error::TryRecvError> {
617 self.inner.try_recv()
618 }
619}
620
621pub struct Builder<State = GlobalGateway> {
623 proc_id: Option<ProcId>,
624 state: State,
625}
626
627pub struct GlobalGateway;
629
630pub struct SharedGateway {
632 gateway: Gateway,
633}
634
635pub struct PrivateGateway {
637 forwarder: BoxedMailboxSender,
638}
639
640impl Builder<GlobalGateway> {
641 pub fn new() -> Self {
643 Self {
644 proc_id: None,
645 state: GlobalGateway,
646 }
647 }
648
649 pub fn shared_gateway(self, gateway: Gateway) -> Builder<SharedGateway> {
651 Builder {
652 proc_id: self.proc_id,
653 state: SharedGateway { gateway },
654 }
655 }
656
657 pub fn private_gateway(self, forwarder: BoxedMailboxSender) -> Builder<PrivateGateway> {
659 Builder {
660 proc_id: self.proc_id,
661 state: PrivateGateway { forwarder },
662 }
663 }
664
665 pub fn build(self) -> Result<Proc, anyhow::Error> {
673 let proc_id = self
674 .proc_id
675 .unwrap_or_else(|| ProcId::new(Uid::anonymous(), None));
676 if is_legacy_pseudo_singleton_proc_id(&proc_id) {
677 anyhow::bail!(
678 "legacy pseudo-singleton proc id '{}' is reserved for host-scoped construction; use Builder::shared_gateway",
679 proc_id
680 );
681 }
682 Ok(Proc::from_parts_unchecked(
683 proc_id,
684 Gateway::global().clone(),
685 ))
686 }
687}
688
689impl<State> Builder<State> {
690 pub fn proc_id(mut self, proc_id: ProcId) -> Self {
692 self.proc_id = Some(proc_id);
693 self
694 }
695
696 fn build_proc(proc_id: Option<ProcId>, gateway: Gateway) -> Result<Proc, anyhow::Error> {
697 let proc_id = proc_id.unwrap_or_else(ProcId::anonymous);
698 Ok(Proc::from_parts_unchecked(proc_id, gateway))
699 }
700}
701
702impl Builder<SharedGateway> {
703 pub fn build(self) -> Result<Proc, anyhow::Error> {
707 let Builder {
708 proc_id,
709 state: SharedGateway { gateway },
710 } = self;
711 Self::build_proc(proc_id, gateway)
712 }
713}
714
715impl Builder<PrivateGateway> {
716 pub fn build(self) -> Result<Proc, anyhow::Error> {
718 let Builder {
719 proc_id,
720 state: PrivateGateway { forwarder },
721 } = self;
722 let gateway = Gateway::configured(channel::reserve_local_addr().into(), forwarder);
723 Self::build_proc(proc_id, gateway)
724 }
725}
726
727impl Proc {
728 fn from_parts_unchecked(proc_id: ProcId, gateway: Gateway) -> Self {
729 let proc_addr = ProcAddr::new(proc_id.clone(), gateway.default_location());
730 tracing::info!(
731 subject = %proc_addr.subject(),
732 name = "ProcStatus",
733 status = "Created"
734 );
735
736 let proc = Self {
737 inner: Arc::new(ProcState {
738 proc_id: proc_id.clone(),
739 gateway: gateway.clone(),
740 proc_muxer: MailboxMuxer::new(),
741 reserved_roots: DashSet::new(),
742 reserved_child_uids: DashSet::new(),
743 instances: DashMap::new(),
744 root_actors: DashSet::new(),
745 queue_stats: Arc::new(ProcQueueStats::new()),
746 terminated_snapshots: DashMap::new(),
747 actor_tombstones: DashMap::new(),
748 supervision_coordinator_port: OnceLock::new(),
749 supervision_coordinator_actor_id: OnceLock::new(),
750 mailbox_server_handle: std::sync::Mutex::new(None),
751 _attached_proc_guard: OnceLock::new(),
752 }),
753 };
754 let bound = proc
755 .inner
756 .proc_muxer
757 .bind_status(StatusSender::new(proc.downgrade()));
758 assert!(
759 bound,
760 "fresh proc muxer must not have a status control port"
761 );
762 let guard = gateway.attach_proc(&proc);
766 proc.inner
767 ._attached_proc_guard
768 .set(guard)
769 .expect("fresh ProcState's attached-proc guard slot is empty");
770 proc
771 }
772
773 fn status_for_actor(&self, actor_id: &ActorId) -> Option<ActorStatus> {
774 let live_status = self
775 .inner
776 .instances
777 .get(actor_id)
778 .and_then(|entry| entry.value().upgrade())
779 .map(|cell| cell.status().borrow().clone());
780 live_status.or_else(|| {
781 self.inner
782 .actor_tombstones
783 .get(actor_id)
784 .map(|entry| entry.value().clone())
785 })
786 }
787
788 fn from_parts(proc_id: ProcId, gateway: Gateway) -> Self {
789 assert_not_legacy_pseudo_singleton_proc_id(&proc_id);
790 Self::from_parts_unchecked(proc_id, gateway)
791 }
792
793 pub fn legacy_local_pseudo_singleton(addr: ChannelAddr, forwarder: BoxedMailboxSender) -> Self {
796 Self::legacy_local_pseudo_singleton_on_gateway(Gateway::configured(addr.into(), forwarder))
797 }
798
799 pub fn legacy_service_pseudo_singleton(
802 addr: ChannelAddr,
803 forwarder: BoxedMailboxSender,
804 ) -> Self {
805 Self::legacy_service_pseudo_singleton_on_gateway(Gateway::configured(
806 addr.into(),
807 forwarder,
808 ))
809 }
810
811 pub fn legacy_local_pseudo_singleton_on_gateway(gateway: Gateway) -> Self {
814 Self::legacy_pseudo_singleton_on_gateway(LEGACY_LOCAL_PROC_NAME, gateway)
815 }
816
817 pub fn legacy_service_pseudo_singleton_on_gateway(gateway: Gateway) -> Self {
820 Self::legacy_pseudo_singleton_on_gateway(LEGACY_SERVICE_PROC_NAME, gateway)
821 }
822
823 fn legacy_pseudo_singleton_on_gateway(name: &'static str, gateway: Gateway) -> Self {
824 let proc_id = ProcId::singleton(Label::strip(name));
825 Self::from_parts_unchecked(proc_id, gateway)
826 }
827
828 pub fn anonymous() -> Self {
830 Self::builder()
831 .build()
832 .expect("anonymous proc builder is valid")
833 }
834
835 pub fn instance(label: impl AsRef<str>) -> Self {
837 Self::builder()
838 .proc_id(ProcId::instance(Label::strip(label.as_ref())))
839 .build()
840 .expect("instance proc builder is valid")
841 }
842
843 pub fn singleton(name: impl AsRef<str>) -> Self {
845 Self::builder()
846 .proc_id(ProcId::singleton(Label::strip(name.as_ref())))
847 .build()
848 .expect("singleton proc builder is valid")
849 }
850
851 pub fn isolated() -> Self {
853 Self::builder()
854 .shared_gateway(Gateway::isolated())
855 .build()
856 .expect("isolated proc builder is valid")
857 }
858
859 pub fn builder() -> Builder {
861 Builder::new()
862 }
863
864 pub fn configured(proc_id: impl Into<ProcAddr>, forwarder: BoxedMailboxSender) -> Self {
866 let proc_addr = proc_id.into();
867 Self::from_parts(
868 proc_addr.id().clone(),
869 Gateway::configured(proc_addr.location().clone(), forwarder),
870 )
871 }
872
873 pub fn direct(addr: ChannelAddr, name: String) -> Result<Self, ChannelError> {
878 let (addr, rx) = channel::serve(addr)?;
879 let proc_id = ProcAddr::instance(addr, name);
880 let proc = Self::builder()
881 .proc_id(proc_id.id().clone())
882 .shared_gateway(Gateway::configured(
883 proc_id.location().clone(),
884 DialMailboxRouter::new().into_boxed(),
885 ))
886 .build()
887 .expect("direct proc builder is valid");
888 let handle = proc.gateway().serve_rx(rx);
889 *proc.inner.mailbox_server_handle.lock().unwrap() = Some(handle);
890 Ok(proc)
891 }
892
893 pub fn set_supervision_coordinator(
896 &self,
897 port: PortRef<ActorSupervisionEvent>,
898 ) -> Result<(), anyhow::Error> {
899 let actor_ref: ActorAddr = port.port_addr().actor_addr();
900 self.state()
901 .supervision_coordinator_port
902 .set(port)
903 .map_err(|existing| anyhow::anyhow!("coordinator port is already set to {existing}"))?;
904 let _ = self.state().supervision_coordinator_actor_id.set(actor_ref);
905 Ok(())
906 }
907
908 pub fn supervision_coordinator_actor_addr(&self) -> Option<&ActorAddr> {
911 self.state().supervision_coordinator_actor_id.get()
912 }
913
914 pub fn handle_unhandled_supervision_event(
917 &self,
918 cx: &impl context::Actor,
919 event: ActorSupervisionEvent,
920 ) {
921 let result = match self.state().supervision_coordinator_port.get() {
922 Some(port) => {
923 port.post(cx, event.clone());
924 Ok(())
925 }
926 None => {
927 if !event.is_error() {
928 return;
931 }
932 Err(anyhow::anyhow!(
933 "coordinator port is not set for proc {}",
934 self.proc_addr(),
935 ))
936 }
937 };
938 if let Err(err) = result {
939 if !event.is_error() {
940 tracing::debug!(
943 subject = %self.proc_addr().subject(),
944 "dropping non-error supervision event {}: {:?}",
945 event,
946 err
947 );
948 return;
949 }
950 tracing::error!(
951 subject = %self.proc_addr().subject(),
952 "could not propagate supervision event {} due to error: {:?}: crashing",
953 event,
954 err
955 );
956
957 std::process::exit(1);
958 }
959 }
960
961 pub fn proc_id(&self) -> &ProcId {
963 &self.state().proc_id
964 }
965
966 pub fn default_location(&self) -> Location {
968 self.state().default_location()
969 }
970
971 pub fn set_default_location(&self, location: Location) {
973 self.state().set_default_location(location)
974 }
975
976 pub fn proc_addr(&self) -> ProcAddr {
978 self.state().proc_addr()
979 }
980
981 pub fn gateway(&self) -> Gateway {
983 self.state().gateway.clone()
984 }
985
986 pub fn global() -> Self {
988 static GLOBAL_PROC: OnceLock<Proc> = OnceLock::new();
989 GLOBAL_PROC
990 .get_or_init(|| {
991 let label = global_proc_label();
992 Proc::instance(label.as_str())
993 })
994 .clone()
995 }
996
997 pub fn current() -> Self {
1002 CURRENT_TASK_PROC
1003 .try_with(Clone::clone)
1004 .unwrap_or_else(|_| Self::global())
1005 }
1006
1007 async fn with_current<F>(&self, future: F) -> F::Output
1008 where
1009 F: Future,
1010 {
1011 CURRENT_TASK_PROC.scope(self.clone(), future).await
1012 }
1013
1014 pub fn forwarder(&self) -> BoxedMailboxSender {
1017 self.state().gateway.forwarder()
1018 }
1019
1020 pub fn muxer(&self) -> &MailboxMuxer {
1023 &self.inner.proc_muxer
1024 }
1025
1026 fn state(&self) -> &ProcState {
1028 self.inner.as_ref()
1029 }
1030
1031 pub fn attach(&self, name: &str) -> Result<Mailbox, anyhow::Error> {
1033 let actor_id: ActorAddr = self.allocate_root_id(name)?;
1034 Ok(self.bind_mailbox(actor_id))
1035 }
1036
1037 pub fn attach_child(&self, parent_id: &ActorAddr) -> Result<Mailbox, anyhow::Error> {
1039 let actor_id: ActorAddr = self.allocate_anonymous_child_id(parent_id);
1040 Ok(self.bind_mailbox(actor_id))
1041 }
1042
1043 fn bind_mailbox(&self, actor_id: ActorAddr) -> Mailbox {
1045 let mbox = Mailbox::new(actor_id);
1046
1047 self.state().proc_muxer.bind_mailbox(mbox.clone());
1050 mbox
1051 }
1052
1053 pub fn attach_actor<R, M>(
1056 &self,
1057 name: &str,
1058 ) -> Result<(Client, ActorRef<R>, PortReceiver<M>), anyhow::Error>
1059 where
1060 M: RemoteMessage,
1061 R: Referable + RemoteHandles<M>,
1062 {
1063 let client = self.client(name);
1064 let (_handle, rx) = client.bind_handler_port::<M>();
1065 let actor_ref = ActorRef::attest(client.self_addr().clone());
1066 Ok((client, actor_ref, rx))
1067 }
1068
1069 pub fn spawn<A: Actor>(&self, actor: A) -> ActorHandle<A> {
1071 let actor_id: ActorAddr = self.allocate_root_type::<A>();
1072 self.spawn_inner(actor_id, actor, None)
1073 }
1074
1075 pub fn spawn_with_label<A: Actor>(&self, label: &str, actor: A) -> ActorHandle<A> {
1080 let actor_id: ActorAddr = self.allocate_root_label(label);
1081 self.spawn_inner(actor_id, actor, None)
1082 }
1083
1084 pub fn spawn_with_uid<A: Actor>(
1091 &self,
1092 uid: Uid,
1093 actor: A,
1094 ) -> Result<ActorHandle<A>, anyhow::Error> {
1095 let actor_id: ActorAddr = self.allocate_root_uid(uid)?;
1096 Ok(self.spawn_inner(actor_id, actor, None))
1097 }
1098
1099 fn spawn_inner<A: Actor>(
1101 &self,
1102 actor_id: ActorAddr,
1103 actor: A,
1104 parent: Option<InstanceCell>,
1105 ) -> ActorHandle<A> {
1106 let (instance, receivers) = Instance::new(self.clone(), actor_id, false, parent);
1107 instance.start(actor, receivers)
1108 }
1109
1110 pub fn client(&self, label: &str) -> Client {
1115 let actor_id = self.allocate_client_id(label);
1116 let (instance, _receivers) =
1117 Instance::<ClientActor>::new(self.clone(), actor_id, false, None);
1118 instance.change_status(ActorStatus::Client);
1119 Client::new(instance)
1120 }
1121
1122 pub fn introspectable_instance(
1135 &self,
1136 name: &str,
1137 ) -> Result<(Instance<()>, ActorHandle<()>), anyhow::Error> {
1138 let actor_id: ActorAddr = self.allocate_root_id(name)?;
1139 let (instance, receivers) = Instance::new(self.clone(), actor_id, false, None);
1140 let handle = ActorHandle::new(instance.inner.cell.clone(), instance.inner.ports.clone());
1141 instance.change_status(ActorStatus::Client);
1142 instance.spawn_detached_introspect(receivers.introspect);
1143 Ok((instance, handle))
1144 }
1145
1146 pub fn actor_instance<A: Actor>(&self, name: &str) -> Result<ActorInstance<A>, anyhow::Error> {
1152 let actor_id: ActorAddr = self.allocate_root_id(name)?;
1153 let span = tracing::debug_span!(
1154 "actor_instance",
1155 subject = %actor_id.subject(),
1156 );
1157 let _guard = span.enter();
1158 let (instance, receivers) = Instance::new(self.clone(), actor_id.clone(), false, None);
1159 let handle = ActorHandle::new(instance.inner.cell.clone(), instance.inner.ports.clone());
1160 instance.change_status(ActorStatus::Client);
1161
1162 instance.spawn_detached_introspect(receivers.introspect);
1163
1164 let (signal_rx, supervision_rx) = receivers.actor_loop.unwrap();
1165 Ok(ActorInstance {
1166 instance,
1167 handle,
1168 supervision: supervision_rx,
1169 signal: signal_rx,
1170 work: receivers.work,
1171 })
1172 }
1173
1174 pub fn traverse<F>(&self, f: &mut F)
1176 where
1177 F: FnMut(&InstanceCell, usize),
1178 {
1179 for entry in self.state().root_actors.iter() {
1180 if let Some(cell) = self.get_instance_by_id(entry.key()) {
1181 cell.traverse(f);
1182 }
1183 }
1184 }
1185
1186 pub fn queue_depth_total(&self) -> u64 {
1188 self.state().queue_stats.running_total()
1189 }
1190
1191 pub fn queue_depth_high_water_mark(&self) -> u64 {
1193 self.state().queue_stats.high_water_mark()
1194 }
1195
1196 pub fn last_nonzero_queue_depth_age_ms(&self) -> Option<u64> {
1198 self.state().queue_stats.last_nonzero_age_ms()
1199 }
1200
1201 pub fn get_instance(&self, actor_id: &ActorAddr) -> Option<InstanceCell> {
1203 self.get_instance_by_id(actor_id.id())
1204 }
1205
1206 pub fn get_instance_by_id(&self, actor_id: &ActorId) -> Option<InstanceCell> {
1208 self.state()
1209 .instances
1210 .get(actor_id)
1211 .and_then(|cell| cell.upgrade())
1212 }
1213
1214 pub fn root_actor_ids(&self) -> Vec<ActorAddr> {
1216 self.state()
1217 .root_actors
1218 .iter()
1219 .filter_map(|entry| {
1220 self.get_instance_by_id(entry.key())
1221 .map(|cell| cell.actor_addr().clone())
1222 })
1223 .collect()
1224 }
1225
1226 pub fn all_actor_ids(&self) -> Vec<ActorAddr> {
1235 self.state()
1236 .instances
1237 .iter()
1238 .filter_map(|entry| {
1239 let cell = entry.value().upgrade()?;
1240 (!cell.status().borrow().is_terminal()).then(|| cell.actor_addr().clone())
1241 })
1242 .collect()
1243 }
1244
1245 pub fn all_instance_keys(&self) -> Vec<ActorId> {
1256 self.state()
1257 .instances
1258 .iter()
1259 .map(|entry| entry.key().clone())
1260 .collect()
1261 }
1262
1263 pub fn terminated_snapshot(
1265 &self,
1266 actor_id: &ActorAddr,
1267 ) -> Option<crate::introspect::IntrospectResult> {
1268 self.state()
1269 .terminated_snapshots
1270 .get(actor_id.id())
1271 .map(|entry| entry.value().payload.clone())
1272 }
1273
1274 pub fn all_terminated_actor_ids(&self) -> Vec<ActorAddr> {
1276 self.state()
1277 .terminated_snapshots
1278 .iter()
1279 .map(|entry| entry.value().actor_addr.clone())
1280 .collect()
1281 }
1282
1283 fn child_instance(&self, parent: InstanceCell) -> (Instance<()>, ActorHandle<()>) {
1285 let actor_id = self.allocate_anonymous_child_id(parent.actor_addr());
1286 let _ = tracing::debug_span!(
1287 "child_actor_instance",
1288 subject = %actor_id.subject(),
1289 );
1290
1291 let (instance, _receivers) = Instance::new(self.clone(), actor_id, false, Some(parent));
1292 let handle = ActorHandle::new(instance.inner.cell.clone(), instance.inner.ports.clone());
1295 instance.change_status(ActorStatus::Client);
1296 (instance, handle)
1297 }
1298
1299 pub(crate) fn spawn_child<A: Actor>(&self, parent: InstanceCell, actor: A) -> ActorHandle<A> {
1305 let actor_id = self.allocate_child_id::<A>(parent.actor_addr());
1306 self.spawn_inner(actor_id, actor, Some(parent))
1307 }
1308
1309 pub(crate) fn spawn_child_with_uid<A: Actor>(
1311 &self,
1312 parent: InstanceCell,
1313 uid: Uid,
1314 actor: A,
1315 ) -> Result<ActorHandle<A>, anyhow::Error> {
1316 let actor_id = self.ensure_child_uid(parent.actor_addr(), uid)?;
1317 Ok(self.spawn_inner(actor_id, actor, Some(parent)))
1318 }
1319
1320 pub(crate) fn spawn_named_child<A: Actor>(
1324 &self,
1325 parent: InstanceCell,
1326 name: &str,
1327 actor: A,
1328 ) -> ActorHandle<A> {
1329 let actor_id = self.allocate_named_child_id(parent.actor_addr(), name);
1330 self.spawn_inner(actor_id, actor, Some(parent))
1331 }
1332
1333 pub fn abort_root_actor(&self, root: &ActorId) -> Option<impl Future<Output = ActorAddr>> {
1337 self.state()
1338 .instances
1339 .get(root)
1340 .into_iter()
1341 .flat_map(|entry| entry.value().upgrade())
1342 .map(|cell| {
1343 let actor_addr = cell.actor_addr().clone();
1344 let r1 = actor_addr.clone();
1345 let r2 = actor_addr;
1346 async move {
1350 tokio::task::spawn_blocking(move || {
1351 let h = cell.inner.actor_task_handle.wait();
1352 tracing::debug!("{}: aborting {:?}", r1, h);
1353 h.abort();
1354 })
1355 .await
1356 .unwrap();
1357 r2
1358 }
1359 })
1360 .next()
1361 }
1362
1363 pub fn stop_actor(
1366 &self,
1367 actor_id: &ActorId,
1368 reason: String,
1369 ) -> Option<watch::Receiver<ActorStatus>> {
1370 let cell = match self.state().instances.get(actor_id) {
1375 None => {
1376 tracing::error!(subject = %self.proc_addr().subject(), "no actor {} found", actor_id);
1377 return None;
1378 }
1379 Some(entry) => entry.value().upgrade(),
1380 }; match cell {
1382 None => None, Some(cell) => {
1384 tracing::info!("sending stop signal to {}", cell.actor_addr());
1385 if let Err(err) = cell.signal(Signal::DrainAndStop(reason)) {
1386 tracing::error!(
1387 "failed to send stop signal to uid {}: {:?}",
1388 cell.uid(),
1389 err
1390 );
1391 None
1392 } else {
1393 Some(cell.status().clone())
1394 }
1395 }
1396 }
1397 }
1398
1399 #[hyperactor::instrument(fields(subject = self.proc_addr().subject().to_string()))]
1403 pub async fn destroy_and_wait(
1404 &mut self,
1405 timeout: Duration,
1406 reason: &str,
1407 ) -> Result<(Vec<ActorAddr>, Vec<ActorAddr>), anyhow::Error> {
1408 tracing::debug!("proc stopping");
1409
1410 let coordinator_id = self.supervision_coordinator_actor_addr().cloned();
1411
1412 let mut statuses = HashMap::new();
1415 for actor_id in self
1416 .state()
1417 .root_actors
1418 .iter()
1419 .filter_map(|entry| self.get_instance_by_id(entry.key()))
1420 .filter(|cell| !matches!(*cell.status().borrow(), ActorStatus::Client))
1421 .map(|cell| cell.actor_addr().clone())
1422 .collect::<Vec<_>>()
1423 {
1424 if coordinator_id.as_ref() == Some(&actor_id) {
1425 continue;
1426 }
1427 if let Some(status) = self.stop_actor(actor_id.id(), reason.to_string()) {
1428 statuses.insert(actor_id, status);
1429 }
1430 }
1431 tracing::debug!("non-coordinator actors stopped");
1432
1433 let waits: Vec<_> = statuses
1434 .iter_mut()
1435 .map(|(actor_id, root)| {
1436 let actor_id = actor_id.clone();
1437 async move {
1438 tokio::time::timeout(
1439 timeout,
1440 root.wait_for(|state: &ActorStatus| state.is_terminal()),
1441 )
1442 .await
1443 .ok()
1444 .map(|_| actor_id)
1445 }
1446 })
1447 .collect();
1448
1449 let results = futures::future::join_all(waits).await;
1450 let mut stopped_actors: Vec<_> = results
1451 .iter()
1452 .filter_map(|actor_id| actor_id.as_ref())
1453 .cloned()
1454 .collect();
1455 let aborted_actors: Vec<_> = statuses
1456 .iter()
1457 .filter(|(actor_id, _)| !stopped_actors.contains(actor_id))
1458 .map(|(actor_id, _)| {
1459 let f = self.abort_root_actor(actor_id.id());
1460 async move {
1461 let _ = if let Some(f) = f { Some(f.await) } else { None };
1462 actor_id.clone()
1467 }
1468 })
1469 .collect();
1470 let mut aborted_actors = futures::future::join_all(aborted_actors).await;
1471
1472 if let Some(ref coord_id) = coordinator_id
1478 && let Some(mut status) = self.stop_actor(coord_id.id(), reason.to_string())
1479 {
1480 let stopped =
1481 tokio::time::timeout(timeout, status.wait_for(|s: &ActorStatus| s.is_terminal()))
1482 .await
1483 .is_ok();
1484 if stopped {
1485 stopped_actors.push(coord_id.clone());
1486 } else {
1487 if let Some(f) = self.abort_root_actor(coord_id.id()) {
1488 f.await;
1489 }
1490 aborted_actors.push(coord_id.clone());
1491 }
1492 }
1493
1494 let flush_timeout = hyperactor_config::global::get(crate::config::FORWARDER_FLUSH_TIMEOUT);
1501 let gateway = self.gateway();
1502 match tokio::time::timeout(flush_timeout, gateway.flush()).await {
1503 Ok(Err(err)) => {
1504 tracing::warn!("gateway flush failed during proc exit: {:?}", err);
1505 }
1506 Err(_elapsed) => {
1507 tracing::warn!("gateway flush timed out during proc exit");
1508 }
1509 Ok(Ok(())) => {}
1510 }
1511
1512 tracing::info!(
1513 "destroy_and_wait: {} actors stopped, {} actors aborted",
1514 stopped_actors.len(),
1515 aborted_actors.len()
1516 );
1517 Ok((stopped_actors, aborted_actors))
1518 }
1519
1520 pub fn resolve_actor_ref<R: Actor + Referable>(
1539 &self,
1540 actor_ref: &ActorRef<R>,
1541 ) -> Option<ActorHandle<R>> {
1542 let cell = self
1543 .inner
1544 .instances
1545 .get(actor_ref.actor_addr().id())?
1546 .upgrade()?;
1547 if cell.status().borrow().is_terminal() {
1551 return None;
1552 }
1553 cell.downcast_handle()
1554 }
1555
1556 fn allocate_root_id(&self, name: &str) -> Result<ActorAddr, anyhow::Error> {
1560 self.reserve_root(Uid::singleton(Label::strip(name)))
1561 }
1562
1563 fn allocate_root_type<A: Actor>(&self) -> ActorAddr {
1565 self.root_addr(Uid::instance(default_actor_label::<A>()))
1566 }
1567
1568 fn allocate_root_label(&self, label: &str) -> ActorAddr {
1570 self.root_addr(Uid::instance(Label::strip(label)))
1571 }
1572
1573 fn allocate_root_uid(&self, uid: Uid) -> Result<ActorAddr, anyhow::Error> {
1575 self.reserve_root(uid)
1576 }
1577
1578 fn allocate_client_id(&self, label: &str) -> ActorAddr {
1579 let actor_id = if label.is_empty() {
1580 ActorId::anonymous(self.proc_id().clone())
1581 } else {
1582 ActorId::instance(Label::strip(label), self.proc_id().clone())
1583 };
1584 ActorAddr::new(actor_id, self.default_location())
1585 }
1586
1587 fn reserve_root(&self, uid: Uid) -> Result<ActorAddr, anyhow::Error> {
1588 let actor_id = ActorId::new(uid.clone(), self.proc_id().clone(), None);
1589 if !self.state().reserved_roots.insert(uid) {
1590 anyhow::bail!("an actor with id '{}' has already been spawned", actor_id)
1591 }
1592 Ok(ActorAddr::new(actor_id, self.default_location()))
1593 }
1594
1595 fn root_addr(&self, uid: Uid) -> ActorAddr {
1596 ActorAddr::new(
1597 ActorId::new(uid, self.proc_id().clone(), None),
1598 self.default_location(),
1599 )
1600 }
1601
1602 pub(crate) fn allocate_anonymous_child_id(&self, parent_id: &ActorAddr) -> ActorAddr {
1604 assert_eq!(parent_id.proc_id(), self.proc_id());
1605 ActorAddr::new(
1606 ActorId::anonymous(self.proc_id().clone()),
1607 self.default_location(),
1608 )
1609 }
1610
1611 pub(crate) fn allocate_child_id<A: Actor>(&self, parent_id: &ActorAddr) -> ActorAddr {
1613 assert_eq!(parent_id.proc_id(), self.proc_id());
1614 let actor_id = ActorId::instance(default_actor_label::<A>(), self.proc_id().clone());
1615 ActorAddr::new(actor_id, self.default_location())
1616 }
1617
1618 fn ensure_child_uid(
1620 &self,
1621 parent_id: &ActorAddr,
1622 uid: Uid,
1623 ) -> Result<ActorAddr, anyhow::Error> {
1624 assert_eq!(parent_id.proc_id(), self.proc_id());
1625 let actor_id = ActorId::new(uid.clone(), self.proc_id().clone(), None);
1626 let actor_addr = ActorAddr::new(actor_id, self.default_location());
1627 if !self.state().reserved_child_uids.insert(uid) {
1628 anyhow::bail!("an actor with id {} has already been spawned", actor_addr);
1629 }
1630 Ok(actor_addr)
1631 }
1632
1633 pub(crate) fn allocate_named_child_id(&self, parent_id: &ActorAddr, name: &str) -> ActorAddr {
1635 assert_eq!(parent_id.proc_id(), self.proc_id());
1636 let proc_id = self.proc_id().clone();
1637 let actor_id = crate::id::ActorId::instance(crate::id::Label::strip(name), proc_id);
1638 ActorAddr::new(actor_id, self.default_location())
1639 }
1640
1641 pub fn downgrade(&self) -> WeakProc {
1643 WeakProc::new(self)
1644 }
1645
1646 pub async fn flush(&self) -> Result<(), anyhow::Error> {
1649 self.gateway().flush().await
1650 }
1651
1652 pub async fn join_mailbox_server(&self) {
1661 let handle = self.inner.mailbox_server_handle.lock().unwrap().take();
1662 if let Some(handle) = handle {
1663 handle.stop("proc shutting down");
1664 let _ = handle.await;
1665 }
1666 }
1667
1668 pub(crate) fn is_local_delivery_target(&self, dest_proc: &ProcAddr) -> bool {
1669 self.is_local_delivery_target_at(dest_proc, &[self.default_location()])
1670 }
1671
1672 pub(crate) fn is_local_delivery_target_at(
1673 &self,
1674 dest_proc: &ProcAddr,
1675 local_locations: &[Location],
1676 ) -> bool {
1677 dest_proc.id() == self.proc_id()
1678 && (!requires_location_for_local_delivery_identity(dest_proc.id())
1679 || local_locations
1680 .iter()
1681 .any(|location| dest_proc.location() == location))
1682 }
1683}
1684
1685fn requires_location_for_local_delivery_identity(proc_id: &ProcId) -> bool {
1686 is_legacy_pseudo_singleton_proc_id(proc_id)
1692}
1693
1694fn default_actor_label<A>() -> Label {
1695 let type_name = std::any::type_name::<A>();
1696 let type_name = type_name
1697 .split_once('<')
1698 .map_or(type_name, |(base, _)| base);
1699 Label::strip(type_name.rsplit("::").next().unwrap_or(type_name))
1700}
1701
1702fn global_proc_label() -> Label {
1703 let hostname = hostname::get().expect("hostname should be available");
1704 global_proc_label_from(&hostname.to_string_lossy(), std::process::id())
1705}
1706
1707fn global_proc_label_from(hostname: &str, pid: u32) -> Label {
1708 let short_hostname = hostname
1709 .split_once('.')
1710 .map_or(hostname, |(short, _)| short);
1711 Label::strip(&format!("{}-{}", short_hostname, pid))
1712}
1713
1714fn assert_not_legacy_pseudo_singleton_proc_id(proc_id: &ProcId) {
1715 if is_legacy_pseudo_singleton_proc_id(proc_id) {
1716 panic!(
1717 "legacy pseudo-singleton proc id '{}' must be constructed with a dedicated Proc constructor",
1718 proc_id
1719 );
1720 }
1721}
1722
1723fn is_legacy_pseudo_singleton_proc_id(proc_id: &ProcId) -> bool {
1724 matches!(
1725 proc_id.uid(),
1726 Uid::Singleton(label) if is_legacy_pseudo_singleton_label(label)
1727 )
1728}
1729
1730fn is_legacy_pseudo_singleton_label(label: &Label) -> bool {
1731 matches!(
1732 label.as_str(),
1733 LEGACY_SERVICE_PROC_NAME | LEGACY_LOCAL_PROC_NAME
1734 )
1735}
1736
1737#[async_trait]
1738impl MailboxSender for Proc {
1739 fn post_unchecked(
1740 &self,
1741 envelope: MessageEnvelope,
1742 return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
1743 ) {
1744 let dest_proc = envelope.dest().actor_addr().proc_addr();
1745 if self.is_local_delivery_target(&dest_proc) {
1746 self.state().proc_muxer.post(envelope, return_handle);
1747 return;
1748 }
1749 self.state().gateway.post(envelope, return_handle);
1755 }
1756
1757 async fn flush(&self) -> Result<(), anyhow::Error> {
1758 self.gateway().flush().await
1759 }
1760}
1761
1762#[derive(Clone, Debug)]
1764pub struct WeakProc(Weak<ProcState>);
1765
1766impl WeakProc {
1767 fn new(proc: &Proc) -> Self {
1768 Self(Arc::downgrade(&proc.inner))
1769 }
1770
1771 pub fn upgrade(&self) -> Option<Proc> {
1773 self.0.upgrade().map(|inner| Proc { inner })
1774 }
1775
1776 pub(crate) fn ptr_eq(&self, other: &WeakProc) -> bool {
1778 self.0.ptr_eq(&other.0)
1779 }
1780}
1781
1782#[async_trait]
1783impl MailboxSender for WeakProc {
1784 fn post_unchecked(
1785 &self,
1786 envelope: MessageEnvelope,
1787 return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
1788 ) {
1789 match self.upgrade() {
1790 Some(proc) => proc.post(envelope, return_handle),
1791 None => {
1792 let target = envelope.dest().clone();
1793 let failure =
1794 DeliveryFailure::new(UndeliverableReason::Transport(TransportFailure::new(
1795 target,
1796 TransportFailureReason::LinkUnavailable("proc is gone".to_string()),
1797 )));
1798 envelope.undeliverable(failure, return_handle)
1799 }
1800 }
1801 }
1802
1803 async fn flush(&self) -> Result<(), anyhow::Error> {
1804 match self.upgrade() {
1805 Some(proc) => proc.flush().await,
1806 None => Ok(()),
1807 }
1808 }
1809}
1810
1811pub struct WorkCell<A: Actor + Send>(
1814 Box<
1815 dyn for<'a> FnOnce(
1816 &'a mut A,
1817 &'a Instance<A>,
1818 )
1819 -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>> + 'a + Send>>
1820 + Send
1821 + Sync,
1822 >,
1823);
1824
1825impl<A: Actor + Send> WorkCell<A> {
1826 fn new(
1828 f: impl for<'a> FnOnce(
1829 &'a mut A,
1830 &'a Instance<A>,
1831 )
1832 -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>> + 'a + Send>>
1833 + Send
1834 + Sync
1835 + 'static,
1836 ) -> Self {
1837 Self(Box::new(f))
1838 }
1839
1840 pub fn handle<'a>(
1842 self,
1843 actor: &'a mut A,
1844 instance: &'a Instance<A>,
1845 ) -> Pin<Box<dyn Future<Output = Result<(), anyhow::Error>> + Send + 'a>> {
1846 (self.0)(actor, instance)
1847 }
1848}
1849
1850pub struct Context<'a, A: Actor> {
1852 instance: &'a Instance<A>,
1853 headers: Flattrs,
1854}
1855
1856impl<'a, A: Actor> Context<'a, A> {
1857 pub fn new(instance: &'a Instance<A>, headers: Flattrs) -> Self {
1859 Self { instance, headers }
1860 }
1861
1862 pub fn headers(&self) -> &Flattrs {
1864 &self.headers
1865 }
1866}
1867
1868impl<A: Actor> Deref for Context<'_, A> {
1869 type Target = Instance<A>;
1870
1871 fn deref(&self) -> &Self::Target {
1872 self.instance
1873 }
1874}
1875
1876pub struct Instance<A: Actor> {
1880 inner: Arc<InstanceState<A>>,
1881}
1882
1883impl<A: Actor> fmt::Debug for Instance<A> {
1884 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1885 f.debug_struct("Instance").field("inner", &"..").finish()
1886 }
1887}
1888
1889struct InstanceState<A: Actor> {
1890 proc: Proc,
1892
1893 cell: InstanceCell,
1895
1896 mailbox: Mailbox,
1898
1899 ports: Arc<HandlerPorts<A>>,
1900
1901 delayed_posts: DelayedPosts<A>,
1903
1904 introspect_shutdown_tx: Mutex<Option<oneshot::Sender<ActorStatus>>>,
1906
1907 introspect_task_handle: Mutex<Option<JoinHandle<()>>>,
1909
1910 detached_introspect_shutdown_tx: Mutex<Option<oneshot::Sender<ActorStatus>>>,
1914
1915 id: Uuid,
1917
1918 sequencer: Sequencer,
1920
1921 instance_locals: ActorLocalStorage,
1923}
1924
1925struct ActorStopped {
1926 reason: String,
1927 stop_mode: StopMode,
1928}
1929
1930#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1931enum ChildTeardown {
1932 Cooperative(StopMode),
1933 Kill,
1934}
1935
1936impl ChildTeardown {
1937 fn from_run_result(result: &Result<ActorStopped, ActorError>) -> Self {
1938 match result {
1939 Ok(stopped) => Self::Cooperative(stopped.stop_mode),
1940 Err(err) if matches!(err.kind.as_ref(), ActorErrorKind::Aborted(_)) => Self::Kill,
1941 Err(_) => Self::Cooperative(StopMode::Stop),
1942 }
1943 }
1944
1945 fn cooperative_signal(mode: StopMode) -> Signal {
1946 match mode {
1947 StopMode::DrainAndStop => Signal::DrainAndStop("parent draining".to_string()),
1948 StopMode::Stop => Signal::Stop("parent stopping".to_string()),
1949 }
1950 }
1951}
1952
1953type DelayedPost<A> = Box<dyn FnOnce(&Instance<A>) + Send>;
1954
1955trait PostAfterEndpoint<A: Actor, M: Message>: Send {
1956 fn endpoint_location(&self) -> crate::EndpointLocation;
1957
1958 fn into_delayed_post(self, message: M) -> DelayedPost<A>;
1959}
1960
1961impl<A, M> PostAfterEndpoint<A, M> for &Instance<A>
1962where
1963 A: Actor + Handler<M>,
1964 M: Message,
1965{
1966 fn endpoint_location(&self) -> crate::EndpointLocation {
1967 crate::EndpointLocation::Actor(self.self_addr().clone())
1968 }
1969
1970 fn into_delayed_post(self, message: M) -> DelayedPost<A> {
1971 let dest = self.clone_for_py();
1972 Box::new(move |this| crate::Endpoint::post(&dest, this, message))
1973 }
1974}
1975
1976impl<A, M> PostAfterEndpoint<A, M> for &Context<'_, A>
1977where
1978 A: Actor + Handler<M>,
1979 M: Message,
1980{
1981 fn endpoint_location(&self) -> crate::EndpointLocation {
1982 crate::EndpointLocation::Actor(self.self_addr().clone())
1983 }
1984
1985 fn into_delayed_post(self, message: M) -> DelayedPost<A> {
1986 let dest = self.clone_for_py();
1987 Box::new(move |this| crate::Endpoint::post(&dest, this, message))
1988 }
1989}
1990
1991impl<A, M> PostAfterEndpoint<A, M> for Instance<A>
1992where
1993 A: Actor + Handler<M>,
1994 M: Message,
1995{
1996 fn endpoint_location(&self) -> crate::EndpointLocation {
1997 crate::EndpointLocation::Actor(self.self_addr().clone())
1998 }
1999
2000 fn into_delayed_post(self, message: M) -> DelayedPost<A> {
2001 Box::new(move |this| crate::Endpoint::post(&self, this, message))
2002 }
2003}
2004
2005impl<A, B, M> PostAfterEndpoint<A, M> for ActorHandle<B>
2006where
2007 A: Actor,
2008 B: Actor + Handler<M>,
2009 M: Message,
2010{
2011 fn endpoint_location(&self) -> crate::EndpointLocation {
2012 crate::Endpoint::endpoint_location(&self)
2013 }
2014
2015 fn into_delayed_post(self, message: M) -> DelayedPost<A> {
2016 Box::new(move |this| crate::Endpoint::post(&self, this, message))
2017 }
2018}
2019
2020impl<A, M> PostAfterEndpoint<A, M> for PortHandle<M>
2021where
2022 A: Actor,
2023 M: Message,
2024{
2025 fn endpoint_location(&self) -> crate::EndpointLocation {
2026 crate::Endpoint::endpoint_location(&self)
2027 }
2028
2029 fn into_delayed_post(self, message: M) -> DelayedPost<A> {
2030 Box::new(move |this| crate::Endpoint::post(&self, this, message))
2031 }
2032}
2033
2034impl<A, M> PostAfterEndpoint<A, M> for OncePortHandle<M>
2035where
2036 A: Actor,
2037 M: Message,
2038{
2039 fn endpoint_location(&self) -> crate::EndpointLocation {
2040 crate::Endpoint::endpoint_location(self)
2041 }
2042
2043 fn into_delayed_post(self, message: M) -> DelayedPost<A> {
2044 Box::new(move |this| crate::Endpoint::post(self, this, message))
2045 }
2046}
2047
2048impl<A, B, M> PostAfterEndpoint<A, M> for ActorRef<B>
2049where
2050 A: Actor,
2051 B: Referable + RemoteHandles<M>,
2052 M: RemoteMessage,
2053{
2054 fn endpoint_location(&self) -> crate::EndpointLocation {
2055 crate::Endpoint::endpoint_location(&self)
2056 }
2057
2058 fn into_delayed_post(self, message: M) -> DelayedPost<A> {
2059 Box::new(move |this| crate::Endpoint::post(&self, this, message))
2060 }
2061}
2062
2063impl<A, M> PostAfterEndpoint<A, M> for crate::PortRef<M>
2064where
2065 A: Actor,
2066 M: RemoteMessage,
2067{
2068 fn endpoint_location(&self) -> crate::EndpointLocation {
2069 crate::Endpoint::endpoint_location(&self)
2070 }
2071
2072 fn into_delayed_post(self, message: M) -> DelayedPost<A> {
2073 Box::new(move |this| crate::Endpoint::post(&self, this, message))
2074 }
2075}
2076
2077impl<A, M> PostAfterEndpoint<A, M> for crate::OncePortRef<M>
2078where
2079 A: Actor,
2080 M: RemoteMessage,
2081{
2082 fn endpoint_location(&self) -> crate::EndpointLocation {
2083 crate::Endpoint::endpoint_location(self)
2084 }
2085
2086 fn into_delayed_post(self, message: M) -> DelayedPost<A> {
2087 Box::new(move |this| crate::Endpoint::post(self, this, message))
2088 }
2089}
2090
2091struct DelayedPosts<A: Actor> {
2092 ingress: Arc<DelayedPostIngressGate>,
2093 state: Mutex<DelayedPostState<A>>,
2094 notify: Notify,
2095}
2096
2097struct DelayedPostState<A: Actor> {
2098 queue: BTreeMap<(tokio::time::Instant, u64), DelayedPost<A>>,
2099 next_order: u64,
2100}
2101
2102impl<A: Actor> DelayedPosts<A> {
2103 fn new() -> Self {
2104 Self {
2105 ingress: Arc::new(DelayedPostIngressGate::new()),
2106 state: Mutex::new(DelayedPostState {
2107 queue: BTreeMap::new(),
2108 next_order: 0,
2109 }),
2110 notify: Notify::new(),
2111 }
2112 }
2113
2114 fn push(&self, deadline: tokio::time::Instant, post: DelayedPost<A>) {
2115 let mut state = self.state.lock().unwrap();
2116 let order = state.next_order;
2117 state.next_order = state.next_order.wrapping_add(1);
2118 state.queue.insert((deadline, order), post);
2119 drop(state);
2120 self.notify.notify_one();
2121 }
2122
2123 fn next_deadline(&self) -> Option<tokio::time::Instant> {
2124 self.state
2125 .lock()
2126 .unwrap()
2127 .queue
2128 .keys()
2129 .next()
2130 .map(|(deadline, _)| *deadline)
2131 }
2132
2133 fn pop_due(&self, now: tokio::time::Instant) -> Vec<DelayedPost<A>> {
2134 let mut posts = Vec::new();
2135 let mut state = self.state.lock().unwrap();
2136 while let Some((&(deadline, _), _)) = state.queue.first_key_value() {
2137 if deadline > now {
2138 break;
2139 }
2140 let (_, post) = state.queue.pop_first().expect("delayed post should exist");
2141 posts.push(post);
2142 }
2143 posts
2144 }
2145
2146 fn drain(&self) {
2147 self.ingress.drain();
2148 }
2149
2150 fn is_draining(&self) -> bool {
2151 self.ingress.is_draining()
2152 }
2153}
2154
2155const DELAYED_POST_INGRESS_DRAINING: usize = 1usize << (usize::BITS as usize - 1);
2156const DELAYED_POST_INGRESS_ACTIVE_MASK: usize = !DELAYED_POST_INGRESS_DRAINING;
2157
2158struct DelayedPostIngressGate {
2159 state: AtomicUsize,
2160 wait_lock: Mutex<()>,
2161 drained: Condvar,
2162}
2163
2164struct DelayedPostIngressGuard {
2165 gate: Arc<DelayedPostIngressGate>,
2166}
2167
2168impl DelayedPostIngressGate {
2169 fn new() -> Self {
2170 Self {
2171 state: AtomicUsize::new(0),
2172 wait_lock: Mutex::new(()),
2173 drained: Condvar::new(),
2174 }
2175 }
2176
2177 fn try_enter(self: &Arc<Self>) -> Result<DelayedPostIngressGuard, ()> {
2178 let mut state = self.state.load(Ordering::Acquire);
2179 loop {
2180 if state & DELAYED_POST_INGRESS_DRAINING != 0 {
2181 return Err(());
2182 }
2183
2184 let active = state & DELAYED_POST_INGRESS_ACTIVE_MASK;
2185 assert!(
2186 active < DELAYED_POST_INGRESS_ACTIVE_MASK,
2187 "too many active delayed post sends"
2188 );
2189
2190 match self.state.compare_exchange_weak(
2191 state,
2192 state + 1,
2193 Ordering::AcqRel,
2194 Ordering::Acquire,
2195 ) {
2196 Ok(_) => {
2197 return Ok(DelayedPostIngressGuard {
2198 gate: Arc::clone(self),
2199 });
2200 }
2201 Err(next_state) => state = next_state,
2202 }
2203 }
2204 }
2205
2206 fn drain(&self) {
2207 let mut state = self.state.load(Ordering::Acquire);
2208 loop {
2209 if state & DELAYED_POST_INGRESS_DRAINING != 0 {
2210 break;
2211 }
2212 match self.state.compare_exchange_weak(
2213 state,
2214 state | DELAYED_POST_INGRESS_DRAINING,
2215 Ordering::AcqRel,
2216 Ordering::Acquire,
2217 ) {
2218 Ok(_) => break,
2219 Err(next_state) => state = next_state,
2220 }
2221 }
2222
2223 let mut wait_guard = self.wait_lock.lock().unwrap();
2224 while self.state.load(Ordering::Acquire) & DELAYED_POST_INGRESS_ACTIVE_MASK != 0 {
2225 wait_guard = self.drained.wait(wait_guard).unwrap();
2226 }
2227 }
2228
2229 fn is_draining(&self) -> bool {
2230 self.state.load(Ordering::Acquire) & DELAYED_POST_INGRESS_DRAINING != 0
2231 }
2232}
2233
2234impl Drop for DelayedPostIngressGuard {
2235 fn drop(&mut self) {
2236 let previous = self.gate.state.fetch_sub(1, Ordering::AcqRel);
2237 assert!(
2238 previous & DELAYED_POST_INGRESS_ACTIVE_MASK != 0,
2239 "delayed post ingress active count underflow"
2240 );
2241 if previous & DELAYED_POST_INGRESS_DRAINING != 0
2242 && previous & DELAYED_POST_INGRESS_ACTIVE_MASK == 1
2243 {
2244 let _wait_guard = self.gate.wait_lock.lock().unwrap();
2245 self.gate.drained.notify_all();
2246 }
2247 }
2248}
2249
2250impl<A: Actor> InstanceState<A> {
2251 fn self_addr(&self) -> &ActorAddr {
2252 self.mailbox.actor_addr()
2253 }
2254}
2255
2256impl<A: Actor> Drop for InstanceState<A> {
2257 fn drop(&mut self) {
2258 let terminal_status = ActorStatus::Stopped("instance is dropped".into());
2259 if let Some(shutdown_tx) = self.detached_introspect_shutdown_tx.lock().unwrap().take() {
2260 let _ = shutdown_tx.send(terminal_status.clone());
2261 return;
2262 }
2263
2264 if let Some(shutdown_tx) = self.introspect_shutdown_tx.lock().unwrap().take() {
2265 let _ = shutdown_tx.send(terminal_status.clone());
2266 }
2267 let _ = self.introspect_task_handle.lock().unwrap().take();
2268
2269 self.cell.publish_dropped_status(terminal_status);
2270 }
2271}
2272
2273pub struct InstanceReceivers<A: Actor> {
2280 actor_loop: Option<(
2283 mpsc::UnboundedReceiver<Signal>,
2284 mpsc::UnboundedReceiver<ActorSupervisionEvent>,
2285 )>,
2286 work: ActorWorkReceiver<A>,
2288 introspect: PortReceiver<IntrospectMessage>,
2290}
2291
2292impl<A: Actor> Instance<A> {
2293 fn new(
2295 proc: Proc,
2296 actor_id: ActorAddr,
2297 detached: bool,
2298 parent: Option<InstanceCell>,
2299 ) -> (Self, InstanceReceivers<A>) {
2300 let mailbox = Mailbox::new(actor_id.clone());
2302 let enable_buffering =
2303 hyperactor_config::global::get(config::ENABLE_DEST_ACTOR_REORDERING_BUFFER);
2304 let (work_tx, work_rx) = sequenced_unbounded_with_buffering(enable_buffering);
2305 let inbound_ordering_snapshot_handle = work_rx.snapshot_handle();
2306 let queue_depth = Arc::new(AtomicU64::new(0));
2307 let proc_stats = Arc::clone(&proc.state().queue_stats);
2308 let ports: Arc<HandlerPorts<A>> = Arc::new(HandlerPorts::new(
2309 mailbox.clone(),
2310 work_tx,
2311 enable_buffering,
2312 Arc::clone(&queue_depth),
2313 proc_stats,
2314 ));
2315 proc.state().proc_muxer.bind_mailbox(mailbox.clone());
2316 let (status_tx, status_rx) = watch::channel(ActorStatus::Created);
2317
2318 let actor_type = match TypeInfo::of::<A>() {
2319 Some(info) => ActorType::Named(info),
2320 None => ActorType::Anonymous(std::any::type_name::<A>()),
2321 };
2322 let actor_loop_ports = if detached {
2323 None
2324 } else {
2325 let (signal_tx, signal_receiver) = mpsc::unbounded_channel::<Signal>();
2326 let (supervision_tx, supervision_receiver) =
2327 mpsc::unbounded_channel::<ActorSupervisionEvent>();
2328 Some((
2329 (signal_tx, supervision_tx),
2330 (signal_receiver, supervision_receiver),
2331 ))
2332 };
2333
2334 let (actor_loop, actor_loop_receivers) = actor_loop_ports.unzip();
2335
2336 let (introspect_port, introspect_receiver) = mailbox.open_port::<IntrospectMessage>();
2342 introspect_port.bind_control_port(crate::port::ControlPort::Introspect);
2343
2344 let instance_id = Uuid::now_v7();
2345
2346 let inbound_ordering_snapshot: Option<
2349 Box<dyn Fn() -> crate::ordering::OrderingSnapshot + Send + Sync>,
2350 > = Some(Box::new(move || {
2351 inbound_ordering_snapshot_handle.snapshot()
2352 }));
2353
2354 let cell = InstanceCell::new(
2355 actor_id,
2356 instance_id,
2357 actor_type,
2358 proc.clone(),
2359 actor_loop,
2360 status_tx,
2361 status_rx,
2362 parent,
2363 ports.clone(),
2364 queue_depth,
2365 inbound_ordering_snapshot,
2366 );
2367 let inner = Arc::new(InstanceState {
2368 proc,
2369 cell,
2370 mailbox,
2371 ports,
2372 delayed_posts: DelayedPosts::new(),
2373 introspect_shutdown_tx: Mutex::new(None),
2374 introspect_task_handle: Mutex::new(None),
2375 detached_introspect_shutdown_tx: Mutex::new(None),
2376 sequencer: Sequencer::new(instance_id),
2377 id: instance_id,
2378 instance_locals: ActorLocalStorage::new(),
2379 });
2380 (
2381 Self { inner },
2382 InstanceReceivers {
2383 actor_loop: actor_loop_receivers,
2384 work: ActorWorkReceiver::new(work_rx),
2385 introspect: introspect_receiver,
2386 },
2387 )
2388 }
2389
2390 fn spawn_introspect(&self, receiver: PortReceiver<IntrospectMessage>) {
2391 let (shutdown_tx, shutdown_rx) = oneshot::channel();
2392 let handle = tokio::spawn(crate::introspect::serve_introspect(
2393 self.inner.cell.clone(),
2394 receiver,
2395 shutdown_rx,
2396 ));
2397
2398 let mut shutdown = self.inner.introspect_shutdown_tx.lock().unwrap();
2399 assert!(
2400 shutdown.is_none(),
2401 "introspection shutdown handle already set"
2402 );
2403 let mut task = self.inner.introspect_task_handle.lock().unwrap();
2404 assert!(task.is_none(), "introspection task already set");
2405 *shutdown = Some(shutdown_tx);
2406 *task = Some(handle);
2407 }
2408
2409 fn spawn_detached_introspect(&self, receiver: PortReceiver<IntrospectMessage>) {
2410 let (detached_shutdown_tx, detached_shutdown_rx) = oneshot::channel::<ActorStatus>();
2411 let (introspect_shutdown_tx, introspect_shutdown_rx) = oneshot::channel();
2412 let introspect_handle = tokio::spawn(crate::introspect::serve_introspect(
2413 self.inner.cell.clone(),
2414 receiver,
2415 introspect_shutdown_rx,
2416 ));
2417
2418 let cell = self.inner.cell.clone();
2419 tokio::spawn(async move {
2420 let Ok(terminal_status) = detached_shutdown_rx.await else {
2421 return;
2422 };
2423 let _ = introspect_shutdown_tx.send(terminal_status.clone());
2424 if let Err(err) = introspect_handle.await {
2425 tracing::debug!("introspect task join failed: {:?}", err);
2426 }
2427 cell.publish_dropped_status(terminal_status);
2428 });
2429
2430 let mut shutdown = self.inner.detached_introspect_shutdown_tx.lock().unwrap();
2431 assert!(
2432 shutdown.is_none(),
2433 "detached introspection shutdown handle already set"
2434 );
2435 *shutdown = Some(detached_shutdown_tx);
2436 }
2437
2438 fn signal_introspect_stop(&self, terminal_status: ActorStatus) -> Option<JoinHandle<()>> {
2439 if let Some(shutdown_tx) = self.inner.introspect_shutdown_tx.lock().unwrap().take() {
2440 let _ = shutdown_tx.send(terminal_status);
2441 }
2442 self.inner.introspect_task_handle.lock().unwrap().take()
2443 }
2444
2445 async fn stop_introspect(&self, terminal_status: ActorStatus) {
2446 if let Some(handle) = self.signal_introspect_stop(terminal_status)
2447 && let Err(err) = handle.await
2448 {
2449 tracing::debug!("introspect task join failed: {:?}", err);
2450 }
2451 }
2452
2453 #[track_caller]
2456 pub fn change_status(&self, new: ActorStatus) {
2457 self.inner.cell.change_status(new);
2458 }
2459
2460 fn is_terminal(&self) -> bool {
2461 self.inner.cell.status().borrow().is_terminal()
2462 }
2463
2464 fn is_stopping(&self) -> bool {
2465 self.inner.cell.status().borrow().is_stopping()
2466 }
2467
2468 pub fn self_addr(&self) -> &ActorAddr {
2470 self.inner.self_addr()
2471 }
2472
2473 pub(crate) fn report_delivery_failure(&self, report: crate::mailbox::DeliveryFailureReport) {
2475 static REPORT_WARNED_MAILBOXES: OnceLock<DashSet<ActorAddr>> = OnceLock::new();
2476
2477 let mailbox = &self.inner.mailbox;
2478 let return_handle = mailbox.bound_return_handle().unwrap_or_else(|| {
2479 let actor_id = mailbox.actor_addr();
2480 if REPORT_WARNED_MAILBOXES
2481 .get_or_init(DashSet::new)
2482 .insert(actor_id.clone())
2483 {
2484 let bt = std::backtrace::Backtrace::force_capture();
2485 tracing::warn!(
2486 actor_id = ?actor_id,
2487 backtrace = ?bt,
2488 "actor attempted to report delivery failure without binding Undeliverable<MessageEnvelope>"
2489 );
2490 }
2491 crate::mailbox::monitored_return_handle()
2492 });
2493
2494 if let Err(error) =
2495 return_handle.try_post(self, crate::mailbox::Undeliverable::report(report.clone()))
2496 {
2497 tracing::error!(
2498 sender = %report.sender,
2499 dest = %report.dest,
2500 message_type = report.message_type.as_deref().unwrap_or("unknown"),
2501 error = %report.error_msg().unwrap_or_default(),
2502 return_error = %error,
2503 "delivery failure report could not be returned"
2504 );
2505 }
2506 }
2507
2508 pub fn introspect_payload(&self) -> crate::introspect::IntrospectResult {
2524 crate::introspect::live_actor_payload(&self.inner.cell)
2525 }
2526
2527 pub fn recording_span(&self) -> tracing::Span {
2531 use crate::subject::AsSubject;
2532 self.inner
2533 .cell
2534 .recording()
2535 .span(&self.self_addr().subject().to_string())
2536 }
2537
2538 pub fn publish_attrs(&self, attrs: hyperactor_config::Attrs) {
2546 #[cfg(debug_assertions)]
2547 {
2548 use std::collections::HashSet;
2549 use std::sync::OnceLock;
2550
2551 use hyperactor_config::attrs::AttrKeyInfo;
2552
2553 static INTROSPECT_KEYS: OnceLock<HashSet<&'static str>> = OnceLock::new();
2554 let allowed = INTROSPECT_KEYS.get_or_init(|| {
2555 inventory::iter::<AttrKeyInfo>()
2556 .filter(|info| info.meta.get(hyperactor_config::INTROSPECT).is_some())
2557 .map(|info| info.name)
2558 .collect()
2559 });
2560 for (name, _) in attrs.iter() {
2561 debug_assert!(
2562 allowed.contains(name),
2563 "publish_attrs: key {:?} is not tagged with INTROSPECT",
2564 name
2565 );
2566 }
2567 }
2568 self.inner.cell.set_published_attrs(attrs);
2569 }
2570
2571 pub fn publish_attr<T: hyperactor_config::AttrValue>(
2577 &self,
2578 key: hyperactor_config::Key<T>,
2579 value: T,
2580 ) {
2581 debug_assert!(
2582 key.attrs().get(hyperactor_config::INTROSPECT).is_some(),
2583 "publish_attr called with non-introspection key: {}",
2584 key.name()
2585 );
2586 self.inner.cell.merge_published_attr(key, value);
2587 }
2588
2589 pub fn set_attrs_snapshot(
2599 &self,
2600 callback: impl Fn() -> hyperactor_config::Attrs + Send + Sync + 'static,
2601 ) {
2602 self.inner.cell.set_attrs_snapshot(callback);
2603 }
2604
2605 pub fn set_system(&self) {
2608 self.inner
2609 .cell
2610 .inner
2611 .is_system
2612 .store(true, Ordering::Relaxed);
2613 }
2614
2615 pub fn set_query_child_handler(
2624 &self,
2625 handler: impl (Fn(&Addr) -> IntrospectResult) + Send + Sync + 'static,
2626 ) {
2627 self.inner.cell.set_query_child_handler(handler);
2628 }
2629
2630 pub fn stop(&self, reason: &str) -> Result<(), ActorError> {
2632 tracing::info!(
2633 actor_id = %self.inner.cell.actor_addr(),
2634 reason,
2635 "instance stop called",
2636 );
2637 self.inner.cell.signal(Signal::Stop(reason.to_string()))
2638 }
2639
2640 pub fn drain_and_stop(&self, reason: &str) -> Result<(), ActorError> {
2642 tracing::info!(
2643 actor_id = %self.inner.cell.actor_addr(),
2644 reason,
2645 "instance drain_and_stop called",
2646 );
2647 self.inner
2648 .cell
2649 .signal(Signal::DrainAndStop(reason.to_string()))
2650 }
2651
2652 pub fn kill(&self, reason: &str) -> Result<(), ActorError> {
2654 tracing::info!(
2655 actor_id = %self.inner.cell.actor_addr(),
2656 reason,
2657 "instance kill called",
2658 );
2659 self.inner.cell.signal(Signal::Kill(reason.to_string()))
2660 }
2661
2662 pub fn abort(&self, reason: &str) -> Result<(), ActorError> {
2664 tracing::info!(
2665 actor_id = %self.inner.cell.actor_addr(),
2666 reason,
2667 "instance abort called",
2668 );
2669 self.kill(reason)
2670 }
2671
2672 pub fn close(&self) {
2674 self.inner.delayed_posts.drain();
2675 self.inner.mailbox.drain();
2676 }
2677
2678 pub(crate) fn status(&self) -> watch::Receiver<ActorStatus> {
2679 self.inner.cell.status().clone()
2680 }
2681
2682 pub(crate) fn close_client(&self, reason: &str) {
2683 let status = ActorStatus::Stopped(reason.to_string());
2684 self.inner.mailbox.close(status.clone());
2685 self.change_status(status);
2686 }
2687
2688 pub fn exit(&self, reason: &str) -> Result<(), ActorError> {
2690 self.inner
2691 .cell
2692 .signal(Signal::ExitRequested(reason.to_string()))
2693 }
2694
2695 pub fn exit_after_drain(&self, reason: &str) -> Result<(), ActorError> {
2702 let this = self.clone_for_py();
2703 let reason = reason.to_string();
2704 let work = WorkCell::new(move |_actor: &mut A, _instance: &Instance<A>| {
2705 Box::pin(async move {
2706 this.exit(&reason).map_err(anyhow::Error::from)?;
2707 Ok(())
2708 })
2709 });
2710 self.enqueue_runtime_work(work)
2711 }
2712
2713 pub fn open_port<M: Message>(&self) -> (PortHandle<M>, PortReceiver<M>) {
2718 self.inner.mailbox.open_port()
2719 }
2720
2721 pub fn open_once_port<M: Message>(&self) -> (OncePortHandle<M>, OncePortReceiver<M>) {
2725 self.inner.mailbox.open_once_port()
2726 }
2727
2728 #[doc(hidden)]
2730 pub fn signal_sender(&self) -> mpsc::UnboundedSender<Signal> {
2731 self.inner.cell.signal_sender()
2732 }
2733
2734 pub fn locals(&self) -> &ActorLocalStorage {
2736 &self.inner.instance_locals
2737 }
2738
2739 pub fn post(&self, port_id: impl Into<PortAddr>, headers: Flattrs, message: wirevalue::Any) {
2741 let port_id: PortAddr = port_id.into();
2742 <Self as context::MailboxExt>::post(
2743 self,
2744 port_id,
2745 headers,
2746 message,
2747 true,
2748 context::SeqInfoPolicy::AssignNew,
2749 )
2750 }
2751
2752 #[doc(hidden)]
2758 pub fn post_with_external_seq_info(
2759 &self,
2760 port_id: impl Into<PortAddr>,
2761 headers: Flattrs,
2762 message: wirevalue::Any,
2763 ) {
2764 <Self as context::MailboxExt>::post(
2765 self,
2766 port_id.into(),
2767 headers,
2768 message,
2769 true,
2770 context::SeqInfoPolicy::AllowExternal,
2771 )
2772 }
2773
2774 fn enqueue_runtime_work(&self, work: WorkCell<A>) -> Result<(), ActorError> {
2775 let actor_id_str = self.self_addr().to_string();
2776 account_enqueue(
2777 &self.inner.cell.inner.queue_depth,
2778 &self.inner.proc.state().queue_stats,
2779 &actor_id_str,
2780 );
2781 let result = self
2782 .inner
2783 .ports
2784 .workq
2785 .send(SequencedEnvelope::new(SeqInfo::Direct, None, work))
2786 .map_err(anyhow::Error::from);
2787 if result.is_err() {
2788 account_cancel_enqueue(
2789 &self.inner.cell.inner.queue_depth,
2790 &self.inner.proc.state().queue_stats,
2791 &actor_id_str,
2792 );
2793 }
2794 result.map_err(|err| ActorError::new(self.self_addr(), ActorErrorKind::processing(err)))
2795 }
2796
2797 pub fn self_client() -> &'static Client {
2803 static CLIENT: OnceLock<Client> = OnceLock::new();
2804 CLIENT.get_or_init(|| Proc::global().client("self_message_client"))
2805 }
2806
2807 #[allow(private_bounds)]
2813 pub fn post_after<D, M>(&self, dest: D, message: M, delay: Duration)
2814 where
2815 M: Message,
2816 D: PostAfterEndpoint<A, M>,
2817 {
2818 let dest_location = dest.endpoint_location();
2819 if matches!(*self.inner.cell.status().borrow(), ActorStatus::Client) {
2820 self.report_delivery_failure(
2821 crate::mailbox::DeliveryFailureReport::link_unavailable::<M>(
2822 self.mailbox().actor_addr().clone(),
2823 dest_location,
2824 "delayed posts require an actor runtime",
2825 ),
2826 );
2827 return;
2828 }
2829 let Ok(_guard) = self.inner.delayed_posts.ingress.try_enter() else {
2830 self.report_delivery_failure(
2831 crate::mailbox::DeliveryFailureReport::link_unavailable::<M>(
2832 self.mailbox().actor_addr().clone(),
2833 dest_location,
2834 "actor runtime is stopping",
2835 ),
2836 );
2837 return;
2838 };
2839 if self.is_stopping() || self.is_terminal() {
2840 self.report_delivery_failure(
2841 crate::mailbox::DeliveryFailureReport::link_unavailable::<M>(
2842 self.mailbox().actor_addr().clone(),
2843 dest_location,
2844 "actor runtime is stopping",
2845 ),
2846 );
2847 return;
2848 }
2849
2850 self.inner.delayed_posts.push(
2851 tokio::time::Instant::now() + delay,
2852 dest.into_delayed_post(message),
2853 );
2854 }
2855
2856 fn start(self, actor: A, receivers: InstanceReceivers<A>) -> ActorHandle<A> {
2859 let instance_cell = self.inner.cell.clone();
2860 let actor_id = self.inner.cell.actor_addr().clone();
2861 let actor_handle = ActorHandle::new(self.inner.cell.clone(), self.inner.ports.clone());
2862
2863 self.spawn_introspect(receivers.introspect);
2867
2868 let actor_loop_receivers = receivers
2869 .actor_loop
2870 .expect("non-detached instance must have actor loop receivers");
2871 let actor_task_handle = A::spawn_server_task(
2872 panic_handler::with_backtrace_tracking(self.serve(
2873 actor,
2874 actor_loop_receivers,
2875 receivers.work,
2876 ))
2877 .instrument(Span::current()),
2878 );
2879 tracing::debug!("{}: spawned with {:?}", actor_id, actor_task_handle);
2880 instance_cell
2881 .inner
2882 .actor_task_handle
2883 .set(actor_task_handle)
2884 .unwrap_or_else(|_| panic!("{}: task handle store failed", actor_id));
2885
2886 actor_handle
2887 }
2888
2889 async fn serve(
2890 mut self,
2891 mut actor: A,
2892 actor_loop_receivers: (
2893 mpsc::UnboundedReceiver<Signal>,
2894 mpsc::UnboundedReceiver<ActorSupervisionEvent>,
2895 ),
2896 mut work_rx: ActorWorkReceiver<A>,
2897 ) {
2898 let result = self
2899 .run_actor_tree(&mut actor, actor_loop_receivers, &mut work_rx)
2900 .await;
2901
2902 assert!(self.is_stopping());
2903 let (terminal_status, event) = match result {
2909 Ok(stop_reason) => {
2910 let status = ActorStatus::Stopped(stop_reason);
2911 let event = ActorSupervisionEvent::new(
2912 self.inner.cell.actor_addr().clone(),
2913 actor.display_name(),
2914 status.clone(),
2915 None,
2916 );
2917 (status, Some(event))
2918 }
2919 Err(err) => match *err.kind {
2920 ActorErrorKind::UnhandledSupervisionEvent(box event) => {
2921 assert!(event.actor_status.is_terminal());
2923 let status = event.actor_status.clone();
2924 (status, Some(event))
2925 }
2926 _ => {
2927 let error_kind = ActorErrorKind::Generic(err.kind.to_string());
2928 let status = ActorStatus::Failed(error_kind);
2929 let event = ActorSupervisionEvent::new(
2930 self.inner.cell.actor_addr().clone(),
2931 actor.display_name(),
2932 status.clone(),
2933 None,
2934 );
2935 (status, Some(event))
2936 }
2937 },
2938 };
2939
2940 let current_status = self.inner.cell.status().borrow().clone();
2944 let event = if current_status.is_zombie() {
2945 Some(ActorSupervisionEvent::new(
2946 self.inner.cell.actor_addr().clone(),
2947 actor.display_name(),
2948 current_status,
2949 None,
2950 ))
2951 } else {
2952 event
2953 };
2954
2955 self.mailbox().close(terminal_status.clone());
2956 if let Some(event) = &event {
2958 *self.inner.cell.inner.supervision_event.lock().unwrap() = Some(event.clone());
2959 }
2960
2961 if let Some(parent) = self.inner.cell.maybe_unlink_parent() {
2966 if let Some(event) = event {
2967 parent.send_supervision_event_or_crash(event);
2969 }
2970 if let Err(err) = parent.signal(Signal::ChildStopped(self.inner.cell.uid().clone())) {
2973 tracing::error!(
2974 "{}: failed to send stop message to parent uid {}: {:?}",
2975 self.self_addr(),
2976 parent.uid(),
2977 err
2978 );
2979 }
2980 } else {
2981 if let Some(event) = event {
2987 self.inner
2988 .proc
2989 .handle_unhandled_supervision_event(&self, event);
2990 }
2991 }
2992
2993 self.stop_introspect(terminal_status.clone()).await;
2994 self.change_status(terminal_status);
2995 }
2996
2997 async fn run_actor_tree(
3001 &mut self,
3002 actor: &mut A,
3003 mut actor_loop_receivers: (
3004 mpsc::UnboundedReceiver<Signal>,
3005 mpsc::UnboundedReceiver<ActorSupervisionEvent>,
3006 ),
3007 work_rx: &mut ActorWorkReceiver<A>,
3008 ) -> Result<String, ActorError> {
3009 let mut did_panic = false;
3015 let result = match AssertUnwindSafe(self.run(actor, &mut actor_loop_receivers, work_rx))
3016 .catch_unwind()
3017 .await
3018 {
3019 Ok(result) => result,
3020 Err(_) => {
3021 did_panic = true;
3022 let panic_info = panic_handler::take_panic_info()
3023 .map(|info| info.to_string())
3024 .unwrap_or_else(|e| format!("Cannot take backtrace due to: {:?}", e));
3025 Err(ActorError::new(
3026 self.self_addr(),
3027 ActorErrorKind::panic(anyhow::anyhow!(panic_info)),
3028 ))
3029 }
3030 };
3031
3032 assert!(!self.is_terminal());
3033 if !self.inner.cell.status().borrow().is_zombie() {
3036 self.change_status(ActorStatus::stopping());
3037 }
3038 if let Err(err) = &result {
3039 tracing::error!("{}: actor failure: {}", self.self_addr(), err);
3040 }
3041
3042 let mut to_unlink = Vec::new();
3045 let child_signal = match ChildTeardown::from_run_result(&result) {
3046 ChildTeardown::Cooperative(mode) => ChildTeardown::cooperative_signal(mode),
3047 ChildTeardown::Kill => {
3048 ChildTeardown::cooperative_signal(StopMode::Stop)
3051 }
3052 };
3053 for child in self.inner.cell.child_iter() {
3054 if let Err(err) = child.value().signal(child_signal.clone()) {
3055 tracing::error!(
3056 "{}: failed to send stop signal to child pid {}: {:?}",
3057 self.self_addr(),
3058 child.key(),
3059 err
3060 );
3061 to_unlink.push(child.value().clone());
3062 }
3063 }
3064 for child in to_unlink {
3066 self.inner.cell.unlink(&child);
3067 }
3068
3069 let (mut signal_receiver, _) = actor_loop_receivers;
3070 while self.inner.cell.child_count() > 0 {
3071 match tokio::time::timeout(Duration::from_millis(500), signal_receiver.recv()).await {
3072 Ok(Some(Signal::ChildStopped(uid))) => {
3073 assert!(self.inner.cell.get_child(&uid).is_none());
3074 }
3075 Ok(Some(_)) => {}
3078 Ok(None) => {
3079 self.inner.cell.unlink_all();
3084 break;
3085 }
3086 Err(_) => {
3087 tracing::warn!(
3088 "timeout waiting for ChildStopped signal from child on actor: {}, ignoring",
3089 self.self_addr()
3090 );
3091 self.inner.cell.unlink_all();
3094 break;
3095 }
3096 }
3097 }
3098 let cleanup_result = if !did_panic {
3104 let cleanup_timeout = hyperactor_config::global::get(config::CLEANUP_TIMEOUT);
3105 match tokio::time::timeout(
3106 cleanup_timeout,
3107 self.inner
3108 .proc
3109 .with_current(actor.cleanup(self, result.as_ref().err())),
3110 )
3111 .await
3112 {
3113 Ok(Ok(x)) => Ok(x),
3114 Ok(Err(e)) => Err(ActorError::new(
3115 self.self_addr(),
3116 ActorErrorKind::cleanup(e),
3117 )),
3118 Err(e) => Err(ActorError::new(
3119 self.self_addr(),
3120 ActorErrorKind::cleanup(e.into()),
3121 )),
3122 }
3123 } else {
3124 Ok(())
3125 };
3126 if let Err(ref actor_err) = result {
3127 if let Err(ref err) = cleanup_result {
3130 tracing::warn!(
3131 cleanup_err = %err,
3132 %actor_err,
3133 "ignoring cleanup error after actor error",
3134 );
3135 }
3136 }
3137 result.and_then(|stopped| cleanup_result.map(|_| stopped.reason))
3140 }
3141
3142 async fn run(
3146 &mut self,
3147 actor: &mut A,
3148 actor_loop_receivers: &mut (
3149 mpsc::UnboundedReceiver<Signal>,
3150 mpsc::UnboundedReceiver<ActorSupervisionEvent>,
3151 ),
3152 work_rx: &mut ActorWorkReceiver<A>,
3153 ) -> Result<ActorStopped, ActorError> {
3154 let (signal_receiver, supervision_event_receiver) = actor_loop_receivers;
3155 let mut stop_mode = StopMode::Stop;
3156
3157 self.change_status(ActorStatus::Initializing);
3158 self.inner
3159 .proc
3160 .with_current(actor.init(self))
3161 .await
3162 .map_err(|err| ActorError::new(self.self_addr(), ActorErrorKind::init(err)))?;
3163 let actor_id_str = self.self_addr().to_string();
3164 let stop_reason = 'messages: loop {
3165 if !self.is_stopping() {
3166 self.change_status(ActorStatus::Idle);
3167 }
3168 let next_delayed_deadline = self.inner.delayed_posts.next_deadline();
3169 let metric_pairs = hyperactor_telemetry::kv_pairs!("actor_id" => actor_id_str.clone());
3170 tokio::select! {
3171 biased;
3172 signal = signal_receiver.recv() => {
3173 let signal = signal.ok_or_else(|| {
3174 ActorError::new(self.self_addr(), ActorErrorKind::SignalChannelClosed)
3175 })?;
3176 tracing::debug!("received signal {signal:?}");
3177 match signal {
3178 Signal::Stop(reason) => {
3179 stop_mode = StopMode::Stop;
3180 self.change_status(ActorStatus::stopping());
3181 self.inner
3182 .proc
3183 .with_current(actor.handle_stop(self, StopMode::Stop, &reason))
3184 .await
3185 .map_err(|err| ActorError::new(self.self_addr(), ActorErrorKind::processing(err)))?;
3186 },
3187 Signal::DrainAndStop(reason) => {
3188 stop_mode = StopMode::DrainAndStop;
3189 self.change_status(ActorStatus::stopping());
3190 self.inner
3191 .proc
3192 .with_current(actor.handle_stop(self, StopMode::DrainAndStop, &reason))
3193 .await
3194 .map_err(|err| ActorError::new(self.self_addr(), ActorErrorKind::processing(err)))?;
3195 },
3196 Signal::ChildStopped(uid) => {
3197 assert!(self.inner.cell.get_child(&uid).is_none());
3198 },
3199 Signal::ExitRequested(reason) => {
3200 break 'messages reason;
3201 }
3202 Signal::Kill(reason) => {
3203 return Err(ActorError { actor_id: Box::new(self.self_addr().clone()), kind: Box::new(ActorErrorKind::Aborted(reason)) });
3204 }
3205 }
3206 }
3207 work = work_rx.recv() => {
3208 ACTOR_MESSAGES_RECEIVED.add(1, metric_pairs);
3209 account_dequeue(&self.inner.cell.inner.queue_depth, &self.inner.proc.state().queue_stats, &actor_id_str);
3210 let _ = ACTOR_MESSAGE_HANDLER_DURATION.start(metric_pairs);
3211 let work = work.expect("inconsistent work queue state");
3212 if let Err(err) = work.handle(actor, self).await {
3213 while let Ok(supervision_event) = supervision_event_receiver.try_recv() {
3214 self.handle_supervision_event(actor, supervision_event).await?;
3215 }
3216 let kind = ActorErrorKind::processing(err);
3217 return Err(ActorError {
3218 actor_id: Box::new(self.self_addr().clone()),
3219 kind: Box::new(kind),
3220 });
3221 }
3222 }
3223 _ = self.inner.delayed_posts.notify.notified(), if !self.is_stopping() && !self.inner.delayed_posts.is_draining() => {
3224 }
3225 _ = async {
3226 match next_delayed_deadline {
3227 Some(deadline) => tokio::time::sleep_until(deadline).await,
3228 None => std::future::pending::<()>().await,
3229 }
3230 }, if !self.is_stopping() && !self.inner.delayed_posts.is_draining() && next_delayed_deadline.is_some() => {
3231 let now = tokio::time::Instant::now();
3232 if let Ok(_guard) = self.inner.delayed_posts.ingress.try_enter() {
3233 for post in self.inner.delayed_posts.pop_due(now) {
3234 post(self);
3235 }
3236 }
3237 }
3238 Some(supervision_event) = supervision_event_receiver.recv() => {
3239 self.handle_supervision_event(actor, supervision_event).await?;
3240 }
3241 }
3242 self.inner
3243 .cell
3244 .inner
3245 .num_processed_messages
3246 .fetch_add(1, Ordering::SeqCst);
3247 };
3248 tracing::debug!(
3249 actor_id = %self.self_addr(),
3250 reason = stop_reason,
3251 "exited actor loop",
3252 );
3253 Ok(ActorStopped {
3254 reason: stop_reason,
3255 stop_mode,
3256 })
3257 }
3258
3259 pub async fn handle_supervision_event(
3261 &self,
3262 actor: &mut A,
3263 supervision_event: ActorSupervisionEvent,
3264 ) -> Result<(), ActorError> {
3265 match self
3267 .inner
3268 .proc
3269 .with_current(actor.handle_supervision_event(self, &supervision_event))
3270 .await
3271 {
3272 Ok(true) => {
3273 Ok(())
3275 }
3276 Ok(false) => {
3277 let kind = ActorErrorKind::UnhandledSupervisionEvent(Box::new(supervision_event));
3278 Err(ActorError::new(self.self_addr(), kind))
3279 }
3280 Err(err) => {
3281 let kind = ActorErrorKind::ErrorDuringHandlingSupervision(
3284 err.to_string(),
3285 Box::new(supervision_event),
3286 );
3287 Err(ActorError::new(self.self_addr(), kind))
3288 }
3289 }
3290 }
3291
3292 async unsafe fn handle_message<M: Message>(
3293 &self,
3294 actor: &mut A,
3295 type_info: Option<&'static TypeInfo>,
3296 headers: Flattrs,
3297 message: M,
3298 ) -> Result<(), anyhow::Error>
3299 where
3300 A: Handler<M>,
3301 {
3302 let handler_info = match type_info {
3304 Some(info) => {
3305 let arm = unsafe { info.arm_unchecked(&message as *const M as *const ()) };
3307 HandlerInfo::from_static(info.typename(), arm)
3308 }
3309 None => {
3310 HandlerInfo::from_static(std::any::type_name::<M>(), None)
3312 }
3313 };
3314
3315 let endpoint = type_info.and_then(|info| {
3316 unsafe { info.endpoint_name(&message as *const M as *const ()) }
3318 });
3319
3320 self.handle_message_with_handler_info(actor, handler_info, headers, message, endpoint)
3322 .await
3323 }
3324
3325 #[tracing::instrument(level = "debug", name = "handle_message", skip_all, fields(message_type = %handler_info))]
3326 async fn handle_message_with_handler_info<M: Message>(
3327 &self,
3328 actor: &mut A,
3329 handler_info: HandlerInfo,
3330 headers: Flattrs,
3331 message: M,
3332 endpoint: Option<String>,
3333 ) -> Result<(), anyhow::Error>
3334 where
3335 A: Handler<M>,
3336 {
3337 let now = std::time::SystemTime::now();
3338 let handler_info = Some(handler_info);
3339 self.change_status(ActorStatus::Processing(now, handler_info.clone()));
3340 crate::mailbox::headers::log_message_latency_if_sampling(
3341 &headers,
3342 self.self_addr().to_string(),
3343 );
3344
3345 let message_id = headers.get(crate::mailbox::headers::TELEMETRY_MESSAGE_ID);
3346
3347 if let Some(message_id) = message_id {
3348 let from_actor_id = headers
3349 .get(crate::mailbox::headers::SENDER_ACTOR_ID_HASH)
3350 .unwrap_or(0);
3351 let to_actor_id = hash_to_u64(self.self_addr().id());
3352 let port_index = headers.get(crate::mailbox::headers::TELEMETRY_PORT_INDEX);
3353
3354 notify_message(hyperactor_telemetry::MessageEvent {
3355 timestamp: now,
3356 id: message_id,
3357 from_actor_id,
3358 to_actor_id,
3359 endpoint,
3360 port_index,
3361 });
3362
3363 notify_message_status(hyperactor_telemetry::MessageStatusEvent {
3364 timestamp: now,
3365 id: hyperactor_telemetry::generate_status_event_id(message_id),
3366 message_id,
3367 status: "active".to_string(),
3368 });
3369 }
3370
3371 *self.inner.cell.inner.last_message_handler.write().unwrap() = handler_info;
3373
3374 let context = Context::new(self, headers);
3375 let start = Instant::now();
3379 let subject_str = self.self_addr().subject().to_string();
3380 let result = self
3381 .inner
3382 .proc
3383 .with_current(actor.handle(&context, message))
3384 .instrument(self.inner.cell.inner.recording.span(&subject_str))
3385 .await;
3386 let elapsed_us = start.elapsed().as_micros() as u64;
3387 self.inner
3388 .cell
3389 .inner
3390 .total_processing_time_us
3391 .fetch_add(elapsed_us, Ordering::SeqCst);
3392
3393 if let Some(message_id) = message_id {
3394 notify_message_status(hyperactor_telemetry::MessageStatusEvent {
3395 timestamp: std::time::SystemTime::now(),
3396 id: hyperactor_telemetry::generate_status_event_id(message_id),
3397 message_id,
3398 status: "complete".to_string(),
3399 });
3400 }
3401
3402 result
3403 }
3404
3405 pub fn spawn<C: Actor>(&self, actor: C) -> ActorHandle<C> {
3407 self.inner.proc.spawn_child(self.inner.cell.clone(), actor)
3408 }
3409
3410 pub fn spawn_with_name<C: Actor>(&self, name: &str, actor: C) -> ActorHandle<C> {
3414 self.inner
3415 .proc
3416 .spawn_named_child(self.inner.cell.clone(), name, actor)
3417 }
3418
3419 pub fn spawn_with_label<C: Actor>(&self, label: &str, actor: C) -> ActorHandle<C> {
3424 self.inner
3425 .proc
3426 .spawn_named_child(self.inner.cell.clone(), label, actor)
3427 }
3428
3429 pub fn spawn_with_uid<C: Actor>(&self, uid: Uid, actor: C) -> anyhow::Result<ActorHandle<C>> {
3435 self.inner
3436 .proc
3437 .spawn_child_with_uid(self.inner.cell.clone(), uid, actor)
3438 }
3439
3440 pub fn child(&self) -> (Instance<()>, ActorHandle<()>) {
3442 self.inner.proc.child_instance(self.inner.cell.clone())
3443 }
3444
3445 pub async fn gspawn(&self, actor_type: &str, params: Data) -> anyhow::Result<AnyActorHandle> {
3450 self.gspawn_uid(actor_type, crate::id::Uid::anonymous(), params)
3451 .await
3452 }
3453
3454 pub async fn gspawn_uid(
3459 &self,
3460 actor_type: &str,
3461 uid: crate::id::Uid,
3462 params: Data,
3463 ) -> anyhow::Result<AnyActorHandle> {
3464 crate::actor::remote::Remote::global()
3465 .gspawn_child(
3466 &self.inner.proc,
3467 self.inner.cell.clone(),
3468 actor_type,
3469 uid,
3470 params,
3471 Flattrs::default(),
3472 )
3473 .await
3474 }
3475
3476 pub fn port<M: Message>(&self) -> PortHandle<M>
3479 where
3480 A: Handler<M>,
3481 {
3482 self.inner.ports.get()
3483 }
3484
3485 pub fn handle(&self) -> ActorHandle<A> {
3487 ActorHandle::new(self.inner.cell.clone(), Arc::clone(&self.inner.ports))
3488 }
3489
3490 pub fn bind<R: Binds<A>>(&self) -> ActorRef<R> {
3492 self.inner.cell.bind(self.inner.ports.as_ref())
3493 }
3494
3495 #[doc(hidden)]
3497 pub fn mailbox_for_py(&self) -> &Mailbox {
3498 &self.inner.mailbox
3499 }
3500
3501 pub fn proc(&self) -> &Proc {
3503 &self.inner.proc
3504 }
3505
3506 #[doc(hidden)]
3510 pub fn clone_for_py(&self) -> Self {
3511 Self {
3512 inner: Arc::clone(&self.inner),
3513 }
3514 }
3515
3516 fn actor_task_handle(&self) -> Option<&JoinHandle<()>> {
3518 self.inner.cell.inner.actor_task_handle.get()
3519 }
3520
3521 pub fn sequencer(&self) -> &Sequencer {
3523 &self.inner.sequencer
3524 }
3525
3526 #[doc(hidden)]
3535 pub fn debug_skip_next_ordering_seq(&self, dest: &PortAddr, count: u64) {
3536 let sequencer = self.sequencer();
3537 for _ in 0..count {
3538 let _ = sequencer.assign_seq(dest);
3539 }
3540 }
3541
3542 pub fn instance_id(&self) -> Uuid {
3544 self.inner.id
3545 }
3546
3547 pub fn parent_handle<P: Actor>(&self) -> Option<ActorHandle<P>> {
3549 let parent_cell = self.inner.cell.inner.parent.upgrade()?;
3550 let ports = if let Ok(ports) = parent_cell.inner.ports.clone().downcast() {
3551 ports
3552 } else {
3553 return None;
3554 };
3555 Some(ActorHandle::new(parent_cell, ports))
3556 }
3557}
3558
3559impl Instance<ClientActor> {
3560 pub(crate) fn child_client(&self) -> Client {
3561 let actor_id = self
3562 .inner
3563 .proc
3564 .allocate_anonymous_child_id(self.inner.cell.actor_addr());
3565 let (instance, _receivers) = Instance::new(
3566 self.inner.proc.clone(),
3567 actor_id,
3568 false,
3569 Some(self.inner.cell.clone()),
3570 );
3571 instance.change_status(ActorStatus::Client);
3572 Client::new(instance)
3573 }
3574}
3575
3576impl<A: Actor> context::Mailbox for Instance<A> {
3577 fn mailbox(&self) -> &Mailbox {
3578 &self.inner.mailbox
3579 }
3580}
3581
3582impl<A: Actor> context::Mailbox for Context<'_, A> {
3583 fn mailbox(&self) -> &Mailbox {
3584 &self.instance.inner.mailbox
3585 }
3586}
3587
3588impl<A: Actor> context::Mailbox for &Instance<A> {
3589 fn mailbox(&self) -> &Mailbox {
3590 &self.inner.mailbox
3591 }
3592}
3593
3594impl<A: Actor> context::Mailbox for &Context<'_, A> {
3595 fn mailbox(&self) -> &Mailbox {
3596 &self.instance.inner.mailbox
3597 }
3598}
3599
3600impl<A: Actor> context::Actor for Instance<A> {
3601 type A = A;
3602 fn instance(&self) -> &Instance<A> {
3603 self
3604 }
3605}
3606
3607impl<A: Actor> context::Actor for Context<'_, A> {
3608 type A = A;
3609 fn instance(&self) -> &Instance<A> {
3610 self
3611 }
3612
3613 fn headers(&self) -> &Flattrs {
3614 Context::headers(self)
3615 }
3616}
3617
3618impl<A: Actor> context::Actor for &Instance<A> {
3619 type A = A;
3620 fn instance(&self) -> &Instance<A> {
3621 self
3622 }
3623}
3624
3625impl<A: Actor> context::Actor for &Context<'_, A> {
3626 type A = A;
3627 fn instance(&self) -> &Instance<A> {
3628 self
3629 }
3630
3631 fn headers(&self) -> &Flattrs {
3632 Context::headers(self)
3633 }
3634}
3635
3636impl<A, M> crate::Endpoint<M> for &Instance<A>
3637where
3638 A: Actor + Handler<M>,
3639 M: Message,
3640{
3641 fn endpoint_location(&self) -> crate::EndpointLocation {
3642 crate::EndpointLocation::Actor(self.self_addr().clone())
3643 }
3644
3645 fn post<C>(self, cx: &C, message: M)
3646 where
3647 C: context::Actor,
3648 {
3649 let port = self.port();
3650 crate::Endpoint::post(&port, cx, message)
3651 }
3652}
3653
3654impl<A, M> crate::Endpoint<M> for &Context<'_, A>
3655where
3656 A: Actor + Handler<M>,
3657 M: Message,
3658{
3659 fn endpoint_location(&self) -> crate::EndpointLocation {
3660 crate::EndpointLocation::Actor(self.self_addr().clone())
3661 }
3662
3663 fn post<C>(self, cx: &C, message: M)
3664 where
3665 C: context::Actor,
3666 {
3667 crate::Endpoint::post(self.instance, cx, message)
3668 }
3669}
3670
3671impl<A, M> crate::Endpoint<M> for Instance<A>
3672where
3673 A: Actor + Handler<M>,
3674 M: Message,
3675{
3676 fn endpoint_location(&self) -> crate::EndpointLocation {
3677 crate::EndpointLocation::Actor(self.self_addr().clone())
3678 }
3679
3680 fn post<C>(self, cx: &C, message: M)
3681 where
3682 C: context::Actor,
3683 {
3684 crate::Endpoint::post(&self, cx, message)
3685 }
3686}
3687
3688impl Instance<()> {
3689 pub fn bind_handler_port<M: RemoteMessage>(&self) -> (PortHandle<M>, PortReceiver<M>) {
3691 assert!(
3692 self.actor_task_handle().is_none(),
3693 "can only bind handler port on instance with no running actor task"
3694 );
3695 self.inner.mailbox.bind_handler_port()
3696 }
3697}
3698
3699#[derive(Debug)]
3700enum ActorType {
3701 Named(&'static TypeInfo),
3702 Anonymous(&'static str),
3703}
3704
3705impl ActorType {
3706 fn type_name(&self) -> &str {
3707 match self {
3708 ActorType::Named(info) => info.typename(),
3709 ActorType::Anonymous(name) => name,
3710 }
3711 }
3712}
3713
3714#[derive(Clone)]
3720pub struct InstanceCell {
3721 inner: Arc<InstanceCellState>,
3722}
3723
3724impl fmt::Debug for InstanceCell {
3725 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3726 f.debug_struct("InstanceCell")
3727 .field("actor_id", &self.inner.actor_id)
3728 .field("actor_type", &self.inner.actor_type)
3729 .finish()
3730 }
3731}
3732
3733struct InstanceCellState {
3734 actor_id: ActorAddr,
3736
3737 instance_id: Uuid,
3741
3742 actor_type: ActorType,
3744
3745 proc: Proc,
3747
3748 actor_loop: Option<(
3750 mpsc::UnboundedSender<Signal>,
3751 mpsc::UnboundedSender<ActorSupervisionEvent>,
3752 )>,
3753
3754 status_tx: watch::Sender<ActorStatus>,
3756
3757 status: watch::Receiver<ActorStatus>,
3759
3760 parent: WeakInstanceCell,
3762
3763 children: DashMap<crate::id::Uid, InstanceCell>,
3765
3766 actor_task_handle: OnceLock<JoinHandle<()>>,
3768
3769 exported_named_ports: DashMap<Port, &'static str>,
3771
3772 num_processed_messages: AtomicU64,
3774
3775 created_at: SystemTime,
3777
3778 last_message_handler: RwLock<Option<HandlerInfo>>,
3780
3781 total_processing_time_us: AtomicU64,
3783
3784 queue_depth: Arc<AtomicU64>,
3795
3796 recording: Recording,
3799
3800 published_attrs: RwLock<Option<hyperactor_config::Attrs>>,
3809
3810 query_child_handler: RwLock<Option<Box<dyn (Fn(&Addr) -> IntrospectResult) + Send + Sync>>>,
3818
3819 supervision_event: std::sync::Mutex<Option<crate::supervision::ActorSupervisionEvent>>,
3822
3823 is_system: AtomicBool,
3827
3828 ports: Arc<dyn Any + Send + Sync>,
3831
3832 inbound_ordering_snapshot:
3847 Option<Box<dyn Fn() -> crate::ordering::OrderingSnapshot + Send + Sync>>,
3848
3849 actor_attrs_snapshot: RwLock<Option<Box<dyn Fn() -> hyperactor_config::Attrs + Send + Sync>>>,
3862}
3863
3864impl InstanceCellState {
3865 fn maybe_unlink_parent(&self) -> Option<InstanceCell> {
3868 self.parent
3869 .upgrade()
3870 .filter(|parent| parent.inner.unlink(self))
3871 }
3872
3873 fn unlink(&self, child: &InstanceCellState) -> bool {
3875 assert_eq!(self.actor_id.proc_id(), child.actor_id.proc_id());
3876 self.children.remove(child.actor_id.uid()).is_some()
3877 }
3878}
3879
3880fn select_eviction_candidates(
3893 entries: &[(ActorAddr, Option<String>)],
3894 excess: usize,
3895) -> Vec<ActorAddr> {
3896 let mut clean: Vec<&ActorAddr> = Vec::new();
3897 let mut failed: Vec<(&ActorAddr, &str)> = Vec::new();
3898 for (id, occurred_at) in entries {
3899 match occurred_at {
3900 Some(ts) => failed.push((id, ts.as_str())),
3901 None => clean.push(id),
3902 }
3903 }
3904
3905 let mut to_remove: Vec<ActorAddr> = Vec::new();
3906 let mut remaining = excess;
3907
3908 for id in clean {
3910 if remaining == 0 {
3911 break;
3912 }
3913 to_remove.push(id.clone());
3914 remaining -= 1;
3915 }
3916
3917 if remaining > 0 {
3919 failed.sort_by(|a, b| b.1.cmp(a.1));
3920 for (id, _) in failed.into_iter().take(remaining) {
3921 to_remove.push(id.clone());
3922 }
3923 }
3924
3925 to_remove
3926}
3927
3928#[derive(Copy, Clone, Debug, Eq, PartialEq)]
3929enum IllegalStatusChange {
3930 LinkedZombie,
3931 TerminalRewrite,
3932}
3933
3934#[derive(Copy, Clone, Debug, Eq, PartialEq)]
3935enum StatusChange {
3936 Apply,
3937 Ignore,
3938 Illegal(IllegalStatusChange),
3939}
3940
3941fn classify_status_change(old: &ActorStatus, new: &ActorStatus) -> StatusChange {
3942 if old == new || old.is_zombie() && !new.is_terminal() || old.is_terminal() && new.is_zombie() {
3945 StatusChange::Ignore
3946 } else if old.is_terminal() {
3947 StatusChange::Illegal(IllegalStatusChange::TerminalRewrite)
3948 } else {
3949 StatusChange::Apply
3950 }
3951}
3952
3953impl InstanceCell {
3954 #[allow(clippy::too_many_arguments)]
3957 fn new(
3958 actor_id: ActorAddr,
3959 instance_id: Uuid,
3960 actor_type: ActorType,
3961 proc: Proc,
3962 actor_loop: Option<(
3963 mpsc::UnboundedSender<Signal>,
3964 mpsc::UnboundedSender<ActorSupervisionEvent>,
3965 )>,
3966 status_tx: watch::Sender<ActorStatus>,
3967 status: watch::Receiver<ActorStatus>,
3968 parent: Option<InstanceCell>,
3969 ports: Arc<dyn Any + Send + Sync>,
3970 queue_depth: Arc<AtomicU64>,
3971 inbound_ordering_snapshot: Option<
3972 Box<dyn Fn() -> crate::ordering::OrderingSnapshot + Send + Sync>,
3973 >,
3974 ) -> Self {
3975 let is_root = parent.is_none();
3976 let _ais = actor_id.to_string();
3977 let cell = Self {
3978 inner: Arc::new(InstanceCellState {
3979 actor_id: actor_id.clone(),
3980 instance_id,
3981 actor_type,
3982 proc: proc.clone(),
3983 actor_loop,
3984 status_tx,
3985 status,
3986 parent: parent.map_or_else(WeakInstanceCell::new, |cell| cell.downgrade()),
3987 children: DashMap::new(),
3988 actor_task_handle: OnceLock::new(),
3989 exported_named_ports: DashMap::new(),
3990 num_processed_messages: AtomicU64::new(0),
3991 created_at: std::time::SystemTime::now(),
3992 last_message_handler: RwLock::new(None),
3993 total_processing_time_us: AtomicU64::new(0),
3994 queue_depth,
3995 recording: hyperactor_telemetry::recorder().record(64),
3996 published_attrs: RwLock::new(None),
3997 query_child_handler: RwLock::new(None),
3998 supervision_event: std::sync::Mutex::new(None),
3999 is_system: AtomicBool::new(false),
4000 ports,
4001 inbound_ordering_snapshot,
4002 actor_attrs_snapshot: RwLock::new(None),
4003 }),
4004 };
4005 cell.maybe_link_parent();
4006 proc.inner.actor_tombstones.remove(actor_id.id());
4008 proc.inner
4009 .instances
4010 .insert(actor_id.id().clone(), cell.downgrade());
4011 if is_root {
4012 proc.inner.root_actors.insert(actor_id.id().clone());
4013 }
4014 cell
4015 }
4016
4017 fn wrap(inner: Arc<InstanceCellState>) -> Self {
4018 Self { inner }
4019 }
4020
4021 pub fn actor_addr(&self) -> &ActorAddr {
4023 &self.inner.actor_id
4024 }
4025
4026 pub(crate) fn proc(&self) -> &Proc {
4028 &self.inner.proc
4029 }
4030
4031 pub(crate) fn uid(&self) -> &crate::id::Uid {
4033 self.inner.actor_id.uid()
4034 }
4035
4036 #[allow(dead_code)]
4038 pub(crate) fn actor_task_handle(&self) -> Option<&JoinHandle<()>> {
4039 self.inner.actor_task_handle.get()
4040 }
4041
4042 pub fn status(&self) -> &watch::Receiver<ActorStatus> {
4044 &self.inner.status
4045 }
4046
4047 #[track_caller]
4050 fn change_status(&self, new: ActorStatus) {
4051 let mut old_status = None;
4052 let mut illegal_status = None;
4053 let actor_id = self.actor_addr().id().clone();
4054 let changed = self.inner.status_tx.send_if_modified(|status| {
4055 let old = status.clone();
4056 let mut status_change = classify_status_change(&old, &new);
4057 if status_change == StatusChange::Apply && new.is_zombie() && self.parent().is_some() {
4058 status_change = StatusChange::Illegal(IllegalStatusChange::LinkedZombie);
4059 }
4060
4061 match status_change {
4062 StatusChange::Apply => {}
4063 StatusChange::Ignore => return false,
4064 StatusChange::Illegal(reason) => {
4065 illegal_status = Some((old, reason));
4068 return false;
4069 }
4070 }
4071
4072 if new.is_terminal() {
4073 self.inner
4074 .proc
4075 .inner
4076 .actor_tombstones
4077 .insert(actor_id.clone(), new.clone());
4078 }
4079 old_status = Some(old);
4080 *status = new.clone();
4081 true
4082 });
4083
4084 if let Some((old, reason)) = illegal_status {
4085 match reason {
4086 IllegalStatusChange::LinkedZombie => {
4087 panic!(
4088 "zombie actor must be detached from parent before status update: actor_id={}, prev_status={}, status={}",
4089 self.actor_addr(),
4090 old,
4091 new
4092 );
4093 }
4094 IllegalStatusChange::TerminalRewrite => {
4095 panic!(
4096 "actor changing status illegally, only allow non-terminal -> non-terminal \
4097 and non-terminal -> terminal statuses. actor_id={}, prev_status={}, status={}",
4098 self.actor_addr(),
4099 old,
4100 new
4101 );
4102 }
4103 }
4104 }
4105
4106 if !changed {
4107 return;
4108 }
4109 let old = old_status.expect("status change should capture previous status");
4110
4111 if !((old.is_idle() && new.is_processing())
4116 || (old.is_processing() && new.is_idle())
4117 || old == new)
4118 {
4119 let new_status = new.arm().unwrap_or("unknown");
4120 let change_reason = match &new {
4121 ActorStatus::Failed(reason) => reason.to_string(),
4122 ActorStatus::Stopping(ActorStoppingReason::Zombie(reason)) => reason.clone(),
4123 ActorStatus::Stopped(reason) => reason.clone(),
4124 _ => "".to_string(),
4125 };
4126 tracing::info!(
4127 name = "ActorStatus",
4128 actor_id = %self.actor_addr(),
4129 actor_name = self.actor_addr().log_name(),
4130 status = new_status,
4131 prev_status = old.arm().unwrap_or("unknown"),
4132 caller = %PanicLocation::caller(),
4133 change_reason,
4134 );
4135 let actor_id = hash_to_u64(self.actor_addr().id());
4136 notify_actor_status_changed(ActorStatusEvent {
4137 id: generate_actor_status_event_id(actor_id),
4138 timestamp: std::time::SystemTime::now(),
4139 actor_id,
4140 new_status: new_status.to_string(),
4141 reason: if change_reason.is_empty() {
4142 None
4143 } else {
4144 Some(change_reason)
4145 },
4146 });
4147 }
4148 }
4149
4150 fn publish_dropped_status(&self, terminal_status: ActorStatus) {
4151 let actor_id = self.actor_addr().id().clone();
4152 let actor_addr = self.actor_addr().clone();
4153 self.inner.status_tx.send_if_modified(|status| {
4154 if status.is_terminal() {
4155 false
4156 } else {
4157 self.inner
4158 .proc
4159 .inner
4160 .actor_tombstones
4161 .insert(actor_id.clone(), terminal_status.clone());
4162 tracing::info!(
4163 name = "ActorStatus",
4164 actor_id = %actor_addr,
4165 actor_name = actor_addr.log_name(),
4166 status = "Stopped",
4167 prev_status = status.arm().unwrap_or("unknown"),
4168 "instance is dropped",
4169 );
4170 *status = terminal_status.clone();
4171 true
4172 }
4173 });
4174 }
4175
4176 pub fn supervision_event(&self) -> Option<crate::supervision::ActorSupervisionEvent> {
4179 self.inner.supervision_event.lock().unwrap().clone()
4180 }
4181
4182 fn signal_sender(&self) -> mpsc::UnboundedSender<Signal> {
4183 self.inner
4184 .actor_loop
4185 .as_ref()
4186 .map(|(signal_tx, _)| signal_tx.clone())
4187 .unwrap_or_else(|| panic!("{} has no runtime signal sender", self.actor_addr()))
4188 }
4189
4190 pub fn signal(&self, signal: Signal) -> Result<(), ActorError> {
4192 if let Some((signal_tx, _)) = &self.inner.actor_loop {
4193 signal_tx.send(signal).map_err(|_| {
4194 ActorError::new(self.actor_addr(), ActorErrorKind::SignalChannelClosed)
4195 })
4196 } else {
4197 tracing::warn!(
4198 "{}: attempted to send signal {} to detached actor",
4199 self.inner.actor_id,
4200 signal
4201 );
4202 Ok(())
4203 }
4204 }
4205
4206 pub fn send_supervision_event_or_crash(&self, event: ActorSupervisionEvent) {
4215 match &self.inner.actor_loop {
4216 Some((_, supervision_tx)) => {
4217 if let Err(err) = supervision_tx.send(event.clone()) {
4218 if !event.is_error() {
4219 tracing::debug!(
4225 "{}: dropping non-error supervision event {}: {:?}",
4226 self.actor_addr(),
4227 event,
4228 err
4229 );
4230 return;
4231 }
4232 tracing::error!(
4233 "{}: failed to send supervision event to actor: {:?}. Crash the process.",
4234 self.actor_addr(),
4235 err
4236 );
4237 std::process::exit(1);
4238 }
4239 }
4240 None => {
4241 if !event.is_error() {
4242 tracing::debug!(
4243 "{}: dropping non-error supervision event {} to detached actor",
4244 self.actor_addr(),
4245 event,
4246 );
4247 return;
4248 }
4249 tracing::error!(
4250 "{}: failed: {}: cannot send supervision event to detached actor: crashing",
4251 self.actor_addr(),
4252 event,
4253 );
4254 std::process::exit(1);
4255 }
4256 }
4257 }
4258
4259 pub fn downgrade(&self) -> WeakInstanceCell {
4261 WeakInstanceCell {
4262 inner: Arc::downgrade(&self.inner),
4263 }
4264 }
4265
4266 fn link(&self, child: InstanceCell) {
4268 assert_eq!(self.actor_addr().proc_id(), child.actor_addr().proc_id());
4269 self.inner.children.insert(child.uid().clone(), child);
4270 }
4271
4272 fn unlink(&self, child: &InstanceCell) {
4274 assert_eq!(self.actor_addr().proc_id(), child.actor_addr().proc_id());
4275 self.inner.children.remove(child.uid());
4276 }
4277
4278 fn unlink_all(&self) {
4280 self.inner.children.clear();
4281 }
4282
4283 fn maybe_link_parent(&self) {
4285 if let Some(parent) = self.inner.parent.upgrade() {
4286 parent.link(self.clone());
4287 }
4288 }
4289
4290 fn maybe_unlink_parent(&self) -> Option<InstanceCell> {
4293 self.inner.maybe_unlink_parent()
4294 }
4295
4296 fn child_iter(&self) -> impl Iterator<Item = RefMulti<'_, crate::id::Uid, InstanceCell>> {
4299 self.inner.children.iter()
4300 }
4301
4302 pub fn child_count(&self) -> usize {
4304 self.inner.children.len()
4305 }
4306
4307 pub fn child_actor_ids(&self) -> Vec<ActorAddr> {
4309 self.inner
4310 .children
4311 .iter()
4312 .map(|entry| entry.value().actor_addr().clone())
4313 .collect()
4314 }
4315
4316 fn get_child(&self, uid: &crate::id::Uid) -> Option<InstanceCell> {
4318 self.inner.children.get(uid).map(|child| child.clone())
4319 }
4320
4321 pub fn recording(&self) -> &Recording {
4323 &self.inner.recording
4324 }
4325
4326 pub fn created_at(&self) -> SystemTime {
4328 self.inner.created_at
4329 }
4330
4331 pub fn num_processed_messages(&self) -> u64 {
4333 self.inner.num_processed_messages.load(Ordering::SeqCst)
4334 }
4335
4336 pub fn last_message_handler(&self) -> Option<HandlerInfo> {
4338 self.inner.last_message_handler.read().unwrap().clone()
4339 }
4340
4341 pub fn total_processing_time_us(&self) -> u64 {
4343 self.inner.total_processing_time_us.load(Ordering::SeqCst)
4344 }
4345
4346 pub fn queue_depth(&self) -> u64 {
4348 self.inner.queue_depth.load(Ordering::Relaxed)
4349 }
4350
4351 pub fn instance_id(&self) -> Uuid {
4354 self.inner.instance_id
4355 }
4356
4357 pub fn inbound_ordering_snapshot(&self) -> Option<crate::ordering::OrderingSnapshot> {
4362 self.inner.inbound_ordering_snapshot.as_ref().map(|f| f())
4363 }
4364
4365 pub fn set_attrs_snapshot(
4371 &self,
4372 callback: impl Fn() -> hyperactor_config::Attrs + Send + Sync + 'static,
4373 ) {
4374 *self.inner.actor_attrs_snapshot.write().unwrap() = Some(Box::new(callback));
4375 }
4376
4377 pub fn actor_attrs_snapshot(&self) -> Option<hyperactor_config::Attrs> {
4382 let guard = self.inner.actor_attrs_snapshot.read().unwrap();
4383 let callback = guard.as_ref()?;
4384 let attrs = std::panic::catch_unwind(std::panic::AssertUnwindSafe(callback))
4385 .map_err(|_| {
4386 tracing::warn!(
4387 actor_id = %self.actor_addr(),
4388 "actor_attrs_snapshot callback panicked; omitting actor attrs",
4389 );
4390 })
4391 .ok()?;
4392 #[cfg(debug_assertions)]
4397 for (name, _) in attrs.iter() {
4398 debug_assert!(
4399 inventory::iter::<hyperactor_config::attrs::AttrKeyInfo>().any(|info| {
4400 info.name == name && info.meta.get(hyperactor_config::INTROSPECT).is_some()
4401 }),
4402 "actor_attrs_snapshot callback returned non-INTROSPECT key `{name}`; \
4403 actor introspection keys must carry @meta(INTROSPECT)"
4404 );
4405 }
4406 Some(attrs)
4407 }
4408
4409 pub fn parent(&self) -> Option<InstanceCell> {
4411 self.inner.parent.upgrade()
4412 }
4413
4414 pub fn actor_type_name(&self) -> &str {
4416 self.inner.actor_type.type_name()
4417 }
4418
4419 pub fn set_published_attrs(&self, attrs: hyperactor_config::Attrs) {
4421 *self.inner.published_attrs.write().unwrap() = Some(attrs);
4422 }
4423
4424 pub fn merge_published_attr<T: hyperactor_config::AttrValue>(
4427 &self,
4428 key: hyperactor_config::Key<T>,
4429 value: T,
4430 ) {
4431 self.inner
4432 .published_attrs
4433 .write()
4434 .unwrap()
4435 .get_or_insert_with(hyperactor_config::Attrs::new)
4436 .set(key, value);
4437 }
4438
4439 pub fn published_attrs(&self) -> Option<hyperactor_config::Attrs> {
4441 self.inner.published_attrs.read().unwrap().clone()
4442 }
4443
4444 pub fn set_query_child_handler(
4452 &self,
4453 handler: impl (Fn(&Addr) -> IntrospectResult) + Send + Sync + 'static,
4454 ) {
4455 *self.inner.query_child_handler.write().unwrap() = Some(Box::new(handler));
4456 }
4457
4458 pub fn query_child(&self, child_ref: &Addr) -> Option<IntrospectResult> {
4460 let guard = self.inner.query_child_handler.read().unwrap();
4461 guard.as_ref().map(|handler| handler(child_ref))
4462 }
4463
4464 pub fn is_system(&self) -> bool {
4466 self.inner.is_system.load(Ordering::Relaxed)
4467 }
4468
4469 pub fn store_terminated_snapshot(&self, payload: crate::introspect::IntrospectResult) {
4479 let snapshots = &self.inner.proc.inner.terminated_snapshots;
4480 snapshots.insert(
4481 self.actor_addr().id().clone(),
4482 TerminatedSnapshot {
4483 actor_addr: self.actor_addr().clone(),
4484 payload,
4485 },
4486 );
4487 let max = hyperactor_config::global::get(crate::config::TERMINATED_SNAPSHOT_RETENTION);
4488 let excess = snapshots.len().saturating_sub(max);
4489 if excess > 0 {
4490 let entries: Vec<_> = snapshots
4492 .iter()
4493 .map(|entry| {
4494 let occurred_at = serde_json::from_str::<hyperactor_config::Attrs>(
4495 &entry.value().payload.attrs,
4496 )
4497 .ok()
4498 .and_then(|attrs| {
4499 attrs
4501 .get(crate::introspect::FAILURE_ERROR_MESSAGE)
4502 .cloned()?;
4503 attrs
4505 .get(crate::introspect::FAILURE_OCCURRED_AT)
4506 .map(|t| humantime::format_rfc3339(*t).to_string())
4507 });
4508 (entry.value().actor_addr.clone(), occurred_at)
4509 })
4510 .collect();
4511
4512 for key in select_eviction_candidates(&entries, excess) {
4513 snapshots.remove(key.id());
4514 }
4515 }
4516 }
4517
4518 pub(crate) fn bind<A: Actor, R: Binds<A>>(&self, ports: &HandlerPorts<A>) -> ActorRef<R> {
4521 <R as Binds<A>>::bind(ports);
4522 ports.bind::<Undeliverable<MessageEnvelope>>();
4528 for entry in ports.bound.iter() {
4530 self.inner
4531 .exported_named_ports
4532 .insert(entry.key().clone(), entry.value());
4533 }
4534 ActorRef::attest(ActorAddr::new(
4535 self.actor_addr().id().clone(),
4536 self.inner.proc.default_location(),
4537 ))
4538 }
4539
4540 pub(crate) fn downcast_handle<A: Actor>(&self) -> Option<ActorHandle<A>> {
4542 let ports = Arc::clone(&self.inner.ports)
4543 .downcast::<HandlerPorts<A>>()
4544 .ok()?;
4545 Some(ActorHandle::new(self.clone(), ports))
4546 }
4547
4548 pub fn traverse<F>(&self, f: &mut F)
4552 where
4553 F: FnMut(&InstanceCell, usize),
4554 {
4555 self.traverse_inner(0, f);
4556 }
4557
4558 fn traverse_inner<F>(&self, depth: usize, f: &mut F)
4559 where
4560 F: FnMut(&InstanceCell, usize),
4561 {
4562 f(self, depth);
4563 let mut children: Vec<_> = self.child_iter().map(|r| r.value().clone()).collect();
4565 children.sort_by_key(|c| c.uid().clone());
4566 for child in children {
4567 child.traverse_inner(depth + 1, f);
4568 }
4569 }
4570}
4571
4572impl Drop for InstanceCellState {
4573 fn drop(&mut self) {
4574 if let Some(parent) = self.maybe_unlink_parent() {
4575 tracing::debug!(
4576 "instance {} was dropped with parent {} still linked",
4577 self.actor_id,
4578 parent.actor_addr()
4579 );
4580 }
4581 if self
4582 .proc
4583 .inner
4584 .instances
4585 .remove(self.actor_id.id())
4586 .is_none()
4587 {
4588 tracing::error!("instance {} was dropped but not in proc", self.actor_id);
4589 }
4590 self.proc.inner.root_actors.remove(self.actor_id.id());
4591 }
4592}
4593
4594#[derive(Debug, Clone)]
4597pub struct WeakInstanceCell {
4598 inner: Weak<InstanceCellState>,
4599}
4600
4601impl Default for WeakInstanceCell {
4602 fn default() -> Self {
4603 Self::new()
4604 }
4605}
4606
4607impl WeakInstanceCell {
4608 pub fn new() -> Self {
4610 Self { inner: Weak::new() }
4611 }
4612
4613 pub fn upgrade(&self) -> Option<InstanceCell> {
4615 self.inner.upgrade().map(InstanceCell::wrap)
4616 }
4617}
4618
4619pub struct HandlerPorts<A: Actor> {
4624 ports: DashMap<TypeId, Box<dyn Any + Send + Sync + 'static>>,
4625 bound: DashMap<Port, &'static str>,
4626 mailbox: Mailbox,
4627 workq: mpsc::UnboundedSender<SequencedEnvelope<WorkCell<A>>>,
4628 enable_buffering: bool,
4629 queue_depth: Arc<AtomicU64>,
4631 proc_stats: Arc<ProcQueueStats>,
4633}
4634
4635impl<A: Actor> HandlerPorts<A> {
4636 fn new(
4637 mailbox: Mailbox,
4638 workq: mpsc::UnboundedSender<SequencedEnvelope<WorkCell<A>>>,
4639 enable_buffering: bool,
4640 queue_depth: Arc<AtomicU64>,
4641 proc_stats: Arc<ProcQueueStats>,
4642 ) -> Self {
4643 Self {
4644 ports: DashMap::new(),
4645 bound: DashMap::new(),
4646 mailbox,
4647 workq,
4648 enable_buffering,
4649 queue_depth,
4650 proc_stats,
4651 }
4652 }
4653
4654 pub(crate) fn get<M: Message>(&self) -> PortHandle<M>
4656 where
4657 A: Handler<M>,
4658 {
4659 let key = TypeId::of::<M>();
4660 match self.ports.entry(key) {
4661 Entry::Vacant(entry) => {
4662 assert!(
4666 !crate::ordering::is_bypass_workq_type_id(key),
4667 "cannot provision bypass-workq port {} through `Ports::get`; \
4668 it must be pre-registered via `open_message_port` in `Instance::new`",
4669 std::any::type_name::<M>()
4670 );
4671
4672 let type_info = TypeInfo::get_by_typeid(key);
4673 let workq = self.workq.clone();
4674 let enable_buffering = self.enable_buffering;
4675 let actor_id = self.mailbox.actor_addr().to_string();
4676 let enqueue_depth = Arc::clone(&self.queue_depth);
4677 let enqueue_proc_stats = Arc::clone(&self.proc_stats);
4678 let enqueue = move |headers: Flattrs, msg: M| {
4688 let seq_info = if enable_buffering {
4693 match headers.get(SEQ_INFO) {
4694 Some(seq_info) => seq_info,
4695 None => {
4696 let error_msg = format!(
4697 "in enqueue func for {}, buffering is enabled, but SEQ_INFO is not set for message type {}",
4698 actor_id,
4699 std::any::type_name::<M>(),
4700 );
4701 tracing::error!(error_msg);
4702 return Err(anyhow::anyhow!(error_msg));
4703 }
4704 }
4705 } else {
4706 SeqInfo::Direct
4707 };
4708 if !seq_info.is_valid() {
4709 let error_msg = format!(
4710 "in enqueue func for {}, got seq 0 for message type {}",
4711 actor_id,
4712 std::any::type_name::<M>(),
4713 );
4714 tracing::error!(error_msg);
4715 return Err(anyhow::anyhow!(error_msg));
4716 }
4717 let sender = headers.get(crate::mailbox::headers::SENDER_ACTOR_ID);
4718
4719 let work = WorkCell::new(move |actor: &mut A, instance: &Instance<A>| {
4720 Box::pin(async move {
4721 unsafe {
4723 instance
4724 .handle_message(actor, type_info, headers, msg)
4725 .await
4726 }
4727 })
4728 });
4729 account_enqueue(&enqueue_depth, &enqueue_proc_stats, &actor_id);
4736 let result = workq
4739 .send(SequencedEnvelope::new(seq_info, sender, work))
4740 .map_err(anyhow::Error::from);
4741 if result.is_err() {
4742 account_cancel_enqueue(&enqueue_depth, &enqueue_proc_stats, &actor_id);
4743 }
4744 result
4745 };
4746 let port = self.mailbox.open_handler_enqueue_port(enqueue);
4747 entry.insert(Box::new(port.clone()));
4748 port
4749 }
4750 Entry::Occupied(entry) => {
4751 let port = entry.get();
4752 port.downcast_ref::<PortHandle<M>>().unwrap().clone()
4753 }
4754 }
4755 }
4756
4757 pub fn bind<M: RemoteMessage>(&self)
4759 where
4760 A: Handler<M>,
4761 {
4762 let port = Port::handler::<M>();
4763 match self.bound.entry(port.clone()) {
4764 Entry::Vacant(entry) => {
4765 let _ = self.get::<M>().bind();
4766 entry.insert(M::typename());
4767 }
4768 Entry::Occupied(entry) => {
4769 assert_eq!(
4770 *entry.get(),
4771 M::typename(),
4772 "bind {}: port {} already bound to type {}",
4773 M::typename(),
4774 port,
4775 entry.get(),
4776 );
4777 }
4778 }
4779 }
4780}
4781
4782#[cfg(test)]
4783mod tests {
4784 use std::assert_matches;
4785 use std::sync::atomic::AtomicBool;
4786
4787 use hyperactor_macros::export;
4788 use serde_json::json;
4789 use timed_test::async_timed_test;
4790 use tokio::sync::Barrier;
4791 use tokio::sync::oneshot;
4792 use tracing::Level;
4793 use tracing_subscriber::layer::SubscriberExt;
4794 use tracing_test::internal::logs_with_scope_contain;
4795
4796 use super::*;
4797 use crate as hyperactor;
4799 use crate::HandleClient;
4800 use crate::Handler;
4801 use crate::OncePortRef;
4802 use crate::PortRef;
4803 use crate::channel::ChannelTransport;
4804 use crate::mailbox::MailboxClient;
4805 use crate::mailbox::PanickingMailboxSender;
4806 use crate::port::Port;
4807 use crate::testing::ids::test_actor_id;
4808 use crate::testing::proc_supervison::ProcSupervisionCoordinator;
4809 use crate::testing::process_assertion::assert_termination;
4810
4811 #[derive(Debug, Default)]
4812 #[export]
4813 struct TestActor;
4814
4815 impl Actor for TestActor {}
4816
4817 async fn get_status(client: &Client, actor_addr: &ActorAddr) -> Option<ActorStatus> {
4818 let (reply_port, reply_rx) = client.open_once_port::<Option<ActorStatus>>();
4819 actor_addr.status_port().post(
4820 client,
4821 StatusMessage::GetStatus {
4822 reply: reply_port.bind(),
4823 },
4824 );
4825 tokio::time::timeout(Duration::from_secs(5), reply_rx.recv())
4826 .await
4827 .expect("status reply should arrive")
4828 .expect("status reply port should remain open")
4829 }
4830
4831 #[test]
4832 fn classify_status_change_table() {
4833 let cases = [
4834 (
4835 ActorStatus::Idle,
4836 ActorStatus::Processing(SystemTime::UNIX_EPOCH, None),
4837 StatusChange::Apply,
4838 "idle to processing",
4839 ),
4840 (
4841 ActorStatus::Processing(SystemTime::UNIX_EPOCH, None),
4842 ActorStatus::Idle,
4843 StatusChange::Apply,
4844 "processing to idle",
4845 ),
4846 (
4847 ActorStatus::Idle,
4848 ActorStatus::Stopped("done".to_string()),
4849 StatusChange::Apply,
4850 "non-terminal to terminal",
4851 ),
4852 (
4853 ActorStatus::stopping(),
4854 ActorStatus::zombie("hard kill did not finish"),
4855 StatusChange::Apply,
4856 "non-terminal to zombie",
4857 ),
4858 (
4859 ActorStatus::zombie("hard kill did not finish"),
4860 ActorStatus::Stopped("done".to_string()),
4861 StatusChange::Apply,
4862 "zombie to terminal",
4863 ),
4864 (
4865 ActorStatus::zombie("hard kill did not finish"),
4866 ActorStatus::Idle,
4867 StatusChange::Ignore,
4868 "zombie to idle",
4869 ),
4870 (
4871 ActorStatus::zombie("hard kill did not finish"),
4872 ActorStatus::stopping(),
4873 StatusChange::Ignore,
4874 "zombie to stopping",
4875 ),
4876 (
4877 ActorStatus::Stopped("done".to_string()),
4878 ActorStatus::zombie("hard kill did not finish"),
4879 StatusChange::Ignore,
4880 "terminal to zombie",
4881 ),
4882 (
4883 ActorStatus::Stopped("done".to_string()),
4884 ActorStatus::Idle,
4885 StatusChange::Illegal(IllegalStatusChange::TerminalRewrite),
4886 "terminal to idle",
4887 ),
4888 (
4889 ActorStatus::Stopped("done".to_string()),
4890 ActorStatus::Stopped("other".to_string()),
4891 StatusChange::Illegal(IllegalStatusChange::TerminalRewrite),
4892 "terminal to terminal",
4893 ),
4894 (
4895 ActorStatus::Idle,
4896 ActorStatus::Idle,
4897 StatusChange::Ignore,
4898 "same status",
4899 ),
4900 ];
4901
4902 for (old, new, expected, label) in cases {
4903 assert_eq!(
4904 classify_status_change(&old, &new),
4905 expected,
4906 "{label}: {old} -> {new}"
4907 );
4908 }
4909 }
4910
4911 #[derive(Debug)]
4912 struct ChildLabelActor;
4913
4914 impl Actor for ChildLabelActor {}
4915
4916 #[async_timed_test(timeout_secs = 30)]
4917 async fn test_status_control_port_reports_live_actor() {
4918 let proc = Proc::isolated();
4919 let client = proc.client("client");
4920 let handle = proc.spawn(TestActor);
4921
4922 let mut status_rx = handle.status();
4923 status_rx
4924 .wait_for(|status| matches!(status, ActorStatus::Idle))
4925 .await
4926 .expect("actor should become idle");
4927
4928 let status = get_status(&client, handle.actor_addr()).await;
4929 assert_eq!(status, Some(ActorStatus::Idle));
4930
4931 handle.drain_and_stop("test").unwrap();
4932 handle.await;
4933 }
4934
4935 #[async_timed_test(timeout_secs = 30)]
4936 async fn test_status_control_port_reports_tombstoned_actor() {
4937 let proc = Proc::isolated();
4938 let client = proc.client("client");
4939 let handle = proc.spawn(TestActor);
4940 let actor_addr = handle.actor_addr().clone();
4941
4942 handle.drain_and_stop("test").unwrap();
4943 let terminal_status = handle.await;
4944
4945 assert_eq!(
4946 get_status(&client, &actor_addr).await,
4947 Some(terminal_status)
4948 );
4949 }
4950
4951 #[async_timed_test(timeout_secs = 30)]
4952 async fn test_status_control_port_reports_unknown_actor() {
4953 let proc = Proc::isolated();
4954 let client = proc.client("client");
4955 let missing = proc.root_addr(Uid::instance(Label::strip("missing")));
4956
4957 assert_eq!(get_status(&client, &missing).await, None);
4958 }
4959
4960 #[derive(Debug)]
4961 struct DelayedSelfActor {
4962 ready: Option<OncePortRef<()>>,
4963 fired: Option<OncePortRef<()>>,
4964 delay: Duration,
4965 }
4966
4967 #[derive(Debug)]
4968 struct DelayedSelfTick;
4969
4970 #[async_trait]
4971 impl Actor for DelayedSelfActor {
4972 async fn init(&mut self, this: &Instance<Self>) -> anyhow::Result<()> {
4973 if let Some(ready) = self.ready.take() {
4974 ready.post(this, ());
4975 }
4976 this.post_after(this, DelayedSelfTick, self.delay);
4977 Ok(())
4978 }
4979 }
4980
4981 #[async_trait]
4982 impl Handler<DelayedSelfTick> for DelayedSelfActor {
4983 async fn handle(
4984 &mut self,
4985 cx: &crate::Context<Self>,
4986 _message: DelayedSelfTick,
4987 ) -> anyhow::Result<()> {
4988 if let Some(fired) = self.fired.take() {
4989 fired.post(cx, ());
4990 }
4991 Ok(())
4992 }
4993 }
4994
4995 #[derive(Debug)]
4996 struct DelayedPortActor {
4997 reply: Option<PortRef<u64>>,
4998 delay: Duration,
4999 }
5000
5001 #[async_trait]
5002 impl Actor for DelayedPortActor {
5003 async fn init(&mut self, this: &Instance<Self>) -> anyhow::Result<()> {
5004 this.post_after(
5005 self.reply.take().expect("reply port should be present"),
5006 123u64,
5007 self.delay,
5008 );
5009 Ok(())
5010 }
5011 }
5012
5013 #[derive(Handler, HandleClient, Debug)]
5014 enum TestActorMessage {
5015 Reply(oneshot::Sender<()>),
5016 Wait(oneshot::Sender<()>, oneshot::Receiver<()>),
5017 Forward(ActorHandle<TestActor>, Box<TestActorMessage>),
5018 Noop(),
5019 Fail(anyhow::Error),
5020 Panic(String),
5021 Spawn(oneshot::Sender<ActorHandle<TestActor>>),
5022 }
5023
5024 impl TestActor {
5025 async fn spawn_child(
5026 cx: &impl context::Actor,
5027 parent: &ActorHandle<TestActor>,
5028 ) -> ActorHandle<TestActor> {
5029 let (tx, rx) = oneshot::channel();
5030 parent.post(cx, TestActorMessage::Spawn(tx));
5031 rx.await.unwrap()
5032 }
5033 }
5034
5035 #[test]
5036 fn test_proc_identity_constructors() {
5037 let anonymous = Proc::anonymous();
5038 assert!(
5039 matches!(anonymous.proc_id().uid(), crate::id::Uid::Instance(_, None)),
5040 "anonymous proc must have an unlabeled instance id"
5041 );
5042 assert_eq!(anonymous.proc_id().label(), None);
5043
5044 let instance = Proc::instance("worker");
5045 assert!(
5046 matches!(
5047 instance.proc_id().uid(),
5048 crate::id::Uid::Instance(_, Some(label)) if label.as_str() == "worker"
5049 ),
5050 "instance proc must have a labeled instance id"
5051 );
5052 assert_eq!(
5053 instance.proc_id().label().map(|label| label.as_str()),
5054 Some("worker")
5055 );
5056
5057 let singleton = Proc::singleton("controller");
5058 assert!(
5059 matches!(
5060 singleton.proc_id().uid(),
5061 crate::id::Uid::Singleton(label) if label.as_str() == "controller"
5062 ),
5063 "singleton proc must have a singleton id"
5064 );
5065 assert_eq!(
5066 singleton.proc_id().label().map(|label| label.as_str()),
5067 Some("controller")
5068 );
5069 }
5070
5071 #[test]
5072 fn test_default_actor_label_uses_label_compatible_type_basename() {
5073 assert_eq!(default_actor_label::<TestActor>().as_str(), "testactor");
5074 assert_eq!(
5075 default_actor_label::<std::collections::HashMap<String, u64>>().as_str(),
5076 "hashmap"
5077 );
5078 assert_eq!(default_actor_label::<()>().as_str(), "nil");
5079 }
5080
5081 #[test]
5082 fn test_global_proc_label_uses_short_hostname_and_pid() {
5083 assert_eq!(
5084 global_proc_label_from("devvm34959.nha0.facebook.com", 123555).as_str(),
5085 "devvm34959-123555"
5086 );
5087 assert_eq!(
5088 global_proc_label_from("DevVM34959.nha0.facebook.com", 7).as_str(),
5089 "devvm34959-7"
5090 );
5091 }
5092
5093 #[async_timed_test(timeout_secs = 30)]
5094 async fn test_spawn_uses_actor_type_label_for_root_actor() {
5095 let proc = Proc::isolated();
5096 let handle = proc.spawn(TestActor);
5097
5098 assert_eq!(
5099 handle.actor_addr().label().map(Label::as_str),
5100 Some("testactor")
5101 );
5102 assert!(matches!(
5103 handle.actor_addr().uid(),
5104 Uid::Instance(_, Some(label)) if label.as_str() == "testactor"
5105 ));
5106
5107 handle.drain_and_stop("test").unwrap();
5108 handle.await;
5109 }
5110
5111 #[async_timed_test(timeout_secs = 30)]
5112 async fn test_spawn_uses_actor_type_label_for_child_actor() {
5113 let proc = Proc::isolated();
5114 let parent = proc.spawn(TestActor);
5115 let child = proc.spawn_child(parent.cell().clone(), ChildLabelActor);
5116
5117 assert!(!child.actor_addr().is_root());
5118 assert_eq!(
5119 child.actor_addr().label().map(Label::as_str),
5120 Some("childlabelactor")
5121 );
5122 assert!(matches!(
5123 child.actor_addr().uid(),
5124 Uid::Instance(_, Some(label)) if label.as_str() == "childlabelactor"
5125 ));
5126
5127 child.drain_and_stop("test").unwrap();
5128 parent.drain_and_stop("test").unwrap();
5129 child.await;
5130 parent.await;
5131 }
5132
5133 #[async_timed_test(timeout_secs = 30)]
5134 async fn test_root_tracking_does_not_depend_on_singleton_uids() {
5135 let proc = Proc::isolated();
5136 let parent = proc.spawn(TestActor);
5137 let child = proc.spawn_child(parent.cell().clone(), TestActor);
5138
5139 assert!(parent.actor_addr().uid().is_instance());
5140 let roots = proc.root_actor_ids();
5141 assert!(
5142 roots
5143 .iter()
5144 .any(|root| root.id() == parent.actor_addr().id())
5145 );
5146 assert!(
5147 !roots
5148 .iter()
5149 .any(|root| root.id() == child.actor_addr().id())
5150 );
5151
5152 let mut traversed = Vec::new();
5153 proc.traverse(&mut |cell, _depth| {
5154 traversed.push(cell.actor_addr().id().clone());
5155 });
5156 assert!(traversed.contains(parent.actor_addr().id()));
5157 assert!(traversed.contains(child.actor_addr().id()));
5158
5159 child.drain_and_stop("test").unwrap();
5160 parent.drain_and_stop("test").unwrap();
5161 child.await;
5162 parent.await;
5163 }
5164
5165 #[async_timed_test(timeout_secs = 30)]
5166 async fn test_client_spawn_api_labels_and_explicit_uid() {
5167 let proc = Proc::isolated();
5168 let client = proc.client("client");
5169
5170 let spawned = client.spawn(TestActor);
5171 assert_eq!(
5172 spawned.actor_addr().label().map(Label::as_str),
5173 Some("testactor")
5174 );
5175
5176 let labeled = client.spawn_with_label("custom", TestActor);
5177 assert_eq!(
5178 labeled.actor_addr().label().map(Label::as_str),
5179 Some("custom")
5180 );
5181
5182 let uid = Uid::instance(Label::new("explicit").unwrap());
5183 let explicit = client.spawn_with_uid(uid.clone(), TestActor).unwrap();
5184 assert_eq!(explicit.actor_addr().uid(), &uid);
5185
5186 let child = client.child();
5187 assert!(!child.self_addr().is_root());
5188 assert!(matches!(child.self_addr().uid(), Uid::Instance(_, None)));
5189 assert_eq!(child.self_addr().label(), None);
5190 let child_spawned = child.spawn(TestActor);
5191 assert_eq!(
5192 child_spawned.actor_addr().label().map(Label::as_str),
5193 Some("testactor")
5194 );
5195
5196 spawned.drain_and_stop("test").unwrap();
5197 labeled.drain_and_stop("test").unwrap();
5198 explicit.drain_and_stop("test").unwrap();
5199 child_spawned.drain_and_stop("test").unwrap();
5200 spawned.await;
5201 labeled.await;
5202 explicit.await;
5203 child_spawned.await;
5204 }
5205
5206 #[test]
5207 fn test_current_proc_uses_stable_global_proc_outside_actor_context() {
5208 let first = Proc::current();
5209 let second = Proc::current();
5210
5211 assert_eq!(first.proc_id(), second.proc_id());
5212 assert_eq!(
5213 ProcAddr::new(
5214 first.proc_id().clone(),
5215 Gateway::current().default_location()
5216 ),
5217 first.proc_addr()
5218 );
5219 }
5220
5221 #[async_trait]
5222 #[crate::handle(TestActorMessage)]
5223 impl TestActorMessageHandler for TestActor {
5224 async fn reply(
5225 &mut self,
5226 _cx: &crate::Context<Self>,
5227 sender: oneshot::Sender<()>,
5228 ) -> Result<(), anyhow::Error> {
5229 sender.send(()).unwrap();
5230 Ok(())
5231 }
5232
5233 async fn wait(
5234 &mut self,
5235 _cx: &crate::Context<Self>,
5236 sender: oneshot::Sender<()>,
5237 receiver: oneshot::Receiver<()>,
5238 ) -> Result<(), anyhow::Error> {
5239 sender.send(()).unwrap();
5240 receiver.await.unwrap();
5241 Ok(())
5242 }
5243
5244 async fn forward(
5245 &mut self,
5246 cx: &crate::Context<Self>,
5247 destination: ActorHandle<TestActor>,
5248 message: Box<TestActorMessage>,
5249 ) -> Result<(), anyhow::Error> {
5250 destination.post(cx, *message);
5252 Ok(())
5253 }
5254
5255 async fn noop(&mut self, _cx: &crate::Context<Self>) -> Result<(), anyhow::Error> {
5256 Ok(())
5257 }
5258
5259 async fn fail(
5260 &mut self,
5261 _cx: &crate::Context<Self>,
5262 err: anyhow::Error,
5263 ) -> Result<(), anyhow::Error> {
5264 Err(err)
5265 }
5266
5267 async fn panic(
5268 &mut self,
5269 _cx: &crate::Context<Self>,
5270 err_msg: String,
5271 ) -> Result<(), anyhow::Error> {
5272 panic!("{}", err_msg);
5273 }
5274
5275 async fn spawn(
5276 &mut self,
5277 cx: &crate::Context<Self>,
5278 reply: oneshot::Sender<ActorHandle<TestActor>>,
5279 ) -> Result<(), anyhow::Error> {
5280 let handle = cx.spawn(TestActor);
5281 reply.send(handle).unwrap();
5282 Ok(())
5283 }
5284 }
5285
5286 #[derive(Debug)]
5287 struct CurrentProcActor;
5288
5289 impl Actor for CurrentProcActor {}
5290
5291 #[derive(Handler, Debug)]
5292 enum CurrentProcMessage {
5293 Check(oneshot::Sender<CurrentProcSnapshot>),
5294 }
5295
5296 #[derive(Debug)]
5297 struct CurrentProcSnapshot {
5298 current_proc_id: ProcId,
5299 current_gateway_proc_addr: ProcAddr,
5300 spawned_handle: ActorHandle<TestActor>,
5301 client_proc_id: ProcId,
5302 }
5303
5304 #[async_trait]
5305 #[crate::handle(CurrentProcMessage)]
5306 impl CurrentProcMessageHandler for CurrentProcActor {
5307 async fn check(
5308 &mut self,
5309 _cx: &crate::Context<Self>,
5310 reply: oneshot::Sender<CurrentProcSnapshot>,
5311 ) -> Result<(), anyhow::Error> {
5312 let current = Proc::current();
5313 let spawned_handle = crate::spawn(TestActor);
5314 let client = crate::client("current_client");
5315 reply
5316 .send(CurrentProcSnapshot {
5317 current_proc_id: current.proc_id().clone(),
5318 current_gateway_proc_addr: ProcAddr::new(
5319 current.proc_id().clone(),
5320 Gateway::current().default_location(),
5321 ),
5322 spawned_handle,
5323 client_proc_id: client.self_addr().proc_id().clone(),
5324 })
5325 .unwrap();
5326 Ok(())
5327 }
5328 }
5329
5330 #[tokio::test]
5331 async fn test_current_proc_tracks_actor_context() {
5332 let proc = Proc::isolated();
5333 let client = proc.client("client");
5334 let actor = proc.spawn(CurrentProcActor);
5335 let (tx, rx) = oneshot::channel();
5336
5337 crate::Endpoint::post(&actor, &client, CurrentProcMessage::Check(tx));
5338 let snapshot = rx.await.unwrap();
5339
5340 assert_eq!(&snapshot.current_proc_id, proc.proc_id());
5341 assert_eq!(snapshot.current_gateway_proc_addr, proc.proc_addr());
5342 assert_eq!(
5343 snapshot.spawned_handle.actor_addr().proc_id(),
5344 proc.proc_id()
5345 );
5346 assert_eq!(&snapshot.client_proc_id, proc.proc_id());
5347
5348 snapshot
5349 .spawned_handle
5350 .drain_and_stop("test complete")
5351 .unwrap();
5352 snapshot.spawned_handle.await;
5353 actor.drain_and_stop("test complete").unwrap();
5354 actor.await;
5355 }
5356
5357 #[expect(
5358 clippy::await_holding_invalid_type,
5359 reason = "tracing_test::traced_test macro expansion holds tracing::span::Entered across awaits; can't be fixed in our code"
5360 )]
5361 #[tracing_test::traced_test]
5362 #[async_timed_test(timeout_secs = 30)]
5363 async fn test_spawn_actor() {
5364 let proc = Proc::isolated();
5365 let client = proc.client("client");
5366 let handle = proc.spawn(TestActor);
5367
5368 assert!(logs_contain(
5370 format!(
5371 "{}: spawned with {:?}",
5372 handle.actor_addr(),
5373 handle.cell().actor_task_handle().unwrap(),
5374 )
5375 .as_str()
5376 ));
5377
5378 let mut state = handle.status().clone();
5379
5380 let (tx, rx) = oneshot::channel::<()>();
5383 handle.post(&client, TestActorMessage::Reply(tx));
5384 rx.await.unwrap();
5385
5386 state
5387 .wait_for(|state: &ActorStatus| matches!(*state, ActorStatus::Idle))
5388 .await
5389 .unwrap();
5390
5391 let (enter_tx, enter_rx) = oneshot::channel::<()>();
5393 let (exit_tx, exit_rx) = oneshot::channel::<()>();
5394
5395 handle.post(&client, TestActorMessage::Wait(enter_tx, exit_rx));
5396 enter_rx.await.unwrap();
5397 assert_matches!(*state.borrow(), ActorStatus::Processing(instant, _) if instant <= std::time::SystemTime::now());
5398 exit_tx.send(()).unwrap();
5399
5400 state
5401 .wait_for(|state| matches!(*state, ActorStatus::Idle))
5402 .await
5403 .unwrap();
5404
5405 handle.drain_and_stop("test").unwrap();
5406 handle.await;
5407 assert_matches!(&*state.borrow(), ActorStatus::Stopped(reason) if reason == "test");
5408 }
5409
5410 #[async_timed_test(timeout_secs = 30)]
5411 async fn test_proc_actors_messaging() {
5412 let proc = Proc::isolated();
5413 let client = proc.client("client");
5414 let first = proc.spawn_with_label::<TestActor>("first", TestActor);
5415 let second = proc.spawn_with_label::<TestActor>("second", TestActor);
5416 let (tx, rx) = oneshot::channel::<()>();
5417 let reply_message = TestActorMessage::Reply(tx);
5418 first.post(
5419 &client,
5420 TestActorMessage::Forward(second, Box::new(reply_message)),
5421 );
5422 rx.await.unwrap();
5423 }
5424
5425 #[tokio::test]
5430 async fn test_post_routes_by_proc_id() {
5431 use crate::mailbox::monitored_return_handle;
5432
5433 #[derive(Clone)]
5434 struct CountingSender(Arc<AtomicUsize>);
5435
5436 #[async_trait]
5437 impl MailboxSender for CountingSender {
5438 fn post_unchecked(
5439 &self,
5440 _envelope: MessageEnvelope,
5441 _return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
5442 ) {
5443 self.0.fetch_add(1, Ordering::SeqCst);
5444 }
5445 }
5446
5447 let local_addr = ChannelAddr::Local(1);
5450 let remote_addr = ChannelAddr::Local(2);
5451
5452 let proc_local = ProcAddr::instance(local_addr.clone(), "shared");
5453 let proc_same_id_other_location =
5454 ProcAddr::new(proc_local.id().clone(), remote_addr.into());
5455 let proc_other_id_same_location = ProcAddr::instance(local_addr, "other");
5456 assert_eq!(
5457 proc_local.id(),
5458 proc_same_id_other_location.id(),
5459 "test setup: both procs must share a ProcId"
5460 );
5461 assert_ne!(
5462 proc_local.id(),
5463 proc_other_id_same_location.id(),
5464 "test setup: the remote proc must have a distinct ProcId"
5465 );
5466
5467 let forwarded = Arc::new(AtomicUsize::new(0));
5468 let proc = Proc::configured(
5469 proc_local.clone(),
5470 BoxedMailboxSender::new(CountingSender(forwarded.clone())),
5471 );
5472 let sender = test_actor_id("sender", "client");
5473
5474 let local_dest = proc_local.actor_addr("worker").port_addr(Port::from(1234));
5476 proc.post(
5477 MessageEnvelope::new(
5478 sender.clone(),
5479 local_dest,
5480 wirevalue::Any::serialize(&1u64).unwrap(),
5481 Flattrs::new(),
5482 ),
5483 monitored_return_handle(),
5484 );
5485 assert_eq!(forwarded.load(Ordering::SeqCst), 0);
5486
5487 let same_id_other_location_dest = proc_same_id_other_location
5489 .actor_addr("worker")
5490 .port_addr(Port::from(1234));
5491 proc.post(
5492 MessageEnvelope::new(
5493 sender.clone(),
5494 same_id_other_location_dest,
5495 wirevalue::Any::serialize(&1u64).unwrap(),
5496 Flattrs::new(),
5497 ),
5498 monitored_return_handle(),
5499 );
5500 assert_eq!(forwarded.load(Ordering::SeqCst), 0);
5501
5502 let other_id_same_location_dest = proc_other_id_same_location
5504 .actor_addr("worker")
5505 .port_addr(Port::from(1234));
5506 proc.post(
5507 MessageEnvelope::new(
5508 sender,
5509 other_id_same_location_dest,
5510 wirevalue::Any::serialize(&1u64).unwrap(),
5511 Flattrs::new(),
5512 ),
5513 monitored_return_handle(),
5514 );
5515 assert_eq!(forwarded.load(Ordering::SeqCst), 1);
5516 }
5517
5518 #[tokio::test]
5523 async fn test_mailbox_ext_post_stamps_sender_actor_id() {
5524 use crate::mailbox::headers::SENDER_ACTOR_ID;
5525
5526 #[derive(typeuri::Named)]
5527 struct DestHandlerMsg;
5528
5529 #[derive(Clone, Default)]
5530 struct CapturingSender(Arc<Mutex<Vec<MessageEnvelope>>>);
5531
5532 #[async_trait]
5533 impl MailboxSender for CapturingSender {
5534 fn post_unchecked(
5535 &self,
5536 envelope: MessageEnvelope,
5537 _return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
5538 ) {
5539 self.0.lock().unwrap().push(envelope);
5540 }
5541 }
5542
5543 let proc_addr = ProcAddr::instance(ChannelAddr::Local(1), "stamping_test");
5544 let captured: Arc<Mutex<Vec<MessageEnvelope>>> = Arc::new(Mutex::new(Vec::new()));
5545 let proc = Proc::configured(
5546 proc_addr,
5547 BoxedMailboxSender::new(CapturingSender(captured.clone())),
5548 );
5549
5550 let client = proc.client("client");
5551 let client_addr = client.mailbox().actor_addr().clone();
5552
5553 let remote_dest = ProcAddr::instance(ChannelAddr::Local(2), "remote")
5556 .actor_addr("worker")
5557 .port_addr(Port::handler::<DestHandlerMsg>());
5558
5559 <Client as context::MailboxExt>::post(
5563 &client,
5564 remote_dest,
5565 Flattrs::new(),
5566 wirevalue::Any::serialize(&1u64).unwrap(),
5567 false,
5568 context::SeqInfoPolicy::AssignNew,
5569 );
5570
5571 let captured = captured.lock().unwrap();
5572 assert_eq!(
5573 captured.len(),
5574 1,
5575 "exactly one envelope should be forwarded"
5576 );
5577 assert_eq!(
5578 captured[0].headers().get(SENDER_ACTOR_ID),
5579 Some(client_addr),
5580 "MailboxExt::post must stamp SENDER_ACTOR_ID with the client's actor_addr"
5581 );
5582 }
5583
5584 #[test]
5585 fn test_local_delivery_service_and_local_compare_full_proc_addr() {
5586 for name in [LEGACY_SERVICE_PROC_NAME, LEGACY_LOCAL_PROC_NAME] {
5587 let local = ProcAddr::singleton(ChannelAddr::Local(1), name);
5588 let same_id_other_location = ProcAddr::singleton(ChannelAddr::Local(2), name);
5589 let proc = match name {
5590 LEGACY_SERVICE_PROC_NAME => Proc::legacy_service_pseudo_singleton(
5591 ChannelAddr::Local(1),
5592 BoxedMailboxSender::new(PanickingMailboxSender),
5593 ),
5594 LEGACY_LOCAL_PROC_NAME => Proc::legacy_local_pseudo_singleton(
5595 ChannelAddr::Local(1),
5596 BoxedMailboxSender::new(PanickingMailboxSender),
5597 ),
5598 _ => unreachable!("test only covers legacy pseudo-singletons"),
5599 };
5600
5601 assert_eq!(local.id(), same_id_other_location.id());
5602 assert!(proc.is_local_delivery_target(&local));
5603 assert!(!proc.is_local_delivery_target(&same_id_other_location));
5604 }
5605
5606 let shared = ProcAddr::singleton(ChannelAddr::Local(1), "shared");
5607 let shared_other_location = ProcAddr::singleton(ChannelAddr::Local(2), "shared");
5608 let proc = Proc::configured(
5609 shared.clone(),
5610 BoxedMailboxSender::new(PanickingMailboxSender),
5611 );
5612 assert!(proc.is_local_delivery_target(&shared_other_location));
5613
5614 let service_instance = ProcAddr::instance(ChannelAddr::Local(1), "service");
5615 let service_instance_other_location =
5616 ProcAddr::new(service_instance.id().clone(), ChannelAddr::Local(2).into());
5617 let proc = Proc::configured(
5618 service_instance,
5619 BoxedMailboxSender::new(PanickingMailboxSender),
5620 );
5621 assert!(proc.is_local_delivery_target(&service_instance_other_location));
5622 }
5623
5624 #[test]
5625 fn test_legacy_pseudo_singletons_use_dedicated_constructors() {
5626 for name in [LEGACY_SERVICE_PROC_NAME, LEGACY_LOCAL_PROC_NAME] {
5627 let result = std::panic::catch_unwind(|| {
5628 Proc::configured(
5629 ProcAddr::singleton(ChannelAddr::Local(1), name),
5630 BoxedMailboxSender::new(PanickingMailboxSender),
5631 );
5632 });
5633 assert!(result.is_err());
5634 }
5635
5636 let service = Proc::legacy_service_pseudo_singleton(
5637 ChannelAddr::Local(1),
5638 BoxedMailboxSender::new(PanickingMailboxSender),
5639 );
5640 assert_eq!(
5641 service.proc_addr().id().uid().to_string(),
5642 LEGACY_SERVICE_PROC_NAME
5643 );
5644
5645 let local = Proc::legacy_local_pseudo_singleton(
5646 ChannelAddr::Local(2),
5647 BoxedMailboxSender::new(PanickingMailboxSender),
5648 );
5649 assert_eq!(
5650 local.proc_addr().id().uid().to_string(),
5651 LEGACY_LOCAL_PROC_NAME
5652 );
5653 }
5654
5655 #[tokio::test]
5656 async fn test_mailbox_muxer_delivers_by_actor_id() {
5657 use crate::mailbox::PortLocation;
5658 use crate::mailbox::monitored_return_handle;
5659
5660 let proc = Proc::isolated();
5661 let instance = proc.client("worker");
5662 let (port, mut receiver) = instance.bind_handler_port::<u64>();
5663
5664 let PortLocation::Bound(default_dest) = port.location() else {
5665 panic!("handler port must be bound");
5666 };
5667 let alternate_dest =
5668 PortAddr::new(default_dest.id().clone(), ChannelAddr::Local(9876).into());
5669
5670 proc.post(
5671 MessageEnvelope::serialize(
5672 test_actor_id("sender", "client"),
5673 alternate_dest,
5674 &123u64,
5675 Flattrs::new(),
5676 )
5677 .unwrap(),
5678 monitored_return_handle(),
5679 );
5680
5681 assert_eq!(receiver.recv().await.unwrap(), 123);
5682 }
5683
5684 #[test]
5685 fn test_default_location_changes_new_bindings_not_lookup() {
5686 let proc = Proc::isolated();
5687 let gateway = proc.gateway();
5688 let client = proc.client("worker");
5689
5690 let first_ref: ActorRef<()> = client.bind();
5691 let new_location = ChannelAddr::Local(9876).into();
5692 gateway.set_default_location(new_location);
5693 let second_ref: ActorRef<()> = client.bind();
5694
5695 assert_eq!(first_ref.actor_addr().id(), second_ref.actor_addr().id());
5696 assert_ne!(
5697 first_ref.actor_addr().location(),
5698 second_ref.actor_addr().location()
5699 );
5700 assert_eq!(second_ref.actor_addr().location(), &proc.default_location());
5701 assert_eq!(proc.default_location(), gateway.default_location());
5702 assert_eq!(
5703 proc.proc_addr(),
5704 ProcAddr::new(proc.proc_id().clone(), gateway.default_location())
5705 );
5706 assert!(proc.get_instance(second_ref.actor_addr()).is_some());
5707 }
5708
5709 #[async_timed_test(timeout_secs = 10)]
5714 async fn test_default_location_concurrent_with_bind() {
5715 let proc = Proc::isolated();
5716 let gateway = proc.gateway();
5717 let handle = proc.client("worker");
5718
5719 let loc_a: Location = ChannelAddr::Local(40001).into();
5720 let loc_b: Location = ChannelAddr::Local(40002).into();
5721
5722 gateway.set_default_location(loc_a.clone());
5727
5728 let barrier = std::sync::Arc::new(Barrier::new(2));
5729
5730 let setter = {
5731 let gateway = gateway.clone();
5732 let loc_a = loc_a.clone();
5733 let loc_b = loc_b.clone();
5734 let barrier = barrier.clone();
5735 tokio::spawn(async move {
5736 barrier.wait().await;
5737 for i in 0..100 {
5738 let loc = if i % 2 == 0 {
5739 loc_a.clone()
5740 } else {
5741 loc_b.clone()
5742 };
5743 gateway.set_default_location(loc);
5744 tokio::task::yield_now().await;
5745 }
5746 })
5747 };
5748
5749 let binder = {
5750 let handle = handle.clone();
5756 let barrier = barrier.clone();
5757 tokio::spawn(async move {
5758 barrier.wait().await;
5759 let mut refs = Vec::with_capacity(100);
5760 for _ in 0..100 {
5761 refs.push(handle.bind::<()>());
5762 tokio::task::yield_now().await;
5763 }
5764 refs
5765 })
5766 };
5767
5768 setter.await.unwrap();
5769 let refs = binder.await.unwrap();
5770
5771 for r in &refs {
5773 let loc = r.actor_addr().location();
5774 assert!(
5775 loc == &loc_a || loc == &loc_b,
5776 "ref location {loc:?} is neither {loc_a:?} nor {loc_b:?}",
5777 );
5778 }
5779
5780 for r in &refs {
5782 assert!(
5783 proc.get_instance(r.actor_addr()).is_some(),
5784 "ref {:?} no longer resolves",
5785 r.actor_addr(),
5786 );
5787 }
5788 }
5789
5790 #[test]
5791 fn test_builder_procs_can_share_gateway_with_distinct_ids() {
5792 let gateway = Gateway::new();
5793 let first = Proc::builder()
5794 .proc_id(ProcId::instance(Label::strip("first")))
5795 .shared_gateway(gateway.clone())
5796 .build()
5797 .unwrap();
5798 let second = Proc::builder()
5799 .proc_id(ProcId::instance(Label::strip("second")))
5800 .shared_gateway(gateway.clone())
5801 .build()
5802 .unwrap();
5803
5804 assert_ne!(first.proc_id(), second.proc_id());
5805 assert_eq!(first.default_location(), second.default_location());
5806
5807 let new_location = ChannelAddr::Local(9876).into();
5808 gateway.set_default_location(new_location);
5809
5810 assert_eq!(first.default_location(), gateway.default_location());
5811 assert_eq!(second.default_location(), gateway.default_location());
5812 assert_eq!(
5813 first.proc_addr(),
5814 ProcAddr::new(first.proc_id().clone(), gateway.default_location())
5815 );
5816 assert_eq!(
5817 second.proc_addr(),
5818 ProcAddr::new(second.proc_id().clone(), gateway.default_location())
5819 );
5820 }
5821
5822 #[test]
5823 fn test_isolated_procs_use_distinct_gateways() {
5824 let first = Proc::isolated();
5825 let second = Proc::isolated();
5826 let second_location = second.default_location();
5827
5828 first
5829 .gateway()
5830 .set_default_location(ChannelAddr::Local(9876).into());
5831
5832 assert_ne!(first.proc_id(), second.proc_id());
5833 assert_ne!(first.default_location(), second_location);
5834 assert_eq!(second.default_location(), second_location);
5835 }
5836
5837 #[tokio::test]
5838 async fn test_gateway_serve_updates_location_and_stops() {
5839 use crate::mailbox::PortLocation;
5840 use crate::mailbox::monitored_return_handle;
5841
5842 let proc = Proc::isolated();
5843 let gateway = proc.gateway();
5844 let initial_location = proc.default_location();
5845 let client = proc.client("client");
5846 let (port, mut receiver) = client.bind_handler_port::<u64>();
5847 let PortLocation::Bound(default_dest) = port.location() else {
5848 panic!("handler port must be bound");
5849 };
5850
5851 async fn send_to_location(
5852 location: Location,
5853 default_dest: &PortAddr,
5854 value: u64,
5855 receiver: &mut PortReceiver<u64>,
5856 ) {
5857 let dest = PortAddr::new(default_dest.id().clone(), location.clone());
5858 let sender = MailboxClient::dial(location.addr().clone()).unwrap();
5859 sender.post(
5860 MessageEnvelope::serialize(
5861 test_actor_id("sender", "client"),
5862 dest,
5863 &value,
5864 Flattrs::new(),
5865 )
5866 .unwrap(),
5867 monitored_return_handle(),
5868 );
5869 sender.flush().await.unwrap();
5870 let received = tokio::time::timeout(Duration::from_secs(5), receiver.recv())
5871 .await
5872 .unwrap()
5873 .unwrap();
5874 assert_eq!(received, value);
5875 }
5876
5877 let mut server =
5878 Gateway::serve(&gateway, ChannelAddr::any(ChannelTransport::Local)).unwrap();
5879
5880 assert_eq!(proc.default_location(), initial_location);
5881 assert_eq!(proc.default_location(), gateway.default_location());
5882 assert_eq!(
5883 proc.proc_addr(),
5884 ProcAddr::new(proc.proc_id().clone(), gateway.default_location())
5885 );
5886 send_to_location(initial_location.clone(), &default_dest, 1, &mut receiver).await;
5887
5888 let mut next_server =
5889 Gateway::serve(&gateway, ChannelAddr::any(ChannelTransport::Local)).unwrap();
5890 let next_location = proc.default_location();
5891
5892 assert_ne!(proc.default_location(), initial_location);
5893 assert_eq!(proc.default_location(), gateway.default_location());
5894 assert_eq!(
5895 proc.proc_addr(),
5896 ProcAddr::new(proc.proc_id().clone(), gateway.default_location())
5897 );
5898 send_to_location(next_location.clone(), &default_dest, 2, &mut receiver).await;
5899 send_to_location(initial_location.clone(), &default_dest, 3, &mut receiver).await;
5900
5901 next_server.stop("test complete");
5902 next_server.join().await.unwrap();
5903
5904 assert_eq!(proc.default_location(), initial_location);
5905 assert_eq!(proc.default_location(), gateway.default_location());
5906 assert!(MailboxClient::dial(next_location.addr().clone()).is_err());
5907 send_to_location(initial_location.clone(), &default_dest, 4, &mut receiver).await;
5908
5909 server.stop("test complete");
5910 server.join().await.unwrap();
5911
5912 assert_eq!(proc.default_location(), initial_location);
5913 assert_eq!(proc.default_location(), gateway.default_location());
5914 assert!(MailboxClient::dial(initial_location.addr().clone()).is_err());
5915 }
5916
5917 #[tokio::test]
5918 async fn test_direct_proc_server_stops_via_join_mailbox_server() {
5919 let proc = Proc::direct(
5920 ChannelAddr::any(ChannelTransport::Local),
5921 "direct".to_string(),
5922 )
5923 .unwrap();
5924
5925 assert_eq!(
5926 proc.proc_addr(),
5927 ProcAddr::new(proc.proc_id().clone(), proc.gateway().default_location())
5928 );
5929
5930 proc.join_mailbox_server().await;
5931 }
5932
5933 #[tokio::test]
5934 async fn test_local_only_gateway_returns_undeliverable_messages() {
5935 let proc = Proc::isolated();
5936 let client = proc.client("client");
5937 let (return_handle, mut undeliverable_rx) =
5938 client.open_port::<Undeliverable<MessageEnvelope>>();
5939 let remote_proc = ProcAddr::instance(ChannelAddr::Local(1234), "remote");
5940 let remote_dest = remote_proc.actor_addr("worker").port_addr(Port::from(0));
5941
5942 proc.post(
5943 MessageEnvelope::serialize(
5944 test_actor_id("sender", "client"),
5945 remote_dest.clone(),
5946 &123u64,
5947 Flattrs::new(),
5948 )
5949 .unwrap(),
5950 return_handle,
5951 );
5952
5953 let Undeliverable::Returned(envelope) = undeliverable_rx.recv().await.unwrap() else {
5954 panic!("expected returned message");
5955 };
5956 assert_eq!(envelope.dest(), &remote_dest);
5957 }
5958
5959 #[tokio::test]
5960 async fn test_gateway_attach_peer_routes_via_through_sender() {
5961 use tokio::sync::mpsc::UnboundedSender;
5962 use tokio::sync::mpsc::unbounded_channel;
5963
5964 use crate::mailbox::IntoBoxedMailboxSender;
5965 use crate::mailbox::MailboxSender as _;
5966 use crate::mailbox::monitored_return_handle;
5967
5968 #[derive(Clone)]
5969 struct MpscSender(UnboundedSender<MessageEnvelope>);
5970
5971 #[async_trait]
5972 impl crate::mailbox::MailboxSender for MpscSender {
5973 fn post_unchecked(
5974 &self,
5975 envelope: MessageEnvelope,
5976 _return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
5977 ) {
5978 self.0.send(envelope).unwrap();
5979 }
5980 }
5981
5982 let proc = Proc::isolated();
5983 let gateway = proc.gateway();
5984
5985 let remote_uid = Uid::Instance(0xabc123, Some(Label::strip("remote")));
5989 let remote_inner_addr = ChannelAddr::Local(1234);
5990 let remote_location =
5991 Location::from(remote_inner_addr.clone()).with_via(remote_uid.clone());
5992 let remote_proc = ProcAddr::new(ProcId::new(remote_uid.clone(), None), remote_location);
5993 let remote_dest = remote_proc.actor_addr("worker").port_addr(Port::from(0));
5994
5995 let (tx, mut rx) = unbounded_channel::<MessageEnvelope>();
5996 let route_guard = gateway
5997 .attach_peer(remote_uid.clone(), MpscSender(tx).into_boxed())
5998 .unwrap();
5999
6000 gateway.post(
6001 MessageEnvelope::serialize(
6002 test_actor_id("sender", "client"),
6003 remote_dest.clone(),
6004 &42u64,
6005 Flattrs::new(),
6006 )
6007 .unwrap(),
6008 monitored_return_handle(),
6009 );
6010
6011 let routed = tokio::time::timeout(Duration::from_secs(5), rx.recv())
6012 .await
6013 .expect("attach_peer did not route envelope before timeout")
6014 .expect("recording sender closed unexpectedly");
6015 assert_eq!(routed.dest(), &remote_dest);
6018 assert_eq!(routed.next_hop().id(), remote_dest.id());
6019 assert_eq!(routed.next_hop().location().addr(), &remote_inner_addr);
6020 assert!(
6021 !routed.next_hop().location().is_via(),
6022 "outer via hop must be peeled from the next hop"
6023 );
6024
6025 drop(route_guard);
6026
6027 let client = proc.client("client");
6028 let (return_handle, mut undeliverable_rx) =
6029 client.open_port::<Undeliverable<MessageEnvelope>>();
6030 gateway.post(
6031 MessageEnvelope::serialize(
6032 test_actor_id("sender", "client"),
6033 remote_dest.clone(),
6034 &43u64,
6035 Flattrs::new(),
6036 )
6037 .unwrap(),
6038 return_handle,
6039 );
6040 let Undeliverable::Returned(envelope) = undeliverable_rx.recv().await.unwrap() else {
6041 panic!("expected Undeliverable::Returned variant");
6042 };
6043 assert_eq!(envelope.dest(), &remote_dest);
6044 }
6045
6046 #[derive(Debug, Default)]
6047 #[export]
6048 struct LookupTestActor;
6049
6050 impl Actor for LookupTestActor {}
6051
6052 #[derive(Handler, HandleClient, Debug)]
6053 enum LookupTestMessage {
6054 ActorExists(ActorRef<TestActor>, #[reply] OncePortRef<bool>),
6055 }
6056
6057 #[async_trait]
6058 #[crate::handle(LookupTestMessage)]
6059 impl LookupTestMessageHandler for LookupTestActor {
6060 async fn actor_exists(
6061 &mut self,
6062 cx: &crate::Context<Self>,
6063 actor_ref: ActorRef<TestActor>,
6064 ) -> Result<bool, anyhow::Error> {
6065 Ok(actor_ref.downcast_handle(cx).is_some())
6066 }
6067 }
6068
6069 #[async_timed_test(timeout_secs = 30)]
6070 async fn test_actor_lookup() {
6071 let proc = Proc::isolated();
6072 let client = proc.client("client");
6073
6074 let target_actor = proc.spawn(TestActor);
6075 let target_actor_ref = target_actor.bind();
6076 let lookup_actor = proc.spawn(LookupTestActor);
6077
6078 assert!(
6079 lookup_actor
6080 .actor_exists(&client, target_actor_ref.clone())
6081 .await
6082 .unwrap()
6083 );
6084
6085 assert!(
6087 !lookup_actor
6088 .actor_exists(
6089 &client,
6090 ActorRef::attest(target_actor.actor_addr().anonymous_child())
6091 )
6092 .await
6093 .unwrap()
6094 );
6095 assert!(
6097 !lookup_actor
6098 .actor_exists(&client, ActorRef::attest(lookup_actor.actor_addr().clone()))
6099 .await
6100 .unwrap()
6101 );
6102
6103 target_actor.drain_and_stop("test").unwrap();
6104 target_actor.await;
6105
6106 assert!(
6107 !lookup_actor
6108 .actor_exists(&client, target_actor_ref)
6109 .await
6110 .unwrap()
6111 );
6112
6113 lookup_actor.drain_and_stop("test").unwrap();
6114 lookup_actor.await;
6115 }
6116
6117 fn validate_link(child: &InstanceCell, parent: &InstanceCell) {
6118 assert_eq!(
6119 child.actor_addr().proc_addr(),
6120 parent.actor_addr().proc_addr()
6121 );
6122 assert_eq!(
6123 child.inner.parent.upgrade().unwrap().actor_addr(),
6124 parent.actor_addr()
6125 );
6126 assert_matches!(
6127 parent.inner.children.get(child.uid()),
6128 Some(node) if node.actor_addr() == child.actor_addr()
6129 );
6130 }
6131
6132 #[expect(
6133 clippy::await_holding_invalid_type,
6134 reason = "tracing_test::traced_test macro expansion holds tracing::span::Entered across awaits; can't be fixed in our code"
6135 )]
6136 #[tracing_test::traced_test]
6137 #[async_timed_test(timeout_secs = 30)]
6138 async fn test_spawn_child() {
6139 let proc = Proc::isolated();
6140 let client = proc.client("client");
6141
6142 let first = proc.spawn_with_label::<TestActor>("first", TestActor);
6143 let second = TestActor::spawn_child(&client, &first).await;
6144 let third = TestActor::spawn_child(&client, &second).await;
6145
6146 assert!(logs_with_scope_contain(
6148 "hyperactor::proc",
6149 format!(
6150 "{}: spawned with {:?}",
6151 first.actor_addr(),
6152 first.cell().actor_task_handle().unwrap()
6153 )
6154 .as_str()
6155 ));
6156 assert!(logs_with_scope_contain(
6157 "hyperactor::proc",
6158 format!(
6159 "{}: spawned with {:?}",
6160 second.actor_addr(),
6161 second.cell().actor_task_handle().unwrap()
6162 )
6163 .as_str()
6164 ));
6165 assert!(logs_with_scope_contain(
6166 "hyperactor::proc",
6167 format!(
6168 "{}: spawned with {:?}",
6169 third.actor_addr(),
6170 third.cell().actor_task_handle().unwrap()
6171 )
6172 .as_str()
6173 ));
6174
6175 assert_eq!(first.actor_addr().proc_addr(), proc.proc_addr());
6177 assert_eq!(second.actor_addr().proc_addr(), proc.proc_addr());
6178 assert_eq!(third.actor_addr().proc_addr(), proc.proc_addr());
6179
6180 validate_link(third.cell(), second.cell());
6182 validate_link(second.cell(), first.cell());
6183 assert!(first.cell().inner.parent.upgrade().is_none());
6184
6185 let third_cell = third.cell().clone();
6188 third.drain_and_stop("test").unwrap();
6189 third.await;
6190 assert!(third_cell.inner.children.is_empty());
6191 drop(third_cell);
6192 validate_link(second.cell(), first.cell());
6193
6194 let second_cell = second.cell().clone();
6195 second.drain_and_stop("test").unwrap();
6196 second.await;
6197 assert!(second_cell.inner.children.is_empty());
6198 drop(second_cell);
6199
6200 let first_cell = first.cell().clone();
6201 first.drain_and_stop("test").unwrap();
6202 first.await;
6203 assert!(first_cell.inner.children.is_empty());
6204 }
6205
6206 #[async_timed_test(timeout_secs = 30)]
6207 async fn zombie_status_sets_unless_terminal() {
6208 let proc = Proc::isolated();
6209 let alive = proc.spawn_with_label::<TestActor>("alive", TestActor);
6210 alive
6211 .status()
6212 .clone()
6213 .wait_for(ActorStatus::is_idle)
6214 .await
6215 .unwrap();
6216 alive
6217 .cell()
6218 .change_status(ActorStatus::zombie("hard kill did not finish"));
6219 assert!(alive.cell().status().borrow().is_zombie());
6220
6221 alive.cell().change_status(ActorStatus::Idle);
6222 assert!(alive.cell().status().borrow().is_zombie());
6223
6224 let stopped = proc.spawn_with_label::<TestActor>("stopped", TestActor);
6225 let stopped_cell = stopped.cell().clone();
6226 stopped.drain_and_stop("test").unwrap();
6227 let terminal_status = stopped.await;
6228 assert!(terminal_status.is_terminal());
6229
6230 stopped_cell.change_status(ActorStatus::zombie("hard kill did not finish"));
6231 assert_eq!(*stopped_cell.status().borrow(), terminal_status);
6232
6233 alive.drain_and_stop("test").unwrap();
6234 alive.await;
6235 }
6236
6237 #[async_timed_test(timeout_secs = 30)]
6238 async fn change_status_rejects_zombie_for_linked_child() {
6239 let proc = Proc::isolated();
6240 let client = proc.client("client");
6241 let parent = proc.spawn_with_label::<TestActor>("parent", TestActor);
6242 let child = TestActor::spawn_child(&client, &parent).await;
6243 child
6244 .status()
6245 .clone()
6246 .wait_for(ActorStatus::is_idle)
6247 .await
6248 .unwrap();
6249
6250 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
6251 child
6252 .cell()
6253 .change_status(ActorStatus::zombie("hard kill did not finish"));
6254 }));
6255 assert!(
6256 result.is_err(),
6257 "`change_status` must reject zombie status for linked children"
6258 );
6259
6260 parent.drain_and_stop("test").unwrap();
6261 assert_matches!(parent.await, ActorStatus::Stopped(reason) if reason == "test");
6262 assert_matches!(child.await, ActorStatus::Stopped(reason) if reason == "parent draining");
6263 }
6264
6265 #[async_timed_test(timeout_secs = 30)]
6266 async fn zombie_status_does_not_complete_actor_handle() {
6267 let proc = Proc::isolated();
6268 let handle = proc.spawn_with_label::<TestActor>("alive", TestActor);
6269 handle
6270 .status()
6271 .clone()
6272 .wait_for(ActorStatus::is_idle)
6273 .await
6274 .unwrap();
6275 handle
6276 .cell()
6277 .change_status(ActorStatus::zombie("hard kill did not finish"));
6278
6279 let await_handle = handle.clone();
6280 let result = tokio::time::timeout(
6281 Duration::from_millis(100),
6282 async move { await_handle.await },
6283 )
6284 .await;
6285 assert!(
6286 result.is_err(),
6287 "zombie actor must not complete ActorHandle::await"
6288 );
6289 handle.drain_and_stop("test").unwrap();
6290 assert_matches!(handle.await, ActorStatus::Stopped(reason) if reason == "test");
6291 }
6292
6293 #[async_timed_test(timeout_secs = 30)]
6294 async fn zombie_task_failure_reports_non_error_supervision_event() {
6295 let proc = Proc::isolated();
6296 let client = proc.client("client");
6297 let (mut reported_event, _coordinator) =
6298 ProcSupervisionCoordinator::set(&proc).await.unwrap();
6299 let handle = proc.spawn_with_label::<TestActor>("alive", TestActor);
6300 handle
6301 .status()
6302 .clone()
6303 .wait_for(ActorStatus::is_idle)
6304 .await
6305 .unwrap();
6306 handle
6307 .cell()
6308 .change_status(ActorStatus::zombie("hard kill did not finish"));
6309
6310 handle
6311 .fail(&client, anyhow::anyhow!("zombie failure"))
6312 .await
6313 .unwrap();
6314 assert_matches!(handle.await, ActorStatus::Failed(_));
6315
6316 let event = tokio::time::timeout(Duration::from_secs(1), reported_event.recv())
6317 .await
6318 .expect("zombie supervision event should arrive");
6319 assert!(
6320 event.actor_status.is_zombie(),
6321 "zombie actor task failure should report zombie status"
6322 );
6323 assert!(
6324 !event.is_error(),
6325 "zombie actor task failure should report a non-error supervision event"
6326 );
6327 }
6328
6329 #[async_timed_test(timeout_secs = 30)]
6330 async fn test_child_lifecycle() {
6331 let proc = Proc::isolated();
6332 let client = proc.client("client");
6333
6334 let root = proc.spawn_with_label::<TestActor>("root", TestActor);
6335 let root_1 = TestActor::spawn_child(&client, &root).await;
6336 let root_2 = TestActor::spawn_child(&client, &root).await;
6337 let root_2_1 = TestActor::spawn_child(&client, &root_2).await;
6338
6339 root.drain_and_stop("test").unwrap();
6340 root.await;
6341
6342 for actor in [root_1, root_2, root_2_1] {
6343 assert!(
6344 actor
6345 .port::<TestActorMessage>()
6346 .try_post(&client, TestActorMessage::Noop())
6347 .is_err()
6348 );
6349 assert_matches!(actor.await, ActorStatus::Stopped(reason) if reason == "parent draining");
6350 }
6351 }
6352
6353 #[derive(Debug)]
6354 struct DrainCountingActor {
6355 handled: Arc<AtomicUsize>,
6356 }
6357
6358 impl Actor for DrainCountingActor {}
6359
6360 #[derive(Handler, Debug)]
6361 enum DrainCountingMessage {
6362 Block(oneshot::Sender<()>, oneshot::Receiver<()>),
6363 Count(),
6364 }
6365
6366 #[async_trait]
6367 #[crate::handle(DrainCountingMessage)]
6368 impl DrainCountingMessageHandler for DrainCountingActor {
6369 async fn block(
6370 &mut self,
6371 _cx: &crate::Context<Self>,
6372 entered: oneshot::Sender<()>,
6373 release: oneshot::Receiver<()>,
6374 ) -> Result<(), anyhow::Error> {
6375 entered.send(()).unwrap();
6376 release.await.unwrap();
6377 Ok(())
6378 }
6379
6380 async fn count(&mut self, _cx: &crate::Context<Self>) -> Result<(), anyhow::Error> {
6381 self.handled.fetch_add(1, Ordering::SeqCst);
6382 Ok(())
6383 }
6384 }
6385
6386 async fn block_and_queue_counts(
6387 client: &Client,
6388 handle: &ActorHandle<DrainCountingActor>,
6389 count: usize,
6390 ) -> oneshot::Sender<()> {
6391 let (entered_tx, entered_rx) = oneshot::channel();
6392 let (release_tx, release_rx) = oneshot::channel();
6393 handle.post(client, DrainCountingMessage::Block(entered_tx, release_rx));
6394 entered_rx.await.unwrap();
6395 for _ in 0..count {
6396 handle.post(client, DrainCountingMessage::Count());
6397 }
6398 release_tx
6399 }
6400
6401 #[async_timed_test(timeout_secs = 30)]
6402 async fn child_inherits_drain_mode() {
6403 let proc = Proc::isolated();
6404 let client = proc.client("client");
6405 let parent = proc.spawn(TestActor);
6406 let handled = Arc::new(AtomicUsize::new(0));
6407 let child = proc.spawn_child(
6408 parent.cell().clone(),
6409 DrainCountingActor {
6410 handled: handled.clone(),
6411 },
6412 );
6413 let release = block_and_queue_counts(&client, &child, 5).await;
6414
6415 parent.drain_and_stop("test").unwrap();
6416 release.send(()).unwrap();
6417
6418 assert_matches!(parent.await, ActorStatus::Stopped(reason) if reason == "test");
6419 assert_matches!(child.await, ActorStatus::Stopped(reason) if reason == "parent draining");
6420 assert_eq!(handled.load(Ordering::SeqCst), 5);
6421 }
6422
6423 #[async_timed_test(timeout_secs = 30)]
6424 async fn tree_inherits_drain_mode() {
6425 let proc = Proc::isolated();
6426 let client = proc.client("client");
6427 let grandparent = proc.spawn(TestActor);
6428 let parent = proc.spawn_child(grandparent.cell().clone(), TestActor);
6429 let handled = Arc::new(AtomicUsize::new(0));
6430 let grandchild = proc.spawn_child(
6431 parent.cell().clone(),
6432 DrainCountingActor {
6433 handled: handled.clone(),
6434 },
6435 );
6436 let release = block_and_queue_counts(&client, &grandchild, 7).await;
6437
6438 grandparent.drain_and_stop("test").unwrap();
6439 release.send(()).unwrap();
6440
6441 assert_matches!(grandparent.await, ActorStatus::Stopped(reason) if reason == "test");
6442 assert_matches!(parent.await, ActorStatus::Stopped(reason) if reason == "parent draining");
6443 assert_matches!(grandchild.await, ActorStatus::Stopped(reason) if reason == "parent draining");
6444 assert_eq!(handled.load(Ordering::SeqCst), 7);
6445 }
6446
6447 #[derive(Debug)]
6448 struct StopFailingActor;
6449
6450 #[async_trait]
6451 impl Actor for StopFailingActor {
6452 async fn handle_stop(
6453 &mut self,
6454 _this: &Instance<Self>,
6455 _mode: StopMode,
6456 _reason: &str,
6457 ) -> Result<(), anyhow::Error> {
6458 Err(anyhow::anyhow!("stop failed"))
6459 }
6460 }
6461
6462 #[async_timed_test(timeout_secs = 30)]
6463 async fn failed_drain_stop_stops_children_immediately() {
6464 let proc = Proc::isolated();
6465 let (_reported, _coordinator) = ProcSupervisionCoordinator::set(&proc).await.unwrap();
6466 let parent = proc.spawn(StopFailingActor);
6467 let child = proc.spawn_child(parent.cell().clone(), TestActor);
6468
6469 parent.drain_and_stop("test").unwrap();
6470
6471 assert_matches!(child.await, ActorStatus::Stopped(reason) if reason == "parent stopping");
6472 assert_matches!(
6473 parent.await,
6474 ActorStatus::Failed(err) if err.to_string().contains("stop failed")
6475 );
6476 }
6477
6478 #[derive(Debug)]
6479 struct DeferredStopActor {
6480 stop_started: Arc<tokio::sync::Notify>,
6481 release_stop: Arc<tokio::sync::Notify>,
6482 }
6483
6484 #[async_trait]
6485 impl Actor for DeferredStopActor {
6486 async fn handle_stop(
6487 &mut self,
6488 this: &Instance<Self>,
6489 mode: StopMode,
6490 reason: &str,
6491 ) -> Result<(), anyhow::Error> {
6492 let this = this.clone_for_py();
6493 let release_stop = Arc::clone(&self.release_stop);
6494 let reason = reason.to_string();
6495 this.close();
6496 self.stop_started.notify_one();
6497 tokio::spawn(async move {
6498 release_stop.notified().await;
6499 match mode {
6500 StopMode::Stop => this.exit(&reason).unwrap(),
6501 StopMode::DrainAndStop => this.exit_after_drain(&reason).unwrap(),
6502 }
6503 });
6504 Ok(())
6505 }
6506 }
6507
6508 #[async_trait]
6509 impl Handler<()> for DeferredStopActor {
6510 async fn handle(&mut self, _cx: &crate::Context<Self>, _message: ()) -> anyhow::Result<()> {
6511 Ok(())
6512 }
6513 }
6514
6515 #[async_timed_test(timeout_secs = 30)]
6516 async fn test_handle_stop_can_defer_exit() {
6517 let proc = Proc::isolated();
6518 let stop_started = Arc::new(tokio::sync::Notify::new());
6519 let release_stop = Arc::new(tokio::sync::Notify::new());
6520 let handle = proc.spawn(DeferredStopActor {
6521 stop_started: Arc::clone(&stop_started),
6522 release_stop: Arc::clone(&release_stop),
6523 });
6524
6525 let mut status = handle.status();
6526 handle.stop("test").unwrap();
6527 stop_started.notified().await;
6528 status.wait_for(ActorStatus::is_stopping).await.unwrap();
6529
6530 release_stop.notify_one();
6531 assert_matches!(handle.await, ActorStatus::Stopped(reason) if reason == "test");
6532 }
6533
6534 #[async_timed_test(timeout_secs = 30)]
6535 async fn test_drain_and_stop_closes_handler_ingress() {
6536 let proc = Proc::isolated();
6537 let client = proc.client("client");
6538 let stop_started = Arc::new(tokio::sync::Notify::new());
6539 let release_stop = Arc::new(tokio::sync::Notify::new());
6540 let handle = proc.spawn(DeferredStopActor {
6541 stop_started: Arc::clone(&stop_started),
6542 release_stop: Arc::clone(&release_stop),
6543 });
6544
6545 handle.drain_and_stop("test").unwrap();
6546 stop_started.notified().await;
6547
6548 let err = handle.port::<()>().try_post(&client, ()).unwrap_err();
6551 assert_matches!(err.kind(), crate::mailbox::MailboxSenderErrorKind::Closed);
6552
6553 release_stop.notify_one();
6554 assert_matches!(handle.await, ActorStatus::Stopped(reason) if reason == "test");
6555 }
6556
6557 #[async_timed_test(timeout_secs = 30)]
6558 async fn test_parent_failure() {
6559 let proc = Proc::isolated();
6560 let client = proc.client("client");
6561 let (_reported, _coordinator) = ProcSupervisionCoordinator::set(&proc).await.unwrap();
6564
6565 let root = proc.spawn_with_label::<TestActor>("root", TestActor);
6566 let root_1 = TestActor::spawn_child(&client, &root).await;
6567 let root_2 = TestActor::spawn_child(&client, &root).await;
6568 let root_2_1 = TestActor::spawn_child(&client, &root_2).await;
6569
6570 root_2.post(
6571 &client,
6572 TestActorMessage::Fail(anyhow::anyhow!("some random failure")),
6573 );
6574 let _root_2_actor_id = root_2.actor_addr().clone();
6575 assert_matches!(
6576 root_2.await,
6577 ActorStatus::Failed(err) if err.to_string() == "some random failure"
6578 );
6579
6580 assert_matches!(
6584 root.await,
6585 ActorStatus::Failed(err) if err.to_string().contains("some random failure")
6586 );
6587 assert_matches!(root_2_1.await, ActorStatus::Stopped(_));
6588 assert_matches!(root_1.await, ActorStatus::Stopped(_));
6589 }
6590
6591 #[async_timed_test(timeout_secs = 30)]
6592 async fn test_multi_handler() {
6593 #[derive(Debug)]
6597 struct TestActor(Arc<AtomicUsize>);
6598
6599 #[async_trait]
6600 impl Actor for TestActor {}
6601
6602 #[async_trait]
6603 impl Handler<OncePortHandle<PortHandle<usize>>> for TestActor {
6604 async fn handle(
6605 &mut self,
6606 cx: &crate::Context<Self>,
6607 message: OncePortHandle<PortHandle<usize>>,
6608 ) -> anyhow::Result<()> {
6609 message.post(cx, cx.port());
6610 Ok(())
6611 }
6612 }
6613
6614 #[async_trait]
6615 impl Handler<usize> for TestActor {
6616 async fn handle(
6617 &mut self,
6618 _cx: &crate::Context<Self>,
6619 message: usize,
6620 ) -> anyhow::Result<()> {
6621 self.0.fetch_add(message, Ordering::SeqCst);
6622 Ok(())
6623 }
6624 }
6625
6626 let proc = Proc::isolated();
6627 let state = Arc::new(AtomicUsize::new(0));
6628 let actor = TestActor(state.clone());
6629 let handle = proc.spawn(actor);
6630 let client = proc.client("client");
6631 let (tx, rx) = client.open_once_port();
6632 handle.post(&client, tx);
6633 let usize_handle = rx.recv().await.unwrap();
6634 usize_handle.post(&client, 123);
6635
6636 handle.drain_and_stop("test").unwrap();
6637 handle.await;
6638
6639 assert_eq!(state.load(Ordering::SeqCst), 123);
6640 }
6641
6642 #[async_timed_test(timeout_secs = 30)]
6643 async fn test_post_after_self_message() {
6644 let proc = Proc::isolated();
6645 let client = proc.client("client");
6646 let (ready, ready_rx) = client.open_once_port();
6647 let (fired, fired_rx) = client.open_once_port();
6648 let delay = Duration::from_millis(50);
6649 let start = tokio::time::Instant::now();
6650 let handle = proc.spawn(DelayedSelfActor {
6651 ready: Some(ready.bind()),
6652 fired: Some(fired.bind()),
6653 delay,
6654 });
6655
6656 ready_rx.recv().await.unwrap();
6657 fired_rx.recv().await.unwrap();
6658
6659 assert!(start.elapsed() >= delay);
6660 handle.drain_and_stop("test").unwrap();
6661 handle.await;
6662 }
6663
6664 #[async_timed_test(timeout_secs = 30)]
6665 async fn test_post_after_port_ref() {
6666 let proc = Proc::isolated();
6667 let client = proc.client("client");
6668 let (reply, mut reply_rx) = client.open_port();
6669 let delay = Duration::from_millis(50);
6670 let start = tokio::time::Instant::now();
6671 let handle = proc.spawn(DelayedPortActor {
6672 reply: Some(reply.bind()),
6673 delay,
6674 });
6675
6676 assert_eq!(reply_rx.recv().await.unwrap(), 123);
6677 assert!(start.elapsed() >= delay);
6678 handle.drain_and_stop("test").unwrap();
6679 handle.await;
6680 }
6681
6682 #[async_timed_test(timeout_secs = 30)]
6683 async fn test_post_after_discards_pending_messages_on_shutdown() {
6684 let proc = Proc::isolated();
6685 let client = proc.client("client");
6686 let (ready, ready_rx) = client.open_once_port();
6687 let (fired, fired_rx) = client.open_once_port();
6688 let handle = proc.spawn(DelayedSelfActor {
6689 ready: Some(ready.bind()),
6690 fired: Some(fired.bind()),
6691 delay: Duration::from_secs(60),
6692 });
6693
6694 ready_rx.recv().await.unwrap();
6695 handle.drain_and_stop("test").unwrap();
6696 assert_matches!(handle.await, ActorStatus::Stopped(reason) if reason == "test");
6697
6698 let result = tokio::time::timeout(Duration::from_millis(100), fired_rx.recv()).await;
6699 assert!(!matches!(result, Ok(Ok(()))));
6700 }
6701
6702 #[async_timed_test(timeout_secs = 30)]
6703 async fn test_actor_panic() {
6704 panic_handler::set_panic_hook();
6706
6707 let proc = Proc::isolated();
6708 let (_reported, _coordinator) = ProcSupervisionCoordinator::set(&proc).await.unwrap();
6711
6712 let client = proc.client("client");
6713 let actor_handle = proc.spawn(TestActor);
6714 actor_handle
6715 .panic(&client, "some random failure".to_string())
6716 .await
6717 .unwrap();
6718 let actor_status = actor_handle.await;
6719
6720 assert_matches!(actor_status, ActorStatus::Failed(_));
6724 if let ActorStatus::Failed(err) = actor_status {
6725 let error_msg = err.to_string();
6726 assert!(error_msg.contains("some random failure"));
6728 assert!(error_msg.contains("library/std/src/panicking.rs"));
6732 }
6733 }
6734
6735 #[async_timed_test(timeout_secs = 30)]
6753 async fn test_local_supervision_propagation() {
6754 hyperactor_telemetry::initialize_logging_for_test();
6755
6756 #[derive(Debug)]
6757 struct TestActor {
6758 handled: Arc<AtomicBool>,
6759 notify: Arc<tokio::sync::Notify>,
6760 should_handle: bool,
6761 }
6762
6763 #[async_trait]
6764 impl Actor for TestActor {
6765 async fn handle_supervision_event(
6766 &mut self,
6767 _this: &Instance<Self>,
6768 _event: &ActorSupervisionEvent,
6769 ) -> Result<bool, anyhow::Error> {
6770 if !self.should_handle {
6771 return Ok(false);
6772 }
6773
6774 tracing::error!(
6775 "{}: supervision event received: {:?}",
6776 _this.self_addr(),
6777 _event
6778 );
6779 self.handled.store(true, Ordering::SeqCst);
6780 self.notify.notify_one();
6781 Ok(true)
6782 }
6783 }
6784
6785 #[async_trait]
6786 impl Handler<String> for TestActor {
6787 async fn handle(
6788 &mut self,
6789 cx: &crate::Context<Self>,
6790 message: String,
6791 ) -> anyhow::Result<()> {
6792 tracing::info!("{} received message: {}", cx.self_addr(), message);
6793 Err(anyhow::anyhow!(message))
6794 }
6795 }
6796
6797 let make_actor = |handled: &Arc<AtomicBool>, should_handle: bool| TestActor {
6798 handled: handled.clone(),
6799 notify: Arc::new(tokio::sync::Notify::new()),
6800 should_handle,
6801 };
6802
6803 let proc = Proc::isolated();
6804 let client = proc.client("client");
6805 let (mut reported_event, _coordinator) =
6806 ProcSupervisionCoordinator::set(&proc).await.unwrap();
6807
6808 let root_state = Arc::new(AtomicBool::new(false));
6809 let root_1_state = Arc::new(AtomicBool::new(false));
6810 let root_1_notify = Arc::new(tokio::sync::Notify::new());
6811 let root_1_1_state = Arc::new(AtomicBool::new(false));
6812 let root_1_1_1_state = Arc::new(AtomicBool::new(false));
6813 let root_2_state = Arc::new(AtomicBool::new(false));
6814 let root_2_1_state = Arc::new(AtomicBool::new(false));
6815
6816 let root = proc.spawn_with_label::<TestActor>("root", make_actor(&root_state, false));
6817 let root_1 = proc.spawn_child::<TestActor>(
6818 root.cell().clone(),
6819 TestActor {
6820 handled: root_1_state.clone(),
6821 notify: root_1_notify.clone(),
6822 should_handle: true, },
6824 );
6825 let root_1_1 = proc
6826 .spawn_child::<TestActor>(root_1.cell().clone(), make_actor(&root_1_1_state, false));
6827 let root_1_1_1 = proc.spawn_child::<TestActor>(
6828 root_1_1.cell().clone(),
6829 make_actor(&root_1_1_1_state, false),
6830 );
6831 let root_2 = proc.spawn_with_label::<TestActor>("root_2", make_actor(&root_2_state, false));
6834 let root_2_1 = proc
6835 .spawn_child::<TestActor>(root_2.cell().clone(), make_actor(&root_2_1_state, false));
6836
6837 root_1_1_1.post(&client, "some random failure".to_string());
6840
6841 let root_2_1_id = root_2_1.actor_addr().clone();
6844 root_2_1.post(&client, "some random failure".to_string());
6845
6846 root_1_notify.notified().await;
6850
6851 let event = reported_event.recv().await;
6854 assert_eq!(event.actor_id, root_2_1_id);
6855
6856 assert!(!root_state.load(Ordering::SeqCst));
6857 assert!(root_1_state.load(Ordering::SeqCst));
6858 assert!(!root_1_1_state.load(Ordering::SeqCst));
6859 assert!(!root_1_1_1_state.load(Ordering::SeqCst));
6860 assert!(!root_2_state.load(Ordering::SeqCst));
6861 assert!(!root_2_1_state.load(Ordering::SeqCst));
6862 }
6863
6864 #[async_timed_test(timeout_secs = 30)]
6865 async fn test_instance() {
6866 #[derive(Debug, Default)]
6867 struct TestActor;
6868
6869 impl Actor for TestActor {}
6870
6871 #[async_trait]
6872 impl Handler<(String, PortRef<String>)> for TestActor {
6873 async fn handle(
6874 &mut self,
6875 cx: &crate::Context<Self>,
6876 (message, port): (String, PortRef<String>),
6877 ) -> anyhow::Result<()> {
6878 port.post(cx, message);
6879 Ok(())
6880 }
6881 }
6882
6883 let proc = Proc::isolated();
6884
6885 let client = proc.client("my_test_actor");
6886 let status = client.status();
6887
6888 let child_actor = client.spawn(TestActor);
6889
6890 let (port, mut receiver) = client.open_port();
6891 child_actor.post(&client, ("hello".to_string(), port.bind()));
6892
6893 let message = receiver.recv().await.unwrap();
6894 assert_eq!(message, "hello");
6895
6896 child_actor.drain_and_stop("test").unwrap();
6897 child_actor.await;
6898
6899 assert_eq!(*status.borrow(), ActorStatus::Client);
6900 drop(client);
6901 assert_matches!(*status.borrow(), ActorStatus::Stopped(_));
6902 }
6903
6904 #[cfg_attr(target_os = "macos", ignore = "tokio runtime fork assertion on macOS")]
6907 #[tokio::test]
6908 async fn test_proc_terminate_without_coordinator() {
6909 if std::env::var("CARGO_TEST").is_ok() {
6910 eprintln!("test skipped as it hangs when run by cargo in sandcastle");
6911 return;
6912 }
6913
6914 let process = async {
6915 let proc = Proc::isolated();
6916 let root = proc.spawn_with_label("root", TestActor);
6920 let client = proc.client("client");
6921 root.fail(&client, anyhow::anyhow!("some random failure"))
6922 .await
6923 .unwrap();
6924 tokio::time::sleep(Duration::from_secs(30)).await;
6928 };
6929
6930 assert_termination(|| process, 1).await.unwrap();
6931 }
6932
6933 fn trace_and_block(fut: impl Future) {
6934 tracing::subscriber::with_default(
6935 tracing_subscriber::Registry::default().with(hyperactor_telemetry::recorder().layer()),
6936 || {
6937 tokio::runtime::Builder::new_current_thread()
6938 .enable_all()
6939 .build()
6940 .unwrap()
6941 .block_on(fut)
6942 },
6943 );
6944 }
6945
6946 #[test]
6947 fn test_handler_logging() {
6948 #[derive(Debug, Default)]
6949 struct LoggingActor;
6950
6951 impl Actor for LoggingActor {}
6952
6953 impl LoggingActor {
6954 async fn wait(cx: &impl context::Actor, handle: &ActorHandle<Self>) {
6955 let barrier = Arc::new(Barrier::new(2));
6956 handle.post(cx, barrier.clone());
6957 barrier.wait().await;
6958 }
6959 }
6960
6961 #[async_trait]
6962 impl Handler<String> for LoggingActor {
6963 async fn handle(
6964 &mut self,
6965 _cx: &crate::Context<Self>,
6966 message: String,
6967 ) -> anyhow::Result<()> {
6968 tracing::info!("{}", message);
6969 Ok(())
6970 }
6971 }
6972
6973 #[async_trait]
6974 impl Handler<u64> for LoggingActor {
6975 async fn handle(
6976 &mut self,
6977 _cx: &crate::Context<Self>,
6978 message: u64,
6979 ) -> anyhow::Result<()> {
6980 tracing::event!(Level::INFO, number = message);
6981 Ok(())
6982 }
6983 }
6984
6985 #[async_trait]
6986 impl Handler<Arc<Barrier>> for LoggingActor {
6987 async fn handle(
6988 &mut self,
6989 _cx: &crate::Context<Self>,
6990 message: Arc<Barrier>,
6991 ) -> anyhow::Result<()> {
6992 message.wait().await;
6993 Ok(())
6994 }
6995 }
6996
6997 #[async_trait]
6998 impl Handler<Arc<(Barrier, Barrier)>> for LoggingActor {
6999 #[expect(
7000 clippy::await_holding_invalid_type,
7001 reason = "tracing_test::traced_test macro expansion holds tracing::span::Entered across awaits; can't be fixed in our code"
7002 )]
7003 async fn handle(
7004 &mut self,
7005 _cx: &crate::Context<Self>,
7006 barriers: Arc<(Barrier, Barrier)>,
7007 ) -> anyhow::Result<()> {
7008 let inner = tracing::span!(Level::INFO, "child_span");
7009 let _inner_guard = inner.enter();
7010 barriers.0.wait().await;
7011 barriers.1.wait().await;
7012 Ok(())
7013 }
7014 }
7015
7016 trace_and_block(async {
7017 let proc = Proc::isolated();
7018 let client = proc.client("client");
7019 let handle = hyperactor::spawn(LoggingActor).into_guard();
7020 handle.post(&client, "hello world".to_string());
7021 handle.post(&client, "hello world again".to_string());
7022 handle.post(&client, 123u64);
7023
7024 LoggingActor::wait(&client, &handle).await;
7025
7026 let events = handle.cell().inner.recording.tail();
7027 assert_eq!(events.len(), 3);
7028 assert_eq!(events[0].json_value(), json!({ "message": "hello world" }));
7029 assert_eq!(
7030 events[1].json_value(),
7031 json!({ "message": "hello world again" })
7032 );
7033 assert_eq!(events[2].json_value(), json!({ "number": 123 }));
7034
7035 let stacks = {
7036 let barriers = Arc::new((Barrier::new(2), Barrier::new(2)));
7037 handle.post(&client, Arc::clone(&barriers));
7038 barriers.0.wait().await;
7039 let stacks = handle.cell().inner.recording.stacks();
7040 barriers.1.wait().await;
7041 stacks
7042 };
7043 assert_eq!(stacks.len(), 1);
7044 assert_eq!(stacks[0].len(), 1);
7045 assert_eq!(stacks[0][0].name(), "child_span");
7046 })
7047 }
7048
7049 #[async_timed_test(timeout_secs = 30)]
7050 async fn test_mailbox_closed_with_owner_stopped_reason() {
7051 let proc = Proc::isolated();
7052 let client = proc.client("client");
7053 let actor_handle = proc.spawn(TestActor);
7054
7055 let handle_for_send = actor_handle.clone();
7057
7058 actor_handle.drain_and_stop("healthy shutdown").unwrap();
7060 actor_handle.await;
7061
7062 let result = handle_for_send
7064 .port::<TestActorMessage>()
7065 .try_post(&client, TestActorMessage::Noop());
7066
7067 assert!(result.is_err(), "send should fail when actor is stopped");
7068 let err = result.unwrap_err();
7069 assert_matches!(
7070 err.kind(),
7071 crate::mailbox::MailboxSenderErrorKind::Mailbox(mailbox_err)
7072 if matches!(
7073 mailbox_err.kind(),
7074 crate::mailbox::MailboxErrorKind::OwnerTerminated(ActorStatus::Stopped(reason)) if reason == "healthy shutdown"
7075 )
7076 );
7077 }
7078
7079 #[async_timed_test(timeout_secs = 30)]
7080 async fn test_mailbox_closed_with_owner_failed_reason() {
7081 let proc = Proc::isolated();
7082 let client = proc.client("client");
7083 let (_reported, _coordinator) = ProcSupervisionCoordinator::set(&proc).await.unwrap();
7086
7087 let actor_handle = proc.spawn(TestActor);
7088
7089 let handle_for_send = actor_handle.clone();
7091
7092 actor_handle.post(
7094 &client,
7095 TestActorMessage::Fail(anyhow::anyhow!("intentional failure")),
7096 );
7097 actor_handle.await;
7098
7099 let result = handle_for_send
7101 .port::<TestActorMessage>()
7102 .try_post(&client, TestActorMessage::Noop());
7103
7104 assert!(result.is_err(), "send should fail when actor has failed");
7105 let err = result.unwrap_err();
7106 assert_matches!(
7107 err.kind(),
7108 crate::mailbox::MailboxSenderErrorKind::Mailbox(mailbox_err)
7109 if matches!(
7110 mailbox_err.kind(),
7111 crate::mailbox::MailboxErrorKind::OwnerTerminated(ActorStatus::Failed(ActorErrorKind::Generic(msg)))
7112 if msg.contains("intentional failure")
7113 )
7114 );
7115 }
7116
7117 async fn wait_for_terminated_snapshot(
7121 proc: &Proc,
7122 actor_id: &ActorAddr,
7123 ) -> crate::introspect::IntrospectResult {
7124 for i in 0..1000 {
7128 if let Some(snapshot) = proc.terminated_snapshot(actor_id) {
7129 return snapshot;
7130 }
7131 if i < 50 {
7132 tokio::task::yield_now().await;
7133 } else {
7134 tokio::time::sleep(Duration::from_millis(50)).await;
7135 }
7136 }
7137 panic!("timed out waiting for terminated snapshot for {}", actor_id);
7138 }
7139
7140 #[async_timed_test(timeout_secs = 60)]
7149 async fn test_terminated_snapshot_stored_on_stop() {
7150 let proc = Proc::isolated();
7151 let _client = proc.client("client");
7152
7153 let handle = proc.spawn(TestActor);
7154 let actor_id = handle.actor_addr().clone();
7155
7156 assert!(proc.terminated_snapshot(&actor_id).is_none());
7158 assert!(!proc.all_terminated_actor_ids().contains(&actor_id));
7159
7160 handle.drain_and_stop("test").unwrap();
7162 handle.await;
7163
7164 let snapshot = wait_for_terminated_snapshot(&proc, &actor_id).await;
7167 let attrs: hyperactor_config::Attrs =
7168 serde_json::from_str(&snapshot.attrs).expect("snapshot attrs must be valid");
7169 let status = attrs
7170 .get(crate::introspect::STATUS)
7171 .expect("must have status");
7172 assert!(
7173 status.starts_with("stopped"),
7174 "expected stopped status, got: {}",
7175 status
7176 );
7177
7178 assert!(proc.all_terminated_actor_ids().contains(&actor_id));
7180 assert!(
7181 !proc.all_actor_ids().contains(&actor_id),
7182 "stopped actor should not appear in live actor IDs"
7183 );
7184 }
7185
7186 #[async_timed_test(timeout_secs = 60)]
7193 async fn test_terminated_snapshot_stored_on_failure() {
7194 let proc = Proc::isolated();
7195 let client = proc.client("client");
7196 ProcSupervisionCoordinator::set(&proc).await.unwrap();
7198
7199 let handle = proc.spawn(TestActor);
7200 let actor_id = handle.actor_addr().clone();
7201
7202 handle.post(&client, TestActorMessage::Fail(anyhow::anyhow!("boom")));
7204 handle.await;
7205
7206 let snapshot = wait_for_terminated_snapshot(&proc, &actor_id).await;
7207 let attrs: hyperactor_config::Attrs =
7208 serde_json::from_str(&snapshot.attrs).expect("snapshot attrs must be valid");
7209 let status = attrs
7210 .get(crate::introspect::STATUS)
7211 .expect("must have status");
7212 assert!(
7213 status.starts_with("failed"),
7214 "expected failed status, got: {}",
7215 status
7216 );
7217 }
7218
7219 #[async_timed_test(timeout_secs = 30)]
7221 async fn test_supervision_event_stored_on_failure() {
7222 let proc = Proc::isolated();
7223 let client = proc.client("client");
7224 ProcSupervisionCoordinator::set(&proc).await.unwrap();
7225
7226 let handle = proc.spawn(TestActor);
7227 let actor_id = handle.actor_addr().clone();
7228 let cell = handle.cell().clone();
7229
7230 handle.post(&client, TestActorMessage::Fail(anyhow::anyhow!("boom")));
7231 handle.await;
7232
7233 let event = cell
7234 .supervision_event()
7235 .expect("failed actor must have supervision_event");
7236 assert_eq!(event.actor_id, actor_id);
7237 assert!(event.actor_status.is_failed());
7238 assert_eq!(event.actually_failing_actor().unwrap().actor_id, actor_id);
7240 }
7241
7242 #[async_timed_test(timeout_secs = 30)]
7244 async fn test_supervision_event_on_clean_stop() {
7245 let proc = Proc::isolated();
7246 let _client = proc.client("client");
7247
7248 let handle = proc.spawn(TestActor);
7249 let cell = handle.cell().clone();
7250
7251 handle.drain_and_stop("test").unwrap();
7252 handle.await;
7253
7254 let event = cell
7255 .supervision_event()
7256 .expect("clean stop must store supervision event");
7257 assert!(
7258 matches!(event.actor_status, ActorStatus::Stopped(_)),
7259 "expected Stopped status, got {:?}",
7260 event.actor_status
7261 );
7262 assert!(!event.is_error());
7263 }
7264
7265 #[async_timed_test(timeout_secs = 30)]
7266 async fn test_supervision_coordinator_receives_clean_stop() {
7267 let proc = Proc::isolated();
7268 let _client = proc.client("client");
7269 let (mut reported_event, _coordinator_handle) =
7270 ProcSupervisionCoordinator::set(&proc).await.unwrap();
7271
7272 let handle = proc.spawn(TestActor);
7273 let actor_id = handle.actor_addr().clone();
7274
7275 handle.drain_and_stop("test").unwrap();
7276 handle.await;
7277
7278 let event = reported_event.recv().await;
7279 assert_eq!(event.actor_id, actor_id);
7280 assert!(
7281 matches!(event.actor_status, ActorStatus::Stopped(_)),
7282 "expected Stopped status, got {:?}",
7283 event.actor_status
7284 );
7285 assert!(!event.is_error());
7286 }
7287
7288 #[async_timed_test(timeout_secs = 30)]
7289 async fn test_coordinator_shuts_down_last_during_destroy() {
7290 let mut proc = Proc::isolated();
7291 let _client = proc.client("client");
7292 let (mut reported_event, _coordinator_handle) =
7293 ProcSupervisionCoordinator::set(&proc).await.unwrap();
7294
7295 let mut actor_ids = Vec::new();
7297 for i in 0..3 {
7298 let handle = proc.spawn_with_label::<TestActor>(&format!("actor_{i}"), TestActor);
7299 actor_ids.push(handle.actor_addr().clone());
7300 }
7301
7302 proc.destroy_and_wait(Duration::from_secs(5), "test")
7307 .await
7308 .unwrap();
7309
7310 let mut received_ids = Vec::new();
7312 for _ in 0..actor_ids.len() {
7313 let event = reported_event.recv().await;
7314 assert!(
7315 matches!(event.actor_status, ActorStatus::Stopped(_)),
7316 "expected Stopped, got {:?}",
7317 event.actor_status
7318 );
7319 received_ids.push(event.actor_id);
7320 }
7321 received_ids.sort();
7322 actor_ids.sort();
7323 assert_eq!(received_ids, actor_ids);
7324 }
7325
7326 #[async_timed_test(timeout_secs = 30)]
7328 async fn test_supervision_event_on_propagated_failure() {
7329 let proc = Proc::isolated();
7330 let client = proc.client("client");
7331 ProcSupervisionCoordinator::set(&proc).await.unwrap();
7332
7333 let parent = proc.spawn_with_label::<TestActor>("parent", TestActor);
7334 let parent_cell = parent.cell().clone();
7335 let (tx, rx) = oneshot::channel();
7337 parent.post(&client, TestActorMessage::Spawn(tx));
7338 let child = rx.await.unwrap();
7339 let child_id = child.actor_addr().clone();
7340
7341 child.post(
7344 &client,
7345 TestActorMessage::Fail(anyhow::anyhow!("child boom")),
7346 );
7347 parent.await;
7348
7349 let event = parent_cell.supervision_event();
7350 assert!(
7351 event.is_some(),
7352 "parent must have supervision_event from propagated failure"
7353 );
7354 let event = event.unwrap();
7355 assert_eq!(event.actually_failing_actor().unwrap().actor_id, child_id);
7357 }
7358
7359 #[async_timed_test(timeout_secs = 30)]
7368 async fn test_resolve_actor_ref_none_for_terminal_actor() {
7369 let proc = Proc::isolated();
7370 let _client = proc.client("client");
7371
7372 let handle = proc.spawn(TestActor);
7373 let actor_ref: ActorRef<TestActor> = handle.bind();
7374
7375 assert!(
7377 proc.resolve_actor_ref(&actor_ref).is_some(),
7378 "live actor should be resolvable"
7379 );
7380
7381 handle.drain_and_stop("test").unwrap();
7382 handle.await;
7383
7384 assert!(
7387 proc.resolve_actor_ref(&actor_ref).is_none(),
7388 "terminal actor must not be resolvable"
7389 );
7390 }
7391
7392 #[async_timed_test(timeout_secs = 60)]
7394 async fn test_terminated_snapshot_has_failure_info() {
7395 let proc = Proc::isolated();
7396 let client = proc.client("client");
7397 ProcSupervisionCoordinator::set(&proc).await.unwrap();
7398
7399 let handle = proc.spawn(TestActor);
7400 let actor_id = handle.actor_addr().clone();
7401
7402 handle.post(&client, TestActorMessage::Fail(anyhow::anyhow!("kaboom")));
7403 handle.await;
7404
7405 let snapshot = wait_for_terminated_snapshot(&proc, &actor_id).await;
7406 let attrs: hyperactor_config::Attrs =
7407 serde_json::from_str(&snapshot.attrs).expect("snapshot attrs must be valid");
7408 let status = attrs
7409 .get(crate::introspect::STATUS)
7410 .expect("must have status");
7411 assert!(
7412 status.starts_with("failed"),
7413 "expected failed status, got: {}",
7414 status
7415 );
7416 let err_msg = attrs
7417 .get(crate::introspect::FAILURE_ERROR_MESSAGE)
7418 .expect("failed actor must have failure_error_message");
7419 assert!(!err_msg.is_empty());
7420 let root_cause = attrs
7421 .get(crate::introspect::FAILURE_ROOT_CAUSE_ACTOR)
7422 .expect("must have root_cause_actor");
7423 assert_eq!(root_cause, &actor_id);
7424 assert_eq!(
7425 attrs.get(crate::introspect::FAILURE_IS_PROPAGATED),
7426 Some(&false)
7427 );
7428 assert!(
7429 attrs.get(crate::introspect::FAILURE_OCCURRED_AT).is_some(),
7430 "failed actor must have occurred_at"
7431 );
7432 }
7433
7434 #[async_timed_test(timeout_secs = 60)]
7436 async fn test_propagated_failure_info() {
7437 let proc = Proc::isolated();
7438 let client = proc.client("client");
7439 ProcSupervisionCoordinator::set(&proc).await.unwrap();
7440
7441 let parent = proc.spawn_with_label::<TestActor>("parent", TestActor);
7442 let parent_id = parent.actor_addr().clone();
7443
7444 let (tx, rx) = oneshot::channel();
7445 parent.post(&client, TestActorMessage::Spawn(tx));
7446 let child = rx.await.unwrap();
7447 let child_id = child.actor_addr().clone();
7448
7449 child.post(
7450 &client,
7451 TestActorMessage::Fail(anyhow::anyhow!("child fail")),
7452 );
7453 parent.await;
7454
7455 let snapshot = wait_for_terminated_snapshot(&proc, &parent_id).await;
7456 let attrs: hyperactor_config::Attrs =
7457 serde_json::from_str(&snapshot.attrs).expect("snapshot attrs must be valid");
7458 let root_cause = attrs
7459 .get(crate::introspect::FAILURE_ROOT_CAUSE_ACTOR)
7460 .expect("propagated failure must have root_cause_actor");
7461 assert_eq!(root_cause, &child_id);
7462 assert_eq!(
7463 attrs.get(crate::introspect::FAILURE_IS_PROPAGATED),
7464 Some(&true)
7465 );
7466 }
7467
7468 #[async_timed_test(timeout_secs = 30)]
7470 async fn test_spawn_with_name_creates_descriptive_name() {
7471 let proc = Proc::isolated();
7472 let root = proc.spawn_with_label::<TestActor>("root", TestActor);
7473 let handle = proc.spawn_named_child(root.cell().clone(), "my_controller", TestActor);
7474 assert_eq!(
7475 handle.actor_addr().label().unwrap().as_str(),
7476 "my_controller"
7477 );
7478 assert!(!handle.actor_addr().is_root());
7479 }
7480
7481 #[async_timed_test(timeout_secs = 30)]
7483 async fn test_spawn_with_name_increments_index() {
7484 let proc = Proc::isolated();
7485 let root = proc.spawn_with_label::<TestActor>("root", TestActor);
7486 let first = proc.spawn_named_child(root.cell().clone(), "my_controller", TestActor);
7487 let second = proc.spawn_named_child(root.cell().clone(), "my_controller", TestActor);
7488 assert_ne!(first.actor_addr().uid(), second.actor_addr().uid());
7489 }
7490
7491 #[async_timed_test(timeout_secs = 30)]
7494 async fn test_spawn_with_name_preserves_supervision() {
7495 let proc = Proc::isolated();
7496 let root = proc.spawn_with_label::<TestActor>("root", TestActor);
7497 let child = proc.spawn_named_child(root.cell().clone(), "supervised_child", TestActor);
7498 let child_cell = child.cell();
7499 let parent = child_cell.parent().expect("child must have parent");
7500 assert_eq!(parent.actor_addr(), root.actor_addr());
7501 }
7502
7503 #[async_timed_test(timeout_secs = 30)]
7505 async fn test_spawn_unchanged() {
7506 let proc = Proc::isolated();
7507 let root = proc.spawn_with_label::<TestActor>("root", TestActor);
7508 let child = proc.spawn_child(root.cell().clone(), TestActor);
7509 assert!(!child.actor_addr().is_root());
7510 }
7511
7512 #[async_timed_test(timeout_secs = 30)]
7514 async fn test_spawn_with_name_different_names_different_pids() {
7515 let proc = Proc::isolated();
7516 let root = proc.spawn_with_label::<TestActor>("root", TestActor);
7517 let a = proc.spawn_named_child(root.cell().clone(), "controller_a", TestActor);
7518 let b = proc.spawn_named_child(root.cell().clone(), "controller_b", TestActor);
7519 assert_ne!(a.actor_addr().uid(), b.actor_addr().uid());
7520 assert_eq!(a.actor_addr().label().unwrap().as_str(), "controller_a");
7521 assert_eq!(b.actor_addr().label().unwrap().as_str(), "controller_b");
7522 }
7523
7524 #[async_timed_test(timeout_secs = 30)]
7526 async fn test_spawn_with_name_no_child_overwrite() {
7527 let proc = Proc::isolated();
7528 let root = proc.spawn_with_label::<TestActor>("root", TestActor);
7529 let _a = proc.spawn_named_child(root.cell().clone(), "ctrl", TestActor);
7530 let _b = proc.spawn_named_child(root.cell().clone(), "ctrl", TestActor);
7531 let _c = proc.spawn_child(root.cell().clone(), TestActor);
7532 assert_eq!(root.cell().child_count(), 3);
7533 }
7534
7535 #[async_timed_test(timeout_secs = 30)]
7537 async fn test_spawn_with_name_does_not_pollute_roots() {
7538 let proc = Proc::isolated();
7539 let root = proc.spawn_with_label::<TestActor>("root", TestActor);
7540 let _child = proc.spawn_named_child(root.cell().clone(), "foo", TestActor);
7541 let _root = proc.spawn_with_label::<TestActor>("foo", TestActor);
7544 }
7545
7546 #[async_timed_test(timeout_secs = 30)]
7548 async fn test_ai3_controller_actor_ids_unique_across_parents_same_proc() {
7549 let proc = Proc::isolated();
7550 let parent_a = proc.spawn_with_label::<TestActor>("parent_a", TestActor);
7551 let parent_b = proc.spawn_with_label::<TestActor>("parent_b", TestActor);
7552
7553 let ctrl_a =
7555 proc.spawn_named_child(parent_a.cell().clone(), "controller_mesh_a", TestActor);
7556 let ctrl_b =
7557 proc.spawn_named_child(parent_b.cell().clone(), "controller_mesh_b", TestActor);
7558
7559 assert_ne!(
7560 ctrl_a.actor_addr(),
7561 ctrl_b.actor_addr(),
7562 "controller ActorAddrs must be unique across parents"
7563 );
7564 }
7565
7566 #[async_timed_test(timeout_secs = 30)]
7568 async fn test_ai3_no_controller_overwrite_in_parent_or_proc_maps() {
7569 let proc = Proc::isolated();
7570 let parent_a = proc.spawn_with_label::<TestActor>("parent_a", TestActor);
7571 let parent_b = proc.spawn_with_label::<TestActor>("parent_b", TestActor);
7572
7573 let ctrl_a =
7574 proc.spawn_named_child(parent_a.cell().clone(), "controller_mesh_a", TestActor);
7575 let ctrl_b =
7576 proc.spawn_named_child(parent_b.cell().clone(), "controller_mesh_b", TestActor);
7577
7578 assert!(
7580 proc.get_instance(ctrl_a.actor_addr()).is_some(),
7581 "ctrl_a must be resolvable"
7582 );
7583 assert!(
7584 proc.get_instance(ctrl_b.actor_addr()).is_some(),
7585 "ctrl_b must be resolvable"
7586 );
7587 assert_eq!(parent_a.cell().child_count(), 1);
7589 assert_eq!(parent_b.cell().child_count(), 1);
7590 }
7591
7592 #[async_timed_test(timeout_secs = 60)]
7594 async fn test_stopped_snapshot_has_no_failure_info() {
7595 let proc = Proc::isolated();
7596 let _client = proc.client("client");
7597
7598 let handle = proc.spawn(TestActor);
7599 let actor_id = handle.actor_addr().clone();
7600
7601 handle.drain_and_stop("test").unwrap();
7602 handle.await;
7603
7604 let snapshot = wait_for_terminated_snapshot(&proc, &actor_id).await;
7605 let attrs: hyperactor_config::Attrs =
7606 serde_json::from_str(&snapshot.attrs).expect("snapshot attrs must be valid");
7607 let status = attrs
7608 .get(crate::introspect::STATUS)
7609 .expect("must have status");
7610 assert!(
7611 status.starts_with("stopped"),
7612 "expected stopped, got: {}",
7613 status
7614 );
7615 assert!(
7616 attrs
7617 .get(crate::introspect::FAILURE_ERROR_MESSAGE)
7618 .is_none(),
7619 "stopped actor must not have failure attrs"
7620 );
7621 }
7622
7623 #[async_timed_test(timeout_secs = 10)]
7630 async fn test_queue_depth_increment_decrement() {
7631 let proc = Proc::isolated();
7632 let client = proc.client("client");
7633 let handle = proc.spawn_with_label("qd_test", TestActor);
7634 let actor_ref: crate::ActorRef<TestActor> = handle.bind();
7635 let actor_id = actor_ref.actor_addr().clone();
7636
7637 let cell = proc.get_instance(&actor_id).expect("actor must exist");
7639 assert_eq!(cell.queue_depth(), 0, "initial queue depth should be 0");
7640
7641 let (reply_tx, reply_rx) = oneshot::channel();
7644 let (gate_tx, gate_rx) = oneshot::channel::<()>();
7645 handle.wait(&client, reply_tx, gate_rx).await.unwrap();
7646
7647 reply_rx.await.unwrap();
7649
7650 let (reply2_tx, reply2_rx) = oneshot::channel();
7652 handle.reply(&client, reply2_tx).await.unwrap();
7653
7654 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
7656
7657 let depth = cell.queue_depth();
7659 assert!(
7660 depth >= 1,
7661 "expected queue depth >= 1 while actor is busy, got {depth}"
7662 );
7663
7664 let _ = gate_tx.send(());
7666
7667 reply2_rx.await.unwrap();
7669
7670 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
7672
7673 let depth = cell.queue_depth();
7675 assert_eq!(
7676 depth, 0,
7677 "queue depth should return to 0 after all messages handled"
7678 );
7679 }
7680
7681 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, typeuri::Named)]
7688 struct BufferTestMsg;
7689
7690 #[derive(Debug, Default)]
7691 #[hyperactor::export(handlers = [BufferTestMsg])]
7692 struct BufferTestActor;
7693
7694 #[async_trait]
7695 impl Actor for BufferTestActor {}
7696
7697 #[async_trait]
7698 impl Handler<BufferTestMsg> for BufferTestActor {
7699 async fn handle(
7700 &mut self,
7701 _cx: &crate::Context<Self>,
7702 _msg: BufferTestMsg,
7703 ) -> anyhow::Result<()> {
7704 Ok(())
7705 }
7706 }
7707
7708 #[async_timed_test(timeout_secs = 30)]
7723 async fn test_inbound_ordering_snapshot_callback_publishes_session() {
7724 let config = hyperactor_config::global::lock();
7730 let _g = config.override_key(config::ENABLE_DEST_ACTOR_REORDERING_BUFFER, true);
7731
7732 let proc = Proc::isolated();
7733 let client = proc.client("client");
7734 let handle = proc.spawn_with_label("a", BufferTestActor);
7735 let actor_id = handle.actor_addr().clone();
7736
7737 let _actor_ref: crate::ActorRef<BufferTestActor> = handle.bind();
7742
7743 let handler_port = actor_id.port_addr(Port::handler::<BufferTestMsg>());
7747 let _ = client.sequencer().assign_seq(&handler_port);
7752
7753 for _ in 0..3 {
7757 handle.post(&client, BufferTestMsg);
7758 }
7759 let cell = proc.get_instance(&actor_id).expect("actor exists");
7764 let snapshot = tokio::time::timeout(Duration::from_secs(5), async {
7765 loop {
7766 let snapshot = cell
7767 .inbound_ordering_snapshot()
7768 .expect("snapshot callback should be installed for live actors");
7769 if snapshot
7770 .sessions
7771 .first()
7772 .is_some_and(|session| session.buffered_count == 3)
7773 {
7774 break snapshot;
7775 }
7776 tokio::task::yield_now().await;
7777 }
7778 })
7779 .await
7780 .expect("receiver-local sequencing state should be populated");
7781
7782 assert!(snapshot.enabled, "buffering enabled by override");
7783 assert_eq!(snapshot.sessions.len(), 1, "one client session expected");
7784 let session = &snapshot.sessions[0];
7785 assert_eq!(session.expected_next_seq, 1);
7786 assert_eq!(session.buffered_count, 3);
7787 assert_eq!(session.oldest_buffered_seq, Some(2));
7788 assert_eq!(session.newest_buffered_seq, Some(4));
7789 assert_eq!(
7790 session.sender.as_ref(),
7791 Some(client.mailbox().actor_addr()),
7792 "session owner should be the posting client",
7793 );
7794
7795 let payload = crate::introspect::live_actor_payload(&cell);
7799 let attrs: hyperactor_config::Attrs =
7800 serde_json::from_str(&payload.attrs).expect("payload.attrs is well-formed JSON");
7801 let view = crate::introspect::ActorAttrsView::from_attrs(&attrs)
7802 .expect("attrs decode through ActorAttrsView");
7803 let view_snapshot = view
7804 .inbound_ordering
7805 .as_ref()
7806 .expect("INBOUND_ORDERING attr should be set by build_actor_attrs");
7807 assert_eq!(view_snapshot, &snapshot);
7808
7809 let _: u64 = cell.queue_depth();
7812 let _: u64 = view.queue_depth;
7813 }
7814
7815 #[async_timed_test(timeout_secs = 10)]
7819 async fn test_proc_queue_depth_aggregation_under_pressure() {
7820 let proc = Proc::isolated();
7821 let client = proc.client("client");
7822
7823 let h1 = proc.spawn_with_label("a1", TestActor);
7825 let h2 = proc.spawn_with_label("a2", TestActor);
7826
7827 let (reply1, rx1) = oneshot::channel();
7829 let (gate1, grx1) = oneshot::channel::<()>();
7830 h1.wait(&client, reply1, grx1).await.unwrap();
7831 rx1.await.unwrap();
7832
7833 let (reply2, rx2) = oneshot::channel();
7834 let (gate2, grx2) = oneshot::channel::<()>();
7835 h2.wait(&client, reply2, grx2).await.unwrap();
7836 rx2.await.unwrap();
7837
7838 h1.noop(&client).await.unwrap();
7840 h1.noop(&client).await.unwrap();
7841 h2.noop(&client).await.unwrap();
7842
7843 let aggregate = || -> (u64, u64) {
7846 let mut total: u64 = 0;
7847 let mut max: u64 = 0;
7848 for actor_id in proc.all_instance_keys() {
7849 if let Some(cell) = proc.get_instance_by_id(&actor_id) {
7850 let depth = cell.queue_depth();
7851 total = total.saturating_add(depth);
7852 max = max.max(depth);
7853 }
7854 }
7855 (total, max)
7856 };
7857
7858 let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
7861 loop {
7862 let (total, max) = aggregate();
7863 if total >= 3 {
7864 assert!(max >= 1, "expected max >= 1, got {max}");
7865 assert!(max <= total, "PD-1: max ({max}) <= total ({total})");
7866 break;
7867 }
7868 assert!(
7869 tokio::time::Instant::now() < deadline,
7870 "timed out waiting for queue depth >= 3, got {total}",
7871 );
7872 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
7873 }
7874
7875 let _ = gate1.send(());
7877 let _ = gate2.send(());
7878
7879 let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
7881 loop {
7882 let (total, _) = aggregate();
7883 if total == 0 {
7884 break;
7885 }
7886 assert!(
7887 tokio::time::Instant::now() < deadline,
7888 "timed out waiting for queue depth to return to 0, got {total}",
7889 );
7890 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
7891 }
7892 }
7893
7894 #[async_timed_test(timeout_secs = 5)]
7899 async fn test_retained_queue_stats_cold_start() {
7900 let proc = Proc::isolated();
7901 assert_eq!(proc.queue_depth_total(), 0);
7902 assert_eq!(proc.queue_depth_high_water_mark(), 0);
7903 assert_eq!(proc.last_nonzero_queue_depth_age_ms(), None);
7904 }
7905
7906 #[async_timed_test(timeout_secs = 10)]
7909 async fn test_retained_queue_stats_burst_then_drain() {
7910 let proc = Proc::isolated();
7911 let client = proc.client("client");
7912 let h = proc.spawn_with_label("ret_test", TestActor);
7913
7914 let (ready_tx, ready_rx) = oneshot::channel();
7916 let (gate_tx, gate_rx) = oneshot::channel::<()>();
7917 h.wait(&client, ready_tx, gate_rx).await.unwrap();
7918 ready_rx.await.unwrap();
7919
7920 h.noop(&client).await.unwrap();
7922 h.noop(&client).await.unwrap();
7923
7924 let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
7926 loop {
7927 let hwm = proc.queue_depth_high_water_mark();
7928 if hwm >= 2 {
7929 assert!(hwm >= proc.queue_depth_total());
7931 let age = proc.last_nonzero_queue_depth_age_ms();
7933 assert!(
7934 age.is_some(),
7935 "last-nonzero should be Some while pressure is active"
7936 );
7937 assert!(age.unwrap() < 2000, "last-nonzero age should be near zero");
7938 break;
7939 }
7940 assert!(
7941 tokio::time::Instant::now() < deadline,
7942 "timed out waiting for watermark >= 2",
7943 );
7944 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
7945 }
7946
7947 let _ = gate_tx.send(());
7949 let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
7950 loop {
7951 if proc.queue_depth_total() == 0 {
7952 break;
7953 }
7954 assert!(
7955 tokio::time::Instant::now() < deadline,
7956 "timed out waiting for total to drain",
7957 );
7958 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
7959 }
7960
7961 assert!(
7963 proc.queue_depth_high_water_mark() >= 2,
7964 "watermark should retain the peak after drain",
7965 );
7966
7967 let age = proc.last_nonzero_queue_depth_age_ms();
7969 assert!(age.is_some(), "last-nonzero should be Some after pressure");
7970 }
7971
7972 #[test]
7976 fn test_last_nonzero_refreshed_on_dequeue_deterministic() {
7977 use std::sync::atomic::AtomicU64;
7978
7979 static FAKE_NOW: AtomicU64 = AtomicU64::new(0);
7980 fn fake_clock() -> u64 {
7981 FAKE_NOW.load(Ordering::Relaxed)
7982 }
7983
7984 let stats = ProcQueueStats::with_clock(fake_clock);
7985 let depth = Arc::new(AtomicU64::new(0));
7986
7987 assert_eq!(stats.last_nonzero_age_ms(), None);
7989
7990 FAKE_NOW.store(1000, Ordering::Relaxed);
7992 account_enqueue(&depth, &stats, "a");
7993 account_enqueue(&depth, &stats, "a");
7994 assert_eq!(stats.running_total(), 2);
7995 assert_eq!(stats.high_water_mark(), 2);
7996
7997 FAKE_NOW.store(2000, Ordering::Relaxed);
7999 assert_eq!(stats.last_nonzero_age_ms(), Some(1000));
8000
8001 FAKE_NOW.store(3000, Ordering::Relaxed);
8004 account_dequeue(&depth, &stats, "a");
8005 assert_eq!(stats.running_total(), 1);
8006
8007 FAKE_NOW.store(4000, Ordering::Relaxed);
8011 assert_eq!(stats.last_nonzero_age_ms(), Some(1000));
8012
8013 FAKE_NOW.store(5000, Ordering::Relaxed);
8017 account_dequeue(&depth, &stats, "a");
8018 assert_eq!(stats.running_total(), 0);
8019
8020 FAKE_NOW.store(6000, Ordering::Relaxed);
8022 assert_eq!(stats.last_nonzero_age_ms(), Some(3000));
8023
8024 assert_eq!(stats.high_water_mark(), 2);
8026 }
8027
8028 #[test]
8034 fn test_account_cancel_enqueue_restores_counters() {
8035 let stats = ProcQueueStats::new();
8036 let depth = Arc::new(AtomicU64::new(0));
8037
8038 account_enqueue(&depth, &stats, "a");
8039 assert_eq!(stats.running_total(), 1);
8040 assert_eq!(depth.load(Ordering::Relaxed), 1);
8041
8042 account_cancel_enqueue(&depth, &stats, "a");
8043 assert_eq!(
8044 stats.running_total(),
8045 0,
8046 "cancel must restore running_total"
8047 );
8048 assert_eq!(
8049 depth.load(Ordering::Relaxed),
8050 0,
8051 "cancel must restore queue_depth"
8052 );
8053
8054 assert_eq!(stats.high_water_mark(), 1);
8056
8057 account_enqueue(&depth, &stats, "a");
8060 assert_eq!(stats.running_total(), 1);
8061 }
8062
8063 #[test]
8064 fn child_teardown_distinguishes_kill_from_failure() {
8065 let actor_addr = test_actor_id("proc", "actor");
8066
8067 let stopped = Ok(ActorStopped {
8068 reason: "test".to_string(),
8069 stop_mode: StopMode::DrainAndStop,
8070 });
8071 assert_eq!(
8072 ChildTeardown::from_run_result(&stopped),
8073 ChildTeardown::Cooperative(StopMode::DrainAndStop)
8074 );
8075
8076 let killed = Err(ActorError::new(
8077 &actor_addr,
8078 ActorErrorKind::Aborted("test kill".to_string()),
8079 ));
8080 assert_eq!(ChildTeardown::from_run_result(&killed), ChildTeardown::Kill);
8081
8082 let failed = Err(ActorError::new(
8083 &actor_addr,
8084 ActorErrorKind::Generic("test failure".to_string()),
8085 ));
8086 assert_eq!(
8087 ChildTeardown::from_run_result(&failed),
8088 ChildTeardown::Cooperative(StopMode::Stop)
8089 );
8090 }
8091}