use std::fmt::Debug;
use tracing::{trace, warn};
use std::sync::Arc;
use tokio::sync::watch;
use super::plan::{evaluate, RestartPlan, SupervisionOutcome};
use super::registry::{PendingSlot, SlotState, StartRecorded, StartTicket};
use super::{
BackoffDelay, ChildBlueprint, ChildIndex, ChildSpawner, NewSlot, SupervisedChild,
SupervisionError, SupervisionRegistry, SupervisionState, SupervisionStatus, TypedSpawner,
};
use crate::actor::managed_actor::started::Started;
use crate::actor::{
ActorConfig, Idle, ManagedActor, RestartGeneration, RestartLimitExceeded, RestartLimiter,
RestartLimiterConfig,
};
use crate::common::config::CONFIG;
use crate::common::ActorHandle;
use crate::message::{
ChildTerminated, RegisterSupervisedChild, RestartDue, SupervisedChildStarted,
UnregisterSupervisedChild,
};
use crate::traits::ActorHandleInterface;
pub fn status_channel(
child: &acton_ern::Ern,
handle: Option<ActorHandle>,
) -> (
watch::Sender<SupervisionStatus>,
watch::Receiver<SupervisionStatus>,
) {
watch::channel(SupervisionStatus::new(
child.clone(),
handle,
RestartGeneration::FIRST,
SupervisionState::Starting,
0,
))
}
async fn start_supervised_child(
ticket: StartTicket,
runtime: crate::common::ActorRuntime,
supervisor: ActorHandle,
) {
let outcome = ticket.spawner.spawn(runtime, supervisor.clone()).await;
let started = SupervisedChildStarted {
child: ticket.ern,
index: ticket.index,
outcome: outcome.clone(),
};
let envelope = supervisor.create_envelope(Some(supervisor.reply_address()));
let delivery = envelope.try_send(started).await;
let Err(undeliverable) = delivery else {
return;
};
match outcome {
Ok(handle) => {
warn!(
"Supervisor {} could not be told about child {} ({}); stopping the child rather than losing it",
supervisor.id(),
handle.id(),
undeliverable
);
stop_stray_child(handle).await;
}
Err(error) => {
trace!(
"Supervisor {} could not be told that a child failed to start ({}): {}",
supervisor.id(),
undeliverable,
error
);
}
}
}
async fn stop_stray_child(handle: ActorHandle) {
let deadline = std::time::Duration::from_millis(CONFIG.timeouts.actor_shutdown);
match tokio::time::timeout(deadline, handle.stop()).await {
Ok(Ok(())) => trace!("Stopped unsupervised child {}", handle.id()),
Ok(Err(error)) => warn!(
"Unsupervised child {} reported an error while stopping: {error:?}",
handle.id()
),
Err(_) => warn!(
"Unsupervised child {} did not stop within {} ms",
handle.id(),
CONFIG.timeouts.actor_shutdown
),
}
}
async fn stop_group_member(handle: ActorHandle) {
let deadline = std::time::Duration::from_millis(CONFIG.timeouts.actor_shutdown);
match tokio::time::timeout(deadline, handle.stop()).await {
Ok(Ok(())) => trace!("Stopped {} for a group restart", handle.id()),
Ok(Err(error)) => warn!(
"Child {} reported an error while stopping for a group restart: {error:?}",
handle.id()
),
Err(_) => warn!(
"Child {} did not stop within {} ms, so its group restart may be refused as stale",
handle.id(),
CONFIG.timeouts.actor_shutdown
),
}
}
impl<Model: Default + Send + Debug + 'static> ManagedActor<Started, Model> {
fn resolve_limiter(&self, child: Option<&RestartLimiterConfig>) -> RestartLimiter {
child.or(self.restart_limiter_config.as_ref()).map_or_else(
RestartLimiter::default,
|config| RestartLimiter::new(config.clone()),
)
}
pub(crate) fn register_supervised_child(&mut self, message: &RegisterSupervisedChild) {
let slot = NewSlot {
ern: message.child.clone(),
handle: message.handle.clone(),
spawner: message.spawner.clone(),
restart_policy: message.restart_policy,
limiter: self.resolve_limiter(message.limiter.as_ref()),
status: message.status.clone(),
};
let outcome = match self.supervision.register(slot) {
Ok(index) => {
trace!(
"Actor {} now supervises child {} at {}",
self.id(),
message.child,
index
);
Ok(())
}
Err(error) => {
warn!(
"Actor {} rejected supervision of child {}: {}",
self.id(),
message.child,
error
);
Err(error)
}
};
Self::report(message.outcome.as_ref(), outcome);
}
pub(crate) fn unregister_supervised_child(&mut self, message: &UnregisterSupervisedChild) {
if message.liveness.receiver_count() == 0 {
trace!(
"Releasing child {} with no caller waiting on the result",
message.child
);
}
let outcome = match self.supervision.retire(&message.child) {
Ok(handle) => {
trace!(
"Actor {} released child {} (handle retained: {}, caller is stopping it: {})",
self.id(),
message.child,
handle.is_some(),
message.stopping
);
#[cfg(feature = "ipc")]
if message.stopping {
let forgotten = self.runtime.ipc_forget(&message.child);
if forgotten > 0 {
trace!(
"Actor {} dropped {} IPC name(s) for released child {}",
self.id(),
forgotten,
message.child
);
}
}
Ok(handle)
}
Err(error) => {
warn!(
"Actor {} cannot release child {}: {}",
self.id(),
message.child,
error
);
Err(error)
}
};
if message.outcome.set(outcome).is_err() {
warn!("Release outcome cell was already set; ignoring the later result");
}
}
fn report(
cell: Option<&crate::message::RegistrationOutcome>,
outcome: Result<(), SupervisionError>,
) {
if let Some(cell) = cell {
if cell.set(outcome).is_err() {
warn!("Supervision outcome cell was already set; ignoring the later result");
}
}
}
pub(crate) fn shutdown_child_handles(&self, late_arrivals: Vec<ActorHandle>) -> Vec<ActorHandle> {
let mut seen = std::collections::HashSet::new();
let mut handles = Vec::new();
for handle in self.supervision.live_handles() {
if seen.insert(handle.id()) {
handles.push(handle);
}
}
for entry in self.handle.children() {
let handle = entry.value().clone();
if seen.insert(handle.id()) {
handles.push(handle);
}
}
for handle in late_arrivals {
if seen.insert(handle.id()) {
handles.push(handle);
}
}
handles
}
pub(crate) const fn supervision_mut(&mut self) -> &mut SupervisionRegistry {
&mut self.supervision
}
pub(crate) async fn supervise_with<C>(
&mut self,
config: ActorConfig,
configure: impl Fn(&mut ManagedActor<Idle, C>) + Send + Sync + 'static,
) -> Result<SupervisedChild, SupervisionError>
where
C: Default + Send + Debug + 'static,
{
let blueprint: Arc<ChildBlueprint<C>> = Arc::new(configure);
let spawner: Arc<dyn ChildSpawner> =
Arc::new(TypedSpawner::new(config.clone(), blueprint));
let child_id = config.id();
let restart_policy = spawner.restart_policy();
let limiter = self.resolve_limiter(config.restart_limiter_config());
let handle = spawner.spawn(self.runtime.clone(), self.handle.clone()).await?;
let (status, receiver) = status_channel(&child_id, Some(handle.clone()));
let slot = NewSlot {
ern: child_id.clone(),
handle: handle.clone(),
spawner: Some(spawner),
restart_policy,
limiter,
status,
};
let registered = self.supervision.register(slot);
if let Err(error) = registered {
let _ = handle.stop().await;
return Err(error);
}
Ok(SupervisedChild::new(child_id, self.id.clone(), receiver))
}
pub fn supervise_deferred<C>(
&mut self,
config: ActorConfig,
configure: impl Fn(&mut ManagedActor<Idle, C>) + Send + Sync + 'static,
) -> Result<SupervisedChild, SupervisionError>
where
C: Default + Send + Debug + 'static,
{
let blueprint: Arc<ChildBlueprint<C>> = Arc::new(configure);
let limiter = self.resolve_limiter(config.restart_limiter_config());
let spawner: Arc<dyn ChildSpawner> = Arc::new(TypedSpawner::new(config, blueprint));
let child_id = spawner.child_id().clone();
let restart_policy = spawner.restart_policy();
let (status, receiver) = status_channel(&child_id, None);
self.supervision.register_pending(PendingSlot {
ern: child_id.clone(),
spawner,
restart_policy,
limiter,
status,
})?;
trace!(
"Actor {} recorded child {} for a deferred start",
self.id(),
child_id
);
Ok(SupervisedChild::new(child_id, self.id.clone(), receiver))
}
pub(crate) fn launch_pending_starts(&mut self) {
while !self.is_cancelled() {
let Some(ticket) = self.supervision.begin_start() else {
break;
};
trace!(
"Actor {} is starting supervised child {}",
self.id(),
ticket.ern
);
let runtime = self.runtime.clone();
let supervisor = self.handle.clone();
self.start_tasks
.spawn(start_supervised_child(ticket, runtime, supervisor));
}
}
pub(crate) fn record_child_terminated(&mut self, notice: &ChildTerminated) {
let Some(index) = self.supervision.index_of(¬ice.child_id) else {
return;
};
let Some(snapshot) = self.supervision.snapshot(index) else {
return;
};
let views = self.supervision.views();
let strategy = self.supervision_strategy;
let now = std::time::Instant::now();
let Some(slot) = self.supervision.slot_mut(index) else {
return;
};
let outcome = evaluate(notice, &snapshot, strategy, slot.limiter_mut(), &views, now);
match outcome {
SupervisionOutcome::Ignore => {
trace!(
"Actor {} expected child {} to stop; no restart considered",
self.id(),
notice.child_id
);
}
SupervisionOutcome::GroupStopLanded { then_restart } => {
self.record_group_stop_landed(index, ¬ice.child_id, then_restart);
}
SupervisionOutcome::Forget => self.record_child_down(index, ¬ice.child_id),
SupervisionOutcome::Escalate(exceeded) => {
self.record_child_escalated(index, ¬ice.child_id, exceeded);
}
SupervisionOutcome::Restart { plan, backoff } => {
self.schedule_restart(¬ice.child_id, &plan, backoff);
}
}
}
fn record_group_stop_landed(
&mut self,
index: ChildIndex,
child: &acton_ern::Ern,
then_restart: bool,
) {
if !then_restart {
trace!(
"Actor {} stopped child {} for a group restart and is leaving it down",
self.id(),
child
);
self.record_child_down(index, child);
return;
}
let Some(slot) = self.supervision.slot_mut(index) else {
return;
};
slot.set_handle(None);
slot.set_state(SlotState::AwaitingBackoff);
slot.publish();
trace!(
"Actor {} has child {} down and waiting on its group's restart",
self.id(),
child
);
}
fn record_child_down(&mut self, index: ChildIndex, child: &acton_ern::Ern) {
trace!("Actor {} is leaving child {} down", self.id(), child);
self.supervision.mark_terminal(index, SlotState::Down, None);
#[cfg(feature = "ipc")]
self.forget_ipc_names(index, child);
}
fn record_child_escalated(
&mut self,
index: ChildIndex,
child: &acton_ern::Ern,
exceeded: RestartLimitExceeded,
) {
warn!(
"Actor {} is giving up on child {} ({}): {}",
self.id(),
child,
self.escalation,
exceeded
);
let stats = crate::actor::RestartStats {
restarts_in_window: exceeded.attempts,
consecutive_restarts: exceeded.attempts,
window_secs: exceeded.window_secs,
max_restarts: exceeded.max_restarts,
};
self.supervision.mark_terminal(
index,
SlotState::Escalated,
Some(SupervisionError::RestartLimit {
child: child.clone(),
limit: exceeded,
}),
);
#[cfg(feature = "ipc")]
self.forget_ipc_names(index, child);
self.apply_escalation(child, stats);
}
fn apply_escalation(&self, child: &acton_ern::Ern, stats: crate::actor::RestartStats) {
match self.escalation {
super::Escalation::NotifyParent => self.notify_parent_of_escalation(child, stats),
super::Escalation::StopSupervisor => self.stop_self_after_escalation(child),
}
}
fn notify_parent_of_escalation(&self, child: &acton_ern::Ern, stats: crate::actor::RestartStats) {
let Some(parent) = self.parent.clone() else {
trace!(
"Actor {} has no parent to tell that it gave up on child {}",
self.id(),
child
);
return;
};
let notification = super::SupervisionEscalated::new(
self.id.clone(),
child.clone(),
stats,
crate::actor::TerminationReason::Normal,
);
trace!(
"Actor {} is telling parent {} that it gave up on child {}",
self.id(),
parent.id(),
child
);
tokio::spawn(async move {
parent.send(notification).await;
});
}
fn stop_self_after_escalation(&self, child: &acton_ern::Ern) {
warn!(
"Actor {} is stopping itself because it could not keep child {} running",
self.id(),
child
);
if let Some(token) = &self.cancellation_token {
token.cancel();
}
}
#[cfg(feature = "ipc")]
fn forget_ipc_names(&self, index: ChildIndex, child: &acton_ern::Ern) {
let engine_managed = self
.supervision
.slot(index)
.is_some_and(super::registry::ChildSlot::is_restartable);
if !engine_managed {
return;
}
let forgotten = self.runtime.ipc_forget(child);
if forgotten > 0 {
trace!(
"Actor {} dropped {} IPC name(s) for departed child {}",
self.id(),
forgotten,
child
);
}
}
fn schedule_restart(
&mut self,
child: &acton_ern::Ern,
plan: &RestartPlan,
backoff: BackoffDelay,
) {
let stops = self.begin_group_stops(plan);
let dues = self.park_group_for_restart(plan);
trace!(
"Actor {} will stop {} child(ren) and restart {} after {}, prompted by child {}",
self.id(),
stops.len(),
dues.len(),
backoff,
child
);
self.spawn_group_restart(stops, dues, backoff);
}
fn begin_group_stops(&mut self, plan: &RestartPlan) -> Vec<ActorHandle> {
let mut stops = Vec::with_capacity(plan.stop.len());
for index in &plan.stop {
let then_restart = plan.restart.contains(index);
let Some(slot) = self.supervision.slot_mut(*index) else {
continue;
};
let handle = slot.handle().cloned();
slot.set_state(SlotState::ExpectedStop { then_restart });
slot.publish();
if let Some(handle) = handle {
stops.push(handle);
}
}
stops
}
fn park_group_for_restart(&mut self, plan: &RestartPlan) -> Vec<RestartDue> {
let mut dues = Vec::with_capacity(plan.restart.len());
for index in &plan.restart {
let being_stopped = plan.stop.contains(index);
let Some(slot) = self.supervision.slot_mut(*index) else {
continue;
};
if !being_stopped {
slot.set_handle(None);
slot.set_state(SlotState::AwaitingBackoff);
slot.publish();
}
dues.push(RestartDue {
child: slot.ern().clone(),
index: *index,
generation: slot.generation(),
});
}
dues
}
fn spawn_group_restart(
&self,
stops: Vec<ActorHandle>,
dues: Vec<RestartDue>,
backoff: BackoffDelay,
) {
let supervisor = self.handle.clone();
let token = self.cancellation_token.clone();
let delay = backoff.duration();
tokio::spawn(async move {
for handle in stops {
stop_group_member(handle).await;
}
if let Some(token) = token {
tokio::select! {
() = token.cancelled() => return,
() = tokio::time::sleep(delay) => {}
}
} else {
tokio::time::sleep(delay).await;
}
for due in dues {
let envelope = supervisor.create_envelope(Some(supervisor.reply_address()));
if let Err(undeliverable) = envelope.try_send(due).await {
trace!(
"Supervisor {} was gone before a restart came due ({})",
supervisor.id(),
undeliverable
);
return;
}
}
});
}
pub(crate) fn record_restart_due(&mut self, due: &RestartDue) {
if self
.supervision
.queue_restart(due.index, &due.child, due.generation)
{
trace!(
"Actor {} queued a restart of child {} ({})",
self.id(),
due.child,
due.generation
);
} else {
trace!(
"Actor {} discarded a stale restart timer for child {} ({})",
self.id(),
due.child,
due.generation
);
}
}
pub(crate) fn record_started_child(&mut self, message: &SupervisedChildStarted) {
match &message.outcome {
Ok(handle) => {
let recorded = self.supervision.complete_start(
message.index,
&message.child,
handle.clone(),
std::time::Instant::now(),
);
if recorded.is_recorded() {
trace!(
"Actor {} now supervises child {} ({})",
self.id(),
message.child,
recorded
);
if recorded == StartRecorded::Restart {
#[cfg(feature = "ipc")]
self.rebind_ipc_names(&message.child, handle);
}
} else {
warn!(
"Actor {} no longer supervises child {}; stopping the incarnation it started",
self.id(),
message.child
);
self.stop_disowned_child(handle.clone());
}
}
Err(error) => {
warn!(
"Actor {} could not start supervised child {}: {}",
self.id(),
message.child,
error
);
self.supervision.fail_start(message.index, error);
}
}
}
#[cfg(feature = "ipc")]
fn rebind_ipc_names(&self, child: &acton_ern::Ern, fresh: &ActorHandle) {
let repointed = self.runtime.ipc_rebind(child, fresh);
if repointed > 0 {
trace!(
"Actor {} repointed {} IPC name(s) at the new incarnation of {}",
self.id(),
repointed,
child
);
}
}
#[cfg(feature = "ipc")]
pub(crate) fn forget_children_ipc_names(&self) {
for child in self.supervision.engine_managed_children() {
let forgotten = self.runtime.ipc_forget(&child);
if forgotten > 0 {
trace!(
"Actor {} dropped {} IPC name(s) for child {} while stopping",
self.id(),
forgotten,
child
);
}
}
}
fn stop_disowned_child(&self, handle: ActorHandle) {
self.start_tasks.spawn(async move {
stop_stray_child(handle).await;
});
}
fn is_cancelled(&self) -> bool {
self.cancellation_token
.as_ref()
.is_some_and(tokio_util::sync::CancellationToken::is_cancelled)
}
pub(crate) fn cancel_unfinished_children(&mut self) {
let supervisor = self.id.clone();
let abandoned = self.supervision.cancel_unfinished_starts(&supervisor);
if abandoned > 0 {
trace!(
"Actor {} abandoned {} unfinished child start(s) while stopping",
self.id(),
abandoned
);
}
}
pub(crate) fn take_late_started_children(&mut self) -> Vec<ActorHandle> {
let mut handles = Vec::new();
while let Ok(envelope) = self.inbox.try_recv() {
if let Some(started) = envelope
.message
.as_any()
.downcast_ref::<SupervisedChildStarted>()
{
if let Ok(handle) = &started.outcome {
warn!(
"Actor {} stopped before adopting child {}; stopping it with the rest",
self.id(),
started.child
);
handles.push(handle.clone());
}
}
}
handles
}
pub(crate) async fn unsupervise(&mut self, child: &acton_ern::Ern) -> Result<(), SupervisionError> {
let retired = self.supervision.retire(child);
match retired {
Ok(Some(handle)) => {
let _ = handle.stop().await;
Ok(())
}
Ok(None) => Ok(()),
Err(error) => Err(error),
}
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::time::Duration;
use acton_ern::Ern;
use super::super::registry::{ChildSlot, SlotState};
use super::*;
use crate::actor::{ChildIndex, RestartPolicy};
use crate::common::{ActonApp, ActorRuntime};
const PATIENCE: Duration = Duration::from_secs(5);
#[derive(Debug, Default)]
struct Supervisor;
#[derive(Debug)]
struct RefusingSpawner {
child: Ern,
attempts: Arc<AtomicUsize>,
}
impl ChildSpawner for RefusingSpawner {
fn child_id(&self) -> &Ern {
&self.child
}
fn restart_policy(&self) -> RestartPolicy {
RestartPolicy::Permanent
}
fn spawn(
&self,
_runtime: ActorRuntime,
_parent: ActorHandle,
) -> std::pin::Pin<
Box<
dyn std::future::Future<Output = Result<ActorHandle, SupervisionError>>
+ Send
+ '_,
>,
> {
self.attempts.fetch_add(1, Ordering::SeqCst);
Box::pin(async move {
Err(SupervisionError::ConfigRejected {
child: self.child.clone(),
reason: "this spawner never builds an actor".to_string(),
})
})
}
}
#[derive(Debug)]
struct NeverFinishingSpawner {
child: Ern,
}
impl ChildSpawner for NeverFinishingSpawner {
fn child_id(&self) -> &Ern {
&self.child
}
fn restart_policy(&self) -> RestartPolicy {
RestartPolicy::Permanent
}
fn spawn(
&self,
_runtime: ActorRuntime,
_parent: ActorHandle,
) -> std::pin::Pin<
Box<
dyn std::future::Future<Output = Result<ActorHandle, SupervisionError>>
+ Send
+ '_,
>,
> {
Box::pin(std::future::pending())
}
}
async fn wait_for_flag(flag: &Arc<AtomicBool>) -> bool {
for _ in 0..300 {
if flag.load(Ordering::SeqCst) {
return true;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
flag.load(Ordering::SeqCst)
}
fn supervisor(runtime: &mut ActorRuntime) -> ManagedActor<Started, Supervisor> {
runtime.new_actor::<Supervisor>().into()
}
fn queue_refusal(
actor: &mut ManagedActor<Started, Supervisor>,
attempts: &Arc<AtomicUsize>,
) -> SupervisedChild {
let child = actor
.id()
.add_part("worker")
.expect("'worker' is a valid Ern part");
let (status, receiver) = status_channel(&child, None);
actor
.supervision
.register_pending(PendingSlot {
ern: child.clone(),
spawner: Arc::new(RefusingSpawner {
child: child.clone(),
attempts: Arc::clone(attempts),
}),
restart_policy: RestartPolicy::Permanent,
limiter: RestartLimiter::default(),
status,
})
.expect("the first registration of a name succeeds");
SupervisedChild::new(child, actor.id().clone(), receiver)
}
async fn deliver_one_report(actor: &mut ManagedActor<Started, Supervisor>) {
let envelope = tokio::time::timeout(PATIENCE, actor.inbox.recv())
.await
.expect("a start task must report back")
.expect("the inbox is open");
let started = envelope
.message
.as_any()
.downcast_ref::<SupervisedChildStarted>()
.expect("a start task sends nothing else")
.clone();
actor.record_started_child(&started);
}
#[tokio::test]
async fn a_start_that_fails_reaches_the_caller_instead_of_stranding_it() {
let mut runtime = ActonApp::launch_async().await;
let mut actor = supervisor(&mut runtime);
let attempts = Arc::new(AtomicUsize::new(0));
let mut child = queue_refusal(&mut actor, &attempts);
actor.launch_pending_starts();
deliver_one_report(&mut actor).await;
let error = tokio::time::timeout(PATIENCE, child.wait_running())
.await
.expect("a failed start must end the wait")
.expect_err("the child was never created");
assert!(
matches!(error, SupervisionError::ConfigRejected { .. }),
"the spawner's own reason must survive: {error}"
);
assert_eq!(attempts.load(Ordering::SeqCst), 1);
assert!(!actor.supervision.has_pending_starts());
assert_eq!(
actor.supervision.index_of(child.ern()),
None,
"a failed start frees the name for another attempt"
);
assert!(
actor.supervision.live_handles().is_empty(),
"nothing was created, so there is nothing to stop"
);
}
#[tokio::test]
async fn launching_a_start_does_not_wait_for_it() {
let mut runtime = ActonApp::launch_async().await;
let mut actor = supervisor(&mut runtime);
let child = actor
.id()
.add_part("slow")
.expect("'slow' is a valid Ern part");
let (status, _receiver) = status_channel(&child, None);
actor
.supervision
.register_pending(PendingSlot {
ern: child.clone(),
spawner: Arc::new(NeverFinishingSpawner {
child: child.clone(),
}),
restart_policy: RestartPolicy::Permanent,
limiter: RestartLimiter::default(),
status,
})
.expect("the first registration of a name succeeds");
tokio::time::timeout(PATIENCE, async { actor.launch_pending_starts() })
.await
.expect("launching must not wait on the spawner");
assert!(!actor.supervision.has_pending_starts());
assert_eq!(
actor.supervision.slot_of(&child).map(ChildSlot::state),
Some(SlotState::Starting),
"the slot records that somebody else is building it"
);
}
#[tokio::test]
async fn a_child_retired_before_its_turn_is_never_created() {
let mut runtime = ActonApp::launch_async().await;
let mut actor = supervisor(&mut runtime);
let attempts = Arc::new(AtomicUsize::new(0));
let child = queue_refusal(&mut actor, &attempts);
actor
.supervision
.retire(child.ern())
.expect("the child is supervised, queued or not");
actor.launch_pending_starts();
assert_eq!(
attempts.load(Ordering::SeqCst),
0,
"the spawner must not run for a child nobody supervises any more"
);
assert!(!actor.supervision.has_pending_starts());
}
#[tokio::test]
async fn a_cancelled_supervisor_launches_nothing_new() {
let mut runtime = ActonApp::launch_async().await;
let mut actor = supervisor(&mut runtime);
let attempts = Arc::new(AtomicUsize::new(0));
let mut child = queue_refusal(&mut actor, &attempts);
actor
.cancellation_token
.as_ref()
.expect("a started actor always has a token")
.cancel();
actor.launch_pending_starts();
assert_eq!(
attempts.load(Ordering::SeqCst),
0,
"a supervisor on its way down starts nothing new"
);
assert!(
actor.supervision.has_pending_starts(),
"what it did not launch stays queued for shutdown to answer"
);
actor.cancel_unfinished_children();
let error = tokio::time::timeout(PATIENCE, child.wait_running())
.await
.expect("shutdown must end the wait")
.expect_err("the child was never created");
assert!(
matches!(error, SupervisionError::SupervisorStopped { .. }),
"unexpected error: {error}"
);
assert!(!actor.supervision.has_pending_starts());
}
#[tokio::test]
async fn shutdown_answers_a_start_that_is_still_in_flight() {
let mut runtime = ActonApp::launch_async().await;
let mut actor = supervisor(&mut runtime);
let child = actor
.id()
.add_part("slow")
.expect("'slow' is a valid Ern part");
let (status, receiver) = status_channel(&child, None);
actor
.supervision
.register_pending(PendingSlot {
ern: child.clone(),
spawner: Arc::new(NeverFinishingSpawner {
child: child.clone(),
}),
restart_policy: RestartPolicy::Permanent,
limiter: RestartLimiter::default(),
status,
})
.expect("the first registration of a name succeeds");
let mut waiting = SupervisedChild::new(child.clone(), actor.id().clone(), receiver);
actor.launch_pending_starts();
assert!(
actor
.supervision
.slot_of(&child)
.is_some_and(ChildSlot::is_starting),
"the start really is in flight"
);
actor.cancel_unfinished_children();
let error = tokio::time::timeout(PATIENCE, waiting.wait_running())
.await
.expect("a supervisor stopping mid-start must end the wait")
.expect_err("the child never came up");
assert!(
matches!(error, SupervisionError::SupervisorStopped { .. }),
"unexpected error: {error}"
);
assert_eq!(
actor.supervision.index_of(&child),
None,
"and the record is settled rather than left half-started"
);
}
#[tokio::test]
async fn a_child_this_actor_stopped_supervising_is_stopped_too() {
let mut runtime = ActonApp::launch_async().await;
let mut actor = supervisor(&mut runtime);
let stopped = Arc::new(AtomicBool::new(false));
let config = ActorConfig::for_supervised_child("worker", actor.handle.clone(), None)
.expect("a name plus a live parent is a valid child configuration");
let child_id = config.id();
let (status, _receiver) = status_channel(&child_id, None);
let blueprint: Arc<ChildBlueprint<Supervisor>> = {
let stopped = Arc::clone(&stopped);
Arc::new(move |child: &mut ManagedActor<Idle, Supervisor>| {
let stopped = Arc::clone(&stopped);
child.after_stop(move |_actor| {
let stopped = Arc::clone(&stopped);
async move {
stopped.store(true, Ordering::SeqCst);
}
});
})
};
actor
.supervision
.register_pending(PendingSlot {
ern: child_id.clone(),
spawner: Arc::new(TypedSpawner::new(config, blueprint)),
restart_policy: RestartPolicy::Permanent,
limiter: RestartLimiter::default(),
status,
})
.expect("the first registration of a name succeeds");
actor.launch_pending_starts();
actor
.supervision
.retire(&child_id)
.expect("the child is supervised while its start is in flight");
deliver_one_report(&mut actor).await;
assert!(
wait_for_flag(&stopped).await,
"the child was built, disowned, and then left running"
);
assert!(
actor.supervision.live_handles().is_empty(),
"and it was not recorded on the way past"
);
}
fn supervisor_allowing(
runtime: &mut ActorRuntime,
max_restarts: u32,
) -> ManagedActor<Started, Supervisor> {
let config = ActorConfig::new(
Ern::with_root("pool").expect("'pool' is a valid Ern root"),
None,
)
.with_restart_limiter(RestartLimiterConfig {
max_restarts,
..RestartLimiterConfig::default()
});
runtime.new_actor_with_config::<Supervisor>(config).into()
}
fn queue_child(
actor: &mut ManagedActor<Started, Supervisor>,
name: &str,
max_restarts: Option<u32>,
) -> Ern {
let mut config = ActorConfig::for_supervised_child(name, actor.handle.clone(), None)
.expect("a name plus a live parent is a valid child configuration");
if let Some(max_restarts) = max_restarts {
config = config.with_restart_limiter(RestartLimiterConfig {
max_restarts,
..RestartLimiterConfig::default()
});
}
let child_id = config.id();
actor
.supervise_deferred(config, |_child: &mut ManagedActor<Idle, Supervisor>| {})
.expect("the first registration of a name succeeds");
child_id
}
fn recorded_allowance(actor: &mut ManagedActor<Started, Supervisor>, child: &Ern) -> u32 {
actor
.supervision
.slot_of_mut(child)
.expect("the child is supervised")
.limiter_mut()
.stats()
.max_restarts
}
#[tokio::test]
async fn a_child_that_sets_no_allowance_inherits_its_supervisors() {
let mut runtime = ActonApp::launch_async().await;
let mut actor = supervisor_allowing(&mut runtime, 9);
let child = queue_child(&mut actor, "worker", None);
assert_eq!(recorded_allowance(&mut actor, &child), 9);
}
#[tokio::test]
async fn a_childs_own_allowance_overrides_its_supervisors() {
let mut runtime = ActonApp::launch_async().await;
let mut actor = supervisor_allowing(&mut runtime, 2);
let child = queue_child(&mut actor, "worker", Some(11));
assert_eq!(
recorded_allowance(&mut actor, &child),
11,
"the child's own setting wins where both are set"
);
}
#[tokio::test]
async fn a_child_burning_through_its_allowance_leaves_its_siblings_untouched() {
let mut runtime = ActonApp::launch_async().await;
let mut actor = supervisor_allowing(&mut runtime, 3);
let noisy = queue_child(&mut actor, "noisy", None);
let quiet = queue_child(&mut actor, "quiet", None);
{
let limiter = actor
.supervision
.slot_of_mut(&noisy)
.expect("the child is supervised")
.limiter_mut();
for _ in 0..3 {
limiter.can_restart().expect("within the allowance");
let _ = limiter.record_restart();
}
assert!(
limiter.can_restart().is_err(),
"the noisy child is out of restarts"
);
}
let quiet_limiter = actor
.supervision
.slot_of_mut(&quiet)
.expect("the child is supervised")
.limiter_mut();
assert_eq!(
quiet_limiter.restarts_in_window(),
0,
"a sibling's crashes are not charged to this child"
);
assert!(
quiet_limiter.can_restart().is_ok(),
"and it keeps its full allowance"
);
}
fn adopt_legacy_child(
actor: &mut ManagedActor<Started, Supervisor>,
name: &str,
) -> (Ern, watch::Receiver<SupervisionStatus>) {
let child = actor
.id()
.add_part(name)
.expect("a short name is a valid Ern part");
let (status, receiver) = status_channel(&child, None);
actor
.supervision
.register(NewSlot {
ern: child.clone(),
handle: handle_for(&child),
spawner: None,
restart_policy: RestartPolicy::Permanent,
limiter: RestartLimiter::default(),
status,
})
.expect("the first registration of a name succeeds");
(child, receiver)
}
fn handle_for(id: &Ern) -> ActorHandle {
let (outbox, _inbox) = tokio::sync::mpsc::channel(8);
ActorHandle::new(id.clone(), outbox)
}
#[tokio::test]
async fn a_child_with_no_blueprint_is_left_down_without_spending_an_allowance() {
let mut runtime = ActonApp::launch_async().await;
let mut actor = supervisor(&mut runtime);
let (child, receiver) = adopt_legacy_child(&mut actor, "legacy");
actor.record_child_terminated(&ChildTerminated::new(
child.clone(),
crate::actor::TerminationReason::Normal,
RestartPolicy::Permanent,
));
let slot = actor
.supervision
.slot_of_mut(&child)
.expect("the child is still recorded");
assert_eq!(
slot.state(),
SlotState::Down,
"a child nobody can rebuild is left down"
);
assert_eq!(
slot.limiter_mut().restarts_in_window(),
0,
"and is not charged for a restart that was never going to happen"
);
assert_eq!(receiver.borrow().state(), SupervisionState::Down);
assert!(
!actor.supervision.has_pending_starts(),
"nothing was queued to rebuild it"
);
}
#[tokio::test]
async fn a_child_with_a_blueprint_is_restarted_where_a_legacy_one_is_not() {
let mut runtime = ActonApp::launch_async().await;
let mut actor = supervisor(&mut runtime);
let engine_managed = queue_child(&mut actor, "worker", None);
actor.launch_pending_starts();
let handle = handle_for(&engine_managed);
assert!(actor
.supervision
.complete_start(
ChildIndex::new(0),
&engine_managed,
handle,
std::time::Instant::now(),
)
.is_recorded());
actor.record_child_terminated(&ChildTerminated::new(
engine_managed.clone(),
crate::actor::TerminationReason::Normal,
RestartPolicy::Permanent,
));
let slot = actor
.supervision
.slot_of_mut(&engine_managed)
.expect("the child is still recorded");
assert_eq!(
slot.state(),
SlotState::AwaitingBackoff,
"a child with a blueprint is on its way back"
);
assert_eq!(
slot.limiter_mut().restarts_in_window(),
1,
"and this one really is charged for it"
);
}
fn group_of_one_managed_and_one_adopted(
actor: &mut ManagedActor<Started, Supervisor>,
) -> (Ern, Ern) {
actor.supervision_strategy = super::super::SupervisionStrategy::OneForAll;
let managed = queue_child(actor, "worker", None);
actor.launch_pending_starts();
assert!(actor
.supervision
.complete_start(
ChildIndex::new(0),
&managed,
handle_for(&managed),
std::time::Instant::now(),
)
.is_recorded());
let (adopted, _status) = adopt_legacy_child(actor, "adopted");
(managed, adopted)
}
#[tokio::test]
async fn a_group_restart_marks_a_sibling_it_cannot_rebuild_as_not_coming_back() {
let mut runtime = ActonApp::launch_async().await;
let mut actor = supervisor(&mut runtime);
let (managed, adopted) = group_of_one_managed_and_one_adopted(&mut actor);
actor.record_child_terminated(&ChildTerminated::new(
managed,
crate::actor::TerminationReason::Normal,
RestartPolicy::Permanent,
));
assert_eq!(
actor
.supervision
.slot_of(&adopted)
.expect("the adopted child is still recorded")
.state(),
SlotState::ExpectedStop {
then_restart: false
},
"a sibling the supervisor holds no blueprint for is stopped and not promised back"
);
}
#[tokio::test]
async fn a_group_restart_marks_a_sibling_it_can_rebuild_as_coming_back() {
let mut runtime = ActonApp::launch_async().await;
let mut actor = supervisor(&mut runtime);
actor.supervision_strategy = super::super::SupervisionStrategy::OneForAll;
let failed = queue_child(&mut actor, "worker", None);
let sibling = queue_child(&mut actor, "sibling", None);
actor.launch_pending_starts();
for (index, child) in [(0, &failed), (1, &sibling)] {
assert!(actor
.supervision
.complete_start(
ChildIndex::new(index),
child,
handle_for(child),
std::time::Instant::now(),
)
.is_recorded());
}
actor.record_child_terminated(&ChildTerminated::new(
failed,
crate::actor::TerminationReason::Normal,
RestartPolicy::Permanent,
));
assert_eq!(
actor
.supervision
.slot_of(&sibling)
.expect("the sibling is still recorded")
.state(),
SlotState::ExpectedStop { then_restart: true },
);
}
#[tokio::test]
async fn a_landed_group_stop_moves_a_sibling_to_the_state_its_flag_names() {
for (then_restart, expected) in [
(true, SlotState::AwaitingBackoff),
(false, SlotState::Down),
] {
let mut runtime = ActonApp::launch_async().await;
let mut actor = supervisor(&mut runtime);
let (child, _receiver) = adopt_legacy_child(&mut actor, "sibling");
actor
.supervision
.slot_of_mut(&child)
.expect("the child is supervised")
.set_state(SlotState::ExpectedStop { then_restart });
actor.record_child_terminated(&ChildTerminated::new(
child.clone(),
crate::actor::TerminationReason::Normal,
RestartPolicy::Permanent,
));
assert_eq!(
actor
.supervision
.slot_of(&child)
.expect("the child is still recorded")
.state(),
expected,
"a group stop with then_restart={then_restart} must come to rest in {expected}"
);
}
}
#[tokio::test]
async fn a_supervisor_shutting_down_abandons_a_group_restart_instead_of_driving_it() {
let mut runtime = ActonApp::launch_async().await;
let mut actor = supervisor(&mut runtime);
let (child, _receiver) = adopt_legacy_child(&mut actor, "sibling");
actor
.supervision
.slot_of_mut(&child)
.expect("the child is supervised")
.set_state(SlotState::ExpectedStop { then_restart: true });
actor.supervision.begin_shutdown();
actor.record_child_terminated(&ChildTerminated::new(
child.clone(),
crate::actor::TerminationReason::Normal,
RestartPolicy::Permanent,
));
assert_eq!(
actor
.supervision
.slot_of(&child)
.expect("the child is still recorded")
.state(),
SlotState::ExpectedStop { then_restart: true },
"a shutdown leaves the slot alone rather than parking it for a restart \
that will never be performed"
);
assert!(
!actor.supervision.has_pending_starts(),
"and nothing is queued to come back"
);
}
#[tokio::test]
async fn a_report_that_lands_after_the_loop_stops_is_not_lost() {
let mut runtime = ActonApp::launch_async().await;
let mut actor = supervisor(&mut runtime);
let child = runtime.new_actor::<Supervisor>().start().await;
let envelope = actor
.handle
.create_envelope(Some(actor.handle.reply_address()));
envelope
.try_send(SupervisedChildStarted {
child: child.id(),
index: ChildIndex::new(0),
outcome: Ok(child.clone()),
})
.await
.expect("the inbox is open");
let late = actor.take_late_started_children();
assert_eq!(late.len(), 1, "the child in the undelivered report");
assert_eq!(late[0].id(), child.id());
assert!(
actor
.shutdown_child_handles(late)
.iter()
.any(|handle| handle.id() == child.id()),
"and it joins the children the shutdown stops"
);
}
}