1use async_trait::async_trait;
22use hyperactor::Actor;
23use hyperactor::ActorAddr;
24use hyperactor::ActorHandle;
25use hyperactor::ActorRef;
26use hyperactor::AnyActorHandle;
27use hyperactor::Context;
28use hyperactor::Endpoint;
29use hyperactor::Handler;
30use hyperactor::Instance;
31use hyperactor::OncePortHandle;
32use hyperactor::PortRef;
33use hyperactor::Uid;
34use hyperactor::actor::ActorErrorKind;
35use hyperactor::actor::ActorStatus;
36use hyperactor::actor::StopMode;
37use hyperactor::context;
38use hyperactor::context::Actor as _;
39use hyperactor::mailbox::MessageEnvelope;
40use hyperactor::mailbox::Undeliverable;
41use hyperactor::mailbox::UndeliverableReason;
42use hyperactor::supervision::ActorSupervisionEvent;
43use hyperactor_config::Flattrs;
44
45use crate::Gspawn;
46use crate::KeepaliveLink;
47use crate::KeepaliveSupervisor;
48use crate::OrphanPolicy;
49use crate::RemoteActorDisposition;
50use crate::Supervise;
51use crate::SupervisionOptions;
52use crate::SupervisorEvent;
53use crate::WorkerCommand;
54use crate::WorkerLike;
55
56#[derive(Debug)]
57struct PendingStop {
58 mode: StopMode,
59 reason: String,
60}
61
62#[derive(Debug)]
63struct SupervisorSession {
64 worker: PortRef<WorkerCommand>,
65 child: ActorAddr,
66 display_name: Option<String>,
67}
68
69#[derive(Clone, Debug, PartialEq, Eq)]
70struct SessionId(hyperactor::Uid);
71
72impl SessionId {
73 fn into_uid(self) -> hyperactor::Uid {
74 self.0
75 }
76}
77
78type BootstrapCallback =
79 Box<dyn FnOnce(&Instance<Supervisor>, Supervise) -> anyhow::Result<()> + Send>;
80
81enum SupervisorBootstrap {
82 ExistingWorker {
83 worker: ActorRef<WorkerLike>,
84 },
85 Callback {
86 fallback_actor: ActorAddr,
87 callback: Option<BootstrapCallback>,
88 },
89}
90
91impl std::fmt::Debug for SupervisorBootstrap {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 match self {
94 Self::ExistingWorker { worker } => f
95 .debug_struct("ExistingWorker")
96 .field("worker", worker)
97 .finish(),
98 Self::Callback { fallback_actor, .. } => f
99 .debug_struct("Callback")
100 .field("fallback_actor", fallback_actor)
101 .finish_non_exhaustive(),
102 }
103 }
104}
105
106impl SupervisorBootstrap {
107 fn fallback_actor(&self) -> ActorAddr {
108 match self {
109 Self::ExistingWorker { worker } => worker.actor_addr().clone(),
110 Self::Callback { fallback_actor, .. } => fallback_actor.clone(),
111 }
112 }
113
114 fn worker_endpoint(&self) -> Option<&ActorRef<WorkerLike>> {
115 match self {
116 Self::ExistingWorker { worker } => Some(worker),
117 Self::Callback { .. } => None,
118 }
119 }
120
121 fn start(&mut self, cx: &Instance<Supervisor>, supervise: Supervise) -> anyhow::Result<()> {
122 match self {
123 Self::ExistingWorker { worker } => {
124 (&*worker).post(cx, supervise);
125 }
126 Self::Callback { callback, .. } => {
127 callback
128 .take()
129 .expect("supervisor bootstrap callback started more than once")(
130 cx, supervise
131 )?;
132 }
133 }
134 Ok(())
135 }
136}
137
138#[derive(Debug)]
140pub struct Supervisor {
141 bootstrap: SupervisorBootstrap,
142 liveness: Option<KeepaliveLink>,
143 options: SupervisionOptions,
144 session_id: Uid,
145 liveness_handle: Option<ActorHandle<KeepaliveSupervisor>>,
146 session: Option<SupervisorSession>,
147 pending_stop: Option<PendingStop>,
148 ready: Option<OncePortHandle<()>>,
150 labels: Flattrs,
151}
152
153impl Supervisor {
154 pub fn new(
156 worker: ActorRef<WorkerLike>,
157 liveness: KeepaliveLink,
158 options: SupervisionOptions,
159 ) -> Self {
160 Self::new_uid(worker, liveness, options, hyperactor::Uid::anonymous())
161 }
162
163 pub fn new_uid(
165 worker: ActorRef<WorkerLike>,
166 liveness: KeepaliveLink,
167 options: SupervisionOptions,
168 session_id: hyperactor::Uid,
169 ) -> Self {
170 Self::new_with_bootstrap(
171 SupervisorBootstrap::ExistingWorker { worker },
172 liveness,
173 options,
174 session_id,
175 None,
176 )
177 }
178
179 pub(crate) fn bootstrap_uid<F>(
196 liveness: KeepaliveLink,
197 options: SupervisionOptions,
198 session_id: Uid,
199 fallback_actor: ActorAddr,
200 ready: Option<OncePortHandle<()>>,
201 bootstrap: F,
202 ) -> Self
203 where
204 F: FnOnce(&Instance<Supervisor>, Supervise) -> anyhow::Result<()> + Send + 'static,
205 {
206 Self::new_with_bootstrap(
207 SupervisorBootstrap::Callback {
208 fallback_actor,
209 callback: Some(Box::new(bootstrap)),
210 },
211 liveness,
212 options,
213 session_id,
214 ready,
215 )
216 }
217
218 fn new_with_bootstrap(
219 bootstrap: SupervisorBootstrap,
220 liveness: KeepaliveLink,
221 options: SupervisionOptions,
222 session_id: Uid,
223 ready: Option<OncePortHandle<()>>,
224 ) -> Self {
225 Self {
226 bootstrap,
227 liveness: Some(liveness),
228 options,
229 session_id,
230 liveness_handle: None,
231 session: None,
232 pending_stop: None,
233 ready,
234 labels: Flattrs::new(),
235 }
236 }
237
238 pub fn with_labels(mut self, labels: Flattrs) -> Self {
241 self.labels = labels;
242 self
243 }
244}
245
246#[async_trait]
247impl Actor for Supervisor {
248 async fn init(&mut self, this: &Instance<Self>) -> anyhow::Result<()> {
249 let (liveness_handle, liveness) = self
250 .liveness
251 .take()
252 .expect("supervisor initialized more than once")
253 .spawn_supervisor(this)?;
254 self.liveness_handle = Some(liveness_handle);
255 self.bootstrap.start(
256 this,
257 Supervise {
258 session_id: self.session_id.clone(),
259 supervisor: this.port::<SupervisorEvent>().bind(),
260 parent: this.self_addr().clone(),
261 liveness,
262 options: self.options.clone(),
263 labels: self.labels.clone(),
264 },
265 )?;
266 Ok(())
267 }
268
269 async fn handle_stop(
270 &mut self,
271 this: &Instance<Self>,
272 mode: StopMode,
273 reason: &str,
274 ) -> anyhow::Result<()> {
275 self.pending_stop = Some(PendingStop {
276 mode,
277 reason: reason.to_string(),
278 });
279 self.send_pending_worker_stop(this)
280 }
281
282 async fn handle_supervision_event(
283 &mut self,
284 _this: &Instance<Self>,
285 event: &ActorSupervisionEvent,
286 ) -> anyhow::Result<bool> {
287 if self
288 .liveness_handle
289 .as_ref()
290 .is_some_and(|handle| handle.actor_addr() == &event.actor_id)
291 {
292 let event = self.synthesize_unreachable_event("supervision liveness failed");
293 return self.propagate_event(event);
294 }
295 Ok(!event.is_error())
296 }
297}
298
299#[async_trait]
300impl Handler<SupervisorEvent> for Supervisor {
301 async fn handle(&mut self, cx: &Context<Self>, message: SupervisorEvent) -> anyhow::Result<()> {
302 match message {
303 SupervisorEvent::Linked {
304 session_id,
305 worker,
306 child,
307 display_name,
308 } => {
309 self.ensure_session(&session_id)?;
310 if let Some(ready) = self.ready.take() {
311 ready.post(cx, ());
312 }
313 self.session = Some(SupervisorSession {
314 worker,
315 child,
316 display_name,
317 });
318 self.send_pending_worker_stop(cx)?;
319 Ok(())
320 }
321 SupervisorEvent::SuperviseRejected { session_id, reason } => {
322 self.ensure_session(&session_id)?;
323 anyhow::bail!("remote supervision request rejected: {}", reason)
324 }
325 SupervisorEvent::SupervisionEvent {
326 session_id,
327 event,
328 disposition: _,
329 } => {
330 self.ensure_session(&session_id)?;
331 self.propagate_event(event)
332 }
333 SupervisorEvent::Unlinked { session_id, reason } => {
334 self.ensure_session(&session_id)?;
335 cx.exit(&reason)?;
336 Ok(())
337 }
338 }
339 }
340}
341
342impl Supervisor {
343 fn ensure_session(&self, session_id: &hyperactor::Uid) -> anyhow::Result<()> {
344 if session_id != &self.session_id {
345 anyhow::bail!("remote supervision session id mismatch");
346 }
347 Ok(())
348 }
349
350 fn send_pending_worker_stop(&mut self, cx: &Instance<Self>) -> anyhow::Result<()> {
351 let Some(stop) = self.pending_stop.take() else {
352 return Ok(());
353 };
354 let Some(session) = &self.session else {
355 let Some(worker) = self.bootstrap.worker_endpoint() else {
356 self.pending_stop = Some(stop);
357 return Ok(());
358 };
359 worker.post(
360 cx,
361 WorkerCommand::Stop {
362 session_id: self.session_id.clone(),
363 mode: stop.mode,
364 reason: stop.reason,
365 },
366 );
367 return Ok(());
368 };
369 (&session.worker).post(
370 cx,
371 WorkerCommand::Stop {
372 session_id: self.session_id.clone(),
373 mode: stop.mode,
374 reason: stop.reason,
375 },
376 );
377 Ok(())
378 }
379
380 fn synthesize_unreachable_event(&self, reason: &str) -> ActorSupervisionEvent {
381 if let Some(session) = &self.session {
382 ActorSupervisionEvent::new(
383 session.child.clone(),
384 session.display_name.clone(),
385 ActorStatus::generic_failure(reason),
386 None,
387 )
388 } else {
389 ActorSupervisionEvent::new(
390 self.bootstrap.fallback_actor(),
391 None,
392 ActorStatus::generic_failure(reason),
393 None,
394 )
395 }
396 }
397
398 fn propagate_event<T>(&self, event: ActorSupervisionEvent) -> anyhow::Result<T> {
399 Err(anyhow::Error::new(
400 ActorErrorKind::UnhandledSupervisionEvent(Box::new(
401 event.with_labels(self.labels.clone()),
402 )),
403 ))
404 }
405}
406
407#[derive(Debug)]
408struct WorkerSession {
409 session_id: SessionId,
410 supervisor: PortRef<SupervisorEvent>,
411 options: SupervisionOptions,
412 liveness_handle: AnyActorHandle,
413 labels: Flattrs,
414}
415
416#[derive(Debug)]
418pub struct Spawn<C: Actor> {
419 child: C,
420 uid: Option<Uid>,
421}
422
423impl<C: Actor> Spawn<C> {
424 pub fn new(child: C) -> Self {
426 Self { child, uid: None }
427 }
428
429 pub fn with_uid(uid: Uid, child: C) -> Self {
431 Self {
432 child,
433 uid: Some(uid),
434 }
435 }
436}
437
438#[derive(Debug)]
440pub(crate) struct GspawnAndSupervise {
441 gspawn: Gspawn,
442 supervise: Supervise,
443}
444
445impl GspawnAndSupervise {
446 pub(crate) fn new(gspawn: Gspawn, supervise: Supervise) -> Self {
447 Self { gspawn, supervise }
448 }
449}
450
451#[derive(Debug, Default)]
453#[hyperactor::export(Supervise, WorkerCommand)]
454pub struct Worker {
455 state: SupervisedChild,
456}
457
458impl Worker {
459 pub fn new() -> Self {
461 Self::default()
462 }
463}
464
465#[async_trait]
466impl Actor for Worker {
467 async fn init(&mut self, _this: &Instance<Self>) -> anyhow::Result<()> {
468 Ok(())
469 }
470
471 async fn handle_supervision_event(
472 &mut self,
473 this: &Instance<Self>,
474 event: &ActorSupervisionEvent,
475 ) -> anyhow::Result<bool> {
476 self.state.handle_supervision_event(this, event)
477 }
478
479 async fn handle_undeliverable_message(
480 &mut self,
481 _this: &Instance<Self>,
482 _reason: UndeliverableReason,
483 _envelope: Undeliverable<MessageEnvelope>,
484 ) -> anyhow::Result<()> {
485 self.state
486 .handle_orphaned_supervisor("supervisor session undeliverable")
487 }
488
489 async fn handle_delivery_failure_event(
490 &mut self,
491 _this: &Instance<Self>,
492 _envelope: Undeliverable<MessageEnvelope>,
493 ) -> anyhow::Result<()> {
494 self.state
495 .handle_orphaned_supervisor("supervisor session undeliverable")
496 }
497}
498
499#[async_trait]
500impl Handler<Supervise> for Worker {
501 async fn handle(&mut self, cx: &Context<Self>, message: Supervise) -> anyhow::Result<()> {
502 self.state.supervise(cx, message).await
503 }
504}
505
506#[async_trait]
507impl Handler<WorkerCommand> for Worker {
508 async fn handle(&mut self, cx: &Context<Self>, message: WorkerCommand) -> anyhow::Result<()> {
509 self.state.handle_supervised_worker(cx, message)
510 }
511}
512
513#[async_trait]
514impl<C> Handler<Spawn<C>> for Worker
515where
516 C: Actor + Send + Sync + 'static,
517{
518 async fn handle(&mut self, cx: &Context<Self>, message: Spawn<C>) -> anyhow::Result<()> {
519 let display_name = message.child.display_name();
520 let child = match message.uid {
521 Some(uid) => cx.instance().spawn_with_uid(uid, message.child)?.into_any(),
522 None => cx.instance().spawn(message.child).into_any(),
523 };
524 self.state.set_child(child, display_name)?;
525 Ok(())
526 }
527}
528
529#[async_trait]
530impl Handler<GspawnAndSupervise> for Worker {
531 async fn handle(
532 &mut self,
533 cx: &Context<Self>,
534 message: GspawnAndSupervise,
535 ) -> anyhow::Result<()> {
536 let GspawnAndSupervise { gspawn, supervise } = message;
537 let session_id = supervise.session_id.clone();
538 let supervisor = supervise.supervisor.clone();
539 match gspawn.spawn_child(cx).await {
540 Ok(child) => {
541 self.state.set_child(child, None)?;
542 self.state.supervise(cx, supervise).await?;
543 }
544 Err(err) => {
545 let _ = supervisor.post(
546 cx,
547 SupervisorEvent::SuperviseRejected {
548 session_id,
549 reason: format!("actor spawn failed: {}", err),
550 },
551 );
552 cx.exit("actor spawn failed")?;
553 }
554 }
555 Ok(())
556 }
557}
558
559#[derive(Debug, Default)]
560pub(crate) struct SupervisedChild {
561 child_handle: Option<AnyActorHandle>,
562 child_display_name: Option<String>,
563 session: Option<WorkerSession>,
564}
565
566impl SupervisedChild {
567 pub(crate) fn set_child(
568 &mut self,
569 child_handle: AnyActorHandle,
570 child_display_name: Option<String>,
571 ) -> anyhow::Result<()> {
572 if self.child_handle.is_some() {
573 anyhow::bail!("worker already has a supervised child");
574 }
575 self.child_handle = Some(child_handle);
576 self.child_display_name = child_display_name;
577 Ok(())
578 }
579
580 pub(crate) async fn supervise<C>(&mut self, cx: &C, message: Supervise) -> anyhow::Result<()>
581 where
582 C: context::Actor,
583 C::A: Handler<WorkerCommand>,
584 {
585 if self.session.is_some() {
586 let _ = message.supervisor.post(
587 cx,
588 SupervisorEvent::SuperviseRejected {
589 session_id: message.session_id,
590 reason: "worker already supervised".to_string(),
591 },
592 );
593 return Ok(());
594 }
595
596 if self.child_handle.is_none() {
597 let _ = message.supervisor.post(
598 cx,
599 SupervisorEvent::SuperviseRejected {
600 session_id: message.session_id,
601 reason: "worker has no supervised child".to_string(),
602 },
603 );
604 return Ok(());
605 }
606
607 let liveness_handle = message.liveness.spawn_worker(cx).await?;
608 let child_addr = self
609 .child_handle
610 .as_ref()
611 .expect("worker child must be spawned before linking")
612 .actor_id()
613 .clone();
614 let supervisor = message.supervisor.clone();
615 let session_id = SessionId(message.session_id.clone());
616 self.session = Some(WorkerSession {
617 session_id,
618 supervisor: message.supervisor,
619 options: message.options,
620 liveness_handle,
621 labels: message.labels,
622 });
623 supervisor.post(
624 cx,
625 SupervisorEvent::Linked {
626 session_id: message.session_id.clone(),
627 worker: cx.instance().port::<WorkerCommand>().bind(),
628 child: child_addr,
629 display_name: self.child_display_name.clone(),
630 },
631 );
632 Ok(())
633 }
634
635 pub(crate) fn handle_supervision_event<A: Actor>(
636 &mut self,
637 this: &Instance<A>,
638 event: &ActorSupervisionEvent,
639 ) -> anyhow::Result<bool> {
640 if self.is_liveness_event(event) {
641 self.handle_liveness_event(this, event)?;
642 return Ok(true);
643 }
644 if self.is_child_event(event) {
645 if let Some(session) = &self.session {
646 let supervisor = session.supervisor.clone();
647 let session_id = session.session_id.clone();
648 supervisor.post(
649 this,
650 SupervisorEvent::SupervisionEvent {
651 session_id: session_id.into_uid(),
652 event: event.clone().with_labels(session.labels.clone()),
653 disposition: RemoteActorDisposition::Terminal,
654 },
655 );
656 }
657 self.child_handle = None;
658 this.exit("supervised child stopped")?;
659 return Ok(true);
660 }
661
662 debug_assert!(
663 self.session.is_none(),
664 "worker with active session received supervision event for non-liveness non-child actor: {:?}",
665 event.actor_id
666 );
667 if self.session.is_none() {
668 Ok(true)
669 } else {
670 Ok(!event.is_error())
671 }
672 }
673
674 pub(crate) fn handle_supervised_worker<A: Actor>(
675 &mut self,
676 cx: &Context<A>,
677 message: WorkerCommand,
678 ) -> anyhow::Result<()> {
679 match message {
680 WorkerCommand::Stop {
681 session_id,
682 mode,
683 reason,
684 } => {
685 self.accept_session_id(session_id)?;
686 self.stop_child(mode, &reason)?;
687 }
688 WorkerCommand::Unlink { session_id, reason } => {
689 let session_id = self.accept_session_id(session_id)?;
690 if let Some(session) = self.session.take() {
691 let _ = session.liveness_handle.stop(&reason);
692 match session.options.orphan_policy {
693 OrphanPolicy::Stop => self.stop_child(StopMode::Stop, &reason)?,
694 OrphanPolicy::Detach => (),
695 }
696
697 let _ = session.supervisor.post(
698 cx,
699 SupervisorEvent::Unlinked {
700 session_id: session_id.into_uid(),
701 reason,
702 },
703 );
704 }
705 }
706 }
707 Ok(())
708 }
709
710 fn accept_session_id(&self, raw: hyperactor::Uid) -> anyhow::Result<SessionId> {
711 let Some(session) = &self.session else {
712 anyhow::bail!("worker is not linked");
713 };
714 let session_id = SessionId(raw);
715 if session_id != session.session_id {
716 anyhow::bail!("remote supervision session id mismatch");
717 }
718 Ok(session_id)
719 }
720
721 fn stop_child(&self, mode: StopMode, reason: &str) -> anyhow::Result<()> {
722 let Some(child) = &self.child_handle else {
723 return Ok(());
724 };
725 match mode {
726 StopMode::Stop => child.stop(reason)?,
727 StopMode::DrainAndStop => child.drain_and_stop(reason)?,
728 }
729 Ok(())
730 }
731
732 fn is_child_event(&self, event: &ActorSupervisionEvent) -> bool {
733 self.child_handle
734 .as_ref()
735 .is_some_and(|child| child.actor_id() == &event.actor_id)
736 }
737
738 fn is_liveness_event(&self, event: &ActorSupervisionEvent) -> bool {
739 self.session
740 .as_ref()
741 .is_some_and(|session| session.liveness_handle.actor_id() == &event.actor_id)
742 }
743
744 pub(crate) fn handle_orphaned_supervisor(&mut self, reason: &str) -> anyhow::Result<()> {
745 match self.unlink_orphaned_supervisor(reason) {
746 Some(OrphanPolicy::Stop) => self.stop_child(StopMode::Stop, reason),
747 Some(OrphanPolicy::Detach) | None => Ok(()),
748 }
749 }
750
751 fn unlink_orphaned_supervisor(&mut self, reason: &str) -> Option<OrphanPolicy> {
752 let session = self.session.take()?;
753 let _ = session.liveness_handle.stop(reason);
754 Some(session.options.orphan_policy)
755 }
756
757 fn handle_liveness_event<A: Actor>(
758 &mut self,
759 cx: &Instance<A>,
760 event: &ActorSupervisionEvent,
761 ) -> anyhow::Result<()> {
762 let Some(session) = &self.session else {
763 return Ok(());
764 };
765 let supervisor = session.supervisor.clone();
766 let session_id = session.session_id.clone();
767 let orphan_policy = session.options.orphan_policy;
768 if let Some(child) = &self.child_handle {
769 supervisor.post(
770 cx,
771 SupervisorEvent::SupervisionEvent {
772 session_id: session_id.into_uid(),
773 event: ActorSupervisionEvent::new(
774 child.actor_id().clone(),
775 self.child_display_name.clone(),
776 ActorStatus::generic_failure(format!(
777 "supervision liveness failed: {}",
778 event
779 )),
780 None,
781 )
782 .with_labels(session.labels.clone()),
783 disposition: RemoteActorDisposition::Unreachable,
784 },
785 );
786 }
787 let session = self
788 .session
789 .take()
790 .expect("liveness event must have an active session");
791 let _ = session.liveness_handle.stop("supervision liveness failed");
792 if orphan_policy == OrphanPolicy::Stop {
793 self.stop_child(StopMode::Stop, "supervision liveness failed")?;
794 }
795 Ok(())
796 }
797}
798
799#[cfg(test)]
800mod tests {
801 use std::sync::Arc;
802 use std::time::Duration;
803
804 use hyperactor::PortHandle;
805 use hyperactor::Proc;
806 use hyperactor::Uid;
807 use hyperactor::mailbox::PortReceiver;
808 use hyperactor_config::attrs::declare_attrs;
809 use serde::Deserialize;
810 use serde::Serialize;
811 use tokio::sync::Notify;
812 use typeuri::Named;
813
814 use super::*;
815
816 declare_attrs! {
817 attr TEST_LABEL: u64;
818 }
819
820 fn test_labels(value: u64) -> Flattrs {
821 let mut labels = Flattrs::new();
822 labels.set(TEST_LABEL, value);
823 labels
824 }
825
826 #[derive(Clone, Debug, Serialize, Deserialize, Named)]
827 enum TestChildCommand {
828 Fail,
829 Drain(String),
830 }
831 wirevalue::register_type!(TestChildCommand);
832
833 #[derive(Clone, Debug, Serialize, Deserialize, Named)]
834 struct KillParent;
835 wirevalue::register_type!(KillParent);
836
837 #[derive(Debug)]
838 enum TestChildAction {
839 FailAfter(Duration),
840 DrainAfter(Duration, String),
841 }
842
843 #[derive(Debug)]
844 #[hyperactor::export(TestChildCommand)]
845 struct TestChild {
846 ready: PortHandle<ActorAddr>,
847 stopped: PortHandle<String>,
848 action: Option<TestChildAction>,
849 drain_observer: Option<PortHandle<String>>,
850 }
851
852 impl TestChild {
853 fn with_drain_observer(mut self, port: PortHandle<String>) -> Self {
854 self.drain_observer = Some(port);
855 self
856 }
857 }
858
859 #[async_trait]
860 impl Actor for TestChild {
861 async fn init(&mut self, this: &Instance<Self>) -> anyhow::Result<()> {
862 self.ready.post(this, this.self_addr().clone());
863 match self.action.take() {
864 Some(TestChildAction::FailAfter(delay)) => {
865 this.post_after(this, TestChildCommand::Fail, delay);
866 }
867 Some(TestChildAction::DrainAfter(delay, tag)) => {
868 this.post_after(this, TestChildCommand::Drain(tag), delay);
869 }
870 None => {}
871 }
872 Ok(())
873 }
874
875 async fn handle_stop(
876 &mut self,
877 this: &Instance<Self>,
878 mode: StopMode,
879 reason: &str,
880 ) -> anyhow::Result<()> {
881 self.stopped.post(this, reason.to_string());
882 this.close();
883 match mode {
884 StopMode::Stop => this.exit(reason)?,
885 StopMode::DrainAndStop => this.exit_after_drain(reason)?,
886 }
887 Ok(())
888 }
889 }
890
891 #[async_trait]
892 impl Handler<TestChildCommand> for TestChild {
893 async fn handle(
894 &mut self,
895 cx: &Context<Self>,
896 message: TestChildCommand,
897 ) -> anyhow::Result<()> {
898 match message {
899 TestChildCommand::Fail => cx.kill("test child failed")?,
900 TestChildCommand::Drain(tag) => {
901 if let Some(ref port) = self.drain_observer {
902 port.post(cx, tag);
903 }
904 }
905 }
906 Ok(())
907 }
908 }
909
910 #[derive(Debug)]
911 struct Parent {
912 supervisor: Option<Supervisor>,
913 events: PortHandle<ActorSupervisionEvent>,
914 }
915
916 #[async_trait]
917 impl Actor for Parent {
918 async fn init(&mut self, this: &Instance<Self>) -> anyhow::Result<()> {
919 this.spawn(
920 self.supervisor
921 .take()
922 .expect("parent initialized more than once"),
923 );
924 Ok(())
925 }
926
927 async fn handle_supervision_event(
928 &mut self,
929 this: &Instance<Self>,
930 event: &ActorSupervisionEvent,
931 ) -> anyhow::Result<bool> {
932 self.events.post(this, event.clone());
933 Ok(true)
934 }
935 }
936
937 #[derive(Debug)]
938 #[hyperactor::export(KillParent)]
939 struct Grandparent {
940 parent: Option<Parent>,
941 parent_addr: PortHandle<ActorAddr>,
942 parent_handle: Option<ActorHandle<Parent>>,
943 events: PortHandle<ActorSupervisionEvent>,
944 }
945
946 #[async_trait]
947 impl Actor for Grandparent {
948 async fn init(&mut self, this: &Instance<Self>) -> anyhow::Result<()> {
949 let parent = this.spawn(
950 self.parent
951 .take()
952 .expect("grandparent initialized more than once"),
953 );
954 self.parent_addr.post(this, parent.actor_addr().clone());
955 self.parent_handle = Some(parent);
956 Ok(())
957 }
958
959 async fn handle_supervision_event(
960 &mut self,
961 this: &Instance<Self>,
962 event: &ActorSupervisionEvent,
963 ) -> anyhow::Result<bool> {
964 self.events.post(this, event.clone());
965 Ok(true)
966 }
967 }
968
969 #[async_trait]
970 impl Handler<KillParent> for Grandparent {
971 async fn handle(
972 &mut self,
973 _cx: &Context<Self>,
974 _message: KillParent,
975 ) -> anyhow::Result<()> {
976 self.parent_handle
977 .as_ref()
978 .expect("parent must be spawned before kill")
979 .kill("test parent killed")?;
980 Ok(())
981 }
982 }
983
984 fn test_child(
985 ready: PortHandle<ActorAddr>,
986 stopped: PortHandle<String>,
987 action: Option<TestChildAction>,
988 ) -> TestChild {
989 TestChild {
990 ready,
991 stopped,
992 action,
993 drain_observer: None,
994 }
995 }
996
997 async fn spawn_supervised_pair(
1005 proc: &Proc,
1006 inst: &hyperactor::Client,
1007 session_id: hyperactor::Uid,
1008 options: SupervisionOptions,
1009 child_action: Option<TestChildAction>,
1010 ) -> (
1011 ActorAddr, ActorHandle<Worker>, ActorHandle<Parent>, PortReceiver<String>, PortReceiver<ActorSupervisionEvent>, ) {
1017 let (ready, mut ready_rx) = inst.open_port::<ActorAddr>();
1020 let (stopped, stopped_rx) = inst.open_port::<String>();
1021 let (events, events_rx) = inst.open_port::<ActorSupervisionEvent>();
1022 let worker = proc.spawn(Worker::new());
1023 worker.post(inst, Spawn::new(test_child(ready, stopped, child_action)));
1024 let child_addr = ready_rx.recv().await.unwrap();
1025 let parent = proc.spawn(Parent {
1029 supervisor: Some(Supervisor::new_uid(
1030 worker.bind::<WorkerLike>(),
1031 KeepaliveLink::new(Duration::from_millis(5), Duration::from_secs(60)),
1032 options,
1033 session_id,
1034 )),
1035 events,
1036 });
1037 (child_addr, worker, parent, stopped_rx, events_rx)
1038 }
1039
1040 #[derive(Debug)]
1041 #[hyperactor::export(Supervise, WorkerCommand)]
1042 struct TestSlowWorker {
1043 link_started: PortHandle<()>,
1044 release_link: Arc<Notify>,
1045 received_stop: PortHandle<String>,
1046 session: Option<(hyperactor::Uid, PortRef<SupervisorEvent>)>,
1047 }
1048
1049 #[async_trait]
1050 impl Actor for TestSlowWorker {
1051 async fn init(&mut self, _this: &Instance<Self>) -> anyhow::Result<()> {
1052 Ok(())
1053 }
1054 }
1055
1056 #[async_trait]
1057 impl Handler<Supervise> for TestSlowWorker {
1058 async fn handle(&mut self, cx: &Context<Self>, message: Supervise) -> anyhow::Result<()> {
1059 self.session = Some((message.session_id, message.supervisor));
1060 self.link_started.post(cx, ());
1061 self.release_link.notified().await;
1062 Ok(())
1063 }
1064 }
1065
1066 #[async_trait]
1067 impl Handler<WorkerCommand> for TestSlowWorker {
1068 async fn handle(
1069 &mut self,
1070 cx: &Context<Self>,
1071 message: WorkerCommand,
1072 ) -> anyhow::Result<()> {
1073 if let WorkerCommand::Stop {
1074 session_id, reason, ..
1075 } = message
1076 {
1077 let Some((expected_session_id, supervisor)) = self.session.take() else {
1078 anyhow::bail!("stop received before supervise");
1079 };
1080 anyhow::ensure!(session_id == expected_session_id, "unexpected session id");
1081 self.received_stop.post(cx, reason.clone());
1082 let _ = supervisor.post(cx, SupervisorEvent::Unlinked { session_id, reason });
1083 }
1084 Ok(())
1085 }
1086 }
1087
1088 #[tokio::test]
1106 async fn test_child_failure_propagates_to_parent() {
1107 let proc = Proc::isolated();
1108 let client = proc.client("client");
1109 let session_id = Uid::anonymous();
1110
1111 let (child_addr, worker, parent, _stopped_rx, mut event_rx) = spawn_supervised_pair(
1112 &proc,
1113 &client,
1114 session_id.clone(),
1115 SupervisionOptions::default(),
1116 Some(TestChildAction::FailAfter(Duration::from_millis(200))),
1117 )
1118 .await;
1119
1120 let event = tokio::time::timeout(Duration::from_secs(5), event_rx.recv())
1121 .await
1122 .unwrap()
1123 .unwrap();
1124
1125 assert_eq!(event.actor_id, child_addr);
1126 assert!(matches!(event.actor_status, ActorStatus::Failed(_)));
1127
1128 parent.stop("test").unwrap();
1129 tokio::time::timeout(Duration::from_secs(5), parent)
1130 .await
1131 .unwrap();
1132 tokio::time::timeout(Duration::from_secs(5), worker)
1133 .await
1134 .unwrap();
1135 }
1136
1137 #[tokio::test]
1142 async fn test_supervisor_labels_replace_child_event_labels() {
1143 let proc = Proc::isolated();
1144 let client = proc.client("client");
1145 let (ready, mut ready_rx) = client.open_port::<ActorAddr>();
1146 let (stopped, _stopped_rx) = client.open_port::<String>();
1147 let (events, mut event_rx) = client.open_port::<ActorSupervisionEvent>();
1148 let worker = proc.spawn(Worker::new());
1149 worker.post(
1150 &client,
1151 Spawn::new(test_child(
1152 ready,
1153 stopped,
1154 Some(TestChildAction::FailAfter(Duration::from_millis(200))),
1155 )),
1156 );
1157 let child_addr = ready_rx.recv().await.unwrap();
1158 let parent = proc.spawn(Parent {
1159 supervisor: Some(
1160 Supervisor::new_uid(
1161 worker.bind::<WorkerLike>(),
1162 KeepaliveLink::new(Duration::from_millis(5), Duration::from_secs(60)),
1163 SupervisionOptions::default(),
1164 Uid::anonymous(),
1165 )
1166 .with_labels(test_labels(7)),
1167 ),
1168 events,
1169 });
1170
1171 let event = tokio::time::timeout(Duration::from_secs(5), event_rx.recv())
1172 .await
1173 .unwrap()
1174 .unwrap();
1175 assert_eq!(event.actor_id, child_addr);
1176 assert_eq!(
1177 event.labels.get(TEST_LABEL),
1178 Some(7),
1179 "the parent should observe the supervisor's labels"
1180 );
1181
1182 parent.stop("test").unwrap();
1183 tokio::time::timeout(Duration::from_secs(5), parent)
1184 .await
1185 .unwrap();
1186 tokio::time::timeout(Duration::from_secs(5), worker)
1187 .await
1188 .unwrap();
1189 }
1190
1191 #[tokio::test]
1195 async fn test_worker_labels_child_event() {
1196 let proc = Proc::isolated();
1197 let client = proc.client("client");
1198 let (ready, mut ready_rx) = client.open_port::<ActorAddr>();
1199 let (stopped, _stopped_rx) = client.open_port::<String>();
1200 let (supervisor, mut supervisor_rx) = client.open_port::<SupervisorEvent>();
1201 let worker = proc.spawn(Worker::new());
1202 worker.post(
1203 &client,
1204 Spawn::new(test_child(
1205 ready,
1206 stopped,
1207 Some(TestChildAction::FailAfter(Duration::from_millis(200))),
1208 )),
1209 );
1210 let (liveness_handle, liveness) =
1211 KeepaliveLink::new(Duration::from_secs(60), Duration::from_secs(60))
1212 .spawn_supervisor(&client)
1213 .unwrap();
1214 let child_addr = ready_rx.recv().await.unwrap();
1215
1216 worker.post(
1217 &client,
1218 Supervise {
1219 session_id: Uid::anonymous(),
1220 supervisor: supervisor.bind(),
1221 parent: client.self_addr().clone(),
1222 liveness,
1223 options: SupervisionOptions::default(),
1224 labels: test_labels(11),
1225 },
1226 );
1227 let linked = tokio::time::timeout(Duration::from_secs(5), supervisor_rx.recv())
1228 .await
1229 .unwrap()
1230 .unwrap();
1231 assert!(matches!(linked, SupervisorEvent::Linked { .. }));
1232
1233 let reported = tokio::time::timeout(Duration::from_secs(5), supervisor_rx.recv())
1234 .await
1235 .unwrap()
1236 .unwrap();
1237 let SupervisorEvent::SupervisionEvent { event, .. } = reported else {
1238 panic!("expected a supervision event, got {reported:?}");
1239 };
1240 assert_eq!(event.actor_id, child_addr);
1241 assert!(matches!(event.actor_status, ActorStatus::Failed(_)));
1242 assert_eq!(
1243 event.labels.get(TEST_LABEL),
1244 Some(11),
1245 "the worker should stamp the session's labels"
1246 );
1247
1248 tokio::time::timeout(Duration::from_secs(5), worker)
1249 .await
1250 .unwrap();
1251 liveness_handle.stop("test").unwrap();
1252 tokio::time::timeout(Duration::from_secs(5), liveness_handle)
1253 .await
1254 .unwrap();
1255 }
1256
1257 #[tokio::test]
1273 async fn test_parent_stop_stops_remote_child_before_parent_finishes() {
1274 let proc = Proc::isolated();
1275 let client = proc.client("client");
1276 let session_id = Uid::anonymous();
1277
1278 let (_child_addr, worker, parent, mut stopped_rx, _event_rx) = spawn_supervised_pair(
1279 &proc,
1280 &client,
1281 session_id.clone(),
1282 SupervisionOptions::default(),
1283 None,
1284 )
1285 .await;
1286
1287 tokio::time::sleep(Duration::from_millis(100)).await;
1288 parent.stop("test parent stopping").unwrap();
1289
1290 let reason = tokio::time::timeout(Duration::from_secs(5), stopped_rx.recv())
1291 .await
1292 .unwrap()
1293 .unwrap();
1294 assert_eq!(reason, "parent stopping");
1295
1296 tokio::time::timeout(Duration::from_secs(5), parent)
1297 .await
1298 .unwrap();
1299 tokio::time::timeout(Duration::from_secs(5), worker)
1300 .await
1301 .unwrap();
1302 }
1303
1304 #[tokio::test]
1319 async fn test_worker_undeliverable_supervisor_session_stops_child() {
1320 let proc = Proc::isolated();
1321 let client = proc.client("client");
1322 let (ready, mut ready_rx) = client.open_port::<ActorAddr>();
1323 let (stopped, mut stopped_rx) = client.open_port::<String>();
1324 let (supervisor, mut supervisor_rx) = client.open_port::<SupervisorEvent>();
1325 let supervisor_ref = supervisor.bind();
1326 let worker = proc.spawn(Worker::new());
1327 worker.post(&client, Spawn::new(test_child(ready, stopped, None)));
1328 let (liveness_handle, liveness) =
1329 KeepaliveLink::new(Duration::from_secs(60), Duration::from_secs(60))
1330 .spawn_supervisor(&client)
1331 .unwrap();
1332 let _child_addr = ready_rx.recv().await.unwrap();
1333 let session_id = hyperactor::Uid::anonymous();
1334
1335 worker.post(
1336 &client,
1337 Supervise {
1338 session_id: session_id.clone(),
1339 supervisor: supervisor_ref.clone(),
1340 parent: client.self_addr().clone(),
1341 liveness,
1342 options: SupervisionOptions::default(),
1343 labels: Flattrs::new(),
1344 },
1345 );
1346 let linked = tokio::time::timeout(Duration::from_secs(5), supervisor_rx.recv())
1347 .await
1348 .unwrap()
1349 .unwrap();
1350 assert!(matches!(linked, SupervisorEvent::Linked { .. }));
1351
1352 let envelope = MessageEnvelope::serialize(
1353 worker.actor_addr().clone(),
1354 supervisor_ref.port_addr().clone(),
1355 &SupervisorEvent::Unlinked {
1356 session_id,
1357 reason: "test".to_string(),
1358 },
1359 hyperactor_config::Flattrs::new(),
1360 )
1361 .unwrap();
1362 worker
1363 .port::<Undeliverable<MessageEnvelope>>()
1364 .post(&client, Undeliverable::Returned(envelope));
1365
1366 let reason = tokio::time::timeout(Duration::from_secs(5), stopped_rx.recv())
1367 .await
1368 .unwrap()
1369 .unwrap();
1370 assert_eq!(reason, "supervisor session undeliverable");
1371
1372 let status = tokio::time::timeout(Duration::from_secs(5), worker)
1373 .await
1374 .unwrap();
1375 assert!(matches!(status, ActorStatus::Stopped(_)));
1376
1377 liveness_handle.stop("test").unwrap();
1378 tokio::time::timeout(Duration::from_secs(5), liveness_handle)
1379 .await
1380 .unwrap();
1381 }
1382
1383 #[tokio::test]
1401 async fn test_parent_kill_stops_remote_child() {
1402 let proc = Proc::isolated();
1403 let client = proc.client("client");
1404 let (ready, mut ready_rx) = client.open_port::<ActorAddr>();
1405 let (stopped, mut stopped_rx) = client.open_port::<String>();
1406 let (events, mut event_rx) = client.open_port::<ActorSupervisionEvent>();
1407 let (parent_addr, mut parent_addr_rx) = client.open_port::<ActorAddr>();
1408 let worker = proc.spawn(Worker::new());
1409 worker.post(&client, Spawn::new(test_child(ready, stopped, None)));
1410 let grandparent = proc.spawn(Grandparent {
1411 parent: Some(Parent {
1412 supervisor: Some(Supervisor::new(
1413 worker.bind::<WorkerLike>(),
1414 KeepaliveLink::new(Duration::from_millis(100), Duration::from_millis(300)),
1415 SupervisionOptions::default(),
1416 )),
1417 events: events.clone(),
1418 }),
1419 parent_addr,
1420 parent_handle: None,
1421 events,
1422 });
1423 let _child_addr = ready_rx.recv().await.unwrap();
1424 let parent_addr = parent_addr_rx.recv().await.unwrap();
1425
1426 tokio::time::sleep(Duration::from_millis(100)).await;
1427 grandparent.post(&client, KillParent);
1428
1429 let reason = tokio::time::timeout(Duration::from_secs(5), stopped_rx.recv())
1430 .await
1431 .unwrap()
1432 .unwrap();
1433 assert_eq!(reason, "parent stopping");
1434
1435 let event = tokio::time::timeout(Duration::from_secs(5), event_rx.recv())
1436 .await
1437 .unwrap()
1438 .unwrap();
1439 assert_eq!(event.actor_id, parent_addr);
1440 assert!(matches!(event.actor_status, ActorStatus::Failed(_)));
1441
1442 grandparent.stop("test").unwrap();
1443 tokio::time::timeout(Duration::from_secs(5), grandparent)
1444 .await
1445 .unwrap();
1446 tokio::time::timeout(Duration::from_secs(5), worker)
1447 .await
1448 .unwrap();
1449 }
1450
1451 #[tokio::test]
1468 async fn test_detach_orphan_policy_leaves_child_running_on_supervisor_loss() {
1469 let proc = Proc::isolated();
1470 let inst = proc.client("inst");
1471 let (ready, mut ready_rx) = inst.open_port::<ActorAddr>();
1472 let (stopped, mut stopped_rx) = inst.open_port::<String>();
1473 let (supervisor, mut supervisor_rx) = inst.open_port::<SupervisorEvent>();
1474 let supervisor_ref = supervisor.bind();
1475 let worker = proc.spawn(Worker::new());
1476 worker.post(&inst, Spawn::new(test_child(ready, stopped, None)));
1477 let (keep_alive, link_spec) =
1478 KeepaliveLink::new(Duration::from_secs(60), Duration::from_secs(60))
1479 .spawn_supervisor(&inst)
1480 .unwrap();
1481 let _child_addr = ready_rx.recv().await.unwrap();
1482 let session_id = hyperactor::Uid::anonymous();
1483
1484 worker.post(
1485 &inst,
1486 Supervise {
1487 session_id: session_id.clone(),
1488 supervisor: supervisor_ref.clone(),
1489 parent: inst.self_addr().clone(),
1490 liveness: link_spec,
1491 options: SupervisionOptions {
1492 orphan_policy: OrphanPolicy::Detach,
1493 ..Default::default()
1494 },
1495 labels: Flattrs::new(),
1496 },
1497 );
1498 let linked = tokio::time::timeout(Duration::from_secs(5), supervisor_rx.recv())
1499 .await
1500 .unwrap()
1501 .unwrap();
1502 assert!(matches!(linked, SupervisorEvent::Linked { .. }));
1503
1504 let envelope = MessageEnvelope::serialize(
1509 worker.actor_addr().clone(),
1510 supervisor_ref.port_addr().clone(),
1511 &SupervisorEvent::Unlinked {
1512 session_id,
1513 reason: "test".to_string(),
1514 },
1515 hyperactor_config::Flattrs::new(),
1516 )
1517 .unwrap();
1518 worker
1519 .port::<Undeliverable<MessageEnvelope>>()
1520 .post(&inst, Undeliverable::Returned(envelope));
1521
1522 let timed_out = tokio::time::timeout(Duration::from_millis(500), stopped_rx.recv()).await;
1526 assert!(
1527 timed_out.is_err(),
1528 "child should not be stopped under OrphanPolicy::Detach"
1529 );
1530
1531 worker.stop("test").unwrap();
1534 let status = tokio::time::timeout(Duration::from_secs(5), worker)
1535 .await
1536 .unwrap();
1537 assert!(
1538 matches!(status, ActorStatus::Stopped(_)),
1539 "worker should accept clean stop after Detach orphan, got {:?}",
1540 status
1541 );
1542
1543 keep_alive.stop("test").unwrap();
1544 tokio::time::timeout(Duration::from_secs(5), keep_alive)
1545 .await
1546 .unwrap();
1547 }
1548
1549 #[tokio::test]
1570 async fn test_supervised_worker_unlink_stops_child_under_stop_policy() {
1571 let proc = Proc::isolated();
1572 let inst = proc.client("inst");
1573 let session_id = Uid::anonymous();
1574
1575 let (_child_addr, worker, parent, mut stopped_rx, mut events_rx) = spawn_supervised_pair(
1576 &proc,
1577 &inst,
1578 session_id.clone(),
1579 SupervisionOptions::default(),
1580 None,
1581 )
1582 .await;
1583
1584 tokio::time::sleep(Duration::from_millis(100)).await;
1587
1588 worker.post(
1589 &inst,
1590 WorkerCommand::Unlink {
1591 session_id: session_id.clone(),
1592 reason: "test unlink".to_string(),
1593 },
1594 );
1595
1596 let reason = tokio::time::timeout(Duration::from_secs(5), stopped_rx.recv())
1599 .await
1600 .unwrap()
1601 .unwrap();
1602 assert_eq!(reason, "test unlink");
1603
1604 let event = tokio::time::timeout(Duration::from_secs(5), events_rx.recv())
1608 .await
1609 .unwrap()
1610 .unwrap();
1611 match event.actor_status {
1612 ActorStatus::Stopped(reason) => assert_eq!(reason, "test unlink"),
1613 status => panic!("expected supervisor Stopped event, got {:?}", status),
1614 }
1615
1616 let status = tokio::time::timeout(Duration::from_secs(5), worker)
1621 .await
1622 .unwrap();
1623 assert!(matches!(status, ActorStatus::Stopped(_)));
1624
1625 parent.stop("test").unwrap();
1626 tokio::time::timeout(Duration::from_secs(5), parent)
1627 .await
1628 .unwrap();
1629 }
1630
1631 #[tokio::test]
1653 async fn test_supervised_worker_unlink_leaves_child_running_under_detach_policy() {
1654 let proc = Proc::isolated();
1655 let inst = proc.client("inst");
1656 let session_id = Uid::anonymous();
1657
1658 let (_child_addr, worker, parent, mut stopped_rx, mut events_rx) = spawn_supervised_pair(
1659 &proc,
1660 &inst,
1661 session_id.clone(),
1662 SupervisionOptions {
1663 orphan_policy: OrphanPolicy::Detach,
1664 ..Default::default()
1665 },
1666 None,
1667 )
1668 .await;
1669
1670 tokio::time::sleep(Duration::from_millis(100)).await;
1673
1674 worker.post(
1675 &inst,
1676 WorkerCommand::Unlink {
1677 session_id: session_id.clone(),
1678 reason: "test unlink".to_string(),
1679 },
1680 );
1681
1682 let event = tokio::time::timeout(Duration::from_secs(5), events_rx.recv())
1688 .await
1689 .unwrap()
1690 .unwrap();
1691 match event.actor_status {
1692 ActorStatus::Stopped(reason) => assert_eq!(reason, "test unlink"),
1693 status => panic!("expected supervisor Stopped event, got {:?}", status),
1694 }
1695
1696 let timed_out = tokio::time::timeout(Duration::from_millis(500), stopped_rx.recv()).await;
1700 assert!(
1701 timed_out.is_err(),
1702 "child should not be stopped under OrphanPolicy::Detach"
1703 );
1704
1705 worker.stop("test").unwrap();
1710 let status = tokio::time::timeout(Duration::from_secs(5), worker)
1711 .await
1712 .unwrap();
1713 assert!(
1714 matches!(status, ActorStatus::Stopped(_)),
1715 "worker should accept clean stop after Detach unlink, got {:?}",
1716 status
1717 );
1718
1719 parent.stop("test").unwrap();
1720 tokio::time::timeout(Duration::from_secs(5), parent)
1721 .await
1722 .unwrap();
1723 }
1724
1725 #[tokio::test]
1750 async fn test_concurrent_link_rejects_second_supervisor() {
1751 let proc = Proc::isolated();
1752 let inst = proc.client("inst");
1753 let session_id1 = Uid::anonymous();
1754 let (_child_addr, worker, parent1, _stopped_rx, mut events_rx1) = spawn_supervised_pair(
1755 &proc,
1756 &inst,
1757 session_id1.clone(),
1758 SupervisionOptions::default(),
1759 None,
1760 )
1761 .await;
1762
1763 tokio::time::sleep(Duration::from_millis(100)).await;
1767
1768 let session_id2 = Uid::anonymous();
1772 let (events2, mut events_rx2) = inst.open_port::<ActorSupervisionEvent>();
1773 let parent2 = proc.spawn_with_label(
1774 "parent2",
1775 Parent {
1776 supervisor: Some(Supervisor::new_uid(
1777 worker.bind::<WorkerLike>(),
1778 KeepaliveLink::new(Duration::from_millis(5), Duration::from_secs(60)),
1779 SupervisionOptions::default(),
1780 session_id2.clone(),
1781 )),
1782 events: events2,
1783 },
1784 );
1785
1786 let event = tokio::time::timeout(Duration::from_secs(5), events_rx2.recv())
1791 .await
1792 .unwrap()
1793 .unwrap();
1794 assert!(
1795 matches!(event.actor_status, ActorStatus::Failed(_)),
1796 "expected supervisor2 Failed event, got {:?}",
1797 event.actor_status
1798 );
1799 assert!(
1800 event
1801 .to_string()
1802 .contains("remote supervision request rejected: worker already supervised"),
1803 "expected SuperviseRejected bail string in failure message, got: {}",
1804 event
1805 );
1806
1807 let timed_out = tokio::time::timeout(Duration::from_millis(200), events_rx1.recv()).await;
1810 assert!(
1811 timed_out.is_err(),
1812 "first supervisor should remain linked, no failure expected"
1813 );
1814
1815 parent1.stop("test").unwrap();
1816 tokio::time::timeout(Duration::from_secs(5), parent1)
1817 .await
1818 .unwrap();
1819 tokio::time::timeout(Duration::from_secs(5), worker)
1823 .await
1824 .unwrap();
1825
1826 parent2.stop("test").unwrap();
1827 tokio::time::timeout(Duration::from_secs(5), parent2)
1828 .await
1829 .unwrap();
1830 }
1831
1832 #[tokio::test]
1854 async fn test_stop_before_linked_propagates_via_pending_stop() {
1855 let proc = Proc::isolated();
1856 let inst = proc.client("inst");
1857 let (link_started, mut link_started_rx) = inst.open_port::<()>();
1858 let (received_stop, mut received_stop_rx) = inst.open_port::<String>();
1859 let (events, _events_rx) = inst.open_port::<ActorSupervisionEvent>();
1860 let release_link = Arc::new(Notify::new());
1861 let worker = proc.spawn(TestSlowWorker {
1862 link_started,
1863 release_link: Arc::clone(&release_link),
1864 received_stop,
1865 session: None,
1866 });
1867 let session_id = Uid::anonymous();
1868 let parent = proc.spawn(Parent {
1869 supervisor: Some(Supervisor::new_uid(
1870 worker.bind::<WorkerLike>(),
1871 KeepaliveLink::new(Duration::from_millis(5), Duration::from_secs(60)),
1872 SupervisionOptions::default(),
1873 session_id.clone(),
1874 )),
1875 events,
1876 });
1877
1878 tokio::time::timeout(Duration::from_secs(5), link_started_rx.recv())
1880 .await
1881 .unwrap()
1882 .unwrap();
1883
1884 parent.stop("test stop").unwrap();
1889 release_link.notify_one();
1890
1891 let stop_reason = tokio::time::timeout(Duration::from_secs(5), received_stop_rx.recv())
1892 .await
1893 .unwrap()
1894 .unwrap();
1895 assert_eq!(stop_reason, "parent stopping");
1896
1897 tokio::time::timeout(Duration::from_secs(5), parent)
1898 .await
1899 .unwrap();
1900 worker.stop("test").unwrap();
1901 tokio::time::timeout(Duration::from_secs(5), worker)
1902 .await
1903 .unwrap();
1904 }
1905
1906 #[tokio::test]
1927 async fn test_drain_and_stop_propagates_through_worker_after_child_work() {
1928 let proc = Proc::isolated();
1929 let inst = proc.client("inst");
1930 let (ready, mut ready_rx) = inst.open_port::<ActorAddr>();
1931 let (stopped, mut stopped_rx) = inst.open_port::<String>();
1932 let (drained, mut drained_rx) = inst.open_port::<String>();
1933 let (events, _events_rx) = inst.open_port::<ActorSupervisionEvent>();
1934 let worker = proc.spawn(Worker::new());
1935 worker.post(
1936 &inst,
1937 Spawn::new(
1938 test_child(
1939 ready,
1940 stopped,
1941 Some(TestChildAction::DrainAfter(
1942 Duration::from_millis(10),
1943 "child work".to_string(),
1944 )),
1945 )
1946 .with_drain_observer(drained),
1947 ),
1948 );
1949 let _child_addr = ready_rx.recv().await.unwrap();
1950 let session_id = Uid::anonymous();
1951 let parent = proc.spawn(Parent {
1952 supervisor: Some(Supervisor::new_uid(
1953 worker.bind::<WorkerLike>(),
1954 KeepaliveLink::new(Duration::from_millis(5), Duration::from_secs(60)),
1955 SupervisionOptions::default(),
1956 session_id.clone(),
1957 )),
1958 events,
1959 });
1960
1961 let drained_tag = tokio::time::timeout(Duration::from_secs(5), drained_rx.recv())
1963 .await
1964 .unwrap()
1965 .unwrap();
1966 assert_eq!(drained_tag, "child work");
1967
1968 worker.post(
1970 &inst,
1971 WorkerCommand::Stop {
1972 session_id: session_id.clone(),
1973 mode: StopMode::DrainAndStop,
1974 reason: "drain test".to_string(),
1975 },
1976 );
1977
1978 let reason = tokio::time::timeout(Duration::from_secs(5), stopped_rx.recv())
1980 .await
1981 .unwrap()
1982 .unwrap();
1983 assert_eq!(reason, "drain test");
1984
1985 let status = tokio::time::timeout(Duration::from_secs(5), worker)
1987 .await
1988 .unwrap();
1989 assert!(matches!(status, ActorStatus::Stopped(_)));
1990
1991 parent.stop("test").unwrap();
1992 tokio::time::timeout(Duration::from_secs(5), parent)
1993 .await
1994 .unwrap();
1995 }
1996
1997 #[tokio::test]
2016 async fn test_worker_accepts_relink_after_unlink_under_detach_policy() {
2017 let proc = Proc::isolated();
2018 let inst = proc.client("inst");
2019 let session_id1 = Uid::anonymous();
2020 let (_child_addr, worker, parent1, mut stopped_rx, mut events_rx1) = spawn_supervised_pair(
2021 &proc,
2022 &inst,
2023 session_id1.clone(),
2024 SupervisionOptions {
2025 orphan_policy: OrphanPolicy::Detach,
2026 ..Default::default()
2027 },
2028 None,
2029 )
2030 .await;
2031
2032 tokio::time::sleep(Duration::from_millis(100)).await;
2035
2036 worker.post(
2037 &inst,
2038 WorkerCommand::Unlink {
2039 session_id: session_id1.clone(),
2040 reason: "first unlink".to_string(),
2041 },
2042 );
2043
2044 let event1 = tokio::time::timeout(Duration::from_secs(5), events_rx1.recv())
2045 .await
2046 .unwrap()
2047 .unwrap();
2048 match event1.actor_status {
2049 ActorStatus::Stopped(reason) => assert_eq!(reason, "first unlink"),
2050 status => panic!("expected supervisor1 Stopped event, got {:?}", status),
2051 }
2052
2053 let timed_out = tokio::time::timeout(Duration::from_millis(500), stopped_rx.recv()).await;
2054 assert!(
2055 timed_out.is_err(),
2056 "child should survive unlink under OrphanPolicy::Detach"
2057 );
2058
2059 let session_id2 = Uid::anonymous();
2060 let (events2, mut events_rx2) = inst.open_port::<ActorSupervisionEvent>();
2061 let parent2 = proc.spawn_with_label(
2062 "parent2",
2063 Parent {
2064 supervisor: Some(Supervisor::new_uid(
2065 worker.bind::<WorkerLike>(),
2066 KeepaliveLink::new(Duration::from_millis(5), Duration::from_secs(60)),
2067 SupervisionOptions {
2068 orphan_policy: OrphanPolicy::Detach,
2069 ..Default::default()
2070 },
2071 session_id2.clone(),
2072 )),
2073 events: events2,
2074 },
2075 );
2076
2077 tokio::time::sleep(Duration::from_millis(100)).await;
2079
2080 worker.post(
2081 &inst,
2082 WorkerCommand::Unlink {
2083 session_id: session_id2.clone(),
2084 reason: "second unlink".to_string(),
2085 },
2086 );
2087
2088 let event2 = tokio::time::timeout(Duration::from_secs(5), events_rx2.recv())
2089 .await
2090 .unwrap()
2091 .unwrap();
2092 match event2.actor_status {
2093 ActorStatus::Stopped(reason) => assert_eq!(reason, "second unlink"),
2094 status => panic!("expected supervisor2 Stopped event, got {:?}", status),
2095 }
2096
2097 parent1.stop("test").unwrap();
2098 tokio::time::timeout(Duration::from_secs(5), parent1)
2099 .await
2100 .unwrap();
2101 parent2.stop("test").unwrap();
2102 tokio::time::timeout(Duration::from_secs(5), parent2)
2103 .await
2104 .unwrap();
2105 worker.stop("test").unwrap();
2106 tokio::time::timeout(Duration::from_secs(5), worker)
2107 .await
2108 .unwrap();
2109 }
2110}