use std::any::type_name;
use std::fmt;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use tracing::{instrument, trace};
use crate::message::{Envelope, MessageAddress, MessageError, OutboundEnvelope};
use crate::traits::{ActonMessage, Request};
const REPLY_CHANNEL_CAPACITY: usize = 1;
pub const DEFAULT_ASK_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum AskError {
Undeliverable,
Cancelled,
NoReply,
TimedOut {
after: Duration,
},
UnexpectedReply {
expected: &'static str,
received: String,
},
PeerRejected {
code: Option<String>,
detail: String,
},
TransportFailed {
detail: String,
},
}
impl fmt::Display for AskError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Undeliverable => write!(
f,
"the request could not be delivered: the actor's inbox is closed"
),
Self::Cancelled => write!(f, "the request was cancelled before delivery"),
Self::NoReply => write!(
f,
"the request was delivered but no reply will arrive: the handler did not \
reply, or the actor stopped, panicked, or was restarted first"
),
Self::TimedOut { after } => write!(
f,
"no reply within {after:?}: the actor still holds the request but has \
not answered"
),
Self::UnexpectedReply { expected, received } => write!(
f,
"the request declares its reply type as `{expected}`, but the handler \
sent {received}"
),
Self::PeerRejected {
code: Some(code),
detail,
} => write!(
f,
"the peer refused the request before dispatching it ({code}): {detail}"
),
Self::PeerRejected { code: None, detail } => write!(
f,
"the peer refused the request before dispatching it: {detail}"
),
Self::TransportFailed { detail } => write!(
f,
"the connection failed, so whether the request was processed is unknown: \
{detail}"
),
}
}
}
impl std::error::Error for AskError {}
impl From<MessageError> for AskError {
fn from(error: MessageError) -> Self {
match error {
MessageError::Cancelled => Self::Cancelled,
_ => Self::Undeliverable,
}
}
}
pub fn classify_reply<R>(
reply: Option<Arc<dyn ActonMessage + Send + Sync>>,
) -> Result<R, AskError>
where
R: ActonMessage + Clone,
{
let Some(message) = reply else {
return Err(AskError::NoReply);
};
ActonMessage::as_any(&*message)
.downcast_ref::<R>()
.cloned()
.ok_or_else(|| AskError::UnexpectedReply {
expected: type_name::<R>(),
received: format!("{message:?}"),
})
}
#[instrument(
skip(request, recipient, cancellation_token),
fields(request_type = type_name::<R>())
)]
pub async fn send_request<R>(
recipient: MessageAddress,
cancellation_token: CancellationToken,
request: R,
timeout: Duration,
) -> Result<R::Response, AskError>
where
R: Request,
{
tokio::time::timeout(
timeout,
exchange::<R>(recipient, cancellation_token, request),
)
.await
.unwrap_or(Err(AskError::TimedOut { after: timeout }))
}
async fn exchange<R>(
recipient: MessageAddress,
cancellation_token: CancellationToken,
request: R,
) -> Result<R::Response, AskError>
where
R: Request,
{
let (reply_sender, mut reply_receiver) = mpsc::channel::<Envelope>(REPLY_CHANNEL_CAPACITY);
let reply_address = MessageAddress::new(reply_sender, reply_identifier());
let envelope =
OutboundEnvelope::new_with_recipient(reply_address, recipient, cancellation_token);
envelope.try_send(request).await?;
drop(envelope);
let reply = reply_receiver.recv().await.map(|envelope| envelope.message);
trace!(replied = reply.is_some(), "ask completed");
classify_reply::<R::Response>(reply)
}
fn reply_identifier() -> acton_ern::Ern {
acton_ern::Ern::with_root("ask-reply").unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone, PartialEq, Eq)]
struct Count {
value: usize,
}
#[derive(Debug, Clone)]
struct SomethingElse;
#[test]
fn a_closed_reply_channel_is_reported_as_no_reply() {
let outcome = classify_reply::<Count>(None);
assert_eq!(outcome, Err(AskError::NoReply));
}
#[test]
fn a_reply_of_the_declared_type_is_returned_to_the_caller() {
let reply: Arc<dyn ActonMessage + Send + Sync> = Arc::new(Count { value: 7 });
let outcome = classify_reply::<Count>(Some(reply));
assert_eq!(outcome, Ok(Count { value: 7 }));
}
#[test]
fn a_reply_of_another_type_is_reported_rather_than_discarded() {
let reply: Arc<dyn ActonMessage + Send + Sync> = Arc::new(SomethingElse);
match classify_reply::<Count>(Some(reply)) {
Err(AskError::UnexpectedReply { expected, received }) => {
assert!(
expected.ends_with("Count"),
"the expected type should name the declared reply, got `{expected}`"
);
assert!(
received.contains("SomethingElse"),
"the rendering should identify what was actually sent, got `{received}`"
);
}
other => panic!("expected UnexpectedReply, got {other:?}"),
}
}
#[test]
fn delivery_failures_keep_their_identity() {
assert_eq!(AskError::from(MessageError::Cancelled), AskError::Cancelled);
assert_eq!(
AskError::from(MessageError::ChannelClosed),
AskError::Undeliverable
);
assert_eq!(
AskError::from(MessageError::SendFailed("closed".to_owned())),
AskError::Undeliverable
);
}
#[test]
fn every_variant_describes_itself() {
let variants = [
AskError::Undeliverable,
AskError::Cancelled,
AskError::NoReply,
AskError::UnexpectedReply {
expected: "Count",
received: "SomethingElse".to_owned(),
},
];
for variant in variants {
assert!(
!variant.to_string().is_empty(),
"{variant:?} must render a message"
);
}
}
}