use crate::actor_ref::ActorRef;
use crate::{ActorFailure, ActorResult, ActorWeak, ControlSignal, MailboxMessage};
use futures::stream::{BoxStream, SelectAll, StreamExt};
use std::{fmt::Debug, future::Future};
use tokio::sync::mpsc;
use tracing::{debug, error, Instrument};
pub(crate) type IdleEventStream<T> = BoxStream<'static, <T as Actor>::IdleEvent>;
const PRIORITY_DRAIN_QUIET_PERIOD: std::time::Duration = std::time::Duration::from_millis(10);
fn discard_queued_messages<T: Actor>(
actor_id: crate::Identity,
receiver: &mut mpsc::Receiver<MailboxMessage<T>>,
priority_receiver: Option<&mut mpsc::Receiver<MailboxMessage<T>>>,
idle_subscribe_receiver: Option<&mut mpsc::Receiver<IdleEventStream<T>>>,
) {
fn drain_one<T: Actor>(
actor_id: crate::Identity,
rx: &mut mpsc::Receiver<MailboxMessage<T>>,
tell_op: &'static str,
ask_op: &'static str,
) {
rx.close();
while let Ok(msg) = rx.try_recv() {
if let MailboxMessage::Envelope {
payload,
reply_channel,
..
} = msg
{
let operation = match &reply_channel {
None => Some(tell_op),
Some(tx) if tx.is_closed() => Some(ask_op),
Some(_) => None,
};
if let Some(operation) = operation {
crate::dead_letter::record_erased(
actor_id,
crate::dead_letter::DeadLetterReason::DiscardedAtShutdown,
operation,
payload.message_type_name(),
);
}
}
}
}
drain_one(actor_id, receiver, "tell", "ask");
if let Some(rx) = priority_receiver {
drain_one(actor_id, rx, "tell_priority", "ask_priority");
}
if let Some(rx) = idle_subscribe_receiver {
rx.close();
let mut dropped = 0usize;
while rx.try_recv().is_ok() {
dropped += 1;
}
if dropped > 0 {
debug!("Actor {actor_id} dropped {dropped} idle subscription(s) queued at shutdown");
}
}
}
struct LifecycleChannels<T: Actor> {
actor_id: crate::Identity,
receiver: mpsc::Receiver<MailboxMessage<T>>,
priority_receiver: Option<mpsc::Receiver<MailboxMessage<T>>>,
terminate_receiver: mpsc::Receiver<ControlSignal>,
idle_subscribe_receiver: Option<mpsc::Receiver<IdleEventStream<T>>>,
}
impl<T: Actor> Drop for LifecycleChannels<T> {
fn drop(&mut self) {
self.terminate_receiver.close();
let mut drain = std::panic::AssertUnwindSafe(|| {
discard_queued_messages(
self.actor_id,
&mut self.receiver,
self.priority_receiver.as_mut(),
self.idle_subscribe_receiver.as_mut(),
)
});
if std::thread::panicking() {
let _ = std::panic::catch_unwind(drain);
} else {
drain.0();
}
}
}
macro_rules! run_with_actor_scope {
($actor_id:expr, $fut:expr) => {{
#[cfg(feature = "deadlock-detection")]
{
crate::CURRENT_ACTOR.scope($actor_id, $fut).await
}
#[cfg(not(feature = "deadlock-detection"))]
{
$fut.await
}
}};
}
macro_rules! process_envelope {
(
actor_id = $actor_id:expr,
actor = $actor:expr,
payload = $payload:expr,
reply_channel = $reply_channel:expr,
actor_ref = $actor_ref:expr,
span_name = $span_name:literal,
record = $record_fn:ident $(,)?
) => {{
#[cfg(feature = "tracing")]
let msg_span = tracing::debug_span!($span_name);
#[cfg(not(feature = "tracing"))]
let msg_span = tracing::Span::none();
#[cfg(feature = "tracing")]
let start_time = std::time::Instant::now();
#[cfg(feature = "metrics")]
let metrics_collector = $actor_ref.metrics.clone();
#[cfg(feature = "metrics")]
let metrics_start = std::time::Instant::now();
run_with_actor_scope!(
$actor_id,
$payload
.handle_message(&mut $actor, $actor_ref, $reply_channel)
.instrument(msg_span)
);
#[cfg(feature = "metrics")]
metrics_collector.$record_fn(metrics_start.elapsed());
#[cfg(feature = "tracing")]
debug!(
"Actor {} processed {} in {:?}",
$actor_id,
$span_name,
start_time.elapsed()
);
}};
}
pub trait Actor: Sized + Send + 'static {
type Args: Send;
type Error: Send + std::fmt::Display + Debug;
type IdleEvent: Send + 'static;
fn on_start(
args: Self::Args,
actor_ref: &ActorRef<Self>,
) -> impl Future<Output = std::result::Result<Self, Self::Error>> + Send;
#[allow(unused_variables)]
fn on_idle(
&mut self,
event: Self::IdleEvent,
actor_weak: &ActorWeak<Self>,
) -> impl Future<Output = std::result::Result<(), Self::Error>> + Send {
async { Ok(()) }
}
#[allow(unused_variables)]
fn on_stop(
&mut self,
actor_weak: &ActorWeak<Self>,
killed: bool,
) -> impl Future<Output = std::result::Result<(), Self::Error>> + Send {
async { Ok(()) }
}
}
pub trait Message<T: Send + 'static>: Actor {
type Reply: Send + 'static;
fn handle(
&mut self,
msg: T,
actor_ref: &ActorRef<Self>,
) -> impl Future<Output = Self::Reply> + Send;
fn on_tell_result(_result: &Self::Reply, _actor_ref: &ActorRef<Self>) {
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(
level = "debug",
name = "actor_lifecycle",
fields(
actor_id = %actor_ref.identity(),
actor_type = %std::any::type_name::<T>()
),
skip_all
))]
pub(crate) async fn run_actor_lifecycle<T: Actor>(
args: T::Args,
actor_ref: ActorRef<T>,
receiver: mpsc::Receiver<MailboxMessage<T>>,
priority_receiver: Option<mpsc::Receiver<MailboxMessage<T>>>,
terminate_receiver: mpsc::Receiver<ControlSignal>,
idle_subscribe_receiver: Option<mpsc::Receiver<IdleEventStream<T>>>,
) -> ActorResult<T> {
let actor_id = actor_ref.identity();
let mut chans = LifecycleChannels {
actor_id,
receiver,
priority_receiver,
terminate_receiver,
idle_subscribe_receiver,
};
#[cfg(feature = "tracing")]
let on_start_span = tracing::debug_span!("actor_on_start");
#[cfg(not(feature = "tracing"))]
let on_start_span = tracing::Span::none();
let mut actor = match run_with_actor_scope!(
actor_id,
T::on_start(args, &actor_ref).instrument(on_start_span)
) {
Ok(actor) => {
debug!("Actor {actor_id} on_start completed successfully.");
actor
}
Err(e) => {
error!("Actor {actor_id} on_start failed: {e}");
return ActorResult::Failed {
failure: ActorFailure::OnStart { error: e },
killed: false,
};
}
};
#[cfg(feature = "metrics")]
actor_ref.metrics.mark_started();
debug!("Actor {actor_id} runtime starting - entering main processing loop.");
let actor_weak = ActorRef::downgrade(&actor_ref);
#[cfg(feature = "metrics")]
let metrics = actor_ref.metrics.clone();
drop(actor_ref);
let mut killed = false;
let mut idle_streams: SelectAll<IdleEventStream<T>> = SelectAll::new();
if let Some(rx) = chans.idle_subscribe_receiver.as_mut() {
while let Ok(stream) = rx.try_recv() {
idle_streams.push(stream);
}
}
loop {
tokio::select! {
biased;
maybe_terminate = chans.terminate_receiver.recv() => {
match maybe_terminate {
Some(_) => {
debug!("Actor termination via kill() method");
killed = true;
}
None => {
debug!("Actor termination due to all actor_ref instances being dropped");
killed = false;
}
}
if let Some(rx) = chans.idle_subscribe_receiver.as_mut() {
rx.close();
}
#[cfg(feature = "tracing")]
let on_stop_span = tracing::debug_span!("actor_on_stop", killed);
#[cfg(not(feature = "tracing"))]
let on_stop_span = tracing::Span::none();
if let Err(e) = run_with_actor_scope!(
actor_id,
actor.on_stop(&actor_weak, killed).instrument(on_stop_span)
) {
error!("Actor {actor_id} on_stop failed during termination: {e}");
return ActorResult::Failed {
failure: ActorFailure::OnStop { actor, error: e },
killed,
};
}
break; }
maybe_priority = async {
match chans.priority_receiver.as_mut() {
Some(rx) => rx.recv().await,
None => std::future::pending::<Option<MailboxMessage<T>>>().await,
}
} => {
match maybe_priority {
Some(MailboxMessage::Envelope { payload, reply_channel, actor_ref, ask_edge: _ask_edge }) => {
process_envelope!(
actor_id = actor_id,
actor = actor,
payload = payload,
reply_channel = reply_channel,
actor_ref = actor_ref,
span_name = "actor_process_priority_message",
record = record_priority_message,
);
}
Some(MailboxMessage::StopGracefully(_)) => {
debug_assert!(
false,
"MailboxMessage::StopGracefully observed on the priority channel; \
priority channel must only carry Envelope variants"
);
}
None => {
chans.priority_receiver = None;
}
}
}
maybe_subscription = async {
match chans.idle_subscribe_receiver.as_mut() {
Some(rx) => rx.recv().await,
None => std::future::pending::<Option<IdleEventStream<T>>>().await,
}
} => {
match maybe_subscription {
Some(stream) => {
idle_streams.push(stream);
}
None => {
chans.idle_subscribe_receiver = None;
}
}
}
maybe_message = chans.receiver.recv() => {
match maybe_message {
Some(MailboxMessage::Envelope { payload, reply_channel, actor_ref, ask_edge: _ask_edge }) => {
process_envelope!(
actor_id = actor_id,
actor = actor,
payload = payload,
reply_channel = reply_channel,
actor_ref = actor_ref,
span_name = "actor_process_message",
record = record_message,
);
}
msg @ (Some(MailboxMessage::StopGracefully(_)) | None) => {
match &msg {
Some(_) => debug!("Actor termination: explicit graceful stop"),
None => debug!("Actor termination: all strong refs dropped"),
}
if let Some(rx) = chans.idle_subscribe_receiver.as_mut() {
rx.close();
}
if let Some(rx) = chans.priority_receiver.as_mut() {
rx.close();
loop {
let msg = match tokio::time::timeout(
PRIORITY_DRAIN_QUIET_PERIOD,
rx.recv(),
)
.await
{
Ok(Some(msg)) => msg,
Ok(None) | Err(_) => break,
};
match msg {
MailboxMessage::Envelope { payload, reply_channel, actor_ref, ask_edge: _ask_edge } => {
process_envelope!(
actor_id = actor_id,
actor = actor,
payload = payload,
reply_channel = reply_channel,
actor_ref = actor_ref,
span_name = "actor_drain_priority_message",
record = record_priority_message,
);
}
MailboxMessage::StopGracefully(_) => {
debug_assert!(
false,
"MailboxMessage::StopGracefully observed during \
priority drain; priority channel must only \
carry Envelope variants"
);
}
}
}
}
#[cfg(feature = "tracing")]
let on_stop_span = tracing::debug_span!("actor_on_stop", killed = false);
#[cfg(not(feature = "tracing"))]
let on_stop_span = tracing::Span::none();
if let Err(e) = run_with_actor_scope!(
actor_id,
actor.on_stop(&actor_weak, false).instrument(on_stop_span)
) {
error!("Actor {actor_id} on_stop failed during graceful stop: {e}");
return ActorResult::Failed {
failure: ActorFailure::OnStop { actor, error: e },
killed: false,
};
}
break;
}
}
}
Some(event) = idle_streams.next(), if !idle_streams.is_empty() => {
#[cfg(feature = "tracing")]
let on_idle_span = tracing::debug_span!("actor_on_idle");
#[cfg(not(feature = "tracing"))]
let on_idle_span = tracing::Span::none();
#[cfg(feature = "metrics")]
let metrics_start = std::time::Instant::now();
let result = run_with_actor_scope!(
actor_id,
actor.on_idle(event, &actor_weak).instrument(on_idle_span)
);
#[cfg(feature = "metrics")]
if result.is_ok() {
metrics.record_idle_event(metrics_start.elapsed());
}
if let Err(e) = result {
error!("Actor {actor_id} on_idle error: {e}");
if let Some(rx) = chans.idle_subscribe_receiver.as_mut() {
rx.close();
}
#[cfg(feature = "tracing")]
let on_stop_span = tracing::debug_span!("actor_on_stop", killed = false);
#[cfg(not(feature = "tracing"))]
let on_stop_span = tracing::Span::none();
let failure = match run_with_actor_scope!(
actor_id,
actor.on_stop(&actor_weak, false).instrument(on_stop_span)
) {
Err(stop_err) => {
error!(
"Actor {actor_id} on_stop failed during on_idle error cleanup: {stop_err}"
);
ActorFailure::OnIdleThenOnStop {
actor,
error: e,
stop_error: stop_err,
}
}
Ok(()) => ActorFailure::OnIdle { actor, error: e },
};
return ActorResult::Failed {
failure,
killed: false,
};
}
}
}
}
debug!("Actor {actor_id} lifecycle completed - exiting runtime loop.");
ActorResult::Completed { actor, killed }
}