Skip to main content

hyperactor/
monitor.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Actor liveness monitoring.
10
11use std::future::Future;
12use std::sync::Arc;
13use std::sync::atomic::AtomicBool;
14use std::sync::atomic::AtomicUsize;
15use std::sync::atomic::Ordering;
16use std::time::Duration;
17
18use async_trait::async_trait;
19use derivative::Derivative;
20use serde::Deserialize;
21use serde::Serialize;
22use tokio::sync::watch;
23use tokio::time;
24use typeuri::Named;
25use uuid::Uuid;
26
27use crate::Actor;
28use crate::ActorAddr;
29use crate::ActorHandle;
30use crate::ActorRef;
31use crate::Context;
32use crate::Endpoint;
33use crate::EndpointLocation;
34use crate::Handler;
35use crate::Instance;
36use crate::Message;
37use crate::OncePortRef;
38use crate::Port;
39use crate::PortRef;
40use crate::RemoteMessage;
41use crate::StatusMessage;
42use crate::actor::ActorStatus;
43use crate::actor::Referable;
44use crate::context;
45use crate::mailbox::MailboxError;
46use crate::mailbox::OncePortHandle;
47use crate::mailbox::PortHandle;
48use crate::mailbox::PortReceiver;
49use crate::ordering::Sequencer;
50use crate::proc::DeliveryProgressResponse;
51use crate::supervision::local_fence;
52
53const DEFAULT_INITIAL_DELAY: Duration = Duration::from_secs(2);
54const DEFAULT_POLL_INTERVAL: Duration = Duration::from_secs(1);
55const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(5);
56const DEFAULT_DELIVERY_TIMEOUT: Duration = Duration::from_secs(30);
57
58/// The current state of an actor monitor.
59#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Named)]
60pub enum MonitorStatus {
61    /// The monitor has not completed its first status request.
62    Checking,
63    /// The monitored actor responded with a non-terminal status.
64    Alive(ActorStatus),
65    /// The monitor detected a terminal condition.
66    Failed(MonitorFailure),
67}
68wirevalue::register_type!(MonitorStatus);
69
70/// A failure detected by an actor monitor.
71#[derive(
72    thiserror::Error,
73    Debug,
74    Clone,
75    Serialize,
76    Deserialize,
77    PartialEq,
78    Eq,
79    Named
80)]
81pub enum MonitorFailure {
82    /// The monitored actor does not exist in the actor runtime.
83    #[error("monitored actor {actor_id} is gone")]
84    ActorGone {
85        /// The monitored actor.
86        actor_id: ActorAddr,
87    },
88    /// The monitored actor stopped normally.
89    #[error("monitored actor {actor_id} stopped: {status}")]
90    ActorStopped {
91        /// The monitored actor.
92        actor_id: ActorAddr,
93        /// The actor's terminal status.
94        status: ActorStatus,
95    },
96    /// The monitored actor failed.
97    #[error("monitored actor {actor_id} failed: {status}")]
98    ActorFailed {
99        /// The monitored actor.
100        actor_id: ActorAddr,
101        /// The actor's terminal status.
102        status: ActorStatus,
103    },
104    /// The status request did not complete before the monitor timeout.
105    #[error("status request to monitored actor {actor_id} timed out after {timeout_millis}ms")]
106    StatusRequestTimedOut {
107        /// The monitored actor.
108        actor_id: ActorAddr,
109        /// The timeout, in milliseconds.
110        timeout_millis: u64,
111    },
112    /// The status reply port closed before a reply arrived.
113    #[error("status reply from monitored actor {actor_id} closed")]
114    StatusReplyClosed {
115        /// The monitored actor.
116        actor_id: ActorAddr,
117    },
118    /// The monitored actor has not made delivery progress for messages from `from`.
119    #[error(
120        "delivery progress to monitored actor {actor_id} from {from} stalled for {timeout:?}: largest_sent={largest_sent}, largest_dequeueable={largest_dequeueable}"
121    )]
122    DeliveryProgressStalled {
123        /// The monitored actor.
124        actor_id: ActorAddr,
125        /// The actor that sent the monitored messages.
126        from: ActorAddr,
127        /// Largest sequence sent by `from`.
128        largest_sent: u64,
129        /// Largest sequence released into the monitored actor's work queue.
130        largest_dequeueable: u64,
131        /// The delivery progress timeout.
132        timeout: Duration,
133    },
134    /// The delivery progress request did not complete before the monitor timeout.
135    #[error(
136        "delivery progress request to monitored actor {actor_id} from {from} timed out after {timeout:?}"
137    )]
138    DeliveryProgressRequestTimedOut {
139        /// The monitored actor.
140        actor_id: ActorAddr,
141        /// The actor that sent the monitored messages.
142        from: ActorAddr,
143        /// The delivery progress request timeout.
144        timeout: Duration,
145    },
146    /// The monitor actor stopped before reporting a monitored failure.
147    #[error("monitor for actor {actor_id} stopped before reporting a failure")]
148    MonitorStopped {
149        /// The monitored actor.
150        actor_id: ActorAddr,
151    },
152}
153wirevalue::register_type!(MonitorFailure);
154
155impl MonitorFailure {
156    /// The monitored actor this failure concerns. Every variant carries it.
157    pub fn actor_id(&self) -> &ActorAddr {
158        match self {
159            MonitorFailure::ActorGone { actor_id }
160            | MonitorFailure::ActorStopped { actor_id, .. }
161            | MonitorFailure::ActorFailed { actor_id, .. }
162            | MonitorFailure::StatusRequestTimedOut { actor_id, .. }
163            | MonitorFailure::StatusReplyClosed { actor_id }
164            | MonitorFailure::DeliveryProgressStalled { actor_id, .. }
165            | MonitorFailure::DeliveryProgressRequestTimedOut { actor_id, .. }
166            | MonitorFailure::MonitorStopped { actor_id } => actor_id,
167        }
168    }
169}
170
171/// Structured metadata for synthetic supervision events.
172#[derive(
173    thiserror::Error,
174    Debug,
175    Clone,
176    Serialize,
177    Deserialize,
178    Derivative,
179    Named
180)]
181#[derivative(PartialEq, Eq)]
182#[error("synthetic supervision event for {subject}: {failure}")]
183pub struct SyntheticSupervision {
184    /// The actor whose liveness failure caused the synthetic event.
185    pub subject: ActorAddr,
186    /// The monitor failure that caused the event.
187    pub failure: Box<MonitorFailure>,
188    #[serde(skip, default = "local_fence")]
189    #[derivative(PartialEq = "ignore")]
190    pub(crate) local_fence: Arc<AtomicBool>,
191}
192wirevalue::register_type!(SyntheticSupervision);
193
194/// A handle to a child actor that monitors another actor's liveness.
195#[derive(Debug)]
196pub struct ActorMonitor {
197    inner: Option<MonitorInner>,
198}
199
200/// A monitor that reports detected failures through actor supervision.
201#[derive(Debug)]
202pub struct ActorSupervisor {
203    inner: Option<MonitorInner>,
204}
205
206#[derive(Debug)]
207struct MonitorInner {
208    target: ActorAddr,
209    handle: ActorHandle<MonitorActor>,
210    status: watch::Receiver<MonitorStatus>,
211    cancelled: Arc<AtomicBool>,
212    activations: Arc<AtomicUsize>,
213}
214
215/// Keeps an [`ActorMonitor`] active; see [`ActorMonitor::activate`].
216#[derive(Debug)]
217#[must_use = "the monitor goes dormant once its last activation is dropped"]
218pub struct MonitorActivation {
219    activations: Arc<AtomicUsize>,
220}
221
222impl Drop for MonitorActivation {
223    fn drop(&mut self) {
224        // The monitor actor observes the release at its next tick, so dropping
225        // needs no actor context.
226        self.activations.fetch_sub(1, Ordering::AcqRel);
227    }
228}
229
230/// Actor-local endpoint identifier used for delivery progress.
231#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Named)]
232pub enum EndpointId {
233    /// Monitor all handler ports owned by an actor.
234    Handler,
235    /// Monitor one concrete port.
236    Port(Port),
237}
238wirevalue::register_type!(EndpointId);
239
240impl EndpointId {
241    fn for_location(location: &EndpointLocation) -> Self {
242        match location {
243            EndpointLocation::Actor(_) | EndpointLocation::Local { .. } => Self::Handler,
244            EndpointLocation::Port(port) => Self::Port(port.port()),
245        }
246    }
247
248    fn largest_sent(&self, sequencer: &Sequencer, actor: &ActorAddr) -> u64 {
249        match self {
250            Self::Handler => sequencer.last_sent_to_actor(actor),
251            Self::Port(port) => sequencer
252                .last_sent(&actor.port_addr(port.clone()))
253                .unwrap_or_default(),
254        }
255    }
256}
257
258#[derive(Debug, Clone)]
259struct DeliveryMonitor {
260    from: ActorAddr,
261    sequencer: Sequencer,
262    endpoint: EndpointId,
263    timeout: Duration,
264    last_largest_dequeueable: u64,
265    last_progress_at: Option<time::Instant>,
266}
267
268impl DeliveryMonitor {
269    fn new(from: ActorAddr, sequencer: Sequencer, endpoint: EndpointId, timeout: Duration) -> Self {
270        Self {
271            from,
272            sequencer,
273            endpoint,
274            timeout,
275            last_largest_dequeueable: 0,
276            last_progress_at: None,
277        }
278    }
279}
280
281/// An endpoint whose owning actor can be monitored.
282pub trait MonitorableEndpoint {
283    /// Spawn a monitor actor for this endpoint's owning actor as a child of `cx`.
284    fn monitor<C>(&self, cx: &C) -> ActorMonitor
285    where
286        C: context::Actor,
287    {
288        let location = self.monitored_location();
289        let target = location.actor_addr();
290        ActorMonitor::spawn_with_delivery(
291            cx,
292            target,
293            DeliveryMonitor::new(
294                context::Mailbox::mailbox(cx).actor_addr().clone(),
295                cx.instance().sequencer().clone(),
296                EndpointId::for_location(&location),
297                DEFAULT_DELIVERY_TIMEOUT,
298            ),
299        )
300    }
301
302    /// The endpoint location monitored for liveness and delivery progress.
303    fn monitored_location(&self) -> EndpointLocation;
304}
305
306impl<T> MonitorableEndpoint for &T
307where
308    T: MonitorableEndpoint + ?Sized,
309{
310    fn monitor<C>(&self, cx: &C) -> ActorMonitor
311    where
312        C: context::Actor,
313    {
314        (*self).monitor(cx)
315    }
316
317    fn monitored_location(&self) -> EndpointLocation {
318        (*self).monitored_location()
319    }
320}
321
322impl<A> MonitorableEndpoint for ActorHandle<A>
323where
324    A: Actor,
325{
326    fn monitored_location(&self) -> EndpointLocation {
327        EndpointLocation::Actor(self.actor_addr().clone())
328    }
329}
330
331impl<A> MonitorableEndpoint for ActorRef<A>
332where
333    A: Referable,
334{
335    fn monitored_location(&self) -> EndpointLocation {
336        EndpointLocation::Actor(self.actor_addr().clone())
337    }
338}
339
340impl<M> MonitorableEndpoint for PortHandle<M>
341where
342    M: Message,
343{
344    fn monitored_location(&self) -> EndpointLocation {
345        self.location().into()
346    }
347}
348
349impl<M> MonitorableEndpoint for OncePortHandle<M>
350where
351    M: Message,
352{
353    fn monitored_location(&self) -> EndpointLocation {
354        EndpointLocation::Port(self.port_addr().clone())
355    }
356}
357
358impl<M> MonitorableEndpoint for PortRef<M>
359where
360    M: RemoteMessage,
361{
362    fn monitored_location(&self) -> EndpointLocation {
363        EndpointLocation::Port(self.port_addr().clone())
364    }
365}
366
367impl<M> MonitorableEndpoint for OncePortRef<M>
368where
369    M: RemoteMessage,
370{
371    fn monitored_location(&self) -> EndpointLocation {
372        EndpointLocation::Port(self.port_addr().clone())
373    }
374}
375
376impl ActorMonitor {
377    /// Spawn a persistent monitor actor for `target` as a child of `cx`.
378    ///
379    /// A persistent monitor polls the target's status for as long as it runs.
380    pub fn spawn_persistent<C>(cx: &C, target: ActorAddr) -> Self
381    where
382        C: context::Actor,
383    {
384        Self::spawn_with_timings(
385            cx,
386            target,
387            DEFAULT_INITIAL_DELAY,
388            DEFAULT_POLL_INTERVAL,
389            DEFAULT_REQUEST_TIMEOUT,
390            None,
391            true,
392        )
393    }
394
395    /// Spawn a monitor actor for `target` as a child of `cx`, starting dormant.
396    ///
397    /// The monitor is active, polling the target's status, while at least one
398    /// [`MonitorActivation`] from [`Self::activate`] is live, and dormant
399    /// otherwise. It receives the target's terminal-status notification in
400    /// either state. This lets callers share one monitor across intermittent
401    /// waits without polling between them.
402    pub fn spawn_dormant<C>(cx: &C, target: ActorAddr) -> Self
403    where
404        C: context::Actor,
405    {
406        Self::spawn_with_timings(
407            cx,
408            target,
409            DEFAULT_INITIAL_DELAY,
410            DEFAULT_POLL_INTERVAL,
411            DEFAULT_REQUEST_TIMEOUT,
412            None,
413            false,
414        )
415    }
416
417    fn spawn_with_delivery<C>(cx: &C, target: ActorAddr, delivery: DeliveryMonitor) -> Self
418    where
419        C: context::Actor,
420    {
421        Self::spawn_with_timings(
422            cx,
423            target,
424            DEFAULT_INITIAL_DELAY,
425            DEFAULT_POLL_INTERVAL,
426            DEFAULT_REQUEST_TIMEOUT,
427            Some(delivery),
428            true,
429        )
430    }
431
432    fn spawn_with_timings<C>(
433        cx: &C,
434        target: ActorAddr,
435        initial_delay: Duration,
436        poll_interval: Duration,
437        request_timeout: Duration,
438        delivery: Option<DeliveryMonitor>,
439        persistent: bool,
440    ) -> Self
441    where
442        C: context::Actor,
443    {
444        let cancelled = Arc::new(AtomicBool::new(false));
445        // A persistent monitor holds one activation that is never released, so
446        // it is never dormant.
447        let activations = Arc::new(AtomicUsize::new(usize::from(persistent)));
448        let (status_tx, status) = watch::channel(MonitorStatus::Checking);
449        let handle = cx.spawn_with_label(
450            "monitor",
451            MonitorActor {
452                target: target.clone(),
453                initial_delay,
454                poll_interval,
455                request_timeout,
456                status_tx,
457                cancelled: cancelled.clone(),
458                state: MonitorState::Idle,
459                supervised: false,
460                delivery,
461                terminal_status_subscription: None,
462                activations: activations.clone(),
463            },
464        );
465        Self {
466            inner: Some(MonitorInner {
467                target,
468                handle,
469                status,
470                cancelled,
471                activations,
472            }),
473        }
474    }
475
476    /// The actor being monitored.
477    pub fn target(&self) -> &ActorAddr {
478        &self.inner().target
479    }
480
481    /// Return the monitor's current status.
482    pub fn status(&self) -> MonitorStatus {
483        self.inner().status.borrow().clone()
484    }
485
486    async fn wait_for_failure(&self) -> MonitorFailure {
487        let target = self.inner().target.clone();
488        let mut status = self.inner().status.clone();
489        loop {
490            if let MonitorStatus::Failed(failure) = &*status.borrow() {
491                return failure.clone();
492            }
493            if status.changed().await.is_err() {
494                return MonitorFailure::MonitorStopped { actor_id: target };
495            }
496        }
497    }
498
499    /// Keep the monitor active, polling the target's status, until the returned
500    /// activation is dropped.
501    ///
502    /// Activations are counted; the monitor is active while any is live. When
503    /// a dormant monitor becomes active, the first poll waits for the initial
504    /// delay, so short-lived waits that the terminal-status notification
505    /// resolves never poll. The monitor goes dormant at its first tick after the
506    /// last activation is dropped and sends no further polls; reactivating it
507    /// before then keeps its current poll schedule.
508    pub fn activate(&self, cx: &impl context::Actor) -> MonitorActivation {
509        let inner = self.inner();
510        if inner.activations.fetch_add(1, Ordering::AcqRel) == 0 {
511            inner.handle.post(cx, MonitorCommand::Activate);
512        }
513        MonitorActivation {
514            activations: inner.activations.clone(),
515        }
516    }
517
518    /// Convert this monitor into one that reports detected failures through actor supervision.
519    pub fn into_supervisor<C>(mut self, cx: &C) -> ActorSupervisor
520    where
521        C: context::Actor,
522    {
523        let inner = self.inner.take().expect("monitor inner should be present");
524        inner.handle.post(cx, MonitorCommand::Supervise);
525        ActorSupervisor { inner: Some(inner) }
526    }
527
528    /// Run `fut` until it completes or the monitor fails.
529    pub async fn guard<F>(&self, fut: F) -> Result<F::Output, MonitorFailure>
530    where
531        F: Future,
532    {
533        tokio::pin!(fut);
534        tokio::select! {
535            result = fut => Ok(result),
536            failure = self.wait_for_failure() => Err(failure),
537        }
538    }
539
540    /// Receive the next message from `receiver` or return a monitor failure.
541    pub async fn recv<M>(
542        &self,
543        receiver: &mut PortReceiver<M>,
544    ) -> Result<Result<M, MailboxError>, MonitorFailure>
545    where
546        M: Message,
547    {
548        self.guard(receiver.recv()).await
549    }
550
551    fn inner(&self) -> &MonitorInner {
552        self.inner
553            .as_ref()
554            .expect("monitor inner should be present")
555    }
556}
557
558impl Drop for ActorMonitor {
559    fn drop(&mut self) {
560        if let Some(inner) = self.inner.take() {
561            inner.cancelled.store(true, Ordering::Release);
562            let _ = inner.handle.stop("monitor dropped");
563        }
564    }
565}
566
567impl Drop for ActorSupervisor {
568    fn drop(&mut self) {
569        if let Some(inner) = self.inner.take() {
570            inner.cancelled.store(true, Ordering::Release);
571            let _ = inner.handle.stop("supervisor dropped");
572        }
573    }
574}
575
576#[derive(Debug)]
577struct MonitorActor {
578    target: ActorAddr,
579    initial_delay: Duration,
580    poll_interval: Duration,
581    request_timeout: Duration,
582    status_tx: watch::Sender<MonitorStatus>,
583    cancelled: Arc<AtomicBool>,
584    state: MonitorState,
585    supervised: bool,
586    delivery: Option<DeliveryMonitor>,
587    terminal_status_subscription: Option<Uuid>,
588    activations: Arc<AtomicUsize>,
589}
590
591/// The [`MonitorActor`]'s polling state.
592#[derive(Debug)]
593enum MonitorState {
594    /// The monitor has no scheduled or outstanding work.
595    Idle,
596    /// The monitor should poll at or after `deadline`.
597    PollAfter { deadline: time::Instant },
598    /// A [`MonitorPollActor`] is outstanding.
599    Polling,
600    /// A failure was recorded. The monitor no longer polls.
601    Failed(Box<MonitorFailure>),
602}
603
604/// A level-triggered prompt to reconcile the monitor with its current state.
605/// Stale or duplicate ticks are harmless.
606#[derive(Debug)]
607struct MonitorTick;
608
609#[derive(Debug)]
610struct MonitorPollActor {
611    target: ActorAddr,
612    request_timeout: Duration,
613    monitor: ActorHandle<MonitorActor>,
614    state: MonitorPollState,
615}
616
617#[derive(Debug)]
618enum MonitorPollState {
619    Start {
620        delivery: Option<DeliveryPollRequest>,
621    },
622    WaitingForStatus {
623        delivery: Option<PendingDeliveryPoll>,
624        _reply: PortHandle<Option<ActorStatus>>,
625    },
626    WaitingForDelivery {
627        status: Option<ActorStatus>,
628        delivery: DeliveryPollRequest,
629        _reply: PortHandle<DeliveryProgressResponse>,
630    },
631    Done,
632}
633
634#[derive(Debug)]
635struct PendingDeliveryPoll {
636    request: DeliveryPollRequest,
637    reply: PortHandle<DeliveryProgressResponse>,
638}
639
640#[derive(Debug, Clone)]
641struct DeliveryPollRequest {
642    from: ActorAddr,
643    sequencer: Sequencer,
644    endpoint: EndpointId,
645}
646
647#[cfg(test)]
648#[derive(Debug)]
649struct MonitorProbe {
650    reply: crate::OncePortRef<()>,
651}
652
653#[derive(Debug, Clone, Serialize, Deserialize, Named)]
654enum MonitorCommand {
655    Supervise,
656    Activate,
657}
658wirevalue::register_type!(MonitorCommand);
659
660#[derive(Debug)]
661enum MonitorPollResult {
662    Status {
663        status: Option<ActorStatus>,
664        delivery: Option<DeliveryPollResult>,
665    },
666    TimedOut,
667    DeliveryTimedOut {
668        from: ActorAddr,
669    },
670}
671
672#[derive(Debug)]
673struct MonitorPollTimeout;
674
675#[derive(Debug)]
676struct MonitorStatusReply(Option<ActorStatus>);
677
678#[derive(Debug)]
679struct MonitorTerminalStatus(Option<ActorStatus>);
680
681#[derive(Debug)]
682struct MonitorDeliveryReply(DeliveryProgressResponse);
683
684#[derive(Debug)]
685struct DeliveryPollResult {
686    from: ActorAddr,
687    largest_sent: u64,
688    response: DeliveryProgressResponse,
689}
690
691#[async_trait]
692impl Actor for MonitorActor {
693    async fn init(&mut self, this: &Instance<Self>) -> anyhow::Result<()> {
694        let subscriber = this
695            .port::<MonitorTerminalStatus>()
696            .contramap(MonitorTerminalStatus)
697            .bind();
698        let subscription_id = Uuid::new_v4();
699        let mut status_port = self.target.status_port();
700        status_port.return_undeliverable(false);
701        status_port.post(
702            this,
703            StatusMessage::SubscribeTerminal {
704                subscription_id,
705                subscriber,
706            },
707        );
708        self.terminal_status_subscription = Some(subscription_id);
709        self.reconcile(this, time::Instant::now())
710    }
711
712    async fn cleanup(
713        &mut self,
714        this: &Instance<Self>,
715        _err: Option<&crate::actor::ActorError>,
716    ) -> anyhow::Result<()> {
717        let Some(subscription_id) = self.terminal_status_subscription.take() else {
718            return Ok(());
719        };
720        let mut status_port = self.target.status_port();
721        status_port.return_undeliverable(false);
722        status_port.post(this, StatusMessage::UnsubscribeTerminal { subscription_id });
723        Ok(())
724    }
725}
726
727#[async_trait]
728impl Actor for MonitorPollActor {
729    async fn init(&mut self, this: &Instance<Self>) -> anyhow::Result<()> {
730        self.request_status(this);
731        Ok(())
732    }
733}
734
735#[async_trait]
736impl Handler<MonitorStatusReply> for MonitorPollActor {
737    async fn handle(
738        &mut self,
739        cx: &Context<Self>,
740        MonitorStatusReply(status): MonitorStatusReply,
741    ) -> anyhow::Result<()> {
742        let MonitorPollState::WaitingForStatus { delivery, .. } =
743            std::mem::replace(&mut self.state, MonitorPollState::Done)
744        else {
745            panic!("monitor poll actor received status while not waiting for status");
746        };
747        match delivery {
748            Some(delivery) => {
749                self.state = MonitorPollState::WaitingForDelivery {
750                    status,
751                    delivery: delivery.request,
752                    _reply: delivery.reply,
753                };
754                Ok(())
755            }
756            None => self.finish(
757                cx,
758                MonitorPollResult::Status {
759                    status,
760                    delivery: None,
761                },
762            ),
763        }
764    }
765}
766
767#[async_trait]
768impl Handler<MonitorDeliveryReply> for MonitorPollActor {
769    async fn handle(
770        &mut self,
771        cx: &Context<Self>,
772        MonitorDeliveryReply(response): MonitorDeliveryReply,
773    ) -> anyhow::Result<()> {
774        let MonitorPollState::WaitingForDelivery {
775            status, delivery, ..
776        } = std::mem::replace(&mut self.state, MonitorPollState::Done)
777        else {
778            panic!("monitor poll actor received delivery progress while not waiting for it");
779        };
780        let delivery = DeliveryPollResult {
781            largest_sent: delivery
782                .endpoint
783                .largest_sent(&delivery.sequencer, &self.target),
784            from: delivery.from,
785            response,
786        };
787        self.finish(
788            cx,
789            MonitorPollResult::Status {
790                status,
791                delivery: Some(delivery),
792            },
793        )
794    }
795}
796
797#[async_trait]
798impl Handler<MonitorPollTimeout> for MonitorPollActor {
799    async fn handle(
800        &mut self,
801        cx: &Context<Self>,
802        _timeout: MonitorPollTimeout,
803    ) -> anyhow::Result<()> {
804        let result = match &self.state {
805            MonitorPollState::WaitingForStatus { .. } => Some(MonitorPollResult::TimedOut),
806            MonitorPollState::WaitingForDelivery { delivery, .. } => {
807                Some(MonitorPollResult::DeliveryTimedOut {
808                    from: delivery.from.clone(),
809                })
810            }
811            _ => None,
812        };
813
814        match result {
815            Some(result) => self.finish(cx, result),
816            None => Ok(()),
817        }
818    }
819}
820
821impl MonitorPollActor {
822    fn request_status(&mut self, this: &Instance<Self>) {
823        let MonitorPollState::Start { delivery } =
824            std::mem::replace(&mut self.state, MonitorPollState::Done)
825        else {
826            panic!("monitor poll actor must start in the start state");
827        };
828        let reply = this
829            .port::<MonitorStatusReply>()
830            .contramap(MonitorStatusReply);
831        let reply_ref = reply.bind().into_once();
832        let (delivery_request, delivery) = match delivery {
833            Some(delivery) => {
834                let delivery_reply = this
835                    .port::<MonitorDeliveryReply>()
836                    .contramap(MonitorDeliveryReply);
837                let delivery_reply_ref = delivery_reply.bind().into_once();
838                (
839                    Some((
840                        delivery.sequencer.session_id(),
841                        delivery.endpoint.clone(),
842                        delivery_reply_ref,
843                    )),
844                    Some(PendingDeliveryPoll {
845                        request: delivery,
846                        reply: delivery_reply,
847                    }),
848                )
849            }
850            None => (None, None),
851        };
852        self.state = MonitorPollState::WaitingForStatus {
853            delivery,
854            _reply: reply,
855        };
856        self.target.status_port().post(
857            this,
858            StatusMessage::GetStatus {
859                reply: reply_ref,
860                delivery: delivery_request,
861            },
862        );
863        this.post_after(this, MonitorPollTimeout, self.request_timeout);
864    }
865
866    fn finish(&mut self, cx: &Context<Self>, result: MonitorPollResult) -> anyhow::Result<()> {
867        self.state = MonitorPollState::Done;
868        self.monitor.post(cx, result);
869        cx.exit("poll complete").map_err(anyhow::Error::from)
870    }
871}
872
873#[async_trait]
874impl Handler<MonitorTerminalStatus> for MonitorActor {
875    async fn handle(
876        &mut self,
877        cx: &Context<Self>,
878        MonitorTerminalStatus(status): MonitorTerminalStatus,
879    ) -> anyhow::Result<()> {
880        if matches!(self.state, MonitorState::Failed(_)) {
881            return Ok(());
882        }
883        let failure = status.map_or_else(
884            || MonitorFailure::ActorGone {
885                actor_id: self.target.clone(),
886            },
887            |status| {
888                self.classify_failure(status)
889                    .expect("terminal status notification must contain a terminal status")
890            },
891        );
892        self.record_failure(failure);
893        self.reconcile(cx, time::Instant::now())
894    }
895}
896
897#[async_trait]
898impl Handler<MonitorTick> for MonitorActor {
899    async fn handle(&mut self, cx: &Context<Self>, _message: MonitorTick) -> anyhow::Result<()> {
900        self.reconcile(cx, time::Instant::now())
901    }
902}
903
904#[async_trait]
905impl Handler<MonitorPollResult> for MonitorActor {
906    async fn handle(
907        &mut self,
908        cx: &Context<Self>,
909        message: MonitorPollResult,
910    ) -> anyhow::Result<()> {
911        if !matches!(self.state, MonitorState::Polling) {
912            return Ok(());
913        }
914
915        let now = time::Instant::now();
916        let poll_result = match message {
917            MonitorPollResult::Status { status, delivery } => {
918                let Some(status) = status else {
919                    self.record_failure(MonitorFailure::ActorGone {
920                        actor_id: self.target.clone(),
921                    });
922                    return self.reconcile(cx, now);
923                };
924
925                if let Some(failure) = self.classify_failure(status.clone()) {
926                    Err(failure)
927                } else if let Some(delivery) = delivery {
928                    if matches!(delivery.response, DeliveryProgressResponse::ActorGone) {
929                        Err(MonitorFailure::ActorGone {
930                            actor_id: self.target.clone(),
931                        })
932                    } else if matches!(delivery.response, DeliveryProgressResponse::Incomplete) {
933                        self.status_tx.send_replace(MonitorStatus::Alive(status));
934                        Ok(())
935                    } else if let Some(failure) = self.classify_delivery_failure(delivery, now) {
936                        Err(failure)
937                    } else {
938                        self.status_tx.send_replace(MonitorStatus::Alive(status));
939                        Ok(())
940                    }
941                } else {
942                    self.status_tx.send_replace(MonitorStatus::Alive(status));
943                    Ok(())
944                }
945            }
946            MonitorPollResult::TimedOut => Err(MonitorFailure::StatusRequestTimedOut {
947                actor_id: self.target.clone(),
948                timeout_millis: self.request_timeout.as_millis() as u64,
949            }),
950            MonitorPollResult::DeliveryTimedOut { from } => {
951                Err(MonitorFailure::DeliveryProgressRequestTimedOut {
952                    actor_id: self.target.clone(),
953                    from,
954                    timeout: self.request_timeout,
955                })
956            }
957        };
958
959        match poll_result {
960            Ok(()) => {
961                self.poll_after(cx, now, self.poll_interval);
962                Ok(())
963            }
964            Err(failure) => {
965                self.record_failure(failure);
966                self.reconcile(cx, now)
967            }
968        }
969    }
970}
971
972#[cfg(test)]
973#[async_trait]
974impl Handler<MonitorProbe> for MonitorActor {
975    async fn handle(&mut self, cx: &Context<Self>, message: MonitorProbe) -> anyhow::Result<()> {
976        message.reply.post(cx, ());
977        Ok(())
978    }
979}
980
981#[cfg(test)]
982#[async_trait]
983impl Handler<MonitorProbe> for MonitorPollActor {
984    async fn handle(&mut self, cx: &Context<Self>, message: MonitorProbe) -> anyhow::Result<()> {
985        message.reply.post(cx, ());
986        Ok(())
987    }
988}
989
990#[async_trait]
991impl Handler<MonitorCommand> for MonitorActor {
992    async fn handle(&mut self, cx: &Context<Self>, message: MonitorCommand) -> anyhow::Result<()> {
993        if matches!(message, MonitorCommand::Supervise) {
994            self.supervised = true;
995        }
996        self.reconcile(cx, time::Instant::now())
997    }
998}
999
1000impl MonitorActor {
1001    fn is_active(&self) -> bool {
1002        self.activations.load(Ordering::Acquire) > 0
1003    }
1004
1005    /// Reconcile the monitor after any event, doing nothing unless its current
1006    /// state admits a transition.
1007    fn reconcile(&mut self, this: &Instance<Self>, now: time::Instant) -> anyhow::Result<()> {
1008        match &self.state {
1009            MonitorState::Idle => {
1010                if self.is_active() {
1011                    self.poll_after(this, now, self.initial_delay);
1012                }
1013                Ok(())
1014            }
1015            MonitorState::PollAfter { deadline } => {
1016                if now < *deadline {
1017                    return Ok(());
1018                }
1019                if self.is_active() {
1020                    self.start_poll(this);
1021                } else {
1022                    self.state = MonitorState::Idle;
1023                }
1024                Ok(())
1025            }
1026            MonitorState::Polling => Ok(()),
1027            MonitorState::Failed(failure)
1028                if self.supervised && !self.cancelled.load(Ordering::Acquire) =>
1029            {
1030                self.fail_supervised((**failure).clone())
1031            }
1032            MonitorState::Failed(_) => Ok(()),
1033        }
1034    }
1035
1036    fn poll_after(&mut self, this: &Instance<Self>, now: time::Instant, delay: Duration) {
1037        let deadline = now + delay;
1038        self.state = MonitorState::PollAfter { deadline };
1039        this.post_after(
1040            this,
1041            MonitorTick,
1042            deadline.saturating_duration_since(time::Instant::now()),
1043        );
1044    }
1045
1046    fn start_poll(&mut self, this: &Instance<Self>) {
1047        self.state = MonitorState::Polling;
1048        this.spawn_with_label(
1049            "monitor_poll",
1050            MonitorPollActor {
1051                target: self.target.clone(),
1052                request_timeout: self.request_timeout,
1053                monitor: this.handle(),
1054                state: MonitorPollState::Start {
1055                    delivery: self.delivery.as_ref().map(|delivery| DeliveryPollRequest {
1056                        from: delivery.from.clone(),
1057                        sequencer: delivery.sequencer.clone(),
1058                        endpoint: delivery.endpoint.clone(),
1059                    }),
1060                },
1061            },
1062        );
1063    }
1064
1065    fn classify_failure(&self, status: ActorStatus) -> Option<MonitorFailure> {
1066        match status {
1067            ActorStatus::Stopped(_) => Some(MonitorFailure::ActorStopped {
1068                actor_id: self.target.clone(),
1069                status,
1070            }),
1071            ActorStatus::Failed(_)
1072            | ActorStatus::Stopping(crate::actor::ActorStoppingReason::Zombie(_))
1073            | ActorStatus::Unknown => Some(MonitorFailure::ActorFailed {
1074                actor_id: self.target.clone(),
1075                status,
1076            }),
1077            ActorStatus::Created
1078            | ActorStatus::Initializing
1079            | ActorStatus::Client
1080            | ActorStatus::Idle
1081            | ActorStatus::Processing(_, _)
1082            | ActorStatus::Stopping(_) => None,
1083        }
1084    }
1085
1086    fn classify_delivery_failure(
1087        &mut self,
1088        result: DeliveryPollResult,
1089        now: time::Instant,
1090    ) -> Option<MonitorFailure> {
1091        let delivery = self
1092            .delivery
1093            .as_mut()
1094            .expect("delivery poll result requires delivery monitor state");
1095        let DeliveryProgressResponse::Progress(progress) = result.response else {
1096            return None;
1097        };
1098
1099        let largest_dequeueable = progress.largest_dequeueable_sequence;
1100        let is_caught_up = largest_dequeueable >= result.largest_sent;
1101        let made_progress = largest_dequeueable > delivery.last_largest_dequeueable;
1102
1103        if is_caught_up || made_progress {
1104            delivery.last_largest_dequeueable = largest_dequeueable;
1105            delivery.last_progress_at = Some(now);
1106            return None;
1107        }
1108
1109        if largest_dequeueable < result.largest_sent {
1110            let last_progress_at = delivery.last_progress_at.get_or_insert(now);
1111            if now.duration_since(*last_progress_at) >= delivery.timeout {
1112                return Some(MonitorFailure::DeliveryProgressStalled {
1113                    actor_id: self.target.clone(),
1114                    from: result.from,
1115                    largest_sent: result.largest_sent,
1116                    largest_dequeueable,
1117                    timeout: delivery.timeout,
1118                });
1119            }
1120        }
1121
1122        None
1123    }
1124
1125    fn record_failure(&mut self, failure: MonitorFailure) {
1126        self.state = MonitorState::Failed(Box::new(failure.clone()));
1127        self.status_tx.send_replace(MonitorStatus::Failed(failure));
1128    }
1129
1130    fn fail_supervised(&self, failure: MonitorFailure) -> anyhow::Result<()> {
1131        anyhow::bail!(crate::actor::ActorErrorKind::SyntheticSupervision(
1132            Box::new(SyntheticSupervision {
1133                subject: self.target.clone(),
1134                failure: Box::new(failure),
1135                local_fence: self.cancelled.clone(),
1136            },)
1137        ))
1138    }
1139}
1140
1141#[cfg(test)]
1142mod tests {
1143    use std::time::Duration;
1144
1145    use async_trait::async_trait;
1146    use tokio::time;
1147
1148    use super::*;
1149    use crate as hyperactor;
1150    use crate::Proc;
1151    use crate::actor::ActorErrorKind;
1152    use crate::config;
1153    use crate::ordering::DeliveryProgress;
1154    use crate::port::Port;
1155    use crate::supervision::ActorSupervisionEvent;
1156
1157    #[derive(Debug, typeuri::Named)]
1158    struct TestActor;
1159
1160    #[async_trait]
1161    impl Actor for TestActor {}
1162
1163    impl crate::actor::Referable for TestActor {}
1164
1165    #[derive(Debug, Clone, Serialize, Deserialize, Named)]
1166    struct DeliveryTestMsg;
1167
1168    #[derive(Debug)]
1169    #[hyperactor::export(handlers = [DeliveryTestMsg])]
1170    struct DeliveryTestActor;
1171
1172    #[async_trait]
1173    impl Actor for DeliveryTestActor {}
1174
1175    #[async_trait]
1176    impl Handler<DeliveryTestMsg> for DeliveryTestActor {
1177        async fn handle(
1178            &mut self,
1179            _cx: &Context<Self>,
1180            _message: DeliveryTestMsg,
1181        ) -> anyhow::Result<()> {
1182            Ok(())
1183        }
1184    }
1185
1186    #[derive(Debug)]
1187    struct SupervisorActor {
1188        target: ActorAddr,
1189        ready: Option<crate::OncePortRef<ActorAddr>>,
1190        events: crate::PortRef<ActorSupervisionEvent>,
1191        supervisor: Option<ActorSupervisor>,
1192    }
1193
1194    #[derive(Debug)]
1195    struct ConvertFailedMonitorActor {
1196        target: ActorAddr,
1197        ready: Option<crate::OncePortRef<ActorAddr>>,
1198        events: crate::PortRef<ActorSupervisionEvent>,
1199        supervisor: Option<ActorSupervisor>,
1200    }
1201
1202    #[derive(Debug)]
1203    struct DropSupervisorActor {
1204        target: ActorAddr,
1205        ready: Option<crate::OncePortRef<ActorAddr>>,
1206        events: crate::PortRef<ActorSupervisionEvent>,
1207    }
1208
1209    #[derive(Debug)]
1210    struct DropQueuedSupervisorActor {
1211        target: ActorHandle<TestActor>,
1212        ready: Option<crate::OncePortRef<()>>,
1213        events: crate::PortRef<ActorSupervisionEvent>,
1214    }
1215
1216    #[async_trait]
1217    impl Actor for SupervisorActor {
1218        async fn init(&mut self, this: &Instance<Self>) -> anyhow::Result<()> {
1219            let monitor = ActorMonitor::spawn_with_timings(
1220                this,
1221                self.target.clone(),
1222                Duration::ZERO,
1223                Duration::from_millis(10),
1224                Duration::from_millis(50),
1225                None,
1226                true,
1227            );
1228            let monitor_id = monitor
1229                .inner
1230                .as_ref()
1231                .expect("monitor inner should be present")
1232                .handle
1233                .actor_addr()
1234                .clone();
1235            self.supervisor = Some(monitor.into_supervisor(this));
1236            self.ready
1237                .take()
1238                .expect("ready port should be present")
1239                .post(this, monitor_id);
1240            Ok(())
1241        }
1242
1243        async fn handle_supervision_event(
1244            &mut self,
1245            this: &Instance<Self>,
1246            event: &ActorSupervisionEvent,
1247        ) -> anyhow::Result<bool> {
1248            self.events.post(this, event.clone());
1249            Ok(true)
1250        }
1251    }
1252
1253    #[async_trait]
1254    impl Actor for ConvertFailedMonitorActor {
1255        async fn init(&mut self, this: &Instance<Self>) -> anyhow::Result<()> {
1256            let monitor = ActorMonitor::spawn_with_timings(
1257                this,
1258                self.target.clone(),
1259                Duration::ZERO,
1260                Duration::from_millis(10),
1261                Duration::from_millis(50),
1262                None,
1263                true,
1264            );
1265            let monitor_id = monitor
1266                .inner
1267                .as_ref()
1268                .expect("monitor inner should be present")
1269                .handle
1270                .actor_addr()
1271                .clone();
1272            let _failure = monitor.wait_for_failure().await;
1273            self.supervisor = Some(monitor.into_supervisor(this));
1274            self.ready
1275                .take()
1276                .expect("ready port should be present")
1277                .post(this, monitor_id);
1278            Ok(())
1279        }
1280
1281        async fn handle_supervision_event(
1282            &mut self,
1283            this: &Instance<Self>,
1284            event: &ActorSupervisionEvent,
1285        ) -> anyhow::Result<bool> {
1286            self.events.post(this, event.clone());
1287            Ok(true)
1288        }
1289    }
1290
1291    #[async_trait]
1292    impl Actor for DropSupervisorActor {
1293        async fn init(&mut self, this: &Instance<Self>) -> anyhow::Result<()> {
1294            let monitor = ActorMonitor::spawn_with_timings(
1295                this,
1296                self.target.clone(),
1297                Duration::ZERO,
1298                Duration::from_millis(10),
1299                Duration::from_millis(50),
1300                None,
1301                true,
1302            );
1303            let monitor_id = monitor
1304                .inner
1305                .as_ref()
1306                .expect("monitor inner should be present")
1307                .handle
1308                .actor_addr()
1309                .clone();
1310            drop(monitor.into_supervisor(this));
1311            self.ready
1312                .take()
1313                .expect("ready port should be present")
1314                .post(this, monitor_id);
1315            Ok(())
1316        }
1317
1318        async fn handle_supervision_event(
1319            &mut self,
1320            this: &Instance<Self>,
1321            event: &ActorSupervisionEvent,
1322        ) -> anyhow::Result<bool> {
1323            self.events.post(this, event.clone());
1324            Ok(true)
1325        }
1326    }
1327
1328    #[async_trait]
1329    impl Actor for DropQueuedSupervisorActor {
1330        async fn init(&mut self, this: &Instance<Self>) -> anyhow::Result<()> {
1331            let monitor = ActorMonitor::spawn_with_timings(
1332                this,
1333                self.target.actor_addr().clone(),
1334                Duration::ZERO,
1335                Duration::from_millis(10),
1336                Duration::from_millis(50),
1337                None,
1338                true,
1339            );
1340            let supervisor = monitor.into_supervisor(this);
1341            let mut status = supervisor
1342                .inner
1343                .as_ref()
1344                .expect("supervisor inner should be present")
1345                .status
1346                .clone();
1347
1348            self.target.drain_and_stop("done").unwrap();
1349
1350            loop {
1351                if matches!(*status.borrow(), MonitorStatus::Failed(_)) {
1352                    break;
1353                }
1354                status.changed().await?;
1355            }
1356
1357            drop(supervisor);
1358            self.ready
1359                .take()
1360                .expect("ready port should be present")
1361                .post(this, ());
1362            Ok(())
1363        }
1364
1365        async fn handle_supervision_event(
1366            &mut self,
1367            this: &Instance<Self>,
1368            event: &ActorSupervisionEvent,
1369        ) -> anyhow::Result<bool> {
1370            self.events.post(this, event.clone());
1371            Ok(true)
1372        }
1373    }
1374
1375    fn short_monitor(client: &crate::Client, target: ActorAddr) -> ActorMonitor {
1376        ActorMonitor::spawn_with_timings(
1377            client,
1378            target,
1379            Duration::ZERO,
1380            Duration::from_millis(10),
1381            Duration::from_millis(50),
1382            None,
1383            true,
1384        )
1385    }
1386
1387    fn short_delivery_monitor(client: &crate::Client, target: ActorAddr) -> ActorMonitor {
1388        ActorMonitor::spawn_with_timings(
1389            client,
1390            target.clone(),
1391            Duration::ZERO,
1392            Duration::from_millis(10),
1393            Duration::from_millis(50),
1394            Some(DeliveryMonitor::new(
1395                context::Mailbox::mailbox(client).actor_addr().clone(),
1396                client.sequencer().clone(),
1397                EndpointId::Handler,
1398                Duration::from_millis(50),
1399            )),
1400            true,
1401        )
1402    }
1403
1404    fn monitor_actor_for_delivery(target: ActorAddr, delivery: DeliveryMonitor) -> MonitorActor {
1405        let (status_tx, _status_rx) = watch::channel(MonitorStatus::Checking);
1406        MonitorActor {
1407            target,
1408            initial_delay: Duration::ZERO,
1409            poll_interval: Duration::from_millis(10),
1410            request_timeout: Duration::from_millis(50),
1411            status_tx,
1412            cancelled: Arc::new(AtomicBool::new(false)),
1413            state: MonitorState::Idle,
1414            supervised: false,
1415            delivery: Some(delivery),
1416            terminal_status_subscription: None,
1417            activations: Arc::new(AtomicUsize::new(1)),
1418        }
1419    }
1420
1421    async fn wait_for_alive(monitor: &ActorMonitor) -> ActorStatus {
1422        let deadline = time::Instant::now() + Duration::from_secs(5);
1423        loop {
1424            if let MonitorStatus::Alive(status) = monitor.status() {
1425                return status;
1426            }
1427            assert!(
1428                time::Instant::now() < deadline,
1429                "timed out waiting for monitor to report alive"
1430            );
1431            time::sleep(Duration::from_millis(10)).await;
1432        }
1433    }
1434
1435    #[tokio::test]
1436    async fn test_monitor_reports_alive_actor() {
1437        let proc = Proc::isolated();
1438        let client = proc.client("client");
1439        let handle = proc.spawn(TestActor);
1440        let monitor = short_monitor(&client, handle.actor_addr().clone());
1441
1442        assert!(matches!(
1443            wait_for_alive(&monitor).await,
1444            ActorStatus::Idle | ActorStatus::Processing(_, _)
1445        ));
1446    }
1447
1448    #[tokio::test]
1449    async fn test_monitor_reports_nonexistent_actor() {
1450        let config = hyperactor_config::global::lock();
1451        let _config_guard = config.override_key(
1452            crate::config::PENDING_ACTOR_DELIVERY_TIMEOUT,
1453            Duration::ZERO,
1454        );
1455        let proc = Proc::isolated();
1456        let client = proc.client("client");
1457        let missing = proc.proc_addr().actor_addr("missing");
1458        let monitor = short_monitor(&client, missing.clone());
1459
1460        assert_eq!(
1461            monitor.wait_for_failure().await,
1462            MonitorFailure::ActorGone { actor_id: missing }
1463        );
1464    }
1465
1466    #[tokio::test]
1467    async fn test_monitor_fallback_poll_respects_initial_delay() {
1468        let proc = Proc::isolated();
1469        let client = proc.client("client");
1470        let unreachable =
1471            crate::ProcAddr::instance(crate::channel::ChannelAddr::Local(1234), "gone")
1472                .actor_addr("actor");
1473        let monitor = ActorMonitor::spawn_with_timings(
1474            &client,
1475            unreachable.clone(),
1476            Duration::from_millis(100),
1477            Duration::from_millis(10),
1478            Duration::from_millis(50),
1479            None,
1480            true,
1481        );
1482
1483        assert!(
1484            time::timeout(Duration::from_millis(20), monitor.wait_for_failure())
1485                .await
1486                .is_err()
1487        );
1488        assert_eq!(
1489            monitor.wait_for_failure().await,
1490            MonitorFailure::StatusRequestTimedOut {
1491                actor_id: unreachable,
1492                timeout_millis: 50,
1493            }
1494        );
1495    }
1496
1497    #[test]
1498    fn test_default_initial_delay_is_two_seconds() {
1499        assert_eq!(DEFAULT_INITIAL_DELAY, Duration::from_secs(2));
1500    }
1501
1502    #[tokio::test]
1503    async fn test_dropping_monitor_before_first_tick_is_noop() {
1504        let proc = Proc::isolated();
1505        let client = proc.client("client");
1506        let missing = proc.proc_addr().actor_addr("missing");
1507        let monitor = ActorMonitor::spawn_with_timings(
1508            &client,
1509            missing,
1510            Duration::from_millis(200),
1511            Duration::from_millis(10),
1512            Duration::from_millis(50),
1513            None,
1514            true,
1515        );
1516        let mut status = monitor
1517            .inner
1518            .as_ref()
1519            .expect("monitor inner should be present")
1520            .status
1521            .clone();
1522
1523        drop(monitor);
1524
1525        time::timeout(Duration::from_secs(1), async {
1526            while status.changed().await.is_ok() {}
1527        })
1528        .await
1529        .expect("monitor actor should stop when monitor handle is dropped");
1530        assert_eq!(*status.borrow(), MonitorStatus::Checking);
1531
1532        time::sleep(Duration::from_millis(250)).await;
1533        assert_eq!(*status.borrow(), MonitorStatus::Checking);
1534    }
1535
1536    #[tokio::test]
1537    async fn test_monitor_reports_stopped_actor() {
1538        let proc = Proc::isolated();
1539        let client = proc.client("client");
1540        let handle = proc.spawn(TestActor);
1541        let actor_id = handle.actor_addr().clone();
1542        let monitor = short_monitor(&client, actor_id.clone());
1543
1544        handle.drain_and_stop("done").unwrap();
1545
1546        assert_eq!(
1547            monitor.wait_for_failure().await,
1548            MonitorFailure::ActorStopped {
1549                actor_id,
1550                status: ActorStatus::Stopped("done".to_string()),
1551            }
1552        );
1553    }
1554
1555    #[tokio::test]
1556    async fn test_default_monitor_uses_terminal_status_notification() {
1557        let proc = Proc::isolated();
1558        let client = proc.client("client");
1559        let handle = proc.spawn(TestActor);
1560        let actor_id = handle.actor_addr().clone();
1561        let monitor = ActorMonitor::spawn_persistent(&client, actor_id.clone());
1562        let mut monitor_actor_status = monitor
1563            .inner
1564            .as_ref()
1565            .expect("monitor inner should be present")
1566            .handle
1567            .status();
1568        monitor_actor_status
1569            .wait_for(|status| matches!(status, ActorStatus::Idle))
1570            .await
1571            .expect("monitor actor should finish initialization");
1572        assert_eq!(
1573            proc.terminal_status_subscriber_count(handle.actor_addr().id()),
1574            1,
1575            "monitor initialization should register its status subscriber"
1576        );
1577
1578        handle.drain_and_stop("done").unwrap();
1579
1580        assert_eq!(
1581            time::timeout(Duration::from_millis(200), monitor.wait_for_failure())
1582                .await
1583                .expect("terminal status notification should beat the initial poll delay"),
1584            MonitorFailure::ActorStopped {
1585                actor_id,
1586                status: ActorStatus::Stopped("done".to_string()),
1587            }
1588        );
1589    }
1590
1591    #[tokio::test]
1592    async fn test_dormant_monitor_observes_terminal_status() {
1593        let proc = Proc::isolated();
1594        let client = proc.client("client");
1595        let handle = proc.spawn(TestActor);
1596        let actor_id = handle.actor_addr().clone();
1597        let monitor = ActorMonitor::spawn_dormant(&client, actor_id.clone());
1598        monitor
1599            .inner()
1600            .handle
1601            .status()
1602            .wait_for(|status| matches!(status, ActorStatus::Idle))
1603            .await
1604            .expect("monitor actor should finish initialization");
1605
1606        handle.drain_and_stop("done").unwrap();
1607
1608        let expected = MonitorFailure::ActorStopped {
1609            actor_id,
1610            status: ActorStatus::Stopped("done".to_string()),
1611        };
1612        assert_eq!(
1613            time::timeout(Duration::from_secs(1), monitor.wait_for_failure())
1614                .await
1615                .expect("dormant monitor should still receive terminal status"),
1616            expected
1617        );
1618
1619        // Reactivation applies the initial delay only to polling; a recorded
1620        // failure is returned immediately.
1621        let _activation = monitor.activate(&client);
1622        assert_eq!(
1623            time::timeout(Duration::from_millis(100), monitor.wait_for_failure())
1624                .await
1625                .expect("failed monitor should resolve without waiting for a poll"),
1626            expected
1627        );
1628    }
1629
1630    #[tokio::test]
1631    async fn test_monitor_polls_only_while_active() {
1632        let proc = Proc::isolated();
1633        let client = proc.client("client");
1634        let unreachable =
1635            crate::ProcAddr::instance(crate::channel::ChannelAddr::Local(1234), "gone")
1636                .actor_addr("actor");
1637        let monitor = ActorMonitor::spawn_with_timings(
1638            &client,
1639            unreachable.clone(),
1640            Duration::from_millis(100),
1641            Duration::from_millis(10),
1642            Duration::from_millis(50),
1643            None,
1644            false,
1645        );
1646
1647        monitor.inner().handle.post(&client, MonitorTick);
1648
1649        // Only polling can detect an unreachable target, so no failure means no
1650        // poll was sent. A tick while idle must not change that.
1651        assert!(
1652            time::timeout(Duration::from_millis(300), monitor.wait_for_failure())
1653                .await
1654                .is_err(),
1655            "dormant monitor should not poll"
1656        );
1657
1658        // Release the activation after its first tick is scheduled but before it
1659        // fires; the tick must find no activation and return to dormancy.
1660        let activation = monitor.activate(&client);
1661        monitor.inner().handle.post(&client, MonitorTick);
1662        time::sleep(Duration::from_millis(20)).await;
1663        drop(activation);
1664        assert!(
1665            time::timeout(Duration::from_millis(300), monitor.wait_for_failure())
1666                .await
1667                .is_err(),
1668            "monitor should not poll after its last activation is released"
1669        );
1670
1671        let _activation = monitor.activate(&client);
1672        assert_eq!(
1673            time::timeout(Duration::from_secs(5), monitor.wait_for_failure())
1674                .await
1675                .expect("reactivated monitor should poll"),
1676            MonitorFailure::StatusRequestTimedOut {
1677                actor_id: unreachable,
1678                timeout_millis: 50,
1679            }
1680        );
1681    }
1682
1683    #[tokio::test]
1684    async fn test_monitor_guard_returns_operation_success() {
1685        let proc = Proc::isolated();
1686        let client = proc.client("client");
1687        let handle = proc.spawn(TestActor);
1688        let monitor = short_monitor(&client, handle.actor_addr().clone());
1689
1690        let result: Result<Result<u64, &'static str>, MonitorFailure> =
1691            monitor.guard(async { Ok(123u64) }).await;
1692
1693        assert_eq!(result, Ok(Ok(123)));
1694    }
1695
1696    #[tokio::test]
1697    async fn test_monitor_guard_returns_monitor_failure() {
1698        let config = hyperactor_config::global::lock();
1699        let _config_guard = config.override_key(
1700            crate::config::PENDING_ACTOR_DELIVERY_TIMEOUT,
1701            Duration::ZERO,
1702        );
1703        let proc = Proc::isolated();
1704        let client = proc.client("client");
1705        let missing = proc.proc_addr().actor_addr("missing");
1706        let monitor = short_monitor(&client, missing.clone());
1707
1708        let result: Result<Result<(), &'static str>, MonitorFailure> =
1709            monitor.guard(std::future::pending()).await;
1710
1711        assert_eq!(result, Err(MonitorFailure::ActorGone { actor_id: missing }));
1712    }
1713
1714    #[tokio::test]
1715    async fn test_actor_ref_is_monitorable() {
1716        let proc = Proc::isolated();
1717        let client = proc.client("client");
1718        let handle = proc.spawn(TestActor);
1719        let actor_ref = ActorRef::<TestActor>::attest(handle.actor_addr().clone());
1720        let monitor = actor_ref.monitor(&client);
1721
1722        assert_eq!(monitor.target(), handle.actor_addr());
1723        assert!(matches!(
1724            wait_for_alive(&monitor).await,
1725            ActorStatus::Idle | ActorStatus::Processing(_, _)
1726        ));
1727    }
1728
1729    #[tokio::test]
1730    async fn test_ports_are_monitorable_by_owner_actor() {
1731        let proc = Proc::isolated();
1732        let client = proc.client("client");
1733        let (port, _rx) = client.open_port::<u64>();
1734        let port_ref = port.bind();
1735        let (once_port, _once_rx) = client.open_once_port::<u64>();
1736        let once_port_ref = once_port.bind();
1737
1738        assert_eq!(port.monitor(&client).target(), client.self_addr());
1739        assert_eq!(port_ref.monitor(&client).target(), client.self_addr());
1740        assert_eq!(once_port_ref.monitor(&client).target(), client.self_addr());
1741    }
1742
1743    #[tokio::test]
1744    async fn test_monitor_recv_returns_message() {
1745        let proc = Proc::isolated();
1746        let client = proc.client("client");
1747        let handle = proc.spawn(TestActor);
1748        let monitor = handle.monitor(&client);
1749        let (port, mut rx) = client.open_port::<u64>();
1750
1751        port.post(&client, 123);
1752
1753        assert!(matches!(monitor.recv(&mut rx).await, Ok(Ok(123))));
1754    }
1755
1756    #[tokio::test]
1757    async fn test_monitor_recv_returns_monitor_failure() {
1758        let proc = Proc::isolated();
1759        let client = proc.client("client");
1760        let handle = proc.spawn(TestActor);
1761        let actor_id = handle.actor_addr().clone();
1762        let monitor = handle.monitor(&client);
1763        let (_port, mut rx) = client.open_port::<u64>();
1764
1765        handle.drain_and_stop("done").unwrap();
1766
1767        match monitor.recv(&mut rx).await {
1768            Err(MonitorFailure::ActorStopped {
1769                actor_id: failed_actor_id,
1770                status,
1771            }) => {
1772                assert_eq!(failed_actor_id, actor_id);
1773                assert_eq!(status, ActorStatus::Stopped("done".to_string()));
1774            }
1775            other => panic!("expected monitor failure, got {other:?}"),
1776        }
1777    }
1778
1779    #[test]
1780    fn test_delivery_progress_advancing_while_behind_does_not_fail() {
1781        let proc = Proc::isolated();
1782        let target = proc.proc_addr().actor_addr("target");
1783        let from = proc.proc_addr().actor_addr("sender");
1784        let delivery = DeliveryMonitor::new(
1785            from.clone(),
1786            Sequencer::new(uuid::Uuid::now_v7()),
1787            EndpointId::Handler,
1788            Duration::from_millis(50),
1789        );
1790        let mut monitor = monitor_actor_for_delivery(target, delivery);
1791        let delivery = monitor
1792            .delivery
1793            .as_mut()
1794            .expect("delivery monitor should be present");
1795        let now = time::Instant::now();
1796        delivery.last_largest_dequeueable = 1;
1797        delivery.last_progress_at = Some(now - Duration::from_millis(100));
1798
1799        assert!(
1800            monitor
1801                .classify_delivery_failure(
1802                    DeliveryPollResult {
1803                        from,
1804                        largest_sent: 10,
1805                        response: DeliveryProgressResponse::Progress(DeliveryProgress {
1806                            largest_dequeueable_sequence: 2,
1807                        }),
1808                    },
1809                    now
1810                )
1811                .is_none()
1812        );
1813    }
1814
1815    #[test]
1816    fn test_delivery_progress_increasing_sent_only_does_not_reset_stall() {
1817        let proc = Proc::isolated();
1818        let target = proc.proc_addr().actor_addr("target");
1819        let from = proc.proc_addr().actor_addr("sender");
1820        let delivery = DeliveryMonitor::new(
1821            from.clone(),
1822            Sequencer::new(uuid::Uuid::now_v7()),
1823            EndpointId::Handler,
1824            Duration::from_millis(50),
1825        );
1826        let mut monitor = monitor_actor_for_delivery(target.clone(), delivery);
1827        let delivery = monitor
1828            .delivery
1829            .as_mut()
1830            .expect("delivery monitor should be present");
1831        let now = time::Instant::now();
1832        delivery.last_largest_dequeueable = 1;
1833        delivery.last_progress_at = Some(now - Duration::from_millis(100));
1834
1835        assert_eq!(
1836            monitor.classify_delivery_failure(
1837                DeliveryPollResult {
1838                    from: from.clone(),
1839                    largest_sent: 10,
1840                    response: DeliveryProgressResponse::Progress(DeliveryProgress {
1841                        largest_dequeueable_sequence: 1,
1842                    }),
1843                },
1844                now
1845            ),
1846            Some(MonitorFailure::DeliveryProgressStalled {
1847                actor_id: target,
1848                from,
1849                largest_sent: 10,
1850                largest_dequeueable: 1,
1851                timeout: Duration::from_millis(50),
1852            })
1853        );
1854    }
1855
1856    #[tokio::test]
1857    async fn test_healthy_bound_port_delivery_does_not_stall() {
1858        let config = hyperactor_config::global::lock();
1859        let _g = config.override_key(config::ENABLE_DEST_ACTOR_REORDERING_BUFFER, true);
1860
1861        let proc = Proc::isolated();
1862        let client = proc.client("client");
1863        let (port, mut rx) = client.open_port::<u64>();
1864        let port_ref = port.bind();
1865        let delivery_timeout = Duration::from_millis(50);
1866
1867        let monitor = ActorMonitor::spawn_with_timings(
1868            &client,
1869            port_ref.port_addr().actor_addr(),
1870            Duration::ZERO,
1871            Duration::from_millis(10),
1872            Duration::from_millis(50),
1873            Some(DeliveryMonitor::new(
1874                context::Mailbox::mailbox(&client).actor_addr().clone(),
1875                client.sequencer().clone(),
1876                EndpointId::Port(port_ref.port_addr().port()),
1877                delivery_timeout,
1878            )),
1879            true,
1880        );
1881
1882        port_ref.post(&client, 123);
1883        assert_eq!(rx.recv().await.expect("port delivery should succeed"), 123);
1884
1885        if let Ok(failure) = time::timeout(delivery_timeout * 3, monitor.wait_for_failure()).await {
1886            panic!("healthy port delivery was reported as failed: {failure:?}");
1887        }
1888    }
1889
1890    #[tokio::test]
1891    async fn test_monitor_reports_delivery_progress_stalled() {
1892        let config = hyperactor_config::global::lock();
1893        let _g = config.override_key(config::ENABLE_DEST_ACTOR_REORDERING_BUFFER, true);
1894
1895        let proc = Proc::isolated();
1896        let client = proc.client("client");
1897        let handle = proc.spawn(DeliveryTestActor);
1898        let target = handle.actor_addr().clone();
1899        let _actor_ref: ActorRef<DeliveryTestActor> = handle.bind();
1900        let handler_port = target.port_addr(Port::handler::<DeliveryTestMsg>());
1901
1902        let _ = client.sequencer().assign_seq(&handler_port);
1903        let monitor = short_delivery_monitor(&client, target.clone());
1904        handle.post(&client, DeliveryTestMsg);
1905
1906        assert_eq!(
1907            monitor.wait_for_failure().await,
1908            MonitorFailure::DeliveryProgressStalled {
1909                actor_id: target,
1910                from: context::Mailbox::mailbox(&client).actor_addr().clone(),
1911                largest_sent: 2,
1912                largest_dequeueable: 0,
1913                timeout: Duration::from_millis(50),
1914            }
1915        );
1916    }
1917
1918    #[tokio::test]
1919    async fn test_monitor_skips_delivery_progress_when_reordering_is_disabled() {
1920        let config = hyperactor_config::global::lock();
1921        let _g = config.override_key(config::ENABLE_DEST_ACTOR_REORDERING_BUFFER, false);
1922
1923        let proc = Proc::isolated();
1924        let client = proc.client("client");
1925        let handle = proc.spawn(DeliveryTestActor);
1926        let target = handle.actor_addr().clone();
1927        let _actor_ref: ActorRef<DeliveryTestActor> = handle.bind();
1928        let monitor = short_delivery_monitor(&client, target);
1929
1930        handle.post(&client, DeliveryTestMsg);
1931        let _ = wait_for_alive(&monitor).await;
1932        time::sleep(Duration::from_millis(100)).await;
1933
1934        assert!(matches!(monitor.status(), MonitorStatus::Alive(_)));
1935    }
1936
1937    #[tokio::test]
1938    async fn test_monitor_times_out_when_status_proc_is_unreachable() {
1939        let proc = Proc::isolated();
1940        let client = proc.client("client");
1941        let unreachable =
1942            crate::ProcAddr::instance(crate::channel::ChannelAddr::Local(1234), "gone")
1943                .actor_addr("actor");
1944        let monitor = short_monitor(&client, unreachable.clone());
1945
1946        assert_eq!(
1947            monitor.wait_for_failure().await,
1948            MonitorFailure::StatusRequestTimedOut {
1949                actor_id: unreachable,
1950                timeout_millis: 50,
1951            }
1952        );
1953    }
1954
1955    #[tokio::test]
1956    async fn test_monitor_actor_ignores_tick_while_poll_is_pending() {
1957        let proc = Proc::isolated();
1958        let client = proc.client("client");
1959        let unreachable =
1960            crate::ProcAddr::instance(crate::channel::ChannelAddr::Local(1234), "gone")
1961                .actor_addr("actor");
1962        let monitor = ActorMonitor::spawn_with_timings(
1963            &client,
1964            unreachable,
1965            Duration::ZERO,
1966            Duration::from_millis(10),
1967            Duration::from_secs(5),
1968            None,
1969            true,
1970        );
1971
1972        time::sleep(Duration::from_millis(20)).await;
1973        let (reply, reply_rx) = client.open_once_port();
1974        let handle = &monitor
1975            .inner
1976            .as_ref()
1977            .expect("monitor inner should be present")
1978            .handle;
1979        handle.post(&client, MonitorTick);
1980        handle.post(
1981            &client,
1982            MonitorProbe {
1983                reply: reply.bind(),
1984            },
1985        );
1986
1987        time::timeout(Duration::from_millis(100), reply_rx.recv())
1988            .await
1989            .expect("monitor actor should ignore a tick while polling")
1990            .expect("probe reply should arrive");
1991    }
1992
1993    #[tokio::test]
1994    async fn test_monitor_poll_actor_remains_responsive_and_stops_while_request_is_pending() {
1995        let proc = Proc::isolated();
1996        let client = proc.client("client");
1997        let unreachable =
1998            crate::ProcAddr::instance(crate::channel::ChannelAddr::Local(1234), "gone")
1999                .actor_addr("actor");
2000        let monitor = ActorMonitor::spawn_with_timings(
2001            &client,
2002            unreachable.clone(),
2003            Duration::from_secs(60),
2004            Duration::from_millis(10),
2005            Duration::from_secs(5),
2006            None,
2007            true,
2008        );
2009        let poll = proc.spawn(MonitorPollActor {
2010            target: unreachable,
2011            request_timeout: Duration::from_secs(5),
2012            monitor: monitor
2013                .inner
2014                .as_ref()
2015                .expect("monitor inner should be present")
2016                .handle
2017                .clone(),
2018            state: MonitorPollState::Start { delivery: None },
2019        });
2020
2021        time::sleep(Duration::from_millis(20)).await;
2022        let (reply, reply_rx) = client.open_once_port();
2023        poll.post(
2024            &client,
2025            MonitorProbe {
2026                reply: reply.bind(),
2027            },
2028        );
2029        time::timeout(Duration::from_millis(100), reply_rx.recv())
2030            .await
2031            .expect("monitor poll actor should remain responsive")
2032            .expect("probe reply should arrive");
2033
2034        poll.stop("test complete").unwrap();
2035        time::timeout(Duration::from_millis(100), poll)
2036            .await
2037            .expect("monitor poll actor should stop with a pending request");
2038    }
2039
2040    #[tokio::test]
2041    async fn test_supervised_monitor_reports_synthetic_supervision() {
2042        let proc = Proc::isolated();
2043        let client = proc.client("client");
2044        let target = proc.spawn(TestActor);
2045        let target_id = target.actor_addr().clone();
2046        let (ready, ready_rx) = client.open_once_port();
2047        let (events, mut event_rx) = client.open_port();
2048        let supervisor = proc.spawn(SupervisorActor {
2049            target: target_id.clone(),
2050            ready: Some(ready.bind()),
2051            events: events.bind(),
2052            supervisor: None,
2053        });
2054
2055        let monitor_id = ready_rx.recv().await.unwrap();
2056        target.drain_and_stop("done").unwrap();
2057
2058        let event = event_rx.recv().await.unwrap();
2059        assert_eq!(event.actor_id, monitor_id);
2060        let ActorStatus::Failed(ActorErrorKind::SyntheticSupervision(synthetic)) =
2061            event.actor_status
2062        else {
2063            panic!("expected synthetic supervision event");
2064        };
2065        assert_eq!(synthetic.subject, target_id);
2066        assert!(matches!(
2067            *synthetic.failure,
2068            MonitorFailure::ActorStopped {
2069                status: ActorStatus::Stopped(_),
2070                ..
2071            }
2072        ));
2073        supervisor.drain_and_stop("test complete").unwrap();
2074    }
2075
2076    #[tokio::test]
2077    async fn test_failed_monitor_reports_synthetic_supervision_after_conversion() {
2078        let config = hyperactor_config::global::lock();
2079        let _config_guard = config.override_key(
2080            crate::config::PENDING_ACTOR_DELIVERY_TIMEOUT,
2081            Duration::ZERO,
2082        );
2083        let proc = Proc::isolated();
2084        let client = proc.client("client");
2085        let target_id = proc.proc_addr().actor_addr("missing");
2086        let (ready, ready_rx) = client.open_once_port();
2087        let (events, mut event_rx) = client.open_port();
2088        let supervisor = proc.spawn(ConvertFailedMonitorActor {
2089            target: target_id.clone(),
2090            ready: Some(ready.bind()),
2091            events: events.bind(),
2092            supervisor: None,
2093        });
2094
2095        let monitor_id = ready_rx.recv().await.unwrap();
2096
2097        let event = event_rx.recv().await.unwrap();
2098        assert_eq!(event.actor_id, monitor_id);
2099        let ActorStatus::Failed(ActorErrorKind::SyntheticSupervision(synthetic)) =
2100            event.actor_status
2101        else {
2102            panic!("expected synthetic supervision event");
2103        };
2104        assert_eq!(synthetic.subject, target_id);
2105        assert!(matches!(
2106            *synthetic.failure,
2107            MonitorFailure::ActorGone { .. }
2108        ));
2109        supervisor.drain_and_stop("test complete").unwrap();
2110    }
2111
2112    #[tokio::test]
2113    async fn test_dropping_supervisor_disables_synthetic_supervision() {
2114        let proc = Proc::isolated();
2115        let client = proc.client("client");
2116        let target = proc.spawn(TestActor);
2117        let (ready, ready_rx) = client.open_once_port();
2118        let (events, mut event_rx) = client.open_port();
2119        let supervisor = proc.spawn(DropSupervisorActor {
2120            target: target.actor_addr().clone(),
2121            ready: Some(ready.bind()),
2122            events: events.bind(),
2123        });
2124
2125        let monitor_id = ready_rx.recv().await.unwrap();
2126
2127        let stop_event = event_rx.recv().await.unwrap();
2128        assert_eq!(stop_event.actor_id, monitor_id);
2129        assert!(matches!(stop_event.actor_status, ActorStatus::Stopped(_)));
2130
2131        target.drain_and_stop("done").unwrap();
2132
2133        assert!(
2134            time::timeout(Duration::from_millis(200), event_rx.recv())
2135                .await
2136                .is_err()
2137        );
2138        supervisor.drain_and_stop("test complete").unwrap();
2139    }
2140
2141    #[tokio::test]
2142    async fn test_dropping_supervisor_drops_queued_synthetic_supervision() {
2143        let proc = Proc::isolated();
2144        let client = proc.client("client");
2145        let target = proc.spawn(TestActor);
2146        let (ready, ready_rx) = client.open_once_port();
2147        let (events, mut event_rx) = client.open_port();
2148        let supervisor = proc.spawn(DropQueuedSupervisorActor {
2149            target,
2150            ready: Some(ready.bind()),
2151            events: events.bind(),
2152        });
2153
2154        ready_rx.recv().await.unwrap();
2155
2156        assert!(
2157            time::timeout(Duration::from_millis(200), event_rx.recv())
2158                .await
2159                .is_err()
2160        );
2161        supervisor.drain_and_stop("test complete").unwrap();
2162    }
2163}