Skip to main content

hyperactor/
supervision.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//! Messages used in supervision.
10//!
11//! ## Supervision invariants (SV-*)
12//!
13//! - **SV-1 (root-cause attribution):** For an
14//!   `UnhandledSupervisionEvent` chain, `actually_failing_actor()`
15//!   returns the event that should be treated as the root cause
16//!   for structured failure attribution. In particular, if a
17//!   failed parent wraps a stopped child event, the stopped child
18//!   remains the root cause.
19//!
20//! ## Supervision rendering invariants (SR-*)
21//!
22//! - **SR-1 (`display_name` is presentation-only).**
23//!   `ActorSupervisionEvent.display_name` is rendered display text.
24//!   Downstream code must not parse it to recover structured data.
25//!
26//! - **SR-2 (rendering falls back to stable ids).**
27//!   `ActorSupervisionEvent::Display` and its helpers render
28//!   `display_name` when present and otherwise fall back to
29//!   `actor_id.to_string()`. A given actor mention renders one or
30//!   the other, not both.
31//!
32//! - **SR-3 (no rendered-output parsing back into structure).**
33//!   Structured data must not be reconstructed by parsing formatted
34//!   `display_name`, identifier text, or other rendered output from
35//!   this path.
36
37use std::fmt;
38use std::fmt::Debug;
39use std::fmt::Write;
40use std::sync::Arc;
41use std::sync::atomic::AtomicBool;
42use std::sync::atomic::Ordering;
43use std::time::SystemTime;
44
45use derivative::Derivative;
46use hyperactor_config::Flattrs;
47use indenter::indented;
48use serde::Deserialize;
49use serde::Serialize;
50
51use crate::ActorAddr;
52use crate::actor::ActorErrorKind;
53use crate::actor::ActorStatus;
54
55/// This is the local actor supervision event. Child actor will propagate this event to its parent.
56#[derive(Clone, Debug, Derivative, Serialize, Deserialize, typeuri::Named)]
57#[derivative(PartialEq, Eq)]
58pub struct ActorSupervisionEvent {
59    /// The actor id of the child actor where the event is triggered.
60    pub actor_id: ActorAddr,
61    /// Friendly display name, if the actor class customized it.
62    pub display_name: Option<String>,
63    /// The time when the event is triggered.
64    #[derivative(PartialEq = "ignore")]
65    pub occurred_at: SystemTime,
66    /// Status of the child actor.
67    pub actor_status: ActorStatus,
68    /// If this event is associated with a message, the message headers.
69    #[derivative(PartialEq = "ignore")]
70    pub message_headers: Option<Flattrs>,
71    /// Semantic labels that describe the actor, such as its point in a mesh.
72    /// hyperactor does not interpret them. A remote supervision proxy replaces
73    /// them with its own labels, so they describe the actor as of the most
74    /// recent remote hop.
75    #[serde(default)]
76    #[derivative(PartialEq = "ignore")]
77    pub labels: Flattrs,
78    #[serde(skip, default = "local_fence")]
79    #[derivative(PartialEq = "ignore")]
80    pub(crate) local_fence: Arc<AtomicBool>,
81}
82wirevalue::register_type!(ActorSupervisionEvent);
83
84pub(crate) fn local_fence() -> Arc<AtomicBool> {
85    Arc::new(AtomicBool::new(false))
86}
87
88impl ActorSupervisionEvent {
89    /// Create a new supervision event. Timestamp is set to the current time.
90    pub fn new(
91        actor_id: impl Into<ActorAddr>,
92        display_name: Option<String>,
93        actor_status: ActorStatus,
94        message_headers: Option<Flattrs>,
95    ) -> Self {
96        Self {
97            actor_id: actor_id.into(),
98            display_name,
99            occurred_at: std::time::SystemTime::now(),
100            actor_status,
101            message_headers,
102            labels: Flattrs::new(),
103            local_fence: local_fence(),
104        }
105    }
106
107    /// Replace the event's labels.
108    pub fn with_labels(mut self, labels: Flattrs) -> Self {
109        self.labels = labels;
110        self
111    }
112
113    pub(crate) fn with_local_fence(mut self, local_fence: Arc<AtomicBool>) -> Self {
114        self.local_fence = local_fence;
115        self
116    }
117
118    pub(crate) fn is_locally_cancelled(&self) -> bool {
119        self.local_fence.load(Ordering::Acquire)
120    }
121
122    fn actor_name(&self) -> String {
123        self.display_name.clone().unwrap_or_else(|| {
124            if self.actor_id.is_root() {
125                format!("{},{}", self.actor_id.proc_addr(), self.actor_id.log_name())
126            } else {
127                self.actor_id.to_string()
128            }
129        })
130    }
131
132    /// Walk the `UnhandledSupervisionEvent` chain to the root-cause
133    /// event — the first event whose status is not
134    /// `UnhandledSupervisionEvent`.
135    pub fn caused_by(&self) -> &ActorSupervisionEvent {
136        let mut event = self;
137        while let ActorStatus::Failed(ActorErrorKind::UnhandledSupervisionEvent(inner)) =
138            &event.actor_status
139        {
140            event = inner;
141        }
142        event
143    }
144
145    /// Walk the `UnhandledSupervisionEvent` chain to find the root-cause
146    /// actor that originally failed.
147    ///
148    /// Returns `None` if the event is not a failure. Always returns the
149    /// leaf of the chain — the actor whose status is the root cause,
150    /// even if that leaf is a non-failure (e.g. a stopped process).
151    pub fn actually_failing_actor(&self) -> Option<&ActorSupervisionEvent> {
152        if !self.is_error() {
153            return None;
154        }
155        Some(self.caused_by())
156    }
157
158    /// This event is for a supervision error.
159    pub fn is_error(&self) -> bool {
160        self.actor_status.is_failed()
161    }
162
163    /// Produce a concise failure report. Returns `None` for non-failure
164    /// events.
165    pub fn failure_report(&self) -> Option<String> {
166        if !self.is_error() {
167            return None;
168        }
169        let mut output = String::new();
170        self.write_failure_report(&mut output)
171            .expect("writing to String cannot fail");
172        Some(output)
173    }
174
175    fn write_failure_report(&self, f: &mut String) -> fmt::Result {
176        let mut current = self;
177        let mut last_unhandled: Option<&ActorSupervisionEvent> = None;
178        while let ActorStatus::Failed(ActorErrorKind::UnhandledSupervisionEvent(inner)) =
179            &current.actor_status
180        {
181            last_unhandled = Some(current);
182            current = inner;
183        }
184
185        if !current.actor_status.is_failed() {
186            let parent = last_unhandled.expect(
187                "top-level event is a failure but leaf is not; \
188                 chain must contain an UnhandledSupervisionEvent",
189            );
190            writeln!(
191                f,
192                "The actor {} failed because it did not handle a supervision event \
193                 from its child. The event was:",
194                parent.actor_name()
195            )?;
196            return write!(indented(f).with_str("  "), "{}", current);
197        }
198
199        writeln!(
200            f,
201            "The actor {} and all its descendants have failed:",
202            current.actor_name()
203        )?;
204        match &current.actor_status {
205            ActorStatus::Failed(ActorErrorKind::ErrorDuringHandlingSupervision(msg, child)) => {
206                writeln!(indented(f).with_str("  "), "{}", msg.trim_end())?;
207                writeln!(f, "This error occurred while handling another failure:")?;
208                let child_report = child
209                    .failure_report()
210                    .expect("child of ErrorDuringHandlingSupervision is always a failure");
211                write!(indented(f).with_str("  "), "{}", child_report)
212            }
213            ActorStatus::Failed(err) => write!(indented(f).with_str("  "), "{}", err),
214            _ => unreachable!("current.is_failed() was true"),
215        }
216    }
217}
218
219impl std::error::Error for ActorSupervisionEvent {}
220
221impl fmt::Display for ActorSupervisionEvent {
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        let name = self.actor_name();
224        match &self.actor_status {
225            ActorStatus::Failed(
226                err @ (ActorErrorKind::Generic(_) | ActorErrorKind::Aborted(_)),
227            ) => {
228                writeln!(f, "Supervision event: actor {} failed:", name)?;
229                write!(indented(f).with_str("  "), "{}", err)
230            }
231            ActorStatus::Failed(ActorErrorKind::UnhandledSupervisionEvent(child)) => {
232                writeln!(
233                    f,
234                    "Supervision event: actor {} failed because it did not handle \
235                     a supervision event from its child. The child's event was:",
236                    name
237                )?;
238                write!(indented(f).with_str("  "), "{}", child)
239            }
240            ActorStatus::Failed(ActorErrorKind::ErrorDuringHandlingSupervision(msg, child)) => {
241                writeln!(f, "Supervision event: actor {} failed:", name)?;
242                writeln!(indented(f).with_str("  "), "{}", msg.trim_end())?;
243                writeln!(
244                    f,
245                    "This error occurred while handling a supervision event from \
246                     its child. The event was:"
247                )?;
248                write!(indented(f).with_str("  "), "{}", child)
249            }
250            ActorStatus::Stopped(_)
251                if self
252                    .actor_id
253                    .label()
254                    .is_some_and(|l| l.as_str() == "host_agent" || l.as_str() == "proc_agent") =>
255            {
256                let addr = self.actor_id.proc_addr().addr().to_string();
257                write!(
258                    f,
259                    "Supervision event: the process {} owned by actor {} became unresponsive \
260                     and is assumed dead, check the log on the host for details",
261                    addr,
262                    self.actor_name()
263                )
264            }
265            status => {
266                writeln!(f, "Supervision event: actor {} has status:", name)?;
267                write!(indented(f).with_str("  "), "{}", status)
268            }
269        }
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276    use crate::ProcAddr;
277    use crate::actor::ActorErrorKind;
278    use crate::actor::ActorStatus;
279    use crate::channel::ChannelAddr;
280
281    fn test_event(name: &str, status: ActorStatus) -> ActorSupervisionEvent {
282        let proc_id = ProcAddr::singleton(ChannelAddr::Local(0), "test_proc");
283        ActorSupervisionEvent::new(
284            proc_id.actor_addr(name),
285            Some(name.to_string()),
286            status,
287            None,
288        )
289    }
290
291    fn test_event_with_addr(
292        name: &str,
293        addr: ChannelAddr,
294        status: ActorStatus,
295    ) -> ActorSupervisionEvent {
296        let proc_id = ProcAddr::singleton(addr, "test_proc");
297        ActorSupervisionEvent::new(proc_id.actor_addr(name), None, status, None)
298    }
299
300    fn generic(name: &str, msg: &str) -> ActorSupervisionEvent {
301        test_event(
302            name,
303            ActorStatus::Failed(ActorErrorKind::Generic(msg.to_string())),
304        )
305    }
306
307    fn aborted(name: &str, msg: &str) -> ActorSupervisionEvent {
308        test_event(
309            name,
310            ActorStatus::Failed(ActorErrorKind::Aborted(msg.to_string())),
311        )
312    }
313
314    fn unhandled(name: &str, child: ActorSupervisionEvent) -> ActorSupervisionEvent {
315        test_event(
316            name,
317            ActorStatus::Failed(ActorErrorKind::UnhandledSupervisionEvent(Box::new(child))),
318        )
319    }
320
321    fn error_during(name: &str, msg: &str, child: ActorSupervisionEvent) -> ActorSupervisionEvent {
322        test_event(
323            name,
324            ActorStatus::Failed(ActorErrorKind::ErrorDuringHandlingSupervision(
325                msg.to_string(),
326                Box::new(child),
327            )),
328        )
329    }
330
331    fn stopped(name: &str, reason: &str) -> ActorSupervisionEvent {
332        test_event(name, ActorStatus::Stopped(reason.to_string()))
333    }
334
335    // Display tests
336
337    #[test]
338    fn test_display_generic() {
339        let e = generic("actor_a", "something went wrong");
340        assert_eq!(
341            format!("{}", e),
342            "Supervision event: actor actor_a failed:\n\
343             \x20 something went wrong"
344        );
345    }
346
347    #[test]
348    fn test_display_aborted() {
349        let e = aborted("actor_a", "user requested");
350        assert_eq!(
351            format!("{}", e),
352            "Supervision event: actor actor_a failed:\n\
353             \x20 actor explicitly aborted due to: user requested"
354        );
355    }
356
357    #[test]
358    fn test_display_unhandled_with_generic_child() {
359        let child = generic("child", "child error");
360        let parent = unhandled("parent", child);
361        assert_eq!(
362            format!("{}", parent),
363            "Supervision event: actor parent failed because it did not handle \
364             a supervision event from its child. The child's event was:\n\
365             \x20 Supervision event: actor child failed:\n\
366             \x20   child error"
367        );
368    }
369
370    #[test]
371    fn test_display_error_during_handling() {
372        let child = generic("child", "child error");
373        let parent = error_during("parent", "handler crashed", child);
374        assert_eq!(
375            format!("{}", parent),
376            "Supervision event: actor parent failed:\n\
377             \x20 handler crashed\n\
378             This error occurred while handling a supervision event from \
379             its child. The event was:\n\
380             \x20 Supervision event: actor child failed:\n\
381             \x20   child error"
382        );
383    }
384
385    #[test]
386    fn test_display_stopped() {
387        let e = stopped("actor_a", "done");
388        assert_eq!(
389            format!("{}", e),
390            "Supervision event: actor actor_a has status:\n\
391             \x20 stopped: done"
392        );
393    }
394
395    #[test]
396    fn test_display_deep_nesting() {
397        let leaf = generic("leaf", "root cause");
398        let mid = unhandled("mid", leaf);
399        let top = unhandled("top", mid);
400        let output = format!("{}", top);
401        assert!(output.contains("actor top failed because"));
402        assert!(output.contains("  Supervision event: actor mid failed because"));
403        assert!(output.contains("    Supervision event: actor leaf failed:"));
404        assert!(output.contains("      root cause"));
405    }
406
407    #[test]
408    fn test_display_unhandled_stopped_child() {
409        let child = stopped("child", "process exited");
410        let parent = unhandled("parent", child);
411        assert_eq!(
412            format!("{}", parent),
413            "Supervision event: actor parent failed because it did not handle \
414             a supervision event from its child. The child's event was:\n\
415             \x20 Supervision event: actor child has status:\n\
416             \x20   stopped: process exited"
417        );
418    }
419
420    // failure_report tests
421
422    #[test]
423    fn test_failure_report_generic() {
424        let e = generic("actor_a", "boom");
425        assert_eq!(
426            e.failure_report().unwrap(),
427            "The actor actor_a and all its descendants have failed:\n\
428             \x20 boom"
429        );
430    }
431
432    #[test]
433    fn test_failure_report_aborted() {
434        let e = aborted("actor_a", "user requested");
435        assert_eq!(
436            e.failure_report().unwrap(),
437            "The actor actor_a and all its descendants have failed:\n\
438             \x20 actor explicitly aborted due to: user requested"
439        );
440    }
441
442    #[test]
443    fn test_failure_report_unhandled_chain_to_generic() {
444        let leaf = generic("leaf", "root cause");
445        let mid = unhandled("mid", leaf);
446        let top = unhandled("top", mid);
447        assert_eq!(
448            top.failure_report().unwrap(),
449            "The actor leaf and all its descendants have failed:\n\
450             \x20 root cause"
451        );
452    }
453
454    #[test]
455    fn test_failure_report_unhandled_chain_to_stopped() {
456        let leaf = stopped("some_actor", "process exited");
457        let mid = unhandled("mid", leaf);
458        let top = unhandled("top", mid);
459        let report = top.failure_report().unwrap();
460        assert_eq!(
461            report,
462            "The actor mid failed because it did not handle a supervision event \
463             from its child. The event was:\n\
464             \x20 Supervision event: actor some_actor has status:\n\
465             \x20   stopped: process exited"
466        );
467    }
468
469    #[test]
470    fn test_failure_report_unhandled_chain_to_stopped_proc_agent() {
471        let leaf = test_event_with_addr(
472            "proc_agent",
473            ChannelAddr::Local(99),
474            ActorStatus::Stopped("process exited".to_string()),
475        );
476        let mid = unhandled("mid", leaf);
477        let top = unhandled("top", mid);
478        let report = top.failure_report().unwrap();
479        assert!(
480            report.contains("did not handle a supervision event"),
481            "got: {}",
482            report
483        );
484        assert!(
485            report.contains("process local:99 owned by actor") && report.contains("unresponsive"),
486            "got: {}",
487            report
488        );
489    }
490
491    #[test]
492    fn test_failure_report_error_during_handling() {
493        let child = generic("child", "original error");
494        let parent = error_during("parent", "handler failed", child);
495        assert_eq!(
496            parent.failure_report().unwrap(),
497            "The actor parent and all its descendants have failed:\n\
498             \x20 handler failed\n\
499             This error occurred while handling another failure:\n\
500             \x20 The actor child and all its descendants have failed:\n\
501             \x20   original error"
502        );
503    }
504
505    #[test]
506    fn test_failure_report_error_during_handling_nested() {
507        let leaf = generic("leaf", "root cause");
508        let mid = error_during("mid", "mid failed", leaf);
509        let top = error_during("top", "top failed", mid);
510        let report = top.failure_report().unwrap();
511        assert!(report.starts_with(
512            "The actor top and all its descendants have failed:\n\
513             \x20 top failed\n\
514             This error occurred while handling another failure:\n\
515             \x20 The actor mid and all its descendants have failed:\n\
516             \x20   mid failed\n\
517             \x20 This error occurred while handling another failure:\n\
518             \x20   The actor leaf and all its descendants have failed:\n\
519             \x20     root cause"
520        ));
521    }
522
523    #[test]
524    fn test_failure_report_unhandled_to_error_during_handling() {
525        let leaf = generic("leaf", "root cause");
526        let handler_err = error_during("handler", "while handling", leaf);
527        let top = unhandled("top", handler_err);
528        let report = top.failure_report().unwrap();
529        assert!(report.contains("The actor handler and all its descendants have failed:"));
530        assert!(report.contains("while handling"));
531        assert!(report.contains("root cause"));
532    }
533
534    #[test]
535    fn test_failure_report_none_on_non_failure() {
536        let e = stopped("actor_a", "done");
537        assert!(e.failure_report().is_none());
538    }
539
540    #[test]
541    fn test_failure_report_direct_generic_no_chain() {
542        let e = generic("solo", "direct error");
543        assert_eq!(
544            e.failure_report().unwrap(),
545            "The actor solo and all its descendants have failed:\n\
546             \x20 direct error"
547        );
548    }
549
550    #[test]
551    fn test_display_host_agent_stopped() {
552        let e = test_event_with_addr(
553            "host_agent",
554            ChannelAddr::Local(42),
555            ActorStatus::Stopped("gone".to_string()),
556        );
557        let output = format!("{}", e);
558        assert!(
559            output.contains("process local:42 owned by actor") && output.contains("unresponsive"),
560            "got: {}",
561            output
562        );
563    }
564
565    #[test]
566    fn test_display_proc_agent_stopped() {
567        let e = test_event_with_addr(
568            "proc_agent",
569            ChannelAddr::Local(7),
570            ActorStatus::Stopped("dead".to_string()),
571        );
572        let output = format!("{}", e);
573        assert!(
574            output.contains("process local:7 owned by actor") && output.contains("unresponsive"),
575            "got: {}",
576            output
577        );
578    }
579
580    #[test]
581    fn test_display_error_during_handling_trim_end() {
582        let child = generic("child", "child error");
583        let parent = error_during("parent", "msg with trailing newline\n", child);
584        let output = format!("{}", parent);
585        assert!(
586            output.contains("  msg with trailing newline\nThis error occurred"),
587            "writeln! should trim trailing newline from msg: {}",
588            output
589        );
590    }
591
592    #[test]
593    fn test_failure_report_error_during_handling_trim_end() {
594        let child = generic("child", "child error");
595        let parent = error_during("parent", "msg with trailing newline\n", child);
596        let report = parent.failure_report().unwrap();
597        assert!(
598            report.contains("  msg with trailing newline\nThis error occurred"),
599            "writeln! should trim trailing newline from msg: {}",
600            report
601        );
602    }
603
604    /// Exercises SV-1 (see module doc): for a parent wrapping a
605    /// stopped child in `UnhandledSupervisionEvent`,
606    /// `actually_failing_actor()` returns the stopped child as
607    /// root cause for structured failure attribution.
608    #[test]
609    fn test_sv1_actually_failing_actor_returns_stopped_child() {
610        let proc_id = ProcAddr::singleton(ChannelAddr::Local(0), "test_proc");
611        let child_id = proc_id.actor_addr("proc_agent");
612        let parent_id = proc_id.actor_addr("controller");
613
614        let child_event = ActorSupervisionEvent::new(
615            child_id.clone(),
616            Some("proc_agent".into()),
617            ActorStatus::Stopped("host died".into()),
618            None,
619        );
620        let parent_event = ActorSupervisionEvent::new(
621            parent_id,
622            Some("controller".into()),
623            ActorStatus::Failed(ActorErrorKind::UnhandledSupervisionEvent(Box::new(
624                child_event,
625            ))),
626            None,
627        );
628
629        // SV-1: root cause is the stopped child, not the parent.
630        let root = parent_event
631            .actually_failing_actor()
632            .expect("parent_event is a failure");
633        assert_eq!(root.actor_id, child_id);
634        assert!(
635            matches!(root.actor_status, ActorStatus::Stopped(_)),
636            "root cause should be the stopped child, got: {:?}",
637            root.actor_status,
638        );
639    }
640}