use std::fmt;
use acton_ern::Ern;
use tokio::sync::watch;
use super::{RestartGeneration, SupervisionError};
use crate::common::ActorHandle;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SupervisionState {
Starting,
Running,
RestartPending,
Restarting,
Down,
Escalated,
Retired,
}
impl SupervisionState {
#[must_use]
pub const fn is_terminal(self) -> bool {
matches!(self, Self::Down | Self::Escalated | Self::Retired)
}
}
impl fmt::Display for SupervisionState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let text = match self {
Self::Starting => "starting",
Self::Running => "running",
Self::RestartPending => "restart_pending",
Self::Restarting => "restarting",
Self::Down => "down",
Self::Escalated => "escalated",
Self::Retired => "retired",
};
f.write_str(text)
}
}
#[derive(Debug, Clone)]
pub struct SupervisionStatus {
child: Ern,
handle: Option<ActorHandle>,
generation: RestartGeneration,
state: SupervisionState,
restarts_in_window: usize,
failure: Option<SupervisionError>,
}
impl SupervisionStatus {
#[must_use]
pub const fn new(
child: Ern,
handle: Option<ActorHandle>,
generation: RestartGeneration,
state: SupervisionState,
restarts_in_window: usize,
) -> Self {
Self {
child,
handle,
generation,
state,
restarts_in_window,
failure: None,
}
}
#[must_use]
pub fn with_failure(mut self, failure: SupervisionError) -> Self {
self.failure = Some(failure);
self
}
#[must_use]
pub const fn child(&self) -> &Ern {
&self.child
}
#[must_use]
pub const fn handle(&self) -> Option<&ActorHandle> {
self.handle.as_ref()
}
#[must_use]
pub const fn generation(&self) -> RestartGeneration {
self.generation
}
#[must_use]
pub const fn state(&self) -> SupervisionState {
self.state
}
#[must_use]
pub const fn restarts_in_window(&self) -> usize {
self.restarts_in_window
}
#[must_use]
pub const fn failure(&self) -> Option<&SupervisionError> {
self.failure.as_ref()
}
}
impl fmt::Display for SupervisionStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"child '{}' is {} at {} ({} restarts in window)",
self.child, self.state, self.generation, self.restarts_in_window
)
}
}
#[derive(Debug, Clone)]
pub struct SupervisedChild {
ern: Ern,
supervisor: Ern,
status: watch::Receiver<SupervisionStatus>,
}
impl SupervisedChild {
pub(crate) const fn new(
ern: Ern,
supervisor: Ern,
status: watch::Receiver<SupervisionStatus>,
) -> Self {
Self {
ern,
supervisor,
status,
}
}
#[must_use]
pub const fn ern(&self) -> &Ern {
&self.ern
}
#[must_use]
pub const fn supervisor(&self) -> &Ern {
&self.supervisor
}
#[must_use]
pub fn current(&self) -> Option<ActorHandle> {
self.status.borrow().handle().cloned()
}
#[must_use]
pub fn status(&self) -> SupervisionStatus {
self.status.borrow().clone()
}
pub async fn wait_for(
&mut self,
predicate: impl FnMut(&SupervisionStatus) -> bool + Send,
) -> Result<SupervisionStatus, SupervisionError> {
self.status
.wait_for(predicate)
.await
.map(|status| status.clone())
.map_err(|_| SupervisionError::SupervisorStopped {
supervisor: self.supervisor.clone(),
})
}
pub async fn wait_running(&mut self) -> Result<ActorHandle, SupervisionError> {
let status = self
.wait_for(|status| {
(status.state() == SupervisionState::Running && status.handle().is_some())
|| status.state().is_terminal()
})
.await?;
self.running_handle(&status)
}
pub async fn wait_generation(
&mut self,
generation: RestartGeneration,
) -> Result<ActorHandle, SupervisionError> {
let status = self
.wait_for(|status| {
(status.generation() >= generation
&& status.state() == SupervisionState::Running
&& status.handle().is_some())
|| status.state().is_terminal()
})
.await?;
self.running_handle(&status)
}
fn running_handle(
&self,
status: &SupervisionStatus,
) -> Result<ActorHandle, SupervisionError> {
if status.state() == SupervisionState::Running {
if let Some(handle) = status.handle() {
return Ok(handle.clone());
}
}
Err(status.failure().cloned().unwrap_or_else(|| {
SupervisionError::ChildNotRunning {
child: self.ern.clone(),
state: status.state(),
}
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::ActorHandleInterface;
fn child() -> Ern {
Ern::with_root("worker").expect("'worker' is a valid Ern root")
}
#[test]
fn accessors_return_what_was_supplied() {
let child = child();
let snapshot = SupervisionStatus::new(
child.clone(),
None,
RestartGeneration::FIRST.next(),
SupervisionState::Running,
3,
);
assert_eq!(snapshot.child(), &child);
assert!(snapshot.handle().is_none());
assert_eq!(snapshot.generation(), RestartGeneration::FIRST.next());
assert_eq!(snapshot.state(), SupervisionState::Running);
assert_eq!(snapshot.restarts_in_window(), 3);
}
#[test]
fn terminal_states_are_the_ones_a_supervisor_cannot_recover_from() {
for state in [
SupervisionState::Down,
SupervisionState::Escalated,
SupervisionState::Retired,
] {
assert!(state.is_terminal(), "{state} should be terminal");
}
for state in [
SupervisionState::Starting,
SupervisionState::Running,
SupervisionState::RestartPending,
SupervisionState::Restarting,
] {
assert!(!state.is_terminal(), "{state} should not be terminal");
}
}
#[test]
fn every_state_displays_in_snake_case() {
assert_eq!(SupervisionState::Starting.to_string(), "starting");
assert_eq!(SupervisionState::Running.to_string(), "running");
assert_eq!(SupervisionState::RestartPending.to_string(), "restart_pending");
assert_eq!(SupervisionState::Restarting.to_string(), "restarting");
assert_eq!(SupervisionState::Down.to_string(), "down");
assert_eq!(SupervisionState::Escalated.to_string(), "escalated");
assert_eq!(SupervisionState::Retired.to_string(), "retired");
}
#[test]
fn status_displays_child_state_and_generation() {
let child = child();
let text = SupervisionStatus::new(
child.clone(),
None,
RestartGeneration::FIRST,
SupervisionState::Running,
0,
)
.to_string();
assert!(text.contains(&child.to_string()), "{text}");
assert!(text.contains("running"), "{text}");
assert!(text.contains("generation 0"), "{text}");
}
fn supervised(
child: &Ern,
) -> (SupervisedChild, watch::Sender<SupervisionStatus>) {
let supervisor = Ern::with_root("pool").expect("'pool' is a valid Ern root");
let (sender, receiver) = watch::channel(SupervisionStatus::new(
child.clone(),
None,
RestartGeneration::FIRST,
SupervisionState::Starting,
0,
));
(
SupervisedChild::new(child.clone(), supervisor, receiver),
sender,
)
}
fn running(child: &Ern, generation: RestartGeneration) -> SupervisionStatus {
let (outbox, _inbox) = tokio::sync::mpsc::channel(8);
SupervisionStatus::new(
child.clone(),
Some(ActorHandle::new(child.clone(), outbox)),
generation,
SupervisionState::Running,
0,
)
}
#[test]
fn a_supervised_child_reports_both_identities() {
let child = child();
let (reference, _sender) = supervised(&child);
assert_eq!(reference.ern(), &child);
assert_eq!(reference.supervisor().to_string(), reference.supervisor().to_string());
assert!(reference.current().is_none(), "not running yet");
assert_eq!(reference.status().state(), SupervisionState::Starting);
}
#[test]
fn current_follows_whatever_the_supervisor_last_published() {
let child = child();
let (reference, sender) = supervised(&child);
sender.send_replace(running(&child, RestartGeneration::FIRST));
assert!(reference.current().is_some());
assert_eq!(reference.status().state(), SupervisionState::Running);
}
#[tokio::test]
async fn waiting_for_a_state_already_reached_resolves_immediately() {
let child = child();
let (mut reference, sender) = supervised(&child);
sender.send_replace(running(&child, RestartGeneration::FIRST));
let handle = reference
.wait_running()
.await
.expect("the supervisor is still alive");
assert_eq!(handle.id(), child);
}
#[tokio::test]
async fn waiting_resolves_when_the_supervisor_publishes() {
let child = child();
let (mut reference, sender) = supervised(&child);
let publisher = {
let child = child.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
sender.send_replace(running(&child, RestartGeneration::FIRST));
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
})
};
let handle = reference
.wait_running()
.await
.expect("the supervisor published a running status");
assert_eq!(handle.id(), child);
publisher.await.expect("publisher task completes");
}
#[tokio::test]
async fn waiting_for_a_later_generation_ignores_earlier_ones() {
let child = child();
let (mut reference, sender) = supervised(&child);
sender.send_replace(running(&child, RestartGeneration::FIRST));
let target = RestartGeneration::FIRST.next();
let publisher = {
let child = child.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
sender.send_replace(running(&child, target));
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
})
};
let handle = tokio::time::timeout(
std::time::Duration::from_secs(5),
reference.wait_generation(target),
)
.await
.expect("wait_generation must not hang")
.expect("the supervisor published the requested generation");
assert_eq!(handle.id(), child);
publisher.await.expect("publisher task completes");
}
#[tokio::test]
async fn a_start_that_failed_ends_the_wait_with_the_reason_it_failed() {
let child = child();
let (mut reference, sender) = supervised(&child);
let reason = SupervisionError::ConfigRejected {
child: child.clone(),
reason: "the spawner said no".to_string(),
};
sender.send_replace(
SupervisionStatus::new(
child.clone(),
None,
RestartGeneration::FIRST,
SupervisionState::Retired,
0,
)
.with_failure(reason.clone()),
);
let error = tokio::time::timeout(
std::time::Duration::from_secs(5),
reference.wait_running(),
)
.await
.expect("a terminal state must end the wait")
.expect_err("the child never came up");
assert_eq!(error, reason);
}
#[tokio::test]
async fn a_terminal_state_with_no_reason_still_ends_the_wait() {
let child = child();
let (mut reference, sender) = supervised(&child);
sender.send_replace(SupervisionStatus::new(
child.clone(),
None,
RestartGeneration::FIRST,
SupervisionState::Down,
0,
));
let error = tokio::time::timeout(
std::time::Duration::from_secs(5),
reference.wait_generation(RestartGeneration::FIRST),
)
.await
.expect("a terminal state must end the wait")
.expect_err("the child is down");
assert_eq!(
error,
SupervisionError::ChildNotRunning {
child,
state: SupervisionState::Down,
}
);
}
#[tokio::test]
async fn a_restart_in_progress_is_not_a_reason_to_stop_waiting() {
let child = child();
let (mut reference, sender) = supervised(&child);
sender.send_replace(SupervisionStatus::new(
child.clone(),
None,
RestartGeneration::FIRST,
SupervisionState::RestartPending,
1,
));
let publisher = {
let child = child.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
sender.send_replace(running(&child, RestartGeneration::FIRST));
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
})
};
let handle = tokio::time::timeout(
std::time::Duration::from_secs(5),
reference.wait_running(),
)
.await
.expect("the wait must not hang")
.expect("the child came back");
assert_eq!(handle.id(), child);
publisher.await.expect("publisher task completes");
}
#[tokio::test]
async fn a_supervisor_that_goes_away_stops_the_wait() {
let child = child();
let (mut reference, sender) = supervised(&child);
drop(sender);
let error = tokio::time::timeout(
std::time::Duration::from_secs(5),
reference.wait_running(),
)
.await
.expect("the wait must not hang once the channel closes")
.expect_err("the supervisor is gone");
assert!(matches!(
error,
SupervisionError::SupervisorStopped { .. }
));
}
#[test]
fn states_compare_by_variant() {
assert_eq!(SupervisionState::Running, SupervisionState::Running);
assert_ne!(SupervisionState::Running, SupervisionState::Down);
}
}