use crate::actor::IdleEventStream;
use crate::error::{Error, Result};
use crate::Identity;
use crate::{Actor, ControlSignal, MailboxMessage, MailboxSender, Message};
use futures::stream::{BoxStream, Stream, StreamExt};
use std::fmt;
use std::time::Duration;
use tokio::runtime::{Handle, RuntimeFlavor};
use tokio::sync::{mpsc, oneshot};
use tracing::{debug, info, warn};
#[inline]
fn current_multi_thread_handle() -> Option<Handle> {
let handle = Handle::try_current().ok()?;
(handle.runtime_flavor() == RuntimeFlavor::MultiThread).then_some(handle)
}
#[inline]
fn block_on_current_runtime<R>(handle: Handle, fut: impl std::future::Future<Output = R>) -> R {
tokio::task::block_in_place(|| handle.block_on(fut))
}
fn assert_current_thread_can_block() {
if Handle::try_current().is_ok() {
let (tx, _rx) = mpsc::channel::<()>(1);
let _ = tx.blocking_send(());
}
}
fn run_blocking_with_runtime<F, R>(identity: Identity, panic_msg: &'static str, fut: F) -> Result<R>
where
F: std::future::Future<Output = Result<R>> + Send + 'static,
R: Send + 'static,
{
assert_current_thread_can_block();
#[cfg(feature = "deadlock-detection")]
let caller = crate::CURRENT_ACTOR.try_with(|id| *id).ok();
let join = std::thread::Builder::new()
.name("rsactor-blocking".to_string())
.spawn(move || -> Result<R> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.map_err(|e| Error::Runtime {
identity,
details: "Failed to build blocking runtime".to_string(),
source: Some(std::sync::Arc::new(e)),
})?;
#[cfg(feature = "deadlock-detection")]
{
match caller {
Some(id) => runtime.block_on(crate::CURRENT_ACTOR.scope(id, fut)),
None => runtime.block_on(fut),
}
}
#[cfg(not(feature = "deadlock-detection"))]
{
runtime.block_on(fut)
}
})
.map_err(|e| Error::Runtime {
identity,
details: "Failed to spawn blocking thread".to_string(),
source: Some(std::sync::Arc::new(e)),
})?;
join.join().unwrap_or_else(|payload| {
let panic_text = payload
.downcast_ref::<&str>()
.copied()
.map(str::to_owned)
.or_else(|| payload.downcast_ref::<String>().cloned());
#[cfg(feature = "deadlock-detection")]
if panic_text
.as_deref()
.is_some_and(|m| m.starts_with("Deadlock detected"))
{
std::panic::resume_unwind(payload);
}
Err(Error::Runtime {
identity,
details: match panic_text {
Some(text) => format!("{panic_msg}: {text}"),
None => panic_msg.to_string(),
},
source: None,
})
})
}
#[cfg(feature = "metrics")]
use crate::metrics::MetricsCollector;
#[cfg(feature = "metrics")]
use std::sync::Arc;
#[derive(Debug)]
pub struct ActorRef<T: Actor> {
id: Identity,
sender: MailboxSender<T>,
pub(crate) priority_sender: Option<MailboxSender<T>>,
pub(crate) terminate_sender: mpsc::Sender<ControlSignal>,
pub(crate) idle_subscribe_sender: Option<mpsc::Sender<IdleEventStream<T>>>,
#[cfg(feature = "metrics")]
pub(crate) metrics: Arc<MetricsCollector>,
}
impl<T: Actor> ActorRef<T> {
pub(crate) fn new(
id: Identity,
sender: MailboxSender<T>,
priority_sender: Option<MailboxSender<T>>,
terminate_sender: mpsc::Sender<ControlSignal>,
idle_subscribe_sender: Option<mpsc::Sender<IdleEventStream<T>>>,
#[cfg(feature = "metrics")] metrics: Arc<MetricsCollector>,
) -> Self {
ActorRef {
id,
sender,
priority_sender,
terminate_sender,
idle_subscribe_sender,
#[cfg(feature = "metrics")]
metrics,
}
}
pub fn identity(&self) -> Identity {
self.id
}
#[inline]
pub fn has_priority_channel(&self) -> bool {
self.priority_sender.is_some()
}
#[inline]
pub fn has_idle_channel(&self) -> bool {
self.idle_subscribe_sender.is_some()
}
#[cfg(any(test, feature = "test-utils"))]
pub fn drop_priority_sender_for_test(&mut self) {
self.priority_sender = None;
}
#[inline]
pub fn is_alive(&self) -> bool {
!self.sender.is_closed() && !self.terminate_sender.is_closed()
}
pub fn downgrade(&self) -> ActorWeak<T> {
ActorWeak {
id: self.id,
sender: self.sender.downgrade(),
priority_sender: self.priority_sender.as_ref().map(|s| s.downgrade()),
terminate_sender: self.terminate_sender.downgrade(),
idle_subscribe_sender: self.idle_subscribe_sender.as_ref().map(|s| s.downgrade()),
#[cfg(feature = "metrics")]
metrics: self.metrics.clone(),
}
}
pub async fn wait_stopped(&self) {
self.sender.closed().await
}
pub fn subscribe_idle<S>(&self, stream: S) -> std::result::Result<(), IdleSubscribeError<T>>
where
S: Stream<Item = T::IdleEvent> + Send + 'static,
{
let boxed: IdleEventStream<T> = stream.boxed();
let Some(idle_subscribe_sender) = self.idle_subscribe_sender.as_ref() else {
warn!("subscribe_idle called on actor without an idle channel");
return Err(IdleSubscribeError::new(
Error::IdleChannelNotEnabled {
identity: self.identity(),
},
boxed,
));
};
idle_subscribe_sender.try_send(boxed).map_err(|e| match e {
mpsc::error::TrySendError::Full(stream) => IdleSubscribeError::new(
Error::ChannelFull {
identity: self.identity(),
channel: "idle_subscribe",
},
stream,
),
mpsc::error::TrySendError::Closed(stream) => IdleSubscribeError::new(
Error::Send {
identity: self.identity(),
details: "Idle subscribe channel closed (actor is no longer alive)",
},
stream,
),
})
}
#[cfg_attr(feature = "tracing", tracing::instrument(
level = "debug",
name = "actor_tell",
fields(
actor_id = %self.identity(),
message_type = %std::any::type_name::<M>()
),
skip(self, msg)
))]
pub async fn tell<M>(&self, msg: M) -> Result<()>
where
M: Send + 'static,
T: Message<M>,
{
self.tell_inner(msg, true).await
}
async fn tell_inner<M>(&self, msg: M, detect_self_deadlock: bool) -> Result<()>
where
M: Send + 'static,
T: Message<M>,
{
#[cfg(not(feature = "deadlock-detection"))]
let _ = detect_self_deadlock;
let envelope = MailboxMessage::Envelope {
payload: Box::new(msg),
reply_channel: None, actor_ref: self.clone(), ask_edge: Default::default(),
};
debug!("Sending tell message (fire-and-forget)");
#[cfg(feature = "deadlock-detection")]
let envelope = match self.sender.try_send(envelope) {
Ok(()) => {
debug!("Tell message sent successfully");
return Ok(());
}
Err(mpsc::error::TrySendError::Full(env)) => {
if detect_self_deadlock {
self.panic_if_self_send_on_full("tell");
}
env
}
Err(mpsc::error::TrySendError::Closed(env)) => env,
};
let result = if self.sender.send(envelope).await.is_err() {
crate::dead_letter::record::<M>(
self.identity(),
crate::dead_letter::DeadLetterReason::ActorStopped,
"tell",
);
Err(Error::Send {
identity: self.identity(),
details: "Mailbox channel closed",
})
} else {
Ok(())
};
match &result {
Ok(_) => debug!("Tell message sent successfully"),
Err(e) => warn!(error = %e, "Failed to send tell message"),
}
result
}
#[cfg(feature = "deadlock-detection")]
fn panic_if_self_send_on_full(&self, operation: &str) {
let is_self = crate::CURRENT_ACTOR
.try_with(|id| id.id == self.id.id)
.unwrap_or(false);
if is_self {
panic!(
"Deadlock detected: {operation} to self on a full mailbox from inside actor {}'s \
own handler or lifecycle hook.\n\
The mailbox's only consumer is this actor's runtime loop, which is parked \
awaiting the current handler, so admission can never succeed and kill() cannot \
interrupt the wait. Use tell_with_timeout, a larger mailbox capacity, or send \
from a spawned task.",
self.id
);
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(
level = "debug",
name = "actor_tell_with_timeout",
fields(
actor_id = %self.identity(),
message_type = %std::any::type_name::<M>(),
timeout_ms = timeout.as_millis()
),
skip(self, msg)
))]
pub async fn tell_with_timeout<M>(&self, msg: M, timeout: Duration) -> Result<()>
where
T: Message<M>,
M: Send + 'static,
{
debug!(
timeout_ms = timeout.as_millis(),
"Sending tell message with timeout"
);
let result = tokio::time::timeout(timeout, self.tell_inner(msg, false))
.await
.map_err(|_| {
crate::dead_letter::record::<M>(
self.identity(),
crate::dead_letter::DeadLetterReason::Timeout,
"tell",
);
Error::Timeout {
identity: self.identity(),
timeout,
operation: "tell",
}
})?;
match &result {
Ok(_) => debug!("Tell with timeout completed successfully"),
Err(e) => warn!(error = %e, "Tell with timeout failed"),
}
result
}
#[cfg_attr(feature = "tracing", tracing::instrument(
level = "debug",
name = "actor_ask",
fields(
actor_id = %self.identity(),
message_type = %std::any::type_name::<M>(),
reply_type = %std::any::type_name::<T::Reply>()
),
skip(self, msg)
))]
pub async fn ask<M>(&self, msg: M) -> Result<T::Reply>
where
T: Message<M>,
M: Send + 'static,
T::Reply: Send + 'static,
{
self.ask_inner(msg, "ask").await
}
async fn ask_inner<M>(&self, msg: M, operation: &'static str) -> Result<T::Reply>
where
T: Message<M>,
M: Send + 'static,
T::Reply: Send + 'static,
{
#[cfg(feature = "deadlock-detection")]
let _guard = crate::register_ask_edge(self.identity(), operation);
#[cfg(feature = "deadlock-detection")]
let ask_edge = _guard.as_ref().map(|g| g.token());
#[cfg(not(feature = "deadlock-detection"))]
let ask_edge = ();
let (reply_tx, reply_rx) = oneshot::channel();
let envelope = MailboxMessage::Envelope {
payload: Box::new(msg),
reply_channel: Some(reply_tx),
actor_ref: self.clone(), ask_edge,
};
debug!("Sending ask message and waiting for reply");
if self.sender.send(envelope).await.is_err() {
crate::dead_letter::record::<M>(
self.identity(),
crate::dead_letter::DeadLetterReason::ActorStopped,
operation,
);
warn!("Failed to send ask message: mailbox channel closed");
return Err(Error::Send {
identity: self.identity(),
details: "Mailbox channel closed",
});
}
match reply_rx.await {
Ok(reply_any) => {
match reply_any.downcast::<T::Reply>() {
Ok(reply) => {
debug!("Ask reply received successfully");
Ok(*reply)
}
Err(_) => {
warn!(
expected_type = %std::any::type_name::<T::Reply>(),
"Ask reply type downcast failed"
);
Err(Error::Downcast {
identity: self.identity(),
expected_type: std::any::type_name::<T::Reply>(),
})
}
}
}
Err(_recv_err) => {
crate::dead_letter::record::<M>(
self.identity(),
crate::dead_letter::DeadLetterReason::ReplyDropped,
operation,
);
warn!("Ask reply channel closed unexpectedly");
Err(Error::Receive {
identity: self.identity(),
details: "Reply channel closed unexpectedly",
})
}
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(
level = "debug",
name = "actor_ask_with_timeout",
fields(
actor_id = %self.identity(),
message_type = %std::any::type_name::<M>(),
reply_type = %std::any::type_name::<T::Reply>(),
timeout_ms = timeout.as_millis()
),
skip(self, msg)
))]
pub async fn ask_with_timeout<M>(&self, msg: M, timeout: Duration) -> Result<T::Reply>
where
T: Message<M>,
M: Send + 'static,
T::Reply: Send + 'static,
{
self.ask_timeout_inner(msg, timeout, "ask").await
}
async fn ask_timeout_inner<M>(
&self,
msg: M,
timeout: Duration,
operation: &'static str,
) -> Result<T::Reply>
where
T: Message<M>,
M: Send + 'static,
T::Reply: Send + 'static,
{
debug!(
timeout_ms = timeout.as_millis(),
"Sending ask message with timeout"
);
let result = tokio::time::timeout(timeout, self.ask_inner(msg, operation))
.await
.map_err(|_| {
crate::dead_letter::record::<M>(
self.identity(),
crate::dead_letter::DeadLetterReason::Timeout,
operation,
);
Error::Timeout {
identity: self.identity(),
timeout,
operation,
}
})?;
match &result {
Ok(_) => debug!("Ask with timeout completed successfully"),
Err(e) => warn!(error = %e, "Ask with timeout failed"),
}
result
}
#[cfg_attr(feature = "tracing", tracing::instrument(
level = "debug",
name = "actor_tell_priority",
fields(
actor_id = %self.identity(),
message_type = %std::any::type_name::<M>(),
timeout_ms = timeout.as_millis()
),
skip(self, msg)
))]
pub async fn tell_priority<M>(&self, msg: M, timeout: Duration) -> Result<()>
where
M: Send + 'static,
T: Message<M>,
{
let Some(priority_sender) = self.priority_sender.as_ref() else {
warn!("tell_priority called on actor without a priority channel");
return Err(Error::PriorityChannelNotEnabled {
identity: self.identity(),
});
};
let envelope = MailboxMessage::Envelope {
payload: Box::new(msg),
reply_channel: None,
actor_ref: self.clone(),
ask_edge: Default::default(),
};
debug!(
timeout_ms = timeout.as_millis(),
"Sending priority tell message"
);
let send_fut = priority_sender.send(envelope);
let result = match tokio::time::timeout(timeout, send_fut).await {
Ok(Ok(())) => Ok(()),
Ok(Err(_)) => {
crate::dead_letter::record::<M>(
self.identity(),
crate::dead_letter::DeadLetterReason::ActorStopped,
"tell_priority",
);
Err(Error::Send {
identity: self.identity(),
details: "Priority channel closed",
})
}
Err(_) => {
crate::dead_letter::record::<M>(
self.identity(),
crate::dead_letter::DeadLetterReason::Timeout,
"tell_priority",
);
Err(Error::Timeout {
identity: self.identity(),
timeout,
operation: "tell_priority",
})
}
};
match &result {
Ok(_) => debug!("Priority tell message sent successfully"),
Err(e) => warn!(error = %e, "Priority tell failed"),
}
result
}
#[cfg_attr(feature = "tracing", tracing::instrument(
level = "debug",
name = "actor_ask_priority",
fields(
actor_id = %self.identity(),
message_type = %std::any::type_name::<M>(),
reply_type = %std::any::type_name::<T::Reply>(),
timeout_ms = timeout.as_millis()
),
skip(self, msg)
))]
pub async fn ask_priority<M>(&self, msg: M, timeout: Duration) -> Result<T::Reply>
where
T: Message<M>,
M: Send + 'static,
T::Reply: Send + 'static,
{
self.ask_priority_inner(msg, timeout, "ask_priority").await
}
async fn ask_priority_inner<M>(
&self,
msg: M,
timeout: Duration,
operation: &'static str,
) -> Result<T::Reply>
where
T: Message<M>,
M: Send + 'static,
T::Reply: Send + 'static,
{
let Some(priority_sender) = self.priority_sender.as_ref() else {
warn!("ask_priority called on actor without a priority channel");
return Err(Error::PriorityChannelNotEnabled {
identity: self.identity(),
});
};
#[cfg(feature = "deadlock-detection")]
let _guard = crate::register_ask_edge(self.identity(), operation);
#[cfg(feature = "deadlock-detection")]
let ask_edge = _guard.as_ref().map(|g| g.token());
#[cfg(not(feature = "deadlock-detection"))]
let ask_edge = ();
let result = tokio::time::timeout(timeout, async {
let (reply_tx, reply_rx) = oneshot::channel();
let envelope = MailboxMessage::Envelope {
payload: Box::new(msg),
reply_channel: Some(reply_tx),
actor_ref: self.clone(),
ask_edge,
};
if priority_sender.send(envelope).await.is_err() {
crate::dead_letter::record::<M>(
self.identity(),
crate::dead_letter::DeadLetterReason::ActorStopped,
operation,
);
return Err(Error::Send {
identity: self.identity(),
details: "Priority channel closed",
});
}
match reply_rx.await {
Ok(reply_any) => match reply_any.downcast::<T::Reply>() {
Ok(reply) => Ok(*reply),
Err(_) => Err(Error::Downcast {
identity: self.identity(),
expected_type: std::any::type_name::<T::Reply>(),
}),
},
Err(_) => {
crate::dead_letter::record::<M>(
self.identity(),
crate::dead_letter::DeadLetterReason::ReplyDropped,
operation,
);
Err(Error::Receive {
identity: self.identity(),
details: "Reply channel closed unexpectedly",
})
}
}
})
.await;
match result {
Ok(inner) => inner,
Err(_) => {
crate::dead_letter::record::<M>(
self.identity(),
crate::dead_letter::DeadLetterReason::Timeout,
operation,
);
Err(Error::Timeout {
identity: self.identity(),
timeout,
operation,
})
}
}
}
pub fn blocking_tell_priority<M>(&self, msg: M, timeout: Duration) -> Result<()>
where
M: Send + 'static,
T: Message<M>,
{
let Some(priority_sender) = self.priority_sender.as_ref() else {
return Err(Error::PriorityChannelNotEnabled {
identity: self.identity(),
});
};
let envelope = MailboxMessage::Envelope {
payload: Box::new(msg),
reply_channel: None,
actor_ref: self.clone(),
ask_edge: Default::default(),
};
let envelope = match priority_sender.try_send(envelope) {
Ok(()) => return Ok(()),
Err(mpsc::error::TrySendError::Closed(_)) => {
crate::dead_letter::record::<M>(
self.identity(),
crate::dead_letter::DeadLetterReason::ActorStopped,
"blocking_tell_priority",
);
warn!("Failed to send blocking priority tell: priority channel closed");
return Err(Error::Send {
identity: self.identity(),
details: "Priority channel closed",
});
}
Err(mpsc::error::TrySendError::Full(env)) => env,
};
let identity = self.identity();
let priority_sender = priority_sender.clone();
let admit = async move {
match tokio::time::timeout(timeout, priority_sender.send(envelope)).await {
Ok(Ok(())) => {
debug!("Blocking priority tell message sent successfully");
Ok(())
}
Ok(Err(_)) => {
crate::dead_letter::record::<M>(
identity,
crate::dead_letter::DeadLetterReason::ActorStopped,
"blocking_tell_priority",
);
warn!("Failed to send blocking priority tell: priority channel closed");
Err(Error::Send {
identity,
details: "Priority channel closed",
})
}
Err(_) => {
crate::dead_letter::record::<M>(
identity,
crate::dead_letter::DeadLetterReason::Timeout,
"blocking_tell_priority",
);
warn!(
timeout_ms = timeout.as_millis(),
"Blocking priority tell timed out awaiting slot admission"
);
Err(Error::Timeout {
identity,
timeout,
operation: "blocking_tell_priority",
})
}
}
};
match current_multi_thread_handle() {
Some(handle) => block_on_current_runtime(handle, admit),
None => run_blocking_with_runtime(
identity,
"Priority blocking thread terminated unexpectedly",
admit,
),
}
}
pub fn blocking_ask_priority<M>(&self, msg: M, timeout: Duration) -> Result<T::Reply>
where
T: Message<M>,
M: Send + 'static,
T::Reply: Send + 'static,
{
if self.priority_sender.is_none() {
return Err(Error::PriorityChannelNotEnabled {
identity: self.identity(),
});
}
if let Some(handle) = current_multi_thread_handle() {
return block_on_current_runtime(
handle,
self.ask_priority_inner(msg, timeout, "blocking_ask_priority"),
);
}
let self_clone = self.clone();
run_blocking_with_runtime(
self.identity(),
"Priority blocking thread terminated unexpectedly",
async move {
self_clone
.ask_priority_inner(msg, timeout, "blocking_ask_priority")
.await
},
)
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "info", name = "actor_kill", skip(self))
)]
pub fn kill(&self) {
info!(actor_id = %self.identity(), "Killing actor");
match self.terminate_sender.try_send(ControlSignal::Terminate) {
Ok(_) => {
info!(
"Kill signal enqueued; takes effect when the actor next polls control \
signals (no effect if a graceful stop is already in progress)"
);
}
Err(mpsc::error::TrySendError::Full(_)) => {
warn!("Failed to send Terminate to actor {}: terminate mailbox is full. Actor is likely already being terminated.", self.identity());
}
Err(mpsc::error::TrySendError::Closed(_)) => {
warn!("Failed to send Terminate to actor {}: terminate mailbox closed. Actor might already be stopped.", self.identity());
}
}
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "info", name = "actor_stop", skip(self))
)]
pub async fn stop(&self) {
let stop_msg = MailboxMessage::StopGracefully(self.clone());
#[cfg(feature = "deadlock-detection")]
let stop_msg = match self.sender.try_send(stop_msg) {
Ok(()) => {
info!(actor_id = %self.identity(), "Actor stop signal sent successfully");
return;
}
Err(mpsc::error::TrySendError::Full(m)) => {
self.panic_if_self_send_on_full("stop");
m
}
Err(mpsc::error::TrySendError::Closed(m)) => m,
};
match self.sender.send(stop_msg).await {
Ok(_) => {
info!(actor_id = %self.identity(), "Actor stop signal sent successfully");
}
Err(_) => {
warn!("Failed to send StopGracefully to actor {}: mailbox closed. Actor might already be stopped or stopping.", self.identity());
}
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(
level = "debug",
name = "actor_blocking_tell",
fields(
actor_id = %self.identity(),
message_type = %std::any::type_name::<M>(),
timeout_ms = timeout.map(|t| t.as_millis())
),
skip(self, msg)
))]
pub fn blocking_tell<M>(&self, msg: M, timeout: Option<Duration>) -> Result<()>
where
M: Send + 'static,
T: Message<M>,
{
let envelope = MailboxMessage::Envelope {
payload: Box::new(msg),
reply_channel: None, actor_ref: self.clone(), ask_edge: Default::default(),
};
debug!("Sending blocking tell message (fire-and-forget)");
let envelope = match self.sender.try_send(envelope) {
Ok(()) => {
debug!("Blocking tell message sent successfully");
return Ok(());
}
Err(mpsc::error::TrySendError::Closed(_)) => {
crate::dead_letter::record::<M>(
self.identity(),
crate::dead_letter::DeadLetterReason::ActorStopped,
"blocking_tell",
);
warn!("Failed to send blocking tell message: mailbox channel closed");
return Err(Error::Send {
identity: self.identity(),
details: "Mailbox channel closed",
});
}
Err(mpsc::error::TrySendError::Full(env)) => {
#[cfg(feature = "deadlock-detection")]
if timeout.is_none() {
self.panic_if_self_send_on_full("blocking_tell");
}
env
}
};
let identity = self.identity();
match timeout {
Some(timeout) => {
let sender = self.sender.clone();
let admit = async move {
match tokio::time::timeout(timeout, sender.send(envelope)).await {
Ok(Ok(())) => {
debug!("Blocking tell message sent successfully");
Ok(())
}
Ok(Err(_)) => {
crate::dead_letter::record::<M>(
identity,
crate::dead_letter::DeadLetterReason::ActorStopped,
"blocking_tell",
);
warn!("Failed to send blocking tell message: mailbox channel closed");
Err(Error::Send {
identity,
details: "Mailbox channel closed",
})
}
Err(_) => {
crate::dead_letter::record::<M>(
identity,
crate::dead_letter::DeadLetterReason::Timeout,
"blocking_tell",
);
warn!(
timeout_ms = timeout.as_millis(),
"Blocking tell timed out awaiting mailbox admission"
);
Err(Error::Timeout {
identity,
timeout,
operation: "blocking_tell",
})
}
}
};
match current_multi_thread_handle() {
Some(handle) => block_on_current_runtime(handle, admit),
None => run_blocking_with_runtime(
identity,
"Timeout thread terminated unexpectedly",
admit,
),
}
}
None => {
if let Some(handle) = current_multi_thread_handle() {
return block_on_current_runtime(handle, async {
self.sender.send(envelope).await.map_err(|_| {
crate::dead_letter::record::<M>(
identity,
crate::dead_letter::DeadLetterReason::ActorStopped,
"blocking_tell",
);
warn!("Failed to send blocking tell message: mailbox channel closed");
Error::Send {
identity,
details: "Mailbox channel closed",
}
})
});
}
let result = self.sender.blocking_send(envelope).map_err(|_| {
crate::dead_letter::record::<M>(
identity,
crate::dead_letter::DeadLetterReason::ActorStopped,
"blocking_tell",
);
Error::Send {
identity,
details: "Mailbox channel closed",
}
});
match &result {
Ok(_) => debug!("Blocking tell message sent successfully"),
Err(e) => warn!(error = %e, "Failed to send blocking tell message"),
}
result
}
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(
level = "debug",
name = "actor_blocking_ask",
fields(
actor_id = %self.identity(),
message_type = %std::any::type_name::<M>(),
reply_type = %std::any::type_name::<T::Reply>(),
timeout_ms = timeout.map(|t| t.as_millis())
),
skip(self, msg)
))]
pub fn blocking_ask<M>(&self, msg: M, timeout: Option<Duration>) -> Result<T::Reply>
where
T: Message<M>,
M: Send + 'static,
T::Reply: Send + 'static,
{
if let Some(handle) = current_multi_thread_handle() {
return block_on_current_runtime(handle, async {
match timeout {
Some(t) => self.ask_timeout_inner(msg, t, "blocking_ask").await,
None => self.ask_inner(msg, "blocking_ask").await,
}
});
}
match timeout {
Some(timeout_duration) => self.blocking_ask_with_timeout_impl(msg, timeout_duration),
None => self.blocking_ask_no_timeout(msg),
}
}
fn blocking_ask_no_timeout<M>(&self, msg: M) -> Result<T::Reply>
where
T: Message<M>,
M: Send + 'static,
T::Reply: Send + 'static,
{
debug!("Sending blocking ask message and waiting for reply");
let (reply_tx, reply_rx) = oneshot::channel();
let envelope = MailboxMessage::Envelope {
payload: Box::new(msg),
reply_channel: Some(reply_tx),
actor_ref: self.clone(), ask_edge: Default::default(),
};
self.sender.blocking_send(envelope).map_err(|_| {
crate::dead_letter::record::<M>(
self.identity(),
crate::dead_letter::DeadLetterReason::ActorStopped,
"blocking_ask",
);
warn!("Failed to send blocking ask message: mailbox channel closed");
Error::Send {
identity: self.identity(),
details: "Mailbox channel closed",
}
})?;
match reply_rx.blocking_recv() {
Ok(reply_any) => {
match reply_any.downcast::<T::Reply>() {
Ok(reply) => {
debug!("Blocking ask reply received successfully");
Ok(*reply)
}
Err(_) => {
warn!(
expected_type = %std::any::type_name::<T::Reply>(),
"Blocking ask reply type downcast failed"
);
Err(Error::Downcast {
identity: self.identity(),
expected_type: std::any::type_name::<T::Reply>(),
})
}
}
}
Err(_recv_err) => {
crate::dead_letter::record::<M>(
self.identity(),
crate::dead_letter::DeadLetterReason::ReplyDropped,
"blocking_ask",
);
warn!("Blocking ask reply channel closed unexpectedly");
Err(Error::Receive {
identity: self.identity(),
details: "Reply channel closed unexpectedly",
})
}
}
}
fn blocking_ask_with_timeout_impl<M>(&self, msg: M, timeout: Duration) -> Result<T::Reply>
where
T: Message<M>,
M: Send + 'static,
T::Reply: Send + 'static,
{
let self_clone = self.clone();
let identity = self.identity();
run_blocking_with_runtime(
identity,
"Timeout thread terminated unexpectedly",
async move {
self_clone
.ask_timeout_inner(msg, timeout, "blocking_ask")
.await
},
)
}
#[cfg_attr(feature = "tracing", tracing::instrument(
level = "debug",
name = "actor_ask_join",
fields(
actor_id = %self.identity(),
message_type = %std::any::type_name::<M>(),
result_type = %std::any::type_name::<R>()
),
skip(self, msg)
))]
pub async fn ask_join<M, R>(&self, msg: M) -> crate::Result<R>
where
T: Message<M, Reply = tokio::task::JoinHandle<R>>,
M: Send + 'static,
R: Send + 'static,
{
tracing::debug!("Sending ask_join message and waiting for task completion");
let join_handle = self.ask(msg).await?;
tracing::debug!("Received JoinHandle, awaiting task completion");
let result = join_handle.await.map_err(|join_error| crate::Error::Join {
identity: self.identity(),
source: std::sync::Arc::new(join_error),
})?;
tracing::debug!("Task completed successfully");
Ok(result)
}
#[cfg(feature = "metrics")]
pub fn metrics(&self) -> crate::MetricsSnapshot {
self.metrics.snapshot()
}
#[cfg(feature = "metrics")]
#[inline]
pub fn message_count(&self) -> u64 {
self.metrics.message_count()
}
#[cfg(feature = "metrics")]
#[inline]
pub fn avg_processing_time(&self) -> std::time::Duration {
self.metrics.avg_processing_time()
}
#[cfg(feature = "metrics")]
#[inline]
pub fn max_processing_time(&self) -> std::time::Duration {
self.metrics.max_processing_time()
}
#[cfg(feature = "metrics")]
#[inline]
pub fn priority_message_count(&self) -> u64 {
self.metrics.priority_message_count()
}
#[cfg(feature = "metrics")]
#[inline]
pub fn avg_priority_processing_time(&self) -> std::time::Duration {
self.metrics.avg_priority_processing_time()
}
#[cfg(feature = "metrics")]
#[inline]
pub fn max_priority_processing_time(&self) -> std::time::Duration {
self.metrics.max_priority_processing_time()
}
#[cfg(feature = "metrics")]
#[inline]
pub fn idle_event_count(&self) -> u64 {
self.metrics.idle_event_count()
}
#[cfg(feature = "metrics")]
#[inline]
pub fn avg_idle_processing_time(&self) -> std::time::Duration {
self.metrics.avg_idle_processing_time()
}
#[cfg(feature = "metrics")]
#[inline]
pub fn max_idle_processing_time(&self) -> std::time::Duration {
self.metrics.max_idle_processing_time()
}
#[cfg(feature = "metrics")]
#[inline]
pub fn uptime(&self) -> std::time::Duration {
self.metrics.uptime()
}
#[cfg(feature = "metrics")]
#[inline]
pub fn last_activity(&self) -> Option<std::time::SystemTime> {
self.metrics.last_activity()
}
}
impl<T: Actor> Clone for ActorRef<T> {
#[inline]
fn clone(&self) -> Self {
ActorRef {
id: self.id,
sender: self.sender.clone(),
priority_sender: self.priority_sender.clone(),
terminate_sender: self.terminate_sender.clone(),
idle_subscribe_sender: self.idle_subscribe_sender.clone(),
#[cfg(feature = "metrics")]
metrics: self.metrics.clone(),
}
}
}
pub struct IdleSubscribeError<T: Actor> {
pub error: Error,
stream: std::sync::Mutex<Option<BoxStream<'static, T::IdleEvent>>>,
}
impl<T: Actor> IdleSubscribeError<T> {
fn new(error: Error, stream: BoxStream<'static, T::IdleEvent>) -> Self {
Self {
error,
stream: std::sync::Mutex::new(Some(stream)),
}
}
pub fn take_stream(&self) -> Option<BoxStream<'static, T::IdleEvent>> {
self.stream
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take()
}
pub fn into_parts(self) -> (Error, Option<BoxStream<'static, T::IdleEvent>>) {
let stream = self
.stream
.into_inner()
.unwrap_or_else(std::sync::PoisonError::into_inner);
(self.error, stream)
}
}
impl<T: Actor> fmt::Debug for IdleSubscribeError<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let taken = self
.stream
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_none();
f.debug_struct("IdleSubscribeError")
.field("error", &self.error)
.field(
"stream",
&format_args!(
"{}BoxStream<{}>",
if taken { "taken " } else { "" },
std::any::type_name::<T::IdleEvent>()
),
)
.finish()
}
}
impl<T: Actor> fmt::Display for IdleSubscribeError<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.error.fmt(f)
}
}
impl<T: Actor> std::error::Error for IdleSubscribeError<T> {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
std::error::Error::source(&self.error)
}
}
impl<T: Actor> From<IdleSubscribeError<T>> for Error {
fn from(e: IdleSubscribeError<T>) -> Self {
e.error
}
}
#[derive(Debug)]
pub struct ActorWeak<T: Actor> {
id: Identity,
sender: tokio::sync::mpsc::WeakSender<MailboxMessage<T>>,
priority_sender: Option<tokio::sync::mpsc::WeakSender<MailboxMessage<T>>>,
terminate_sender: tokio::sync::mpsc::WeakSender<ControlSignal>,
idle_subscribe_sender: Option<tokio::sync::mpsc::WeakSender<IdleEventStream<T>>>,
#[cfg(feature = "metrics")]
metrics: Arc<MetricsCollector>,
}
impl<T: Actor> ActorWeak<T> {
#[inline]
pub fn upgrade(&self) -> Option<ActorRef<T>> {
let sender = self.sender.upgrade()?;
let terminate_sender = self.terminate_sender.upgrade()?;
if sender.is_closed() || terminate_sender.is_closed() {
return None;
}
let priority_sender = self.priority_sender.as_ref().and_then(|s| s.upgrade());
let idle_subscribe_sender = self
.idle_subscribe_sender
.as_ref()
.and_then(|s| s.upgrade());
Some(ActorRef {
id: self.id,
sender,
priority_sender,
terminate_sender,
idle_subscribe_sender,
#[cfg(feature = "metrics")]
metrics: self.metrics.clone(),
})
}
pub fn identity(&self) -> Identity {
self.id
}
#[inline]
pub fn is_alive(&self) -> bool {
match (self.sender.upgrade(), self.terminate_sender.upgrade()) {
(Some(sender), Some(terminate_sender)) => {
!sender.is_closed() && !terminate_sender.is_closed()
}
_ => false,
}
}
#[cfg(feature = "metrics")]
pub fn metrics(&self) -> crate::MetricsSnapshot {
self.metrics.snapshot()
}
}
impl<T: Actor> Clone for ActorWeak<T> {
#[inline]
fn clone(&self) -> Self {
ActorWeak {
id: self.id,
sender: self.sender.clone(),
priority_sender: self.priority_sender.clone(),
terminate_sender: self.terminate_sender.clone(),
idle_subscribe_sender: self.idle_subscribe_sender.clone(),
#[cfg(feature = "metrics")]
metrics: self.metrics.clone(),
}
}
}