use std::error::Error;
use std::fmt;
use std::time::Duration;
use acton_ern::Ern;
use super::SupervisionState;
use crate::actor::RestartLimitExceeded;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SupervisionError {
DuplicateChild {
child: Ern,
},
UnknownChild {
child: Ern,
},
ConfigRejected {
child: Ern,
reason: String,
},
SupervisorStopped {
supervisor: Ern,
},
RestartLimit {
child: Ern,
limit: RestartLimitExceeded,
},
ChildStopTimeout {
child: Ern,
waited: Duration,
},
ReleaseLost {
child: Ern,
},
ChildNotRunning {
child: Ern,
state: SupervisionState,
},
RegistrationLost {
child: Ern,
},
}
impl fmt::Display for SupervisionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::DuplicateChild { child } => write!(
f,
"child '{child}' is already supervised by this actor; give the child a different name, build a fresh ActorConfig, or call unsupervise() first to replace it"
),
Self::UnknownChild { child } => write!(
f,
"no supervised child '{child}'; it may have been removed, or it was started with supervise() instead of supervise_with()"
),
Self::ConfigRejected { child, reason } => {
write!(f, "cannot create supervised child '{child}': {reason}")
}
Self::SupervisorStopped { supervisor } => write!(
f,
"supervisor '{supervisor}' has stopped; it can no longer supervise children"
),
Self::RestartLimit { child, limit } => {
write!(f, "child '{child}' exceeded its restart limit: {limit}")
}
Self::ChildStopTimeout { child, waited } => write!(
f,
"child '{child}' did not stop within {waited:?}; the supervisor gave up waiting"
),
Self::ChildNotRunning { child, state } => write!(
f,
"child '{child}' is {state} and will not reach running; its supervisor recorded no reason"
),
Self::RegistrationLost { child } => write!(
f,
"supervisor stopped before recording child '{child}'; the child may be running unsupervised and should be stopped"
),
Self::ReleaseLost { child } => write!(
f,
"supervisor stopped before releasing child '{child}'; the child was not removed from supervision"
),
}
}
}
impl Error for SupervisionError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::RestartLimit { limit, .. } => Some(limit),
Self::DuplicateChild { .. }
| Self::UnknownChild { .. }
| Self::ConfigRejected { .. }
| Self::SupervisorStopped { .. }
| Self::ChildStopTimeout { .. }
| Self::ChildNotRunning { .. }
| Self::RegistrationLost { .. }
| Self::ReleaseLost { .. } => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn child() -> Ern {
Ern::with_root("worker").expect("'worker' is a valid Ern root")
}
fn exceeded() -> RestartLimitExceeded {
RestartLimitExceeded {
attempts: 5,
max_restarts: 5,
window_secs: 60,
}
}
#[test]
fn duplicate_child_message_names_the_remedy() {
let child = child();
let message = SupervisionError::DuplicateChild {
child: child.clone(),
}
.to_string();
assert!(message.contains(&child.to_string()), "{message}");
assert!(message.contains("unsupervise()"), "{message}");
assert!(
message.contains("different name"),
"the collision case needs a rename: {message}"
);
assert!(
message.contains("ActorConfig"),
"the config-reuse case needs a fresh config: {message}"
);
}
#[test]
fn two_root_actors_built_from_the_same_name_never_share_an_identifier() {
let first = Ern::with_root("worker").expect("'worker' is a valid Ern root");
let second = Ern::with_root("worker").expect("'worker' is a valid Ern root");
assert_ne!(first, second);
assert_ne!(first.to_string(), second.to_string());
}
#[test]
fn unknown_child_message_explains_the_likely_cause() {
let child = child();
let message = SupervisionError::UnknownChild {
child: child.clone(),
}
.to_string();
assert!(message.contains(&child.to_string()), "{message}");
assert!(message.contains("supervise_with()"), "{message}");
}
#[test]
fn config_rejected_message_carries_the_reason() {
let child = child();
let message = SupervisionError::ConfigRejected {
child: child.clone(),
reason: "parent is not running".to_string(),
}
.to_string();
assert!(message.contains(&child.to_string()), "{message}");
assert!(message.contains("parent is not running"), "{message}");
}
#[test]
fn supervisor_stopped_message_names_the_supervisor() {
let supervisor = Ern::with_root("pool").expect("'pool' is a valid Ern root");
let message = SupervisionError::SupervisorStopped {
supervisor: supervisor.clone(),
}
.to_string();
assert!(message.contains(&supervisor.to_string()), "{message}");
assert!(message.contains("has stopped"), "{message}");
}
#[test]
fn restart_limit_message_nests_the_limit_detail() {
let child = child();
let message = SupervisionError::RestartLimit {
child: child.clone(),
limit: exceeded(),
}
.to_string();
assert!(message.contains(&child.to_string()), "{message}");
assert!(message.contains("5 attempts"), "{message}");
assert!(message.contains("60 seconds"), "{message}");
}
#[test]
fn child_stop_timeout_message_reports_how_long_it_waited() {
let child = child();
let message = SupervisionError::ChildStopTimeout {
child: child.clone(),
waited: Duration::from_secs(5),
}
.to_string();
assert!(message.contains(&child.to_string()), "{message}");
assert!(message.contains("5s"), "{message}");
}
#[test]
fn release_lost_message_says_the_child_is_still_supervised() {
let child = child();
let message = SupervisionError::ReleaseLost {
child: child.clone(),
}
.to_string();
assert!(message.contains(&child.to_string()), "{message}");
assert!(message.contains("not removed"), "{message}");
}
#[test]
fn registration_lost_message_warns_the_child_may_be_orphaned() {
let child = child();
let message = SupervisionError::RegistrationLost {
child: child.clone(),
}
.to_string();
assert!(message.contains(&child.to_string()), "{message}");
assert!(message.contains("unsupervised"), "{message}");
}
#[test]
fn only_restart_limit_exposes_a_source() {
let child = child();
let with_source = SupervisionError::RestartLimit {
child: child.clone(),
limit: exceeded(),
};
assert!(with_source.source().is_some());
let without_source = [
SupervisionError::DuplicateChild {
child: child.clone(),
},
SupervisionError::UnknownChild {
child: child.clone(),
},
SupervisionError::ConfigRejected {
child: child.clone(),
reason: "nope".to_string(),
},
SupervisionError::SupervisorStopped {
supervisor: child.clone(),
},
SupervisionError::ChildStopTimeout {
child: child.clone(),
waited: Duration::from_secs(1),
},
SupervisionError::RegistrationLost {
child: child.clone(),
},
SupervisionError::ReleaseLost { child },
];
for error in &without_source {
assert!(error.source().is_none(), "{error}");
}
}
#[test]
fn restart_limit_source_is_the_exceeded_limit() {
let error = SupervisionError::RestartLimit {
child: child(),
limit: exceeded(),
};
let source = error.source().expect("RestartLimit exposes a source");
assert_eq!(source.to_string(), exceeded().to_string());
}
#[test]
fn converts_into_anyhow_error_via_question_mark() {
fn fallible(child: Ern) -> anyhow::Result<()> {
Err(SupervisionError::UnknownChild { child })?;
Ok(())
}
let child = child();
let error = fallible(child.clone()).expect_err("the function always fails");
assert!(error.to_string().contains(&child.to_string()));
assert!(error.downcast_ref::<SupervisionError>().is_some());
}
#[test]
fn equal_variants_compare_equal() {
let child = child();
assert_eq!(
SupervisionError::DuplicateChild {
child: child.clone()
},
SupervisionError::DuplicateChild {
child: child.clone()
}
);
assert_ne!(
SupervisionError::DuplicateChild {
child: child.clone()
},
SupervisionError::UnknownChild { child }
);
}
}