use std::any::{Any, TypeId};
use std::fmt::Debug;
use std::panic::AssertUnwindSafe;
use std::time::{Duration, Instant};
use futures::future::join_all;
use futures::stream::{FuturesUnordered, StreamExt};
use futures::FutureExt;
use tokio_util::task::TaskTracker;
use tracing::{error, instrument, trace};
use crate::actor::{ManagedActor, TerminationReason};
use crate::common::config::CONFIG;
use crate::common::{
Envelope, FutureBoxReadOnlyOutcome, OutboundEnvelope, ReactorItem, ReactorMap,
ReadOnlyHandlerError,
};
use crate::message::{
BrokerRequestEnvelope, CascadeTerminate, ChildTerminated, MessageAddress,
RegisterSupervisedChild, RemoveAllSubscriptions, RestartDue, SupervisedChildStarted,
SystemSignal, UnregisterSupervisedChild,
};
use crate::traits::ActorHandleInterface;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Started;
mod panic_helpers {
use std::any::Any;
pub(super) fn extract_panic_message(payload: &Box<dyn Any + Send>) -> String {
payload
.downcast_ref::<&str>()
.map_or_else(
|| {
payload.downcast_ref::<String>().map_or_else(
|| format!("Panic with payload type: {:?}", (**payload).type_id()),
Clone::clone,
)
},
|s| (*s).to_string(),
)
}
#[cfg(feature = "catch-handler-panics")]
pub(super) fn log_handler_panic(
actor_id: &acton_ern::Ern,
message_type_id: std::any::TypeId,
panic_payload: &Box<dyn Any + Send>,
context: &str,
) {
let panic_msg = extract_panic_message(panic_payload);
tracing::error!(
actor_id = %actor_id,
message_type = ?message_type_id,
panic_message = %panic_msg,
"{context}"
);
}
#[cfg(feature = "catch-handler-panics")]
pub(super) fn log_error_handler_panic(
actor_id: &acton_ern::Ern,
message_type_id: std::any::TypeId,
error_type_id: std::any::TypeId,
panic_payload: &Box<dyn Any + Send>,
context: &str,
) {
let panic_msg = extract_panic_message(panic_payload);
tracing::error!(
actor_id = %actor_id,
message_type = ?message_type_id,
error_type = ?error_type_id,
panic_message = %panic_msg,
"{context}"
);
}
#[cfg(feature = "catch-handler-panics")]
pub(super) fn log_lifecycle_panic(
actor_id: &acton_ern::Ern,
panic_payload: &Box<dyn Any + Send>,
context: &str,
) {
let panic_msg = extract_panic_message(panic_payload);
tracing::error!(actor_id = %actor_id, panic_message = %panic_msg, "{context}");
}
}
use panic_helpers::extract_panic_message;
#[cfg(feature = "catch-handler-panics")]
use panic_helpers::{log_error_handler_panic, log_handler_panic, log_lifecycle_panic};
macro_rules! run_lifecycle_hook {
($self:expr, $hook:ident, $hook_name:literal) => {{
if let Some(ref hook) = $self.$hook {
#[cfg(feature = "catch-handler-panics")]
{
let hook_result =
std::panic::catch_unwind(AssertUnwindSafe(|| hook($self)));
match hook_result {
Ok(future) => {
if let Err(ref panic_payload) =
AssertUnwindSafe(future).catch_unwind().await
{
log_lifecycle_panic(
$self.id(),
panic_payload,
concat!("Panic in ", $hook_name, " lifecycle hook"),
);
}
}
Err(ref panic_payload) => {
log_lifecycle_panic(
$self.id(),
panic_payload,
concat!("Panic in ", $hook_name, " lifecycle hook"),
);
}
}
}
#[cfg(not(feature = "catch-handler-panics"))]
{
hook($self).await;
}
}
}};
}
impl<Actor: Default + Send + Debug + 'static> ManagedActor<Started, Actor> {
pub fn new_envelope(&self) -> Option<OutboundEnvelope> {
self.cancellation_token.clone().map(|cancellation_token| {
OutboundEnvelope::new(
MessageAddress::new(self.handle.outbox.clone(), self.id.clone()),
cancellation_token,
)
})
}
pub fn new_parent_envelope(&self) -> Option<OutboundEnvelope> {
let cancellation_token = self.cancellation_token.clone()?;
self.parent.as_ref().map(|parent_handle| {
OutboundEnvelope::new_with_recipient(
MessageAddress::new(self.handle.outbox.clone(), self.id.clone()), parent_handle.reply_address(), cancellation_token,
)
})
}
async fn dispatch_mutable_handler(
&mut self,
reactor: &ReactorItem<Actor>,
envelope: &mut Envelope,
) {
let message_type_id = envelope.message.as_any().type_id();
match reactor {
ReactorItem::Mutable(fut) => {
self.dispatch_mutable_infallible(fut, envelope, message_type_id).await;
}
ReactorItem::MutableFallible(fut) => {
self.dispatch_mutable_fallible(fut, envelope, message_type_id).await;
}
ReactorItem::MutableSync(handler) => {
#[cfg(feature = "catch-handler-panics")]
{
let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
handler(self, envelope);
}));
if let Err(ref panic_payload) = result {
log_handler_panic(
self.id(),
message_type_id,
panic_payload,
"Panic in sync mutable message handler",
);
}
}
#[cfg(not(feature = "catch-handler-panics"))]
{
let _ = message_type_id;
handler(self, envelope);
}
}
ReactorItem::ReadOnly(_) | ReactorItem::ReadOnlyFallible(_) | ReactorItem::ReadOnlySync(_) => {
tracing::warn!("Found read-only handler in mutable_reactors map");
}
}
}
async fn dispatch_mutable_infallible(
&mut self,
fut: &crate::common::FutureHandler<Actor>,
envelope: &mut Envelope,
message_type_id: TypeId,
) {
#[cfg(feature = "catch-handler-panics")]
{
let result = AssertUnwindSafe(async { fut(self, envelope).await })
.catch_unwind()
.await;
if let Err(ref panic_payload) = result {
log_handler_panic(
self.id(),
message_type_id,
panic_payload,
"Panic in mutable message handler",
);
}
}
#[cfg(not(feature = "catch-handler-panics"))]
{
let _ = message_type_id;
fut(self, envelope).await;
}
}
async fn dispatch_mutable_fallible(
&mut self,
fut: &crate::common::FutureHandlerResult<Actor>,
envelope: &mut Envelope,
message_type_id: TypeId,
) {
#[cfg(feature = "catch-handler-panics")]
{
let result = AssertUnwindSafe(async { fut(self, envelope).await })
.catch_unwind()
.await;
match result {
Ok(Ok(_)) => { }
Ok(Err((err, error_type_id))) => {
self.handle_fallible_error(envelope, message_type_id, error_type_id, err)
.await;
}
Err(ref panic_payload) => {
log_handler_panic(
self.id(),
message_type_id,
panic_payload,
"Panic in mutable fallible message handler",
);
}
}
}
#[cfg(not(feature = "catch-handler-panics"))]
{
match fut(self, envelope).await {
Ok(_) => { }
Err((err, error_type_id)) => {
self.handle_fallible_error(envelope, message_type_id, error_type_id, err)
.await;
}
}
}
}
async fn handle_fallible_error(
&mut self,
envelope: &mut Envelope,
message_type_id: TypeId,
error_type_id: TypeId,
err: Box<dyn std::error::Error + Send + Sync>,
) {
if let Some(handler) = self.error_handler_map.remove(&(message_type_id, error_type_id)) {
#[cfg(feature = "catch-handler-panics")]
{
let result =
AssertUnwindSafe(async { handler(self, envelope, err.as_ref()).await })
.catch_unwind()
.await;
if let Err(ref panic_payload) = result {
log_error_handler_panic(
self.id(),
message_type_id,
error_type_id,
panic_payload,
"Panic in error handler",
);
}
}
#[cfg(not(feature = "catch-handler-panics"))]
{
handler(self, envelope, err.as_ref()).await;
}
self.error_handler_map
.insert((message_type_id, error_type_id), handler);
} else {
error!(
actor_id = %self.id(),
message_type = ?message_type_id,
error = ?err,
"Unhandled error from message handler"
);
}
}
async fn flush_read_only_handlers(
&mut self,
read_only_futures: &mut FuturesUnordered<FutureBoxReadOnlyOutcome>,
) {
let mut deferred_errors: Vec<ReadOnlyHandlerError> = Vec::new();
while let Some(outcome) = read_only_futures.next().await {
if let Some(handler_error) = outcome {
deferred_errors.push(handler_error);
}
}
for mut handler_error in deferred_errors {
self.handle_fallible_error(
&mut handler_error.envelope,
handler_error.message_type_id,
handler_error.error_type_id,
handler_error.error,
)
.await;
}
}
fn enqueue_read_only_handler(
&self,
reactor: &ReactorItem<Actor>,
envelope: &mut Envelope,
read_only_futures: &FuturesUnordered<FutureBoxReadOnlyOutcome>,
) {
let actor_id = self.id().clone();
let message_type_id = envelope.message.as_any().type_id();
match reactor {
ReactorItem::ReadOnly(fut) => {
#[cfg(feature = "catch-handler-panics")]
{
let future_result = std::panic::catch_unwind(AssertUnwindSafe(|| {
fut(self, envelope)
}));
match future_result {
Ok(future) => {
read_only_futures.push(Box::pin(async move {
if let Err(panic_payload) =
AssertUnwindSafe(future).catch_unwind().await
{
let panic_msg = extract_panic_message(&panic_payload);
error!(
actor_id = %actor_id,
message_type = ?message_type_id,
panic_message = %panic_msg,
"Panic in read-only message handler"
);
}
None
}));
}
Err(panic_payload) => {
log_handler_panic(
&actor_id,
message_type_id,
&panic_payload,
"Panic in read-only message handler (during closure invocation)",
);
}
}
}
#[cfg(not(feature = "catch-handler-panics"))]
{
let _ = (actor_id, message_type_id);
let future = fut(self, envelope);
read_only_futures.push(Box::pin(async move {
future.await;
None
}));
}
}
ReactorItem::ReadOnlyFallible(fut) => {
#[cfg(feature = "catch-handler-panics")]
{
let future_result = std::panic::catch_unwind(AssertUnwindSafe(|| {
fut(self, envelope)
}));
match future_result {
Ok(future) => {
let envelope = envelope.clone();
read_only_futures.push(Box::pin(async move {
match AssertUnwindSafe(future).catch_unwind().await {
Ok(Ok(_)) => None,
Ok(Err((error, error_type_id))) => {
Some(ReadOnlyHandlerError {
envelope,
message_type_id,
error_type_id,
error,
})
}
Err(panic_payload) => {
let panic_msg = extract_panic_message(&panic_payload);
error!(
actor_id = %actor_id,
message_type = ?message_type_id,
panic_message = %panic_msg,
"Panic in read-only fallible message handler"
);
None
}
}
}));
}
Err(panic_payload) => {
log_handler_panic(
&actor_id,
message_type_id,
&panic_payload,
"Panic in read-only fallible message handler (during closure invocation)",
);
}
}
}
#[cfg(not(feature = "catch-handler-panics"))]
{
let future = fut(self, envelope);
let envelope = envelope.clone();
read_only_futures.push(Box::pin(async move {
match future.await {
Ok(_) => None,
Err((error, error_type_id)) => {
Some(ReadOnlyHandlerError {
envelope,
message_type_id,
error_type_id,
error,
})
}
}
}));
}
}
ReactorItem::ReadOnlySync(handler) => {
#[cfg(feature = "catch-handler-panics")]
{
let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
handler(self, envelope);
}));
if let Err(ref panic_payload) = result {
log_handler_panic(
&actor_id,
message_type_id,
panic_payload,
"Panic in sync read-only message handler",
);
}
}
#[cfg(not(feature = "catch-handler-panics"))]
{
let _ = (actor_id, message_type_id);
handler(self, envelope);
}
}
_ => {
tracing::warn!("Found mutable handler in read_only_reactors map");
}
}
}
#[instrument(skip(mutable_reactors, read_only_reactors, self))]
pub(crate) async fn wake(
&mut self,
mutable_reactors: ReactorMap<Actor>,
read_only_reactors: ReactorMap<Actor>,
) {
#[cfg(feature = "catch-handler-panics")]
let termination_reason = self
.run_message_loop(&mutable_reactors, &read_only_reactors)
.await;
#[cfg(not(feature = "catch-handler-panics"))]
let termination_reason = match AssertUnwindSafe(
self.run_message_loop(&mutable_reactors, &read_only_reactors),
)
.catch_unwind()
.await
{
Ok(reason) => reason,
Err(panic_payload) => {
let panic_msg = extract_panic_message(&panic_payload);
error!(
actor_id = %self.id(),
panic_message = %panic_msg,
"Actor terminated due to panic in message handler"
);
TerminationReason::Panic(panic_msg)
}
};
trace!("Message loop finished for actor: {}. Initiating final termination.", self.id());
#[cfg(not(feature = "catch-handler-panics"))]
let termination_reason = match AssertUnwindSafe(self.shutdown_cleanup())
.catch_unwind()
.await
{
Ok(()) => termination_reason,
Err(panic_payload) => {
let panic_msg = extract_panic_message(&panic_payload);
error!(
actor_id = %self.id(),
panic_message = %panic_msg,
"Panic during actor shutdown cleanup; parent notification is still sent"
);
if matches!(termination_reason, TerminationReason::Panic(_)) {
termination_reason
} else {
TerminationReason::Panic(panic_msg)
}
}
};
#[cfg(feature = "catch-handler-panics")]
self.shutdown_cleanup().await;
if let Some(parent) = &self.parent {
let notification = ChildTerminated::new(
self.id.clone(),
termination_reason,
self.restart_policy,
);
trace!(
"Notifying parent {} of child {} termination: {:?}",
parent.id(),
self.id(),
notification
);
let parent_clone = parent.clone();
parent_clone.send(notification).await;
}
trace!("Actor {} stopped.", self.id());
}
async fn run_message_loop(
&mut self,
mutable_reactors: &ReactorMap<Actor>,
read_only_reactors: &ReactorMap<Actor>,
) -> TerminationReason {
run_lifecycle_hook!(self, after_start, "after_start");
assert!(
self.cancellation_token.is_some(),
"ManagedActor in Started state must always have a cancellation_token"
);
let cancel_token = self.cancellation_token.clone().unwrap();
let mut cancel = Box::pin(cancel_token.cancelled());
let mut read_only_futures: FuturesUnordered<FutureBoxReadOnlyOutcome> =
FuturesUnordered::new();
let high_water_mark = CONFIG.limits.concurrent_handlers_high_water_mark;
let max_wait_duration = Duration::from_millis(CONFIG.timeouts.read_only_handler_flush);
let mut last_flush_time = Instant::now();
let mut termination_reason: Option<TerminationReason> = None;
loop {
if self.supervision.has_pending_starts() {
self.launch_pending_starts();
}
tokio::select! {
() = &mut cancel => {
trace!("Forceful cancellation triggered for actor: {}", self.id());
self.flush_read_only_handlers(&mut read_only_futures).await;
termination_reason = Some(TerminationReason::ParentShutdown);
break;
}
() = tokio::time::sleep_until((last_flush_time + max_wait_duration).into()), if !read_only_futures.is_empty() => {
self.flush_read_only_handlers(&mut read_only_futures).await;
last_flush_time = Instant::now();
}
incoming_opt = self.inbox.recv() => {
let Some(incoming_envelope) = incoming_opt else {
if termination_reason.is_none() {
termination_reason = Some(TerminationReason::InboxClosed);
}
break;
};
let (mut envelope, type_id) = if let Some(broker_req) = incoming_envelope
.message.as_any().downcast_ref::<BrokerRequestEnvelope>()
{
(
Envelope::new(broker_req.message.clone(), incoming_envelope.reply_to.clone(), incoming_envelope.recipient.clone()),
broker_req.message.as_any().type_id()
)
} else {
let type_id = incoming_envelope.message.as_any().type_id();
(incoming_envelope, type_id)
};
if type_id == TypeId::of::<RegisterSupervisedChild>() {
if let Some(registration) = envelope.message.as_any().downcast_ref::<RegisterSupervisedChild>() {
self.register_supervised_child(registration);
}
continue;
} else if type_id == TypeId::of::<SupervisedChildStarted>() {
if let Some(started) = envelope.message.as_any().downcast_ref::<SupervisedChildStarted>() {
self.record_started_child(started);
}
continue;
} else if type_id == TypeId::of::<UnregisterSupervisedChild>() {
if let Some(release) = envelope.message.as_any().downcast_ref::<UnregisterSupervisedChild>() {
self.unregister_supervised_child(release);
}
continue;
} else if type_id == TypeId::of::<RestartDue>() {
if let Some(due) = envelope.message.as_any().downcast_ref::<RestartDue>() {
self.record_restart_due(due);
}
continue;
}
if type_id == TypeId::of::<ChildTerminated>() {
if let Some(notice) = envelope.message.as_any().downcast_ref::<ChildTerminated>() {
self.record_child_terminated(notice);
}
}
if let Some(reactor) = mutable_reactors.get(&type_id) {
self.flush_read_only_handlers(&mut read_only_futures).await;
last_flush_time = Instant::now();
self.dispatch_mutable_handler(reactor, &mut envelope).await;
} else if let Some(reactor) = read_only_reactors.get(&type_id) {
self.enqueue_read_only_handler(reactor, &mut envelope, &read_only_futures);
if read_only_futures.len() >= high_water_mark {
self.flush_read_only_handlers(&mut read_only_futures).await;
last_flush_time = Instant::now();
}
} else if let Some(stop_reason) = graceful_stop_reason(type_id, envelope.message.as_any()) {
self.flush_read_only_handlers(&mut read_only_futures).await;
trace!("Stop signal ({:?}) received for actor: {}. Closing inbox.", stop_reason, self.id());
run_lifecycle_hook!(self, before_stop, "before_stop");
self.inbox.close();
termination_reason = Some(stop_reason);
} else {
trace!("No handler found for message type {:?} for actor {}", type_id, self.id());
}
}
}
}
self.flush_read_only_handlers(&mut read_only_futures).await;
termination_reason.unwrap_or(TerminationReason::InboxClosed)
}
async fn shutdown_cleanup(&mut self) {
self.supervision_mut().begin_shutdown();
self.cancel_unfinished_children();
self.inbox.close();
if !self.broker.outbox.is_closed() {
trace!("Unsubscribing actor {} from all broker subscriptions.", self.id());
let unsubscription = RemoveAllSubscriptions {
subscriber_id: self.id.clone(),
};
self.broker.send(unsubscription).await;
}
debug_assert!(
self.inbox.is_closed(),
"the inbox must be closed first, or a start delivering into it can never finish"
);
await_start_tasks(&self.start_tasks, self.id()).await;
let late_arrivals = self.take_late_started_children();
#[cfg(feature = "ipc")]
self.forget_children_ipc_names();
terminate_children(self.shutdown_child_handles(late_arrivals), self.id()).await;
run_lifecycle_hook!(self, after_stop, "after_stop");
}
}
async fn await_start_tasks(start_tasks: &TaskTracker, actor: &acton_ern::Ern) {
start_tasks.close();
if start_tasks.is_empty() {
return;
}
let deadline = Duration::from_millis(CONFIG.timeouts.actor_shutdown);
trace!(
"Actor {actor} is waiting for {} in-flight child start(s)",
start_tasks.len()
);
if tokio::time::timeout(deadline, start_tasks.wait())
.await
.is_err()
{
error!(
"Actor {actor} stopped with {} child start(s) still in flight after {} ms; each remaining task stops its own child",
start_tasks.len(),
CONFIG.timeouts.actor_shutdown
);
}
}
fn graceful_stop_reason(type_id: TypeId, message: &dyn Any) -> Option<TerminationReason> {
if type_id == TypeId::of::<CascadeTerminate>() {
return Some(TerminationReason::ParentShutdown);
}
match message.downcast_ref::<SystemSignal>() {
Some(SystemSignal::Terminate) => Some(TerminationReason::Normal),
_ => None,
}
}
enum ChildStopResult {
Success,
Error { child_id: String, error: String },
Timeout { child_id: String },
}
#[instrument(skip(children))]
async fn terminate_children(children: Vec<crate::common::ActorHandle>, actor_id: &acton_ern::Ern) {
use std::time::Duration;
use tokio::time::timeout as tokio_timeout;
trace!("Terminating children for actor: {}", actor_id);
let timeout_ms = CONFIG.timeouts.actor_shutdown;
let stop_futures: Vec<_> = children
.into_iter()
.map(|child_handle| {
async move {
trace!("Sending stop signal to child: {}", child_handle.id());
let stop_res = tokio_timeout(
Duration::from_millis(timeout_ms),
child_handle.stop_for_parent_shutdown(),
)
.await;
match stop_res {
Ok(Ok(())) => {
trace!(
"Stop signal sent to and child {} shut down successfully.",
child_handle.id()
);
ChildStopResult::Success
}
Ok(Err(e)) => {
trace!(
"Stop signal to child {} returned error: {:?}",
child_handle.id(),
e
);
ChildStopResult::Error {
child_id: child_handle.id().to_string(),
error: format!("{e:?}"),
}
}
Err(_) => {
trace!(
"Shutdown timeout for child {} after {} ms",
child_handle.id(),
timeout_ms
);
ChildStopResult::Timeout {
child_id: child_handle.id().to_string(),
}
}
}
}
})
.collect();
let results = join_all(stop_futures).await;
let mut timeout_children: Vec<&str> = Vec::new();
let mut error_children: Vec<(&str, &str)> = Vec::new();
for result in &results {
match result {
ChildStopResult::Success => {}
ChildStopResult::Timeout { child_id } => {
timeout_children.push(child_id);
}
ChildStopResult::Error { child_id, error } => {
error_children.push((child_id, error));
}
}
}
if !timeout_children.is_empty() {
tracing::error!(
"Shutdown timeout ({} ms) for {} child(ren) of actor {}: [{}]",
timeout_ms,
timeout_children.len(),
actor_id,
timeout_children.join(", ")
);
}
if !error_children.is_empty() {
tracing::error!(
"Shutdown errors for {} child(ren) of actor {}: [{}]",
error_children.len(),
actor_id,
error_children
.iter()
.map(|(id, err)| format!("{id}: {err}"))
.collect::<Vec<_>>()
.join("; ")
);
}
trace!("All children stopped for actor: {}.", actor_id);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_cascade_terminate_maps_to_parent_shutdown() {
let signal: &dyn Any = &CascadeTerminate;
assert_eq!(
graceful_stop_reason(TypeId::of::<CascadeTerminate>(), signal),
Some(TerminationReason::ParentShutdown)
);
}
#[test]
fn a_terminate_signal_maps_to_normal() {
let signal: &dyn Any = &SystemSignal::Terminate;
assert_eq!(
graceful_stop_reason(TypeId::of::<SystemSignal>(), signal),
Some(TerminationReason::Normal)
);
}
#[test]
fn an_unrelated_message_is_not_a_stop_signal() {
let message: &dyn Any = &42_u32;
assert_eq!(graceful_stop_reason(TypeId::of::<u32>(), message), None);
}
}