1#![allow(dead_code)] use std::any::TypeId;
14use std::borrow::Cow;
15use std::fmt;
16use std::fmt::Debug;
17use std::future::Future;
18use std::future::IntoFuture;
19use std::ops::Deref;
20use std::pin::Pin;
21use std::sync::Arc;
22use std::time::SystemTime;
23
24use async_trait::async_trait;
25use enum_as_inner::EnumAsInner;
26use futures::FutureExt;
27use futures::future::BoxFuture;
28use hyperactor_config::Flattrs;
29use serde::Deserialize;
30use serde::Serialize;
31use tokio::sync::watch;
32use tokio::task::JoinHandle;
33use typeuri::Named;
34
35use crate as hyperactor; use crate::ActorAddr;
37use crate::ActorRef;
38use crate::Addr;
39#[cfg(test)]
40use crate::Client;
41use crate::Data;
42use crate::EndpointLocation;
43use crate::Message;
44use crate::RemoteMessage;
45use crate::context;
46use crate::endpoint::Endpoint;
47use crate::mailbox::DeliveryFailure;
48use crate::mailbox::DeliveryFailureKind;
49use crate::mailbox::ExpiredDelivery;
50use crate::mailbox::InvalidReference;
51use crate::mailbox::MailboxError;
52use crate::mailbox::MailboxSenderError;
53use crate::mailbox::MessageEnvelope;
54use crate::mailbox::PortHandle;
55use crate::mailbox::TransportFailureReason;
56use crate::mailbox::Undeliverable;
57use crate::mailbox::UndeliverableReason;
58use crate::proc::Context;
59use crate::proc::HandlerPorts;
60use crate::proc::Instance;
61use crate::proc::InstanceCell;
62use crate::proc::Proc;
63use crate::supervision::ActorSupervisionEvent;
64
65pub mod remote;
66
67#[derive(
69 Clone,
70 Copy,
71 Debug,
72 Serialize,
73 Deserialize,
74 PartialEq,
75 Eq,
76 typeuri::Named
77)]
78pub enum StopMode {
79 Stop,
81 DrainAndStop,
83}
84wirevalue::register_type!(StopMode);
85
86#[async_trait]
94pub trait Actor: Sized + Send + 'static {
95 async fn init(&mut self, _this: &Instance<Self>) -> Result<(), anyhow::Error> {
100 Ok(())
102 }
103
104 async fn handle_stop(
113 &mut self,
114 this: &Instance<Self>,
115 mode: StopMode,
116 reason: &str,
117 ) -> Result<(), anyhow::Error> {
118 handle_stop(this, mode, reason)
119 }
120
121 async fn cleanup(
132 &mut self,
133 _this: &Instance<Self>,
134 _err: Option<&ActorError>,
135 ) -> Result<(), anyhow::Error> {
136 Ok(())
138 }
139
140 #[hyperactor::instrument_infallible]
144 fn spawn_server_task<F>(future: F) -> JoinHandle<F::Output>
145 where
146 F: Future + Send + 'static,
147 F::Output: Send + 'static,
148 {
149 tokio::spawn(future)
150 }
151
152 async fn handle_supervision_event(
154 &mut self,
155 _this: &Instance<Self>,
156 event: &ActorSupervisionEvent,
157 ) -> Result<bool, anyhow::Error> {
158 Ok(!event.is_error())
161 }
162
163 async fn handle_delivery_failure_event(
165 &mut self,
166 cx: &Instance<Self>,
167 undeliverable: Undeliverable<MessageEnvelope>,
168 ) -> Result<(), anyhow::Error> {
169 handle_delivery_failure_event(self, cx, undeliverable).await
170 }
171
172 async fn handle_undeliverable_message(
174 &mut self,
175 cx: &Instance<Self>,
176 reason: UndeliverableReason,
177 undeliverable: Undeliverable<MessageEnvelope>,
178 ) -> Result<(), anyhow::Error> {
179 handle_undeliverable_message(cx, reason, undeliverable)
180 }
181
182 async fn handle_invalid_reference(
184 &mut self,
185 cx: &Instance<Self>,
186 invalid: InvalidReference,
187 undeliverable: Undeliverable<MessageEnvelope>,
188 ) -> Result<(), anyhow::Error> {
189 handle_invalid_reference(cx, invalid, undeliverable)
190 }
191
192 async fn handle_expired_delivery(
194 &mut self,
195 cx: &Instance<Self>,
196 expired: ExpiredDelivery,
197 undeliverable: Undeliverable<MessageEnvelope>,
198 ) -> Result<(), anyhow::Error> {
199 handle_expired_delivery(cx, expired, undeliverable)
200 }
201
202 fn display_name(&self) -> Option<String> {
206 None
207 }
208}
209
210pub async fn handle_delivery_failure_event<A: Actor>(
214 actor: &mut A,
215 cx: &Instance<A>,
216 undeliverable: Undeliverable<MessageEnvelope>,
217) -> Result<(), anyhow::Error> {
218 match undeliverable
219 .root_delivery_failure()
220 .map(|failure| failure.kind.clone())
221 {
222 Some(DeliveryFailureKind::InvalidReference(invalid)) => {
223 actor
224 .handle_invalid_reference(cx, invalid, undeliverable)
225 .await
226 }
227 Some(DeliveryFailureKind::Expired(expired)) => {
228 actor
229 .handle_expired_delivery(cx, expired, undeliverable)
230 .await
231 }
232 Some(DeliveryFailureKind::Undeliverable(reason)) => {
233 actor
234 .handle_undeliverable_message(cx, reason, undeliverable)
235 .await
236 }
237 None => anyhow::bail!(undeliverable.into_error()),
238 }
239}
240
241fn delivery_failure_event_target(undeliverable: &Undeliverable<MessageEnvelope>) -> Addr {
242 match undeliverable {
243 Undeliverable::Returned(envelope) => envelope.dest().clone().into(),
244 Undeliverable::Report(report) => match &report.dest {
245 EndpointLocation::Actor(actor) => actor.clone().into(),
246 EndpointLocation::Port(port) => port.clone().into(),
247 EndpointLocation::Local { actor, .. } => actor.clone().into(),
248 },
249 }
250}
251
252pub fn handle_undeliverable_message<A: Actor>(
256 _cx: &Instance<A>,
257 reason: UndeliverableReason,
258 undeliverable: Undeliverable<MessageEnvelope>,
259) -> Result<(), anyhow::Error> {
260 if undeliverable_reason_fails_actor(&reason) {
261 anyhow::bail!(undeliverable.into_error());
262 }
263 Ok(())
264}
265
266fn undeliverable_reason_fails_actor(reason: &UndeliverableReason) -> bool {
267 matches!(
268 reason,
269 UndeliverableReason::Transport(transport)
270 if matches!(
271 &transport.reason,
272 TransportFailureReason::OversizedFrame { .. }
273 )
274 )
275}
276
277pub fn handle_invalid_reference<A: Actor>(
281 _cx: &Instance<A>,
282 _invalid: InvalidReference,
283 undeliverable: Undeliverable<MessageEnvelope>,
284) -> Result<(), anyhow::Error> {
285 anyhow::bail!(undeliverable.into_error())
286}
287
288pub fn handle_expired_delivery<A: Actor>(
292 _cx: &Instance<A>,
293 _expired: ExpiredDelivery,
294 undeliverable: Undeliverable<MessageEnvelope>,
295) -> Result<(), anyhow::Error> {
296 anyhow::bail!(undeliverable.into_error())
297}
298
299pub fn handle_stop<A: Actor>(
303 this: &Instance<A>,
304 mode: StopMode,
305 reason: &str,
306) -> Result<(), anyhow::Error> {
307 this.close();
310 match mode {
311 StopMode::Stop => this.exit(reason).map_err(anyhow::Error::from),
312 StopMode::DrainAndStop => this.exit_after_drain(reason).map_err(anyhow::Error::from),
313 }
314}
315
316#[async_trait]
319impl Actor for () {}
320
321impl Referable for () {}
322
323impl Binds<()> for () {
324 fn bind(_ports: &HandlerPorts<Self>) {
325 }
327}
328
329#[async_trait]
331pub trait Handler<M>: Actor {
332 async fn handle(&mut self, cx: &Context<Self>, message: M) -> Result<(), anyhow::Error>;
334}
335
336#[async_trait]
346impl<A: Actor> Handler<crate::introspect::IntrospectMessage> for A {
347 async fn handle(
348 &mut self,
349 _cx: &Context<Self>,
350 _message: crate::introspect::IntrospectMessage,
351 ) -> Result<(), anyhow::Error> {
352 unimplemented!("introspect message handler should not be called directly")
353 }
354}
355
356#[cfg(test)]
357#[derive(Debug)]
358enum DeliveryFailurePolicy {
359 InvalidReference,
360 Expired,
361 Undeliverable,
362}
363
364#[cfg(test)]
365fn delivery_failure_policy(message: &Undeliverable<MessageEnvelope>) -> DeliveryFailurePolicy {
366 match message.root_delivery_failure().map(|failure| &failure.kind) {
367 Some(DeliveryFailureKind::InvalidReference(_)) => DeliveryFailurePolicy::InvalidReference,
368 Some(DeliveryFailureKind::Expired(_)) => DeliveryFailurePolicy::Expired,
369 Some(DeliveryFailureKind::Undeliverable(_)) | None => DeliveryFailurePolicy::Undeliverable,
370 }
371}
372
373struct DeliveryFailureLogFields {
374 sender: ActorAddr,
375 dest: EndpointLocation,
376 error: DeliveryFailureLogError,
377}
378
379impl DeliveryFailureLogFields {
380 fn new(message: &Undeliverable<MessageEnvelope>) -> Self {
381 match message {
382 Undeliverable::Returned(envelope) => Self {
383 sender: envelope.sender().clone(),
384 dest: EndpointLocation::Port(envelope.dest().clone()),
385 error: DeliveryFailureLogError::DeliveryFailures(
386 envelope.delivery_failures().to_vec(),
387 ),
388 },
389 Undeliverable::Report(report) => Self {
390 sender: report.sender.clone(),
391 dest: report.dest.clone(),
392 error: DeliveryFailureLogError::DeliveryFailures(report.delivery_failures.clone()),
393 },
394 }
395 }
396}
397
398enum DeliveryFailureLogError {
399 DeliveryFailures(Vec<DeliveryFailure>),
400}
401
402impl fmt::Display for DeliveryFailureLogError {
403 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404 match self {
405 Self::DeliveryFailures(failures) => {
406 if failures.is_empty() {
407 return write!(f, "<none>");
408 }
409 for (index, failure) in failures.iter().enumerate() {
410 if index > 0 {
411 write!(f, "; ")?;
412 }
413 write!(f, "{}", failure)?;
414 }
415 Ok(())
416 }
417 }
418 }
419}
420
421#[async_trait]
424impl<A: Actor> Handler<Undeliverable<MessageEnvelope>> for A {
425 async fn handle(
426 &mut self,
427 cx: &Context<Self>,
428 message: Undeliverable<MessageEnvelope>,
429 ) -> Result<(), anyhow::Error> {
430 let log_fields = (tracing::enabled!(tracing::Level::DEBUG)
431 || tracing::enabled!(tracing::Level::ERROR))
432 .then(|| DeliveryFailureLogFields::new(&message));
433 let result = self.handle_delivery_failure_event(cx, message).await;
434 match result {
435 Ok(_) => {
436 if let Some(log_fields) = log_fields {
437 tracing::debug!(
438 actor_id = %cx.self_addr(),
439 name = "undeliverable_message_handled",
440 sender = %log_fields.sender,
441 dest = %log_fields.dest,
442 error = %log_fields.error,
443 );
444 }
445 Ok(())
446 }
447 Err(e) => {
448 if let Some(log_fields) = log_fields {
449 tracing::error!(
450 actor_id = %cx.self_addr(),
451 name = "undeliverable_message",
452 sender = %log_fields.sender,
453 dest = %log_fields.dest,
454 error = %log_fields.error,
455 handler_error = %e,
456 );
457 } else {
458 tracing::error!(
459 actor_id = %cx.self_addr(),
460 name = "undeliverable_message",
461 handler_error = %e,
462 );
463 }
464 Err(e)
465 }
466 }
467 }
468}
469
470#[async_trait]
486pub trait RemoteSpawn: Actor + Referable + Binds<Self> {
487 type Params: RemoteMessage;
489
490 async fn new(params: Self::Params, environment: Flattrs) -> anyhow::Result<Self>;
494
495 fn gspawn_root_bind(
500 proc: &Proc,
501 uid: crate::id::Uid,
502 serialized_params: Data,
503 environment: Flattrs,
504 ) -> Pin<Box<dyn Future<Output = Result<ActorAddr, anyhow::Error>> + Send>> {
505 let proc = proc.clone();
506 Box::pin(async move {
507 let params =
508 bincode::serde::decode_from_slice(&serialized_params, bincode::config::legacy())
509 .map(|(v, _)| v)?;
510 let actor = Self::new(params, environment).await?;
511 let handle = proc.spawn_with_uid(uid, actor)?;
512 Ok(handle.bind::<Self>().into_actor_addr())
524 })
525 }
526
527 fn gspawn_child(
532 proc: &Proc,
533 parent: InstanceCell,
534 uid: crate::id::Uid,
535 serialized_params: Data,
536 environment: Flattrs,
537 ) -> Pin<Box<dyn Future<Output = Result<AnyActorHandle, anyhow::Error>> + Send>> {
538 let proc = proc.clone();
539 Box::pin(async move {
540 let params =
541 bincode::serde::decode_from_slice(&serialized_params, bincode::config::legacy())
542 .map(|(v, _)| v)?;
543 let actor = Self::new(params, environment).await?;
544 let handle = proc.spawn_child_with_uid(parent, uid, actor)?;
545 handle.bind::<Self>();
546 Ok(handle.into_any())
547 })
548 }
549
550 fn get_type_id() -> TypeId {
552 TypeId::of::<Self>()
553 }
554}
555
556#[async_trait]
559impl<A: Actor + Referable + Binds<Self> + Default> RemoteSpawn for A {
560 type Params = ();
561
562 async fn new(_params: Self::Params, _environment: Flattrs) -> anyhow::Result<Self> {
563 Ok(Default::default())
564 }
565}
566
567#[derive(Debug)]
570pub struct ActorError {
571 pub actor_id: Box<ActorAddr>,
573 pub kind: Box<ActorErrorKind>,
575}
576
577#[derive(thiserror::Error, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
579pub enum ActorErrorKind {
580 #[error("{0}")]
582 Generic(String),
583
584 #[error("{0} while handling {1}")]
586 ErrorDuringHandlingSupervision(String, Box<ActorSupervisionEvent>),
587
588 #[error("{0}")]
590 UnhandledSupervisionEvent(Box<ActorSupervisionEvent>),
591
592 #[error("actor explicitly aborted due to: {0}")]
594 Aborted(String),
595
596 #[error("signal channel closed")]
599 SignalChannelClosed,
600}
601
602impl ActorErrorKind {
603 pub fn processing(err: anyhow::Error) -> Self {
606 err.downcast::<ActorErrorKind>()
611 .unwrap_or_else(|err| Self::Generic(err.to_string()))
612 }
613
614 pub fn panic(err: anyhow::Error) -> Self {
616 Self::Generic(format!("panic: {}", err))
617 }
618
619 pub fn init(err: anyhow::Error) -> Self {
621 Self::Generic(format!("initialization error: {}", err))
622 }
623
624 pub fn cleanup(err: anyhow::Error) -> Self {
626 Self::Generic(format!("cleanup error: {}", err))
627 }
628
629 pub fn mailbox(err: MailboxError) -> Self {
631 Self::Generic(err.to_string())
632 }
633
634 pub fn mailbox_sender(err: MailboxSenderError) -> Self {
636 Self::Generic(err.to_string())
637 }
638
639 pub fn indeterminate_state() -> Self {
641 Self::Generic("actor is in an indeterminate state".to_string())
642 }
643}
644
645impl ActorError {
646 pub(crate) fn new(actor_id: &ActorAddr, kind: ActorErrorKind) -> Self {
648 Self {
649 actor_id: Box::new(actor_id.clone()),
650 kind: Box::new(kind),
651 }
652 }
653}
654
655impl fmt::Display for ActorError {
656 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
657 write!(f, "serving {}: ", self.actor_id)?;
658 fmt::Display::fmt(&self.kind, f)
659 }
660}
661
662impl std::error::Error for ActorError {
663 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
664 self.kind.source()
665 }
666}
667
668impl From<MailboxError> for ActorError {
669 fn from(inner: MailboxError) -> Self {
670 Self {
671 actor_id: Box::new(inner.actor_addr().clone()),
672 kind: Box::new(ActorErrorKind::mailbox(inner)),
673 }
674 }
675}
676
677impl From<MailboxSenderError> for ActorError {
678 fn from(inner: MailboxSenderError) -> Self {
679 Self {
680 actor_id: Box::new(inner.location().actor_addr()),
681 kind: Box::new(ActorErrorKind::mailbox_sender(inner)),
682 }
683 }
684}
685
686#[derive(Clone, Debug, Serialize, Deserialize, typeuri::Named)]
693pub enum Signal {
694 DrainAndStop(String),
696
697 Stop(String),
699
700 ExitRequested(String),
702
703 ChildStopped(crate::id::Uid),
705
706 Kill(String),
710}
711
712impl fmt::Display for Signal {
713 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
714 match self {
715 Signal::DrainAndStop(reason) => write!(f, "DrainAndStop({})", reason),
716 Signal::Stop(reason) => write!(f, "Stop({})", reason),
717 Signal::ExitRequested(reason) => write!(f, "ExitRequested({})", reason),
718 Signal::ChildStopped(uid) => write!(f, "ChildStopped({})", uid),
719 Signal::Kill(reason) => write!(f, "Kill({})", reason),
720 }
721 }
722}
723
724#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
729pub struct HandlerInfo {
730 pub typename: Cow<'static, str>,
732 pub arm: Option<Cow<'static, str>>,
734}
735
736impl HandlerInfo {
737 pub fn from_static(typename: &'static str, arm: Option<&'static str>) -> Self {
739 Self {
740 typename: Cow::Borrowed(typename),
741 arm: arm.map(Cow::Borrowed),
742 }
743 }
744
745 pub fn from_owned(typename: String, arm: Option<String>) -> Self {
747 Self {
748 typename: Cow::Owned(typename),
749 arm: arm.map(Cow::Owned),
750 }
751 }
752}
753
754impl fmt::Display for HandlerInfo {
755 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
756 match &self.arm {
757 Some(arm) => write!(f, "{}.{}", self.typename, arm),
758 None => write!(f, "{}", self.typename),
759 }
760 }
761}
762
763#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
765pub enum ActorStoppingReason {
766 Requested,
768 Zombie(String),
771}
772
773impl fmt::Display for ActorStoppingReason {
774 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
775 match self {
776 Self::Requested => write!(f, "requested"),
777 Self::Zombie(reason) => write!(f, "zombie: {}", reason),
778 }
779 }
780}
781
782#[derive(
784 Debug,
785 Serialize,
786 Deserialize,
787 PartialEq,
788 Eq,
789 Clone,
790 typeuri::Named,
791 EnumAsInner
792)]
793pub enum ActorStatus {
794 Unknown,
796 Created,
798 Initializing,
800 Client,
803 Idle,
805 Processing(SystemTime, Option<HandlerInfo>),
808 Stopping(ActorStoppingReason),
810 Stopped(String),
813 Failed(ActorErrorKind),
815}
816
817impl ActorStatus {
818 pub fn is_terminal(&self) -> bool {
820 self.is_stopped() || self.is_failed()
821 }
822
823 pub fn stopping() -> Self {
825 Self::Stopping(ActorStoppingReason::Requested)
826 }
827
828 pub fn zombie(reason: impl Into<String>) -> Self {
830 Self::Stopping(ActorStoppingReason::Zombie(reason.into()))
831 }
832
833 pub fn is_zombie(&self) -> bool {
835 matches!(self, Self::Stopping(ActorStoppingReason::Zombie(_)))
836 }
837
838 pub fn generic_failure(message: impl Into<String>) -> Self {
840 Self::Failed(ActorErrorKind::Generic(message.into()))
841 }
842
843 fn span_string(&self) -> &'static str {
844 self.arm().unwrap_or_default()
845 }
846}
847
848impl fmt::Display for ActorStatus {
849 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
850 match self {
851 Self::Unknown => write!(f, "unknown"),
852 Self::Created => write!(f, "created"),
853 Self::Initializing => write!(f, "initializing"),
854 Self::Client => write!(f, "client"),
855 Self::Idle => write!(f, "idle"),
856 Self::Processing(instant, None) => {
857 write!(
858 f,
859 "processing for {}ms",
860 std::time::SystemTime::now()
861 .duration_since(*instant)
862 .unwrap_or_default()
863 .as_millis()
864 )
865 }
866 Self::Processing(instant, Some(handler_info)) => {
867 write!(
868 f,
869 "{}: processing for {}ms",
870 handler_info,
871 std::time::SystemTime::now()
872 .duration_since(*instant)
873 .unwrap_or_default()
874 .as_millis()
875 )
876 }
877 Self::Stopping(ActorStoppingReason::Requested) => write!(f, "stopping"),
878 Self::Stopping(ActorStoppingReason::Zombie(reason)) => write!(f, "zombie: {}", reason),
879 Self::Stopped(reason) => write!(f, "stopped: {}", reason),
880 Self::Failed(err) => write!(f, "failed: {}", err),
881 }
882 }
883}
884
885pub struct ActorHandle<A: Actor> {
894 cell: InstanceCell,
895 ports: Arc<HandlerPorts<A>>,
896}
897
898impl<A: Actor> ActorHandle<A> {
900 pub(crate) fn new(cell: InstanceCell, ports: Arc<HandlerPorts<A>>) -> Self {
901 Self { cell, ports }
902 }
903
904 pub(crate) fn cell(&self) -> &InstanceCell {
907 &self.cell
908 }
909
910 pub fn actor_addr(&self) -> &ActorAddr {
912 self.cell.actor_addr()
913 }
914
915 pub fn drain_and_stop(&self, reason: &str) -> Result<(), ActorError> {
917 tracing::info!("ActorHandle::drain_and_stop called: {}", self.actor_addr());
918 self.cell.signal(Signal::DrainAndStop(reason.to_string()))
919 }
920
921 pub fn stop(&self, reason: &str) -> Result<(), ActorError> {
924 tracing::info!("actor handle stop called: {}", self.actor_addr());
925 self.cell.signal(Signal::Stop(reason.to_string()))
926 }
927
928 pub fn kill(&self, reason: &str) -> Result<(), ActorError> {
930 tracing::info!("actor handle kill called: {}", self.actor_addr());
931 self.cell.signal(Signal::Kill(reason.to_string()))
932 }
933
934 pub fn status(&self) -> watch::Receiver<ActorStatus> {
936 self.cell.status().clone()
937 }
938
939 pub fn port<M: Message>(&self) -> PortHandle<M>
941 where
942 A: Handler<M>,
943 {
944 self.ports.get()
945 }
946
947 pub fn try_post<C, M>(&self, cx: &C, message: M) -> Result<(), MailboxSenderError>
953 where
954 C: context::Actor,
955 M: Message,
956 A: Handler<M>,
957 {
958 self.ports.get::<M>().try_post(cx, message)
959 }
960
961 pub fn bind<R: Binds<A>>(&self) -> ActorRef<R> {
964 self.cell.bind(self.ports.as_ref())
965 }
966
967 pub fn into_any(self) -> AnyActorHandle {
969 AnyActorHandle { cell: self.cell }
970 }
971
972 pub fn into_guard(self) -> ActorGuard<A> {
977 ActorGuard { handle: Some(self) }
978 }
979}
980
981pub struct ActorGuard<A: Actor> {
983 handle: Option<ActorHandle<A>>,
984}
985
986impl<A: Actor> ActorGuard<A> {
987 pub fn into_inner(mut self) -> ActorHandle<A> {
989 self.handle
990 .take()
991 .expect("actor guard must contain a handle")
992 }
993}
994
995impl<A: Actor> Deref for ActorGuard<A> {
996 type Target = ActorHandle<A>;
997
998 fn deref(&self) -> &Self::Target {
999 self.handle
1000 .as_ref()
1001 .expect("actor guard must contain a handle")
1002 }
1003}
1004
1005impl<A: Actor> Drop for ActorGuard<A> {
1006 fn drop(&mut self) {
1007 if let Some(handle) = self.handle.take()
1008 && let Err(err) = handle.stop("actor guard dropped")
1009 {
1010 tracing::debug!(
1011 actor_id = %handle.actor_addr(),
1012 "actor guard failed to stop actor: {}",
1013 err
1014 );
1015 }
1016 }
1017}
1018
1019impl<A: Actor> Debug for ActorGuard<A> {
1020 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
1021 f.debug_struct("ActorGuard")
1022 .field(
1023 "actor_id",
1024 &self.handle.as_ref().map(|handle| handle.actor_addr()),
1025 )
1026 .finish()
1027 }
1028}
1029
1030pub struct AnyActorHandle {
1036 cell: InstanceCell,
1037}
1038
1039impl AnyActorHandle {
1040 pub fn actor_id(&self) -> &ActorAddr {
1042 self.cell.actor_addr()
1043 }
1044
1045 pub fn drain_and_stop(&self, reason: &str) -> Result<(), ActorError> {
1047 self.cell.signal(Signal::DrainAndStop(reason.to_string()))
1048 }
1049
1050 pub fn stop(&self, reason: &str) -> Result<(), ActorError> {
1052 self.cell.signal(Signal::Stop(reason.to_string()))
1053 }
1054
1055 pub fn kill(&self, reason: &str) -> Result<(), ActorError> {
1057 self.cell.signal(Signal::Kill(reason.to_string()))
1058 }
1059
1060 pub fn status(&self) -> watch::Receiver<ActorStatus> {
1062 self.cell.status().clone()
1063 }
1064
1065 pub fn downcast<A: Actor>(&self) -> Option<ActorHandle<A>> {
1067 self.cell.downcast_handle()
1068 }
1069
1070 pub fn into_guard(self) -> AnyActorGuard {
1075 AnyActorGuard { handle: Some(self) }
1076 }
1077}
1078
1079pub struct AnyActorGuard {
1081 handle: Option<AnyActorHandle>,
1082}
1083
1084impl AnyActorGuard {
1085 pub fn into_inner(mut self) -> AnyActorHandle {
1087 self.handle
1088 .take()
1089 .expect("actor guard must contain a handle")
1090 }
1091}
1092
1093impl Deref for AnyActorGuard {
1094 type Target = AnyActorHandle;
1095
1096 fn deref(&self) -> &Self::Target {
1097 self.handle
1098 .as_ref()
1099 .expect("actor guard must contain a handle")
1100 }
1101}
1102
1103impl Drop for AnyActorGuard {
1104 fn drop(&mut self) {
1105 if let Some(handle) = self.handle.take()
1106 && let Err(err) = handle.stop("actor guard dropped")
1107 {
1108 tracing::debug!(
1109 actor_id = %handle.actor_id(),
1110 "actor guard failed to stop actor: {}",
1111 err
1112 );
1113 }
1114 }
1115}
1116
1117impl Debug for AnyActorGuard {
1118 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
1119 f.debug_struct("AnyActorGuard")
1120 .field(
1121 "actor_id",
1122 &self.handle.as_ref().map(|handle| handle.actor_id()),
1123 )
1124 .finish()
1125 }
1126}
1127
1128impl IntoFuture for AnyActorHandle {
1132 type Output = ActorStatus;
1133 type IntoFuture = BoxFuture<'static, Self::Output>;
1134
1135 fn into_future(self) -> Self::IntoFuture {
1136 let future = async move {
1137 let mut status_receiver = self.cell.status().clone();
1138 let result = status_receiver.wait_for(ActorStatus::is_terminal).await;
1139 match result {
1140 Err(_) => ActorStatus::Unknown,
1141 Ok(status) => status.clone(),
1142 }
1143 };
1144
1145 future.boxed()
1146 }
1147}
1148
1149impl Debug for AnyActorHandle {
1150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
1151 f.debug_struct("AnyActorHandle")
1152 .field("cell", &"..")
1153 .finish()
1154 }
1155}
1156
1157impl Clone for AnyActorHandle {
1158 fn clone(&self) -> Self {
1159 Self {
1160 cell: self.cell.clone(),
1161 }
1162 }
1163}
1164
1165impl<A: Actor> IntoFuture for ActorHandle<A> {
1169 type Output = ActorStatus;
1170 type IntoFuture = BoxFuture<'static, Self::Output>;
1171
1172 fn into_future(self) -> Self::IntoFuture {
1173 let future = async move {
1174 let mut status_receiver = self.cell.status().clone();
1175 let result = status_receiver.wait_for(ActorStatus::is_terminal).await;
1176 match result {
1177 Err(_) => ActorStatus::Unknown,
1178 Ok(status) => status.clone(),
1179 }
1180 };
1181
1182 future.boxed()
1183 }
1184}
1185
1186impl<A: Actor> Debug for ActorHandle<A> {
1187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
1188 f.debug_struct("ActorHandle").field("cell", &"..").finish()
1189 }
1190}
1191
1192impl<A, M> Endpoint<M> for &ActorHandle<A>
1193where
1194 A: Actor + Handler<M>,
1195 M: Message,
1196{
1197 fn endpoint_location(&self) -> EndpointLocation {
1198 EndpointLocation::Actor(self.actor_addr().clone())
1199 }
1200
1201 fn post<C>(self, cx: &C, message: M)
1202 where
1203 C: context::Actor,
1204 {
1205 Endpoint::post(&self.ports.get(), cx, message)
1206 }
1207}
1208
1209impl<A: Actor> Clone for ActorHandle<A> {
1210 fn clone(&self) -> Self {
1211 Self {
1212 cell: self.cell.clone(),
1213 ports: self.ports.clone(),
1214 }
1215 }
1216}
1217
1218pub trait Referable: Named {}
1233
1234pub trait Binds<A: Actor>: Referable {
1237 fn bind(ports: &HandlerPorts<A>);
1239}
1240
1241pub trait RemoteHandles<M: RemoteMessage>: Referable {}
1244
1245#[macro_export]
1276macro_rules! assert_behaves {
1277 ($ty:ty as $behavior:ty) => {
1278 const _: fn() = || {
1279 fn check<B: hyperactor::actor::Binds<$ty>>() {}
1280 check::<$behavior>();
1281 };
1282 };
1283}
1284
1285#[cfg(test)]
1286mod tests {
1287 use std::assert_matches;
1288 use std::sync::Mutex;
1289 use std::time::Duration;
1290
1291 use rand::seq::SliceRandom;
1292 use timed_test::async_timed_test;
1293 use tokio::sync::mpsc;
1294 use tokio::time::timeout;
1295
1296 use super::*;
1297 use crate as hyperactor;
1298 use crate::Actor;
1299 use crate::ActorRef;
1300 use crate::Addr;
1301 use crate::EndpointLocation;
1302 use crate::OncePortHandle;
1303 use crate::PortAddr;
1304 use crate::PortRef;
1305 use crate::config;
1306 use crate::context::Mailbox as _;
1307 use crate::introspect::IntrospectMessage;
1308 use crate::introspect::IntrospectResult;
1309 use crate::introspect::IntrospectView;
1310 use crate::mailbox::BoxableMailboxSender as _;
1311 use crate::mailbox::DeliveryFailure;
1312 use crate::mailbox::DeliveryFailureReport;
1313 use crate::mailbox::ExpiredDelivery;
1314 use crate::mailbox::InvalidReference;
1315 use crate::mailbox::InvalidReferenceReason;
1316 use crate::mailbox::MailboxSender;
1317 use crate::mailbox::PortGone;
1318 use crate::mailbox::PortLocation;
1319 use crate::mailbox::TransportFailure;
1320 use crate::mailbox::TransportFailureReason;
1321 use crate::mailbox::UndeliverableReason;
1322 use crate::mailbox::monitored_return_handle;
1323 use crate::ordering::SEQ_INFO;
1324 use crate::ordering::SeqInfo;
1325 use crate::port::Port;
1326 use crate::testing::ids::test_proc_id;
1327 use crate::testing::pingpong::PingPongActor;
1328 use crate::testing::pingpong::PingPongMessage;
1329 use crate::testing::proc_supervison::ProcSupervisionCoordinator; #[derive(Debug)]
1332 struct EchoActor(PortRef<u64>);
1333
1334 #[async_trait]
1335 impl Actor for EchoActor {}
1336
1337 #[async_trait]
1338 impl Handler<u64> for EchoActor {
1339 async fn handle(&mut self, cx: &Context<Self>, message: u64) -> Result<(), anyhow::Error> {
1340 let Self(port) = self;
1341 port.post(cx, message);
1342 Ok(())
1343 }
1344 }
1345
1346 #[derive(Debug)]
1347 struct DeliveryPolicyActor(PortRef<()>);
1348
1349 #[async_trait]
1350 impl Actor for DeliveryPolicyActor {}
1351
1352 #[async_trait]
1353 impl Handler<()> for DeliveryPolicyActor {
1354 async fn handle(&mut self, cx: &Context<Self>, _message: ()) -> Result<(), anyhow::Error> {
1355 self.0.post(cx, ());
1356 Ok(())
1357 }
1358 }
1359
1360 fn delivery_policy_envelope(
1361 sender: &ActorAddr,
1362 dest: PortAddr,
1363 failure: DeliveryFailure,
1364 ) -> MessageEnvelope {
1365 let mut envelope =
1366 MessageEnvelope::serialize(sender.clone(), dest, &(), Flattrs::new()).unwrap();
1367 envelope.push_delivery_failure(failure);
1368 envelope
1369 }
1370
1371 fn delivery_policy_report(
1372 sender: ActorAddr,
1373 dest: PortAddr,
1374 failure: DeliveryFailure,
1375 ) -> DeliveryFailureReport {
1376 DeliveryFailureReport::new(
1377 sender,
1378 EndpointLocation::Port(dest),
1379 Some("()".to_string()),
1380 failure,
1381 )
1382 }
1383
1384 async fn assert_delivery_policy_actor_remains_live(
1385 make_undeliverable: impl FnOnce(&ActorAddr, PortAddr) -> Undeliverable<MessageEnvelope>,
1386 ) {
1387 let proc = Proc::isolated();
1388 let client = proc.client("client");
1389 let (sync_port, mut sync_rx) = client.open_port::<()>();
1390 let actor = DeliveryPolicyActor(sync_port.bind());
1391 let handle = proc.spawn_with_label("delivery_policy", actor);
1392 let dest = handle.actor_addr().port_addr(Port::from(1234));
1393
1394 handle.post(
1395 &client,
1396 make_undeliverable(handle.actor_addr(), dest.clone()),
1397 );
1398 handle.post(&client, ());
1399
1400 tokio::time::timeout(Duration::from_secs(1), sync_rx.recv())
1401 .await
1402 .expect("actor should remain live")
1403 .expect("sync port should receive response");
1404 handle.drain_and_stop("test").unwrap();
1405 assert_matches!(handle.await, ActorStatus::Stopped(reason) if reason == "test");
1406 }
1407
1408 async fn assert_delivery_policy_actor_fails(
1409 make_undeliverable: impl FnOnce(&ActorAddr, PortAddr) -> Undeliverable<MessageEnvelope>,
1410 ) {
1411 let proc = Proc::isolated();
1412 let (_reported, _coordinator) = ProcSupervisionCoordinator::set(&proc).await.unwrap();
1413 let client = proc.client("client");
1414 let (sync_port, _sync_rx) = client.open_port::<()>();
1415 let actor = DeliveryPolicyActor(sync_port.bind());
1416 let handle = proc.spawn_with_label("delivery_policy", actor);
1417 let dest = handle.actor_addr().port_addr(Port::from(1234));
1418
1419 handle.post(
1420 &client,
1421 make_undeliverable(handle.actor_addr(), dest.clone()),
1422 );
1423
1424 assert_matches!(handle.await, ActorStatus::Failed(_));
1425 }
1426
1427 #[tokio::test]
1428 async fn test_default_transport_undeliverable_policy_does_not_fail_actor() {
1429 let target = Addr::Proc(test_proc_id("target"));
1430 let failure = DeliveryFailure::new(UndeliverableReason::Transport(TransportFailure::new(
1431 target,
1432 TransportFailureReason::NoRoute,
1433 )));
1434 assert_delivery_policy_actor_remains_live(|sender, dest| {
1435 Undeliverable::Returned(delivery_policy_envelope(sender, dest, failure))
1436 })
1437 .await;
1438 }
1439
1440 #[tokio::test]
1441 async fn test_default_transport_report_policy_does_not_fail_actor() {
1442 let target = Addr::Proc(test_proc_id("target"));
1443 let failure = DeliveryFailure::new(UndeliverableReason::Transport(TransportFailure::new(
1444 target,
1445 TransportFailureReason::NoRoute,
1446 )));
1447 assert_delivery_policy_actor_remains_live(|sender, dest| {
1448 Undeliverable::Report(delivery_policy_report(sender.clone(), dest, failure))
1449 })
1450 .await;
1451 }
1452
1453 #[tokio::test]
1454 async fn test_default_oversized_frame_transport_policy_fails_actor() {
1455 let target = Addr::Proc(test_proc_id("target"));
1456 let failure = DeliveryFailure::new(UndeliverableReason::Transport(TransportFailure::new(
1457 target,
1458 TransportFailureReason::OversizedFrame {
1459 len: 55001392,
1460 max: 50000000,
1461 },
1462 )));
1463 assert_delivery_policy_actor_fails(|sender, dest| {
1464 Undeliverable::Returned(delivery_policy_envelope(sender, dest, failure))
1465 })
1466 .await;
1467 }
1468
1469 #[tokio::test]
1470 async fn test_default_oversized_frame_report_policy_fails_actor() {
1471 let target = Addr::Proc(test_proc_id("target"));
1472 let failure = DeliveryFailure::new(UndeliverableReason::Transport(TransportFailure::new(
1473 target,
1474 TransportFailureReason::OversizedFrame {
1475 len: 55001392,
1476 max: 50000000,
1477 },
1478 )));
1479 assert_delivery_policy_actor_fails(|sender, dest| {
1480 Undeliverable::Report(delivery_policy_report(sender.clone(), dest, failure))
1481 })
1482 .await;
1483 }
1484
1485 #[tokio::test]
1486 async fn test_default_port_gone_policy_does_not_fail_actor() {
1487 let port = test_proc_id("target")
1488 .actor_addr("actor")
1489 .port_addr(Port::from(1234));
1490 let failure = DeliveryFailure::new(UndeliverableReason::PortGone(PortGone::new(
1491 port,
1492 Some("()".to_string()),
1493 )));
1494 assert_delivery_policy_actor_remains_live(|sender, dest| {
1495 Undeliverable::Returned(delivery_policy_envelope(sender, dest, failure))
1496 })
1497 .await;
1498 }
1499
1500 #[tokio::test]
1501 async fn test_default_invalid_reference_policy_fails_actor() {
1502 let target = test_proc_id("target").actor_addr("actor");
1503 let failure = DeliveryFailure::new(InvalidReference::new(
1504 target,
1505 InvalidReferenceReason::ActorNotExist,
1506 ));
1507 assert_delivery_policy_actor_fails(|sender, dest| {
1508 Undeliverable::Returned(delivery_policy_envelope(sender, dest, failure))
1509 })
1510 .await;
1511 }
1512
1513 #[tokio::test]
1514 async fn test_default_invalid_reference_policy_allows_return_handle_sender_mismatch() {
1515 let target = test_proc_id("target").actor_addr("actor");
1516 let failure = DeliveryFailure::new(InvalidReference::new(
1517 target,
1518 InvalidReferenceReason::ActorNotExist,
1519 ));
1520 assert_delivery_policy_actor_fails(|_actor, dest| {
1521 Undeliverable::Returned(delivery_policy_envelope(
1522 &test_proc_id("sender").actor_addr("actor"),
1523 dest,
1524 failure,
1525 ))
1526 })
1527 .await;
1528 }
1529
1530 #[tokio::test]
1531 async fn test_default_expired_delivery_policy_fails_actor() {
1532 let port = test_proc_id("target")
1533 .actor_addr("actor")
1534 .port_addr(Port::from(1234));
1535 let failure = DeliveryFailure::new(ExpiredDelivery::new(port));
1536 assert_delivery_policy_actor_fails(|sender, dest| {
1537 Undeliverable::Returned(delivery_policy_envelope(sender, dest, failure))
1538 })
1539 .await;
1540 }
1541
1542 #[test]
1543 fn test_delivery_failure_policy_ignores_attrs() {
1544 hyperactor_config::attrs::declare_attrs! {
1545 attr TEST_DELIVERY_FAILURE_ATTR: String;
1546 }
1547
1548 let sender = test_proc_id("sender").actor_addr("actor");
1549 let dest = sender.port_addr(Port::from(1234));
1550 let mut attrs = Flattrs::new();
1551 attrs.set(TEST_DELIVERY_FAILURE_ATTR, "context".to_string());
1552
1553 let transport = delivery_policy_envelope(
1554 &sender,
1555 dest.clone(),
1556 DeliveryFailure::with_attrs(
1557 UndeliverableReason::Transport(TransportFailure::new(
1558 dest.clone(),
1559 TransportFailureReason::NoRoute,
1560 )),
1561 attrs.clone(),
1562 ),
1563 );
1564 assert_matches!(
1565 delivery_failure_policy(&Undeliverable::Returned(transport)),
1566 DeliveryFailurePolicy::Undeliverable
1567 );
1568
1569 let invalid_reference = delivery_policy_envelope(
1570 &sender,
1571 dest.clone(),
1572 DeliveryFailure::with_attrs(
1573 InvalidReference::new(dest.clone(), InvalidReferenceReason::PortNeverAllocated),
1574 attrs.clone(),
1575 ),
1576 );
1577 assert_matches!(
1578 delivery_failure_policy(&Undeliverable::Returned(invalid_reference)),
1579 DeliveryFailurePolicy::InvalidReference
1580 );
1581
1582 let expired = delivery_policy_envelope(
1583 &sender,
1584 dest.clone(),
1585 DeliveryFailure::with_attrs(ExpiredDelivery::new(dest), attrs),
1586 );
1587 assert_matches!(
1588 delivery_failure_policy(&Undeliverable::Returned(expired)),
1589 DeliveryFailurePolicy::Expired
1590 );
1591 }
1592
1593 #[test]
1594 fn test_delivery_failure_policy_uses_report_root_failure() {
1595 let sender = test_proc_id("sender").actor_addr("actor");
1596 let dest = sender.port_addr(Port::from(1234));
1597 let report = DeliveryFailureReport::new(
1598 sender,
1599 EndpointLocation::Port(dest.clone()),
1600 Some("()".to_string()),
1601 DeliveryFailure::new(ExpiredDelivery::new(dest)),
1602 );
1603
1604 assert_matches!(
1605 delivery_failure_policy(&Undeliverable::Report(report)),
1606 DeliveryFailurePolicy::Expired
1607 );
1608 }
1609
1610 #[tokio::test]
1611 async fn test_server_basic() {
1612 let proc = Proc::isolated();
1613 let client = proc.client("client");
1614 let (tx, mut rx) = client.open_port();
1615 let actor = EchoActor(tx.bind());
1616 let handle = proc.spawn(actor);
1617 handle.post(&client, 123u64);
1618 handle.drain_and_stop("test").unwrap();
1619 handle.await;
1620
1621 assert_eq!(rx.drain(), vec![123u64]);
1622 }
1623
1624 #[tokio::test]
1625 async fn test_actor_handle_guard_stops_actor_on_drop() {
1626 let proc = Proc::isolated();
1627 let handle = proc.spawn(());
1628 let mut status = handle.status();
1629
1630 {
1631 let _guard = handle.into_guard();
1632 }
1633
1634 let stopped = timeout(
1635 Duration::from_secs(5),
1636 status.wait_for(|status| {
1637 matches!(status, ActorStatus::Stopped(reason) if reason == "actor guard dropped")
1638 }),
1639 )
1640 .await
1641 .unwrap()
1642 .unwrap()
1643 .clone();
1644
1645 match stopped {
1646 ActorStatus::Stopped(reason) => assert_eq!(reason, "actor guard dropped"),
1647 status => panic!("actor guard should stop actor, got {status}"),
1648 }
1649 }
1650
1651 #[tokio::test]
1652 async fn test_actor_handle_guard_into_inner_disarms_stop() {
1653 let proc = Proc::isolated();
1654 let handle = proc.spawn(());
1655 let mut status = handle.status();
1656
1657 let guard = handle.into_guard();
1658 let _ = guard.status();
1659 let guarded_handle = guard.into_inner();
1660 let result = timeout(
1661 Duration::from_millis(100),
1662 status.wait_for(ActorStatus::is_terminal),
1663 )
1664 .await;
1665 assert!(result.is_err());
1666
1667 guarded_handle.drain_and_stop("test").unwrap();
1668 guarded_handle.await;
1669 }
1670
1671 #[tokio::test]
1672 async fn test_ping_pong() {
1673 let proc = Proc::isolated();
1674 let client = proc.client("client");
1675 let (undeliverable_msg_tx, _) = client.open_port();
1676
1677 let ping_actor = PingPongActor::new(Some(undeliverable_msg_tx.bind()), None, None);
1678 let pong_actor = PingPongActor::new(Some(undeliverable_msg_tx.bind()), None, None);
1679 let ping_handle = proc.spawn_with_label::<PingPongActor>("ping", ping_actor);
1680 let pong_handle = proc.spawn_with_label::<PingPongActor>("pong", pong_actor);
1681
1682 let (local_port, local_receiver) = client.open_once_port();
1683
1684 ping_handle.post(
1685 &client,
1686 PingPongMessage(10, pong_handle.bind(), local_port.bind()),
1687 );
1688
1689 assert!(local_receiver.recv().await.unwrap());
1690 }
1691
1692 #[tokio::test]
1693 async fn test_ping_pong_on_handler_error() {
1694 let proc = Proc::isolated();
1695 let client = proc.client("client");
1696 let (undeliverable_msg_tx, _) = client.open_port();
1697
1698 let (_reported, _coordinator) = ProcSupervisionCoordinator::set(&proc).await.unwrap();
1701
1702 let error_ttl = 66;
1703
1704 let ping_actor =
1705 PingPongActor::new(Some(undeliverable_msg_tx.bind()), Some(error_ttl), None);
1706 let pong_actor =
1707 PingPongActor::new(Some(undeliverable_msg_tx.bind()), Some(error_ttl), None);
1708 let ping_handle = proc.spawn_with_label::<PingPongActor>("ping", ping_actor);
1709 let pong_handle = proc.spawn_with_label::<PingPongActor>("pong", pong_actor);
1710
1711 let (local_port, local_receiver) = client.open_once_port();
1712
1713 ping_handle.post(
1714 &client,
1715 PingPongMessage(
1716 error_ttl + 1, pong_handle.bind(),
1718 local_port.bind(),
1719 ),
1720 );
1721
1722 let res: Result<Result<bool, MailboxError>, tokio::time::error::Elapsed> =
1724 timeout(Duration::from_secs(5), local_receiver.recv()).await;
1725 assert!(res.is_err());
1726 }
1727
1728 #[derive(Debug)]
1729 struct InitActor(bool);
1730
1731 #[async_trait]
1732 impl Actor for InitActor {
1733 async fn init(&mut self, _this: &Instance<Self>) -> Result<(), anyhow::Error> {
1734 self.0 = true;
1735 Ok(())
1736 }
1737 }
1738
1739 #[async_trait]
1740 impl Handler<OncePortHandle<bool>> for InitActor {
1741 async fn handle(
1742 &mut self,
1743 cx: &Context<Self>,
1744 port: OncePortHandle<bool>,
1745 ) -> Result<(), anyhow::Error> {
1746 port.post(cx, self.0);
1747 Ok(())
1748 }
1749 }
1750
1751 #[tokio::test]
1752 async fn test_init() {
1753 let proc = Proc::isolated();
1754 let actor = InitActor(false);
1755 let handle = proc.spawn(actor);
1756 let client = proc.client("client");
1757
1758 let (port, receiver) = client.open_once_port();
1759 handle.post(&client, port);
1760 assert!(receiver.recv().await.unwrap());
1761
1762 handle.drain_and_stop("test").unwrap();
1763 handle.await;
1764 }
1765
1766 type MultiValues = Arc<Mutex<(u64, String)>>;
1767
1768 struct MultiValuesTest {
1769 proc: Proc,
1770 values: MultiValues,
1771 handle: ActorHandle<MultiActor>,
1772 client: Client,
1773 }
1774
1775 impl MultiValuesTest {
1776 async fn new() -> Self {
1777 let proc = Proc::isolated();
1778 let values: MultiValues = Arc::new(Mutex::new((0, "".to_string())));
1779 let actor = MultiActor(values.clone());
1780 let handle = proc.spawn(actor);
1781 let client = proc.client("client");
1782 Self {
1783 proc,
1784 values,
1785 handle,
1786 client,
1787 }
1788 }
1789
1790 fn send<M>(&self, message: M)
1791 where
1792 M: RemoteMessage,
1793 MultiActor: Handler<M>,
1794 {
1795 self.handle.post(&self.client, message)
1796 }
1797
1798 async fn sync(&self) {
1799 let (port, done) = self.client.open_once_port::<bool>();
1800 self.handle.post(&self.client, port);
1801 assert!(done.recv().await.unwrap());
1802 }
1803
1804 fn get_values(&self) -> (u64, String) {
1805 self.values.lock().unwrap().clone()
1806 }
1807 }
1808
1809 #[derive(Debug)]
1810 #[hyperactor::export(handlers = [u64, String])]
1811 struct MultiActor(MultiValues);
1812
1813 #[async_trait]
1814 impl Actor for MultiActor {}
1815
1816 #[async_trait]
1817 impl Handler<u64> for MultiActor {
1818 async fn handle(&mut self, _cx: &Context<Self>, message: u64) -> Result<(), anyhow::Error> {
1819 let mut vals = self.0.lock().unwrap();
1820 vals.0 = message;
1821 Ok(())
1822 }
1823 }
1824
1825 #[async_trait]
1826 impl Handler<String> for MultiActor {
1827 async fn handle(
1828 &mut self,
1829 _cx: &Context<Self>,
1830 message: String,
1831 ) -> Result<(), anyhow::Error> {
1832 let mut vals = self.0.lock().unwrap();
1833 vals.1 = message;
1834 Ok(())
1835 }
1836 }
1837
1838 #[async_trait]
1839 impl Handler<OncePortHandle<bool>> for MultiActor {
1840 async fn handle(
1841 &mut self,
1842 cx: &Context<Self>,
1843 message: OncePortHandle<bool>,
1844 ) -> Result<(), anyhow::Error> {
1845 message.post(cx, true);
1846 Ok(())
1847 }
1848 }
1849
1850 #[tokio::test]
1851 async fn test_multi_handler_refs() {
1852 let test = MultiValuesTest::new().await;
1853
1854 test.send(123u64);
1855 test.send("foo".to_string());
1856 test.sync().await;
1857 assert_eq!(test.get_values(), (123u64, "foo".to_string()));
1858
1859 let myref: ActorRef<MultiActor> = test.handle.bind();
1860
1861 myref.port().post(&test.client, 321u64);
1862 test.sync().await;
1863 assert_eq!(test.get_values(), (321u64, "foo".to_string()));
1864
1865 myref.port().post(&test.client, "bar".to_string());
1866 test.sync().await;
1867 assert_eq!(test.get_values(), (321u64, "bar".to_string()));
1868 }
1869
1870 #[tokio::test]
1871 async fn test_ref_behavior() {
1872 let test = MultiValuesTest::new().await;
1873
1874 test.send(123u64);
1875 test.send("foo".to_string());
1876
1877 hyperactor::behavior!(MyActorBehavior, u64, String);
1878
1879 let myref: ActorRef<MyActorBehavior> = test.handle.bind();
1880 myref.port().post(&test.client, "biz".to_string());
1881 myref.port().post(&test.client, 999u64);
1882
1883 test.sync().await;
1884 assert_eq!(test.get_values(), (999u64, "biz".to_string()));
1885 }
1886
1887 #[tokio::test]
1888 async fn test_actor_handle_downcast() {
1889 #[derive(Debug, Default)]
1890 struct NothingActor;
1891
1892 impl Actor for NothingActor {}
1893
1894 let proc = Proc::isolated();
1897 let handle = proc.spawn(NothingActor);
1898 let cell = handle.cell();
1899
1900 assert!(cell.downcast_handle::<EchoActor>().is_none());
1902
1903 let handle = cell.downcast_handle::<NothingActor>().unwrap();
1904 handle.drain_and_stop("test").unwrap();
1905 handle.await;
1906 }
1907
1908 #[derive(Debug)]
1910 #[hyperactor::export(handlers = [String, Callback])]
1911 struct GetSeqActor(PortRef<(String, SeqInfo)>);
1912
1913 #[async_trait]
1914 impl Actor for GetSeqActor {}
1915
1916 #[async_trait]
1917 impl Handler<String> for GetSeqActor {
1918 async fn handle(
1919 &mut self,
1920 cx: &Context<Self>,
1921 message: String,
1922 ) -> Result<(), anyhow::Error> {
1923 let Self(port) = self;
1924 let seq_info = cx.headers().get(SEQ_INFO).unwrap();
1925 port.post(cx, (message, seq_info.clone()));
1926 Ok(())
1927 }
1928 }
1929
1930 #[derive(Clone, Debug, Serialize, Deserialize, Named)]
1935 struct Callback(PortRef<PortRef<String>>);
1936
1937 #[async_trait]
1938 impl Handler<Callback> for GetSeqActor {
1939 async fn handle(
1940 &mut self,
1941 cx: &Context<Self>,
1942 message: Callback,
1943 ) -> Result<(), anyhow::Error> {
1944 let (handle, mut receiver) = cx.open_port::<String>();
1945 let callback_ref = handle.bind();
1946 message.0.post(cx, callback_ref);
1947 let msg = receiver.recv().await.unwrap();
1948 self.handle(cx, msg).await
1949 }
1950 }
1951
1952 #[async_timed_test(timeout_secs = 30)]
1953 async fn test_sequencing_actor_handle_basic() {
1954 let proc = Proc::isolated();
1955 let client = proc.client("client");
1956 let (tx, mut rx) = client.open_port();
1957
1958 let actor_handle = proc.spawn(GetSeqActor(tx.bind()));
1959
1960 actor_handle.post(&client, "unbound".to_string());
1962 assert_eq!(
1963 rx.recv().await.unwrap(),
1964 ("unbound".to_string(), SeqInfo::Direct)
1965 );
1966
1967 let actor_ref: ActorRef<GetSeqActor> = actor_handle.bind();
1968
1969 let session_id = client.sequencer().session_id();
1970 let mut expected_seq = 0;
1971 for m in 0..10 {
1973 actor_handle.post(&client, format!("{m}"));
1974 expected_seq += 1;
1975 assert_eq!(
1976 rx.recv().await.unwrap(),
1977 (
1978 format!("{m}"),
1979 SeqInfo::Session {
1980 session_id,
1981 seq: expected_seq,
1982 }
1983 )
1984 );
1985
1986 for n in 0..2 {
1987 actor_ref.port().post(&client, format!("{m}-{n}"));
1988 expected_seq += 1;
1989 assert_eq!(
1990 rx.recv().await.unwrap(),
1991 (
1992 format!("{m}-{n}"),
1993 SeqInfo::Session {
1994 session_id,
1995 seq: expected_seq,
1996 }
1997 )
1998 );
1999 }
2000 }
2001 }
2002
2003 #[async_timed_test(timeout_secs = 30)]
2005 async fn test_sequencing_mixed_handler_and_non_handler_ports() {
2006 let proc = Proc::isolated();
2007 let client = proc.client("client");
2008
2009 let (actor_tx, mut actor_rx) = client.open_port();
2011
2012 let (non_handler_tx, mut non_handler_rx) = mpsc::unbounded_channel::<Option<SeqInfo>>();
2014
2015 let actor_handle = proc.spawn(GetSeqActor(actor_tx.bind()));
2016 let actor_ref: ActorRef<GetSeqActor> = actor_handle.bind();
2017
2018 let non_handler_tx_clone = non_handler_tx.clone();
2020 let non_handler_port_handle =
2021 client
2022 .mailbox()
2023 .open_enqueue_port(move |headers: Flattrs, _m: ()| {
2024 let seq_info = headers.get(SEQ_INFO);
2025 non_handler_tx_clone.send(seq_info).unwrap();
2026 Ok(())
2027 });
2028
2029 non_handler_port_handle.bind();
2031 let non_handler_port_id = match non_handler_port_handle.location() {
2032 PortLocation::Bound(port_id) => port_id,
2033 _ => panic!("port_handle should be bound"),
2034 };
2035 assert!(!non_handler_port_id.is_handler_port());
2036
2037 let session_id = client.sequencer().session_id();
2038
2039 actor_handle.post(&client, "msg1".to_string());
2041 assert_eq!(
2042 actor_rx.recv().await.unwrap().1,
2043 SeqInfo::Session { session_id, seq: 1 }
2044 );
2045
2046 actor_ref.port().post(&client, "msg2".to_string());
2048 assert_eq!(
2049 actor_rx.recv().await.unwrap().1,
2050 SeqInfo::Session { session_id, seq: 2 }
2051 );
2052
2053 non_handler_port_handle.post(&client, ());
2055 assert_eq!(
2056 non_handler_rx.recv().await.unwrap(),
2057 Some(SeqInfo::Session { session_id, seq: 1 })
2058 );
2059
2060 actor_handle.post(&client, "msg3".to_string());
2062 assert_eq!(
2063 actor_rx.recv().await.unwrap().1,
2064 SeqInfo::Session { session_id, seq: 3 }
2065 );
2066
2067 non_handler_port_handle.post(&client, ());
2069 assert_eq!(
2070 non_handler_rx.recv().await.unwrap(),
2071 Some(SeqInfo::Session { session_id, seq: 2 })
2072 );
2073
2074 actor_ref.port().post(&client, "msg4".to_string());
2076 assert_eq!(
2077 actor_rx.recv().await.unwrap().1,
2078 SeqInfo::Session { session_id, seq: 4 }
2079 );
2080
2081 actor_handle.drain_and_stop("test cleanup").unwrap();
2082 actor_handle.await;
2083 }
2084
2085 #[async_timed_test(timeout_secs = 30)]
2087 async fn test_sequencing_multiple_clients() {
2088 let proc = Proc::isolated();
2089 let client1 = proc.client("client1");
2090 let client2 = proc.client("client2");
2091
2092 let (tx, mut rx) = client1.open_port();
2094
2095 let actor_handle = proc.spawn(GetSeqActor(tx.bind()));
2096 let actor_ref: ActorRef<GetSeqActor> = actor_handle.bind();
2097
2098 let session_id_1 = client1.sequencer().session_id();
2100 let session_id_2 = client2.sequencer().session_id();
2101 assert_ne!(session_id_1, session_id_2);
2102
2103 actor_handle.post(&client1, "c1_msg1".to_string());
2105 assert_eq!(
2106 rx.recv().await.unwrap().1,
2107 SeqInfo::Session {
2108 session_id: session_id_1,
2109 seq: 1
2110 }
2111 );
2112
2113 actor_handle.post(&client2, "c2_msg1".to_string());
2115 assert_eq!(
2116 rx.recv().await.unwrap().1,
2117 SeqInfo::Session {
2118 session_id: session_id_2,
2119 seq: 1
2120 }
2121 );
2122
2123 actor_ref.port().post(&client1, "c1_msg2".to_string());
2125 assert_eq!(
2126 rx.recv().await.unwrap().1,
2127 SeqInfo::Session {
2128 session_id: session_id_1,
2129 seq: 2
2130 }
2131 );
2132
2133 actor_ref.port().post(&client2, "c2_msg2".to_string());
2135 assert_eq!(
2136 rx.recv().await.unwrap().1,
2137 SeqInfo::Session {
2138 session_id: session_id_2,
2139 seq: 2
2140 }
2141 );
2142
2143 actor_handle.post(&client1, "c1_msg3".to_string());
2145 assert_eq!(
2146 rx.recv().await.unwrap().1,
2147 SeqInfo::Session {
2148 session_id: session_id_1,
2149 seq: 3
2150 }
2151 );
2152
2153 actor_ref.port().post(&client2, "c2_msg3".to_string());
2154 assert_eq!(
2155 rx.recv().await.unwrap().1,
2156 SeqInfo::Session {
2157 session_id: session_id_2,
2158 seq: 3
2159 }
2160 );
2161
2162 actor_handle.drain_and_stop("test cleanup").unwrap();
2163 actor_handle.await;
2164 }
2165
2166 #[async_timed_test(timeout_secs = 30)]
2187 async fn test_sequencing_actor_handle_callback() {
2188 let config = hyperactor_config::global::lock();
2189 let _guard = config.override_key(config::ENABLE_DEST_ACTOR_REORDERING_BUFFER, true);
2190
2191 let proc = Proc::isolated();
2192 let client = proc.client("client");
2193 let (tx, mut rx) = client.open_port();
2194
2195 let actor_handle = proc.spawn(GetSeqActor(tx.bind()));
2196 let actor_ref: ActorRef<GetSeqActor> = actor_handle.bind();
2197
2198 let (callback_tx, mut callback_rx) = client.open_port();
2199 actor_ref.post(&client, Callback(callback_tx.bind()));
2201 let msg_port_ref = callback_rx.recv().await.unwrap();
2202 msg_port_ref.post(&client, "finally".to_string());
2205
2206 let session_id = client.sequencer().session_id();
2207 assert_eq!(
2209 rx.recv().await.unwrap(),
2210 (
2211 "finally".to_string(),
2212 SeqInfo::Session { session_id, seq: 1 }
2213 )
2214 );
2215 }
2216
2217 #[derive(Clone, Debug)]
2220 struct DelayedMailboxSender {
2221 relay_tx: mpsc::UnboundedSender<MessageEnvelope>,
2222 }
2223
2224 impl DelayedMailboxSender {
2225 fn new(
2227 dest_proc: Proc,
2230 relay_orders: Vec<usize>,
2234 ) -> Self {
2235 let (relay_tx, mut relay_rx) = mpsc::unbounded_channel::<MessageEnvelope>();
2236
2237 tokio::spawn(async move {
2238 let mut buffer = Vec::new();
2239
2240 for _ in 0..relay_orders.len() {
2241 let envelope = relay_rx.recv().await.unwrap();
2242 buffer.push(envelope);
2243 }
2244
2245 for m in buffer.clone() {
2246 let seq = match m.headers().get(SEQ_INFO) {
2247 Some(SeqInfo::Session { seq, .. }) => seq as usize,
2248 Some(SeqInfo::Direct) => panic!("expected Session variant"),
2249 None => panic!("expected seq info"),
2250 };
2251 let order = relay_orders[seq - 1];
2253 buffer[order] = m;
2254 }
2255
2256 let dest_proc_clone = dest_proc.clone();
2257 for msg in buffer {
2258 dest_proc_clone.post(msg, monitored_return_handle());
2259 }
2260 });
2261
2262 Self { relay_tx }
2263 }
2264 }
2265
2266 #[async_trait]
2267 impl MailboxSender for DelayedMailboxSender {
2268 fn post_unchecked(
2269 &self,
2270 envelope: MessageEnvelope,
2271 _return_handle: PortHandle<Undeliverable<MessageEnvelope>>,
2272 ) {
2273 self.relay_tx.send(envelope).unwrap();
2274 }
2275 }
2276
2277 async fn assert_out_of_order_delivery(expected: Vec<(String, u64)>, relay_orders: Vec<usize>) {
2278 let local_proc: Proc = Proc::isolated();
2279 let client = local_proc.client("local");
2280 let (tx, mut rx) = client.open_port();
2281
2282 let handle = local_proc.spawn(GetSeqActor(tx.bind()));
2283 let actor_ref: ActorRef<GetSeqActor> = handle.bind();
2284
2285 let remote_proc = Proc::configured(
2286 test_proc_id("remote_0"),
2287 DelayedMailboxSender::new(local_proc.clone(), relay_orders).boxed(),
2288 );
2289 let remote_client = remote_proc.client("remote");
2290 let mut messages = expected.clone();
2292 messages.sort_by_key(|v| v.1);
2293 for (message, _seq) in messages {
2294 actor_ref.post(&remote_client, message);
2295 }
2296 let session_id = remote_client.sequencer().session_id();
2297 for expect in expected {
2298 let expected = (
2299 expect.0,
2300 SeqInfo::Session {
2301 session_id,
2302 seq: expect.1,
2303 },
2304 );
2305 assert_eq!(rx.recv().await.unwrap(), expected);
2306 }
2307
2308 handle.drain_and_stop("test cleanup").unwrap();
2309 handle.await;
2310 }
2311
2312 #[async_timed_test(timeout_secs = 30)]
2317 async fn test_sequencing_actor_ref_known_delivery_order() {
2318 let config = hyperactor_config::global::lock();
2319
2320 let relay_orders = vec![2, 0, 1];
2322
2323 let _guard = config.override_key(config::ENABLE_DEST_ACTOR_REORDERING_BUFFER, false);
2326 assert_out_of_order_delivery(
2327 vec![
2328 ("second".to_string(), 2),
2329 ("third".to_string(), 3),
2330 ("first".to_string(), 1),
2331 ],
2332 relay_orders.clone(),
2333 )
2334 .await;
2335
2336 let _guard = config.override_key(config::ENABLE_DEST_ACTOR_REORDERING_BUFFER, true);
2339 assert_out_of_order_delivery(
2340 vec![
2341 ("first".to_string(), 1),
2342 ("second".to_string(), 2),
2343 ("third".to_string(), 3),
2344 ],
2345 relay_orders.clone(),
2346 )
2347 .await;
2348 }
2349
2350 #[async_timed_test(timeout_secs = 30)]
2355 async fn test_sequencing_actor_ref_random_delivery_order() {
2356 let config = hyperactor_config::global::lock();
2357
2358 let _guard = config.override_key(config::ENABLE_DEST_ACTOR_REORDERING_BUFFER, true);
2361 let expected = (0..10000)
2362 .map(|i| (format!("msg{i}"), i + 1))
2363 .collect::<Vec<_>>();
2364
2365 let mut relay_orders: Vec<usize> = (0..10000).collect();
2366 relay_orders.shuffle(&mut rand::rng());
2367 assert_out_of_order_delivery(expected, relay_orders).await;
2368 }
2369
2370 #[tokio::test]
2388 async fn test_introspect_query_default_payload() {
2389 let proc = Proc::isolated();
2390 let client = proc.client("client");
2391 let (tx, _rx) = client.open_port::<u64>();
2392 let actor = EchoActor(tx.bind());
2393 let handle = proc.spawn(actor);
2394
2395 let (reply_port, reply_rx) = client.open_once_port::<IntrospectResult>();
2396 handle.actor_addr().introspect_port().post(
2397 &client,
2398 IntrospectMessage::Query {
2399 view: IntrospectView::Actor,
2400 reply: reply_port.bind(),
2401 },
2402 );
2403 let payload = reply_rx.recv().await.unwrap();
2404
2405 assert_eq!(
2406 payload.identity,
2407 crate::introspect::IntrospectRef::Actor(handle.actor_addr().clone())
2408 );
2409 assert_valid_attrs(&payload);
2410 assert_has_attr(&payload, "status");
2411 assert_has_attr(&payload, "actor_type");
2412 assert_has_attr(&payload, "created_at");
2413 assert!(payload.children.is_empty());
2414 assert!(payload.parent.is_none());
2415
2416 handle.drain_and_stop("test").unwrap();
2417 handle.await;
2418 }
2419
2420 fn attrs_get(attrs_json: &str, short_name: &str) -> Option<serde_json::Value> {
2422 use hyperactor_config::INTROSPECT;
2423 use hyperactor_config::attrs::AttrKeyInfo;
2424 let fq_name = inventory::iter::<AttrKeyInfo>()
2425 .find(|info| {
2426 info.meta
2427 .get(INTROSPECT)
2428 .is_some_and(|ia| ia.name == short_name)
2429 })
2430 .map(|info| info.name)?;
2431 let obj: serde_json::Value = serde_json::from_str(attrs_json).ok()?;
2432 obj.get(fq_name).cloned()
2433 }
2434
2435 fn assert_valid_attrs(result: &IntrospectResult) {
2437 let parsed: serde_json::Value =
2438 serde_json::from_str(&result.attrs).expect("attrs must be valid JSON");
2439 assert!(parsed.is_object(), "IA-1: attrs must be a JSON object");
2440 }
2441
2442 fn assert_status(result: &IntrospectResult, expected: &str) {
2444 let status = attrs_get(&result.attrs, "status")
2445 .and_then(|v| v.as_str().map(String::from))
2446 .expect("attrs must contain status");
2447 assert_eq!(status, expected, "unexpected actor status");
2448 }
2449
2450 fn assert_handler(result: &IntrospectResult, expected: Option<&str>) {
2452 let handler =
2453 attrs_get(&result.attrs, "last_handler").and_then(|v| v.as_str().map(String::from));
2454 assert_eq!(handler.as_deref(), expected);
2455 }
2456
2457 fn assert_error_code(result: &IntrospectResult, expected: &str) {
2459 let code = attrs_get(&result.attrs, "error_code")
2460 .and_then(|v| v.as_str().map(String::from))
2461 .expect("attrs must contain error_code");
2462 assert_eq!(code, expected);
2463 }
2464
2465 fn assert_handler_not_contains(result: &IntrospectResult, forbidden: &str) {
2467 if let Some(handler) =
2468 attrs_get(&result.attrs, "last_handler").and_then(|v| v.as_str().map(String::from))
2469 {
2470 assert!(
2471 !handler.contains(forbidden),
2472 "handler should not contain '{}'; got: {}",
2473 forbidden,
2474 handler
2475 );
2476 }
2477 }
2478
2479 fn assert_has_attr(result: &IntrospectResult, short_name: &str) {
2481 assert!(
2482 attrs_get(&result.attrs, short_name).is_some(),
2483 "attrs must contain '{}'",
2484 short_name
2485 );
2486 }
2487
2488 fn assert_status_contains(result: &IntrospectResult, substring: &str) {
2491 let status = attrs_get(&result.attrs, "status")
2492 .and_then(|v| v.as_str().map(String::from))
2493 .expect("attrs must contain status");
2494 assert!(
2495 status.contains(substring),
2496 "status should contain '{}'; got: {}",
2497 substring,
2498 status
2499 );
2500 }
2501
2502 fn assert_no_status_reason(result: &IntrospectResult) {
2504 assert!(
2505 attrs_get(&result.attrs, "status_reason").is_none(),
2506 "IA-3: must not have status_reason"
2507 );
2508 }
2509
2510 fn assert_has_handler(result: &IntrospectResult) {
2512 assert!(
2513 attrs_get(&result.attrs, "last_handler").is_some(),
2514 "must have a handler"
2515 );
2516 }
2517
2518 fn assert_no_failure_attrs(result: &IntrospectResult) {
2520 assert!(
2521 attrs_get(&result.attrs, "failure_error_message").is_none(),
2522 "IA-4: must not have failure attrs"
2523 );
2524 }
2525
2526 #[tokio::test]
2531 async fn test_ia1_ia4_running_actor_attrs() {
2532 let proc = Proc::isolated();
2533 let client = proc.client("client");
2534 let (tx, _rx) = client.open_port::<u64>();
2535 let actor = EchoActor(tx.bind());
2536 let handle = proc.spawn(actor);
2537
2538 let payload = crate::introspect::live_actor_payload(handle.cell());
2539
2540 assert_valid_attrs(&payload);
2542
2543 assert_has_attr(&payload, "status");
2545 assert_no_status_reason(&payload);
2546
2547 assert_no_failure_attrs(&payload);
2549
2550 handle.drain_and_stop("test").unwrap();
2551 handle.await;
2552 }
2553
2554 #[tokio::test]
2560 async fn test_actor_attrs_snapshot_appears_verbatim() {
2561 let proc = Proc::isolated();
2562 let client = proc.client("client");
2563 let (tx, _rx) = client.open_port::<u64>();
2564 let handle = proc.spawn(EchoActor(tx.bind()));
2565
2566 handle.cell().set_attrs_snapshot(|| {
2567 let mut attrs = hyperactor_config::Attrs::new();
2568 attrs.set(crate::introspect::LAST_HANDLER, "seam-probe".to_string());
2569 attrs
2570 });
2571
2572 let payload = crate::introspect::live_actor_payload(handle.cell());
2573 assert_valid_attrs(&payload);
2574 assert_eq!(
2575 attrs_get(&payload.attrs, "last_handler").and_then(|v| v.as_str().map(String::from)),
2576 Some("seam-probe".to_string()),
2577 "AS-1: actor-supplied attr must appear verbatim"
2578 );
2579
2580 handle.drain_and_stop("test").unwrap();
2581 handle.await;
2582 }
2583
2584 #[tokio::test]
2587 async fn test_actor_attrs_snapshot_core_wins_on_collision() {
2588 let proc = Proc::isolated();
2589 let client = proc.client("client");
2590 let (tx, _rx) = client.open_port::<u64>();
2591 let handle = proc.spawn(EchoActor(tx.bind()));
2592
2593 handle.cell().set_attrs_snapshot(|| {
2597 let mut attrs = hyperactor_config::Attrs::new();
2598 attrs.set(crate::introspect::MESSAGES_PROCESSED, 999u64);
2599 attrs
2600 });
2601
2602 let payload = crate::introspect::live_actor_payload(handle.cell());
2603 let messages = attrs_get(&payload.attrs, "messages_processed")
2604 .and_then(|v| v.as_u64())
2605 .expect("attrs must contain messages_processed");
2606 assert_eq!(messages, 0, "AS-2: core value must win over the snapshot");
2609
2610 handle.drain_and_stop("test").unwrap();
2611 handle.await;
2612 }
2613
2614 #[tokio::test]
2617 async fn test_actor_attrs_snapshot_panic_is_non_fatal() {
2618 let proc = Proc::isolated();
2619 let client = proc.client("client");
2620 let (tx, _rx) = client.open_port::<u64>();
2621 let handle = proc.spawn(EchoActor(tx.bind()));
2622
2623 handle
2624 .cell()
2625 .set_attrs_snapshot(|| -> hyperactor_config::Attrs {
2626 panic!("snapshot callback boom")
2627 });
2628
2629 let payload = crate::introspect::live_actor_payload(handle.cell());
2630 assert_valid_attrs(&payload);
2632 assert_has_attr(&payload, "status");
2633 assert_has_attr(&payload, "actor_type");
2634
2635 handle.drain_and_stop("test").unwrap();
2636 handle.await;
2637 }
2638
2639 #[tokio::test]
2645 async fn test_introspect_query_child_not_found() {
2646 let proc = Proc::isolated();
2647 let client = proc.client("client");
2648 let (tx, _rx) = client.open_port::<u64>();
2649 let actor = EchoActor(tx.bind());
2650 let handle = proc.spawn(actor);
2651
2652 let child_ref = crate::Addr::Actor(test_proc_id("nonexistent").actor_addr("child"));
2653 let (reply_port, reply_rx) = client.open_once_port::<IntrospectResult>();
2654 handle.actor_addr().introspect_port().post(
2655 &client,
2656 IntrospectMessage::QueryChild {
2657 child_ref,
2658 reply: reply_port.bind(),
2659 },
2660 );
2661 let payload = reply_rx.recv().await.unwrap();
2662
2663 assert_eq!(
2664 payload.identity,
2665 crate::introspect::IntrospectRef::Actor(
2666 test_proc_id("nonexistent").actor_addr("child")
2667 )
2668 );
2669 assert_error_code(&payload, "not_found");
2670
2671 handle.drain_and_stop("test").unwrap();
2672 handle.await;
2673 }
2674
2675 #[tokio::test]
2681 async fn test_introspect_override() {
2682 #[derive(Debug, Default)]
2683 #[hyperactor::export(handlers = [])]
2684 struct CustomIntrospectActor;
2685
2686 #[async_trait]
2687 impl Actor for CustomIntrospectActor {}
2688
2689 let proc = Proc::isolated();
2690 let client = proc.client("client");
2691 let handle = proc.spawn(CustomIntrospectActor);
2692
2693 handle
2694 .status()
2695 .wait_for(|s| matches!(s, ActorStatus::Idle))
2696 .await
2697 .unwrap();
2698
2699 let (reply_port, reply_rx) = client.open_once_port::<IntrospectResult>();
2700 handle.actor_addr().introspect_port().post(
2701 &client,
2702 IntrospectMessage::Query {
2703 view: IntrospectView::Actor,
2704 reply: reply_port.bind(),
2705 },
2706 );
2707 let payload = reply_rx.recv().await.unwrap();
2708
2709 assert_has_attr(&payload, "status");
2712
2713 handle.drain_and_stop("test").unwrap();
2714 handle.await;
2715 }
2716
2717 #[tokio::test]
2721 async fn test_introspect_query_supervision_child() {
2722 let proc = Proc::isolated();
2723 let client = proc.client("client");
2724
2725 let (tx_parent, _rx_parent) = client.open_port::<u64>();
2727 let parent_handle =
2728 proc.spawn_with_label::<EchoActor>("parent", EchoActor(tx_parent.bind()));
2729
2730 let (tx_child, _rx_child) = client.open_port::<u64>();
2732 let child_handle =
2733 proc.spawn_child::<EchoActor>(parent_handle.cell().clone(), EchoActor(tx_child.bind()));
2734
2735 let (reply_port, reply_rx) = client.open_once_port::<IntrospectResult>();
2737 child_handle.actor_addr().introspect_port().post(
2738 &client,
2739 IntrospectMessage::Query {
2740 view: IntrospectView::Actor,
2741 reply: reply_port.bind(),
2742 },
2743 );
2744 let child_payload = reply_rx.recv().await.unwrap();
2745
2746 assert_eq!(
2747 child_payload.identity,
2748 crate::introspect::IntrospectRef::Actor(child_handle.actor_addr().clone()),
2749 );
2750 assert!(
2752 attrs_get(&child_payload.attrs, "status").is_some(),
2753 "child should have actor attrs"
2754 );
2755 assert_eq!(
2756 child_payload.parent,
2757 Some(crate::introspect::IntrospectRef::Actor(
2758 parent_handle.actor_addr().clone()
2759 )),
2760 );
2761
2762 let (reply_port, reply_rx) = client.open_once_port::<IntrospectResult>();
2764 parent_handle.actor_addr().introspect_port().post(
2765 &client,
2766 IntrospectMessage::Query {
2767 view: IntrospectView::Actor,
2768 reply: reply_port.bind(),
2769 },
2770 );
2771 let parent_payload = reply_rx.recv().await.unwrap();
2772
2773 assert!(parent_payload.parent.is_none());
2774 assert!(
2775 parent_payload
2776 .children
2777 .contains(&crate::introspect::IntrospectRef::Actor(
2778 child_handle.actor_addr().clone()
2779 )),
2780 );
2781
2782 child_handle.drain_and_stop("test").unwrap();
2783 child_handle.await;
2784 parent_handle.drain_and_stop("test").unwrap();
2785 parent_handle.await;
2786 }
2787
2788 #[tokio::test]
2793 async fn test_introspect_fresh_actor_status() {
2794 let proc = Proc::isolated();
2795 let client = proc.client("client");
2796 let (tx, _rx) = client.open_port::<u64>();
2797 let actor = EchoActor(tx.bind());
2798 let handle = proc.spawn(actor);
2799
2800 handle
2802 .status()
2803 .wait_for(|s| matches!(s, ActorStatus::Idle))
2804 .await
2805 .unwrap();
2806
2807 let (reply_port, reply_rx) = client.open_once_port::<IntrospectResult>();
2808 handle.actor_addr().introspect_port().post(
2809 &client,
2810 IntrospectMessage::Query {
2811 view: IntrospectView::Actor,
2812 reply: reply_port.bind(),
2813 },
2814 );
2815 let payload = reply_rx.recv().await.unwrap();
2816
2817 assert_status(&payload, "idle");
2818 assert_handler(&payload, None);
2819
2820 handle.drain_and_stop("test").unwrap();
2821 handle.await;
2822 }
2823
2824 #[tokio::test]
2829 async fn test_introspect_after_user_message() {
2830 let proc = Proc::isolated();
2831 let client = proc.client("client");
2832 let (tx, mut rx) = client.open_port::<u64>();
2833 let actor = EchoActor(tx.bind());
2834 let handle = proc.spawn(actor);
2835
2836 handle.post(&client, 42u64);
2838 let _ = rx.recv().await.unwrap();
2839
2840 let (reply_port, reply_rx) = client.open_once_port::<IntrospectResult>();
2841 handle.actor_addr().introspect_port().post(
2842 &client,
2843 IntrospectMessage::Query {
2844 view: IntrospectView::Actor,
2845 reply: reply_port.bind(),
2846 },
2847 );
2848 let payload = reply_rx.recv().await.unwrap();
2849
2850 assert_status(&payload, "idle");
2851 assert_has_handler(&payload);
2852 assert_handler_not_contains(&payload, "IntrospectMessage");
2853
2854 handle.drain_and_stop("test").unwrap();
2855 handle.await;
2856 }
2857
2858 #[tokio::test]
2863 async fn test_introspect_consecutive_queries() {
2864 let proc = Proc::isolated();
2865 let client = proc.client("client");
2866 let (tx, _rx) = client.open_port::<u64>();
2867 let actor = EchoActor(tx.bind());
2868 let handle = proc.spawn(actor);
2869
2870 handle
2871 .status()
2872 .wait_for(|s| matches!(s, ActorStatus::Idle))
2873 .await
2874 .unwrap();
2875
2876 let (reply_port, reply_rx) = client.open_once_port::<IntrospectResult>();
2878 handle.actor_addr().introspect_port().post(
2879 &client,
2880 IntrospectMessage::Query {
2881 view: IntrospectView::Actor,
2882 reply: reply_port.bind(),
2883 },
2884 );
2885 let payload1 = reply_rx.recv().await.unwrap();
2886
2887 let (reply_port2, reply_rx2) = client.open_once_port::<IntrospectResult>();
2889 handle.actor_addr().introspect_port().post(
2890 &client,
2891 IntrospectMessage::Query {
2892 view: IntrospectView::Actor,
2893 reply: reply_port2.bind(),
2894 },
2895 );
2896 let payload2 = reply_rx2.recv().await.unwrap();
2897
2898 assert_handler(&payload1, None);
2900 assert_handler(&payload2, None);
2901
2902 handle.drain_and_stop("test").unwrap();
2903 handle.await;
2904 }
2905
2906 #[tokio::test]
2914 async fn test_publish_attrs_round_trip() {
2915 use hyperactor_config::Attrs;
2916 use hyperactor_config::declare_attrs;
2917
2918 declare_attrs! {
2919 attr TEST_KEY_A: String;
2920 attr TEST_KEY_B: u64;
2921 }
2922
2923 let proc = Proc::isolated();
2924 let client = proc.client("client");
2925 let (tx, _rx) = client.open_port::<u64>();
2926 let actor = EchoActor(tx.bind());
2927 let handle = proc.spawn(actor);
2928
2929 assert!(handle.cell().published_attrs().is_none());
2931
2932 let mut attrs = Attrs::new();
2934 attrs.set(TEST_KEY_A, "hello".to_string());
2935 handle.cell().set_published_attrs(attrs);
2936 let published = handle.cell().published_attrs().unwrap();
2937 assert_eq!(published.get(TEST_KEY_A), Some(&"hello".to_string()));
2938
2939 handle.cell().merge_published_attr(TEST_KEY_B, 42u64);
2941 let published = handle.cell().published_attrs().unwrap();
2942 assert_eq!(published.get(TEST_KEY_A), Some(&"hello".to_string()));
2943 assert_eq!(published.get(TEST_KEY_B), Some(&42u64));
2944
2945 handle
2947 .cell()
2948 .merge_published_attr(TEST_KEY_A, "world".to_string());
2949 let published = handle.cell().published_attrs().unwrap();
2950 assert_eq!(published.get(TEST_KEY_A), Some(&"world".to_string()));
2951
2952 handle.drain_and_stop("test").unwrap();
2953 handle.await;
2954 }
2955
2956 #[tokio::test]
2959 async fn test_query_child_handler_round_trip() {
2960 let proc = Proc::isolated();
2961 let client = proc.client("client");
2962 let (tx, _rx) = client.open_port::<u64>();
2963 let actor = EchoActor(tx.bind());
2964 let handle = proc.spawn(actor);
2965
2966 let test_ref = Addr::Actor(test_proc_id("test").actor_addr("child"));
2968 assert!(handle.cell().query_child(&test_ref).is_none());
2969
2970 handle.cell().set_query_child_handler(|child_ref| {
2972 use crate::introspect::IntrospectRef;
2973 let identity = match child_ref {
2974 Addr::Proc(p) => IntrospectRef::Proc(p.clone()),
2975 Addr::Actor(a) => IntrospectRef::Actor(a.clone()),
2976 Addr::Port(p) => IntrospectRef::Actor(p.actor_addr()),
2977 };
2978 IntrospectResult {
2979 identity,
2980 attrs: serde_json::json!({
2981 "proc_name": "test_proc",
2982 "num_actors": 42,
2983 })
2984 .to_string(),
2985 children: Vec::new(),
2986 parent: None,
2987 as_of: std::time::SystemTime::now(),
2988 }
2989 });
2990
2991 let payload = handle
2993 .cell()
2994 .query_child(&test_ref)
2995 .expect("query_child must return payload");
2996 assert_eq!(
2997 payload.identity,
2998 crate::introspect::IntrospectRef::Actor(test_proc_id("test").actor_addr("child"))
2999 );
3000 let attrs: serde_json::Value =
3001 serde_json::from_str(&payload.attrs).expect("attrs must be valid JSON");
3002 assert_eq!(
3003 attrs.get("proc_name").and_then(|v| v.as_str()),
3004 Some("test_proc")
3005 );
3006 assert_eq!(attrs.get("num_actors").and_then(|v| v.as_u64()), Some(42));
3007
3008 handle.drain_and_stop("test").unwrap();
3009 handle.await;
3010 }
3011
3012 #[tokio::test]
3018 async fn test_introspect_wedged() {
3019 #[derive(Debug, Default)]
3020 #[hyperactor::export(handlers = [u64])]
3021 struct WedgedActor;
3022
3023 #[async_trait]
3024 impl Actor for WedgedActor {}
3025
3026 #[async_trait]
3027 impl Handler<u64> for WedgedActor {
3028 async fn handle(
3029 &mut self,
3030 _cx: &Context<Self>,
3031 _message: u64,
3032 ) -> Result<(), anyhow::Error> {
3033 std::future::pending::<()>().await;
3035 Ok(())
3036 }
3037 }
3038
3039 let proc = Proc::isolated();
3040 let client = proc.client("client");
3041 let handle = proc.spawn(WedgedActor);
3042
3043 handle
3045 .status()
3046 .wait_for(|s| matches!(s, ActorStatus::Idle))
3047 .await
3048 .unwrap();
3049
3050 handle.post(&client, 1u64);
3052
3053 tokio::time::sleep(Duration::from_millis(50)).await;
3055
3056 let (reply_port, reply_rx) = client.open_once_port::<IntrospectResult>();
3058 handle.actor_addr().introspect_port().post(
3059 &client,
3060 IntrospectMessage::Query {
3061 view: IntrospectView::Actor,
3062 reply: reply_port.bind(),
3063 },
3064 );
3065
3066 let payload = tokio::time::timeout(Duration::from_secs(5), reply_rx.recv())
3068 .await
3069 .expect("introspect should not hang on a wedged actor")
3070 .unwrap();
3071
3072 assert_status_contains(&payload, "processing");
3073 assert_handler_not_contains(&payload, "IntrospectMessage");
3074 }
3075
3076 #[tokio::test]
3081 async fn test_introspect_no_perturbation() {
3082 let proc = Proc::isolated();
3083 let client = proc.client("client");
3084 let (tx, mut rx) = client.open_port::<u64>();
3085 let actor = EchoActor(tx.bind());
3086 let handle = proc.spawn(actor);
3087
3088 handle
3090 .status()
3091 .wait_for(|s| matches!(s, ActorStatus::Idle))
3092 .await
3093 .unwrap();
3094
3095 handle.post(&client, 42u64);
3097 let _ = rx.recv().await.unwrap();
3098
3099 let (reply_port1, reply_rx1) = client.open_once_port::<IntrospectResult>();
3101 handle.actor_addr().introspect_port().post(
3102 &client,
3103 IntrospectMessage::Query {
3104 view: IntrospectView::Actor,
3105 reply: reply_port1.bind(),
3106 },
3107 );
3108 let payload1 = reply_rx1.recv().await.unwrap();
3109
3110 let (reply_port2, reply_rx2) = client.open_once_port::<IntrospectResult>();
3112 handle.actor_addr().introspect_port().post(
3113 &client,
3114 IntrospectMessage::Query {
3115 view: IntrospectView::Actor,
3116 reply: reply_port2.bind(),
3117 },
3118 );
3119 let payload2 = reply_rx2.recv().await.unwrap();
3120
3121 assert_handler_not_contains(&payload1, "IntrospectMessage");
3123 assert_handler_not_contains(&payload2, "IntrospectMessage");
3124 let attrs1: serde_json::Value = serde_json::from_str(&payload1.attrs).unwrap();
3127 let attrs2: serde_json::Value = serde_json::from_str(&payload2.attrs).unwrap();
3128 assert_eq!(attrs1, attrs2, "consecutive queries should be identical");
3129
3130 handle.drain_and_stop("test").unwrap();
3131 handle.await;
3132 }
3133
3134 #[tokio::test]
3141 async fn test_introspectable_instance_responds_to_query() {
3142 let proc = Proc::isolated();
3143 let (bridge, handle) = proc.introspectable_instance("bridge").unwrap();
3144 let actor_id: crate::ActorAddr = handle.actor_addr().clone();
3145
3146 let (reply_port, reply_rx) = bridge.open_once_port::<IntrospectResult>();
3147 actor_id.introspect_port().post(
3148 &bridge,
3149 IntrospectMessage::Query {
3150 view: IntrospectView::Actor,
3151 reply: reply_port.bind(),
3152 },
3153 );
3154 let payload = reply_rx.recv().await.unwrap();
3155
3156 assert_eq!(
3159 payload.identity,
3160 crate::introspect::IntrospectRef::Actor(actor_id.clone())
3161 );
3162 assert_status(&payload, "client");
3163 let actor_type =
3164 attrs_get(&payload.attrs, "actor_type").and_then(|v| v.as_str().map(String::from));
3165 assert_eq!(
3166 actor_type.as_deref(),
3167 Some("()"),
3168 "CI-1: actor_type must be \"()\""
3169 );
3170 }
3171
3172 #[tokio::test]
3180 async fn test_instance_does_not_respond_to_query() {
3181 let proc = Proc::isolated();
3182 let client = proc.client("client");
3183 let mailbox = proc.client("mailbox");
3184 let mailbox_id: crate::ActorAddr = mailbox.self_addr().clone();
3185
3186 let (reply_port, reply_rx) = client.open_once_port::<IntrospectResult>();
3187 mailbox_id.introspect_port().post(
3188 &client,
3189 IntrospectMessage::Query {
3190 view: IntrospectView::Actor,
3191 reply: reply_port.bind(),
3192 },
3193 );
3194
3195 let result = tokio::time::timeout(Duration::from_millis(100), reply_rx.recv()).await;
3198 assert!(
3199 result.is_err(),
3200 "client() must not respond to IntrospectMessage (introspect receiver dropped)"
3201 );
3202 }
3203
3204 #[tokio::test]
3209 async fn test_introspectable_instance_snapshot_on_drop() {
3210 let proc = Proc::isolated();
3211 let (instance, handle) = proc.introspectable_instance("bridge").unwrap();
3212 let actor_id = handle.actor_addr().clone();
3213
3214 assert!(
3215 proc.all_actor_ids().contains(&actor_id),
3216 "should appear in all_actor_ids while live"
3217 );
3218
3219 drop(instance);
3220 handle.await;
3221
3222 let snapshot = proc.terminated_snapshot(&actor_id).unwrap();
3223 let actor_status = attrs_get(&snapshot.attrs, "status")
3224 .and_then(|v| v.as_str().map(String::from))
3225 .expect("snapshot attrs must contain status");
3226 assert!(
3227 actor_status.starts_with("stopped"),
3228 "CI-2: snapshot actor_status should be stopped, got: {}",
3229 actor_status
3230 );
3231 }
3232
3233 #[test]
3234 fn zombie_status_is_not_terminal() {
3235 let status = ActorStatus::zombie("hard kill did not finish");
3236
3237 assert!(!status.is_terminal(), "zombie status is not terminal");
3238 assert!(status.is_stopping(), "zombie status is stopping");
3239 assert!(status.is_zombie(), "zombie predicate should match");
3240 }
3241}