use std::cmp::PartialEq;
use std::fmt::Debug; use std::hash::{Hash, Hasher}; use std::sync::{Arc, OnceLock};
use tokio::runtime::{Handle, Runtime};
use tracing::{debug, error, instrument, trace, warn};
static SYNC_REPLY_RUNTIME: OnceLock<Runtime> = OnceLock::new();
fn sync_reply_runtime() -> &'static Runtime {
SYNC_REPLY_RUNTIME.get_or_init(|| {
debug!("Creating shared fallback runtime for sync reply() calls");
tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_all()
.thread_name("acton-sync-reply")
.build()
.expect("Failed to create fallback Tokio runtime for sync reply()")
})
}
use crate::common::{Envelope, MessageError};
use crate::message::message_address::MessageAddress;
use crate::traits::ActonMessage;
#[derive(Clone, Debug)]
pub struct OutboundEnvelope {
pub(crate) return_address: MessageAddress,
pub(crate) recipient_address: Option<MessageAddress>,
pub(crate) cancellation_token: tokio_util::sync::CancellationToken,
}
impl PartialEq for MessageAddress {
fn eq(&self, other: &Self) -> bool {
self.sender == other.sender }
}
impl PartialEq for OutboundEnvelope {
fn eq(&self, other: &Self) -> bool {
self.return_address == other.return_address
}
}
impl Eq for OutboundEnvelope {}
impl Hash for OutboundEnvelope {
fn hash<H: Hasher>(&self, state: &mut H) {
self.return_address.sender.hash(state);
}
}
impl OutboundEnvelope {
#[instrument(skip(return_address))]
pub fn new(
return_address: MessageAddress,
cancellation_token: tokio_util::sync::CancellationToken,
) -> Self {
trace!(sender = %return_address.sender, "Creating new OutboundEnvelope");
Self {
return_address,
recipient_address: None,
cancellation_token,
}
}
#[inline]
#[must_use]
pub fn reply_to(&self) -> MessageAddress {
self.return_address.clone()
}
#[inline]
#[must_use]
pub const fn recipient(&self) -> &Option<MessageAddress> {
&self.recipient_address
}
#[instrument(skip(return_address, recipient_address))]
pub(crate) fn new_with_recipient(
return_address: MessageAddress,
recipient_address: MessageAddress,
cancellation_token: tokio_util::sync::CancellationToken,
) -> Self {
trace!(sender = %return_address.sender, recipient = %recipient_address.sender, "Creating new OutboundEnvelope with recipient");
Self {
return_address,
recipient_address: Some(recipient_address),
cancellation_token,
}
}
#[instrument(skip(self, message), fields(message_type = std::any::type_name_of_val(&message)))]
pub fn reply(&self, message: impl ActonMessage + 'static) -> Result<(), MessageError> {
let envelope = self.clone();
let message_arc = Arc::new(message);
if let Ok(handle) = Handle::try_current() {
trace!(
sender = %envelope.return_address.sender,
recipient = ?envelope.recipient_address.as_ref().map(|r| r.sender.to_string()),
"Replying via existing runtime handle"
);
Self::spawn_reply_task(&handle, envelope, message_arc);
} else {
warn!(
sender = %envelope.return_address.sender,
"reply() called outside Tokio context; using shared fallback runtime"
);
Self::spawn_reply_on_fallback(envelope, message_arc);
}
Ok(())
}
fn spawn_reply_task(
handle: &Handle,
envelope: Self,
message: Arc<dyn ActonMessage + Send + Sync>,
) {
handle.spawn(Box::pin(async move {
envelope.send_message_inner(message).await;
}));
}
fn spawn_reply_on_fallback(envelope: Self, message: Arc<dyn ActonMessage + Send + Sync>) {
sync_reply_runtime().spawn(async move {
envelope.send_message_inner(message).await;
});
}
async fn send_message_inner(&self, message: Arc<dyn ActonMessage + Send + Sync>) {
let target_address = self
.recipient_address
.as_ref()
.unwrap_or(&self.return_address)
.clone();
let return_address = self.return_address.clone();
let channel_sender = target_address.address.clone();
if self.cancellation_token.is_cancelled() {
error!(sender = %return_address.sender, recipient = %target_address.sender, "Send aborted: cancellation_token triggered");
return;
}
match channel_sender.try_reserve() {
Ok(permit) => {
permit.send(Envelope::new(message, return_address, target_address));
return;
}
Err(tokio::sync::mpsc::error::TrySendError::Closed(())) => {
error!(sender = %return_address.sender, recipient = %target_address.sender, "Recipient channel is closed");
return;
}
Err(tokio::sync::mpsc::error::TrySendError::Full(())) => {
}
}
match channel_sender.reserve().await {
Ok(permit) => {
permit.send(Envelope::new(message, return_address, target_address));
}
Err(e) => {
error!(sender = %return_address.sender, recipient = %target_address.sender, error = %e, "Failed to reserve channel capacity");
}
};
}
#[instrument(skip(self, message), level = "trace", fields(message_type = std::any::type_name_of_val(&message)))]
pub async fn send(&self, message: impl ActonMessage + 'static) {
self.send_message_inner(Arc::new(message)).await;
}
#[instrument(skip(self, message), level = "trace", fields(message_type = std::any::type_name_of_val(&message)))]
pub async fn try_send(&self, message: impl ActonMessage + 'static) -> Result<(), MessageError> {
let message = Arc::new(message);
let target_address = self
.recipient_address
.as_ref()
.unwrap_or(&self.return_address);
if self.cancellation_token.is_cancelled() {
return Err(MessageError::Cancelled);
}
let channel_sender = &target_address.address;
match channel_sender.try_reserve() {
Ok(permit) => {
let internal_envelope =
Envelope::new(message, self.return_address.clone(), target_address.clone());
permit.send(internal_envelope);
return Ok(());
}
Err(tokio::sync::mpsc::error::TrySendError::Closed(())) => {
return Err(MessageError::ChannelClosed);
}
Err(tokio::sync::mpsc::error::TrySendError::Full(())) => {
}
}
let channel_sender = channel_sender.clone();
let cancellation = self.cancellation_token.clone();
let return_addr = self.return_address.clone();
let target_addr = target_address.clone();
Box::pin(async move {
tokio::select! {
() = cancellation.cancelled() => {
Err(MessageError::Cancelled)
}
permit_result = channel_sender.reserve() => {
match permit_result {
Ok(permit) => {
let internal_envelope = Envelope::new(message, return_addr, target_addr);
permit.send(internal_envelope);
Ok(())
}
Err(e) => {
Err(MessageError::SendFailed(e.to_string()))
}
}
}
}
})
.await
}
#[cfg(feature = "ipc")]
#[instrument(skip(self, message), level = "trace")]
pub async fn send_arc(&self, message: Arc<dyn ActonMessage + Send + Sync>) {
self.send_message_inner(message).await;
}
#[cfg(feature = "ipc")]
#[instrument(skip(self, message), level = "trace")]
pub fn try_send_arc(
&self,
message: Arc<dyn ActonMessage + Send + Sync>,
) -> Result<(), crate::common::ipc::IpcError> {
use crate::common::ipc::IpcError;
use tokio::sync::mpsc::error::TrySendError;
let target_address = self
.recipient_address
.as_ref()
.unwrap_or(&self.return_address);
let target_id = &target_address.sender;
let channel_sender = target_address.address.clone();
trace!(sender = %self.return_address.sender, recipient = %target_id, "Attempting try_send_arc");
if channel_sender.is_closed() {
tracing::error!(sender = %self.return_address.sender, recipient = %target_id, "Recipient channel is closed");
return Err(IpcError::IoError("Recipient channel is closed".to_string()));
}
let permit = match channel_sender.try_reserve() {
Ok(permit) => permit,
Err(TrySendError::Full(())) => {
tracing::warn!(sender = %self.return_address.sender, recipient = %target_id, "Target actor inbox is full");
return Err(IpcError::TargetBusy);
}
Err(TrySendError::Closed(())) => {
tracing::error!(sender = %self.return_address.sender, recipient = %target_id, "Recipient channel is closed");
return Err(IpcError::IoError("Recipient channel is closed".to_string()));
}
};
let internal_envelope =
Envelope::new(message, self.return_address.clone(), target_address.clone());
trace!(sender = %self.return_address.sender, recipient = %target_id, "Sending message via try_reserve permit");
permit.send(internal_envelope);
Ok(())
}
}