use std::panic::AssertUnwindSafe;
use std::sync::{Arc, Mutex};
use futures_util::FutureExt;
use tokio::sync::{mpsc, oneshot};
use crate::actor::{Actor, ActorContext, RemoteDispatch};
use crate::instrument;
use crate::lifecycle::{RestartPolicy, SystemSignal, TerminationReason};
use crate::receptionist::DeregisterGuard;
use crate::wire::{DispatchRequest, EnvelopeProxy, RemoteResponse};
pub(crate) const LOCAL_BURST_LIMIT: u64 = 64;
#[allow(clippy::too_many_arguments)]
pub(crate) async fn run_supervisor<A>(
mut actor: A,
mut state: A::State,
ctx: ActorContext<A>,
mut mailbox_rx: mpsc::UnboundedReceiver<Box<dyn EnvelopeProxy<A>>>,
mut dispatch_rx: mpsc::UnboundedReceiver<DispatchRequest>,
mut shutdown_rx: oneshot::Receiver<()>,
mut system_rx: mpsc::UnboundedReceiver<SystemSignal>,
exit_reason: Arc<Mutex<TerminationReason>>,
_deregister_guard: DeregisterGuard,
) -> (
mpsc::UnboundedReceiver<Box<dyn EnvelopeProxy<A>>>,
mpsc::UnboundedReceiver<DispatchRequest>,
)
where
A: Actor + RemoteDispatch + 'static,
{
let actor_type = std::any::type_name::<A>();
let mut msg_count: u64 = 0;
let mut local_burst: u64 = 0;
loop {
if local_burst >= LOCAL_BURST_LIMIT {
local_burst = 0;
if let Ok(request) = dispatch_rx.try_recv() {
if handle_dispatch(
&mut actor,
&mut state,
&ctx,
request,
actor_type,
&exit_reason,
)
.await
{
break;
}
msg_count += 1;
if msg_count.is_multiple_of(64) {
crate::runtime::yield_now().await;
}
continue;
}
}
tokio::select! {
biased;
msg = mailbox_rx.recv() => {
match msg {
Some(envelope) => {
#[cfg(feature = "monitor")]
let start = std::time::Instant::now();
let fut = envelope.handle(&ctx, &mut actor, &mut state);
let result = AssertUnwindSafe(fut).catch_unwind().await;
if let Err(panic_info) = result {
instrument::message_failed(actor_type);
let msg = extract_panic_message(&panic_info);
*exit_reason.lock().unwrap() = TerminationReason::Panicked(msg);
break;
}
instrument::message_processed(actor_type);
#[cfg(feature = "monitor")]
instrument::message_processing_duration(actor_type, start.elapsed());
local_burst += 1;
msg_count += 1;
if msg_count.is_multiple_of(64) {
crate::runtime::yield_now().await;
}
}
None => break,
}
}
req = dispatch_rx.recv() => {
match req {
Some(request) => {
local_burst = 0;
if handle_dispatch(
&mut actor,
&mut state,
&ctx,
request,
actor_type,
&exit_reason,
)
.await
{
break;
}
msg_count += 1;
if msg_count.is_multiple_of(64) {
crate::runtime::yield_now().await;
}
}
None => break,
}
}
signal = system_rx.recv() => {
match signal {
Some(SystemSignal::ActorTerminated(terminated)) => {
actor.on_actor_terminated(&mut state, &terminated);
}
Some(SystemSignal::Stop) => {
break;
}
None => {} }
}
_ = &mut shutdown_rx => {
break;
}
}
}
if let Some(hook) = ctx.receptionist.terminate_hook() {
let reason = exit_reason.lock().unwrap().clone();
hook.before_deregister(&ctx.label, &reason).await;
}
(mailbox_rx, dispatch_rx)
}
async fn handle_dispatch<A>(
actor: &mut A,
state: &mut A::State,
ctx: &ActorContext<A>,
request: DispatchRequest,
actor_type: &'static str,
exit_reason: &Mutex<TerminationReason>,
) -> bool
where
A: Actor + RemoteDispatch + 'static,
{
#[cfg(feature = "monitor")]
let start = std::time::Instant::now();
let fut = actor.dispatch_remote(
ctx,
state,
&request.invocation.message_type,
&request.invocation.payload,
);
let result = AssertUnwindSafe(fut).catch_unwind().await;
match result {
Ok(dispatch_result) => {
instrument::message_processed(actor_type);
#[cfg(feature = "monitor")]
instrument::message_processing_duration(actor_type, start.elapsed());
let response = RemoteResponse {
call_id: request.invocation.call_id,
result: dispatch_result.map_err(|e| e.to_string()),
};
let _ = request.respond_to.send(response);
false
}
Err(panic_info) => {
instrument::message_failed(actor_type);
let msg = extract_panic_message(&panic_info);
let response = RemoteResponse {
call_id: request.invocation.call_id,
result: Err(format!("actor panicked: {msg}")),
};
let _ = request.respond_to.send(response);
*exit_reason.lock().unwrap() = TerminationReason::Panicked(msg);
true
}
}
}
pub(crate) fn extract_panic_message(payload: &Box<dyn std::any::Any + Send>) -> String {
if let Some(s) = payload.downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"unknown panic".to_string()
}
}
pub(crate) fn should_restart(policy: &RestartPolicy, reason: &TerminationReason) -> bool {
match policy {
RestartPolicy::Temporary => false,
RestartPolicy::Permanent => true,
RestartPolicy::Transient => matches!(reason, TerminationReason::Panicked(_)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::actor::{DispatchError, Handler, Message};
use crate::receptionist::Receptionist;
use crate::wire::{RemoteInvocation, TypedEnvelope};
struct FloodActor;
struct FloodState {
local_processed: u64,
}
impl Actor for FloodActor {
type State = FloodState;
}
#[derive(Debug)]
struct Bump;
impl Message for Bump {
type Result = ();
}
impl Handler<Bump> for FloodActor {
fn handle(&mut self, _ctx: &ActorContext<Self>, state: &mut FloodState, _msg: Bump) {
state.local_processed += 1;
}
}
impl RemoteDispatch for FloodActor {
async fn dispatch_remote<'a>(
&'a mut self,
_ctx: &'a ActorContext<Self>,
state: &'a mut FloodState,
_message_type: &'a str,
_payload: &'a [u8],
) -> Result<Vec<u8>, DispatchError> {
Ok(state.local_processed.to_le_bytes().to_vec())
}
}
#[tokio::test]
async fn remote_dispatch_not_starved_by_saturated_mailbox() {
const FLOOD: u64 = 1000;
let receptionist = Receptionist::new();
let (mailbox_tx, mailbox_rx) =
mpsc::unbounded_channel::<Box<dyn EnvelopeProxy<FloodActor>>>();
let (dispatch_tx, dispatch_rx) = mpsc::unbounded_channel::<DispatchRequest>();
let (_shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
let (system_tx, system_rx) = mpsc::unbounded_channel::<SystemSignal>();
let exit_reason = Arc::new(Mutex::new(TerminationReason::Stopped));
let guard = DeregisterGuard {
receptionist: receptionist.clone(),
label: "flood/test".to_string(),
reason: exit_reason.clone(),
};
let ctx = ActorContext {
label: "flood/test".to_string(),
node_id: "local".to_string(),
mailbox_tx: mailbox_tx.clone(),
receptionist: receptionist.clone(),
system_tx: system_tx.clone(),
reply_token: std::sync::Mutex::new(None),
};
let mut reply_rxs = Vec::new();
for _ in 0..FLOOD {
let (tx, rx) = oneshot::channel();
mailbox_tx
.send(Box::new(TypedEnvelope {
message: Bump,
respond_to: tx,
}))
.unwrap();
reply_rxs.push(rx);
}
let (resp_tx, mut resp_rx) = oneshot::channel::<RemoteResponse>();
dispatch_tx
.send(DispatchRequest {
invocation: RemoteInvocation {
call_id: 1,
actor_label: "flood/test".to_string(),
message_type: "test::Snapshot".to_string(),
payload: Vec::new(),
},
respond_to: resp_tx,
})
.unwrap();
system_tx.send(SystemSignal::Stop).unwrap();
let (_mb, _dp) = run_supervisor(
FloodActor,
FloodState { local_processed: 0 },
ctx,
mailbox_rx,
dispatch_rx,
shutdown_rx,
system_rx,
exit_reason,
guard,
)
.await;
let response = resp_rx
.try_recv()
.expect("remote dispatch should have been served");
let payload = response.result.expect("dispatch should succeed");
let processed_at_dispatch = u64::from_le_bytes(payload.try_into().unwrap());
assert!(
processed_at_dispatch < FLOOD,
"remote dispatch was starved: ran only after all {FLOOD} local messages"
);
assert!(
processed_at_dispatch <= LOCAL_BURST_LIMIT,
"remote dispatch should run within one local burst \
(ran after {processed_at_dispatch} local messages, limit {LOCAL_BURST_LIMIT})"
);
let mut delivered = 0;
for rx in reply_rxs {
if rx.await.is_ok() {
delivered += 1;
}
}
assert_eq!(delivered, FLOOD, "every local message should be handled");
}
#[tokio::test]
async fn terminate_hook_delays_deregistration_on_the_tokio_path() {
use crate::lifecycle::TerminateHook;
use crate::runtime::{BoxFuture, Runtime, TokioRuntime};
use std::time::Duration;
struct SlowToDie;
impl TerminateHook for SlowToDie {
fn before_deregister(
&self,
label: &str,
_reason: &TerminationReason,
) -> BoxFuture<'static, ()> {
assert_eq!(label, "flood/dying");
TokioRuntime.sleep(Duration::from_millis(200))
}
}
let receptionist = Receptionist::new();
receptionist.set_terminate_hook(Some(Arc::new(SlowToDie)));
let _ep = receptionist.start("flood/dying", FloodActor, FloodState { local_processed: 0 });
receptionist.stop("flood/dying");
TokioRuntime.sleep(Duration::from_millis(50)).await;
assert!(
receptionist.lookup::<FloodActor>("flood/dying").is_some(),
"actor must stay registered while the terminate hook is pending"
);
TokioRuntime.sleep(Duration::from_millis(300)).await;
assert!(
receptionist.lookup::<FloodActor>("flood/dying").is_none(),
"actor must de-register once the terminate hook completes"
);
}
}