use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Notify;
use acton_reactive::prelude::*;
use acton_test::prelude::*;
const PATIENCE: Duration = Duration::from_secs(5);
#[acton_actor]
struct Counter {
count: usize,
deferred: Option<OutboundEnvelope>,
}
#[acton_message]
struct Increment;
#[acton_message]
struct GetCount;
#[acton_message]
#[derive(PartialEq, Eq)]
struct Count {
value: usize,
}
impl Request for GetCount {
type Response = Count;
}
#[acton_message]
struct Ignored;
impl Request for Ignored {
type Response = Count;
}
#[acton_message]
struct Panics;
impl Request for Panics {
type Response = Count;
}
#[acton_message]
struct Confused;
impl Request for Confused {
type Response = Count;
}
#[acton_message]
struct Deferred;
impl Request for Deferred {
type Response = Count;
}
#[acton_message]
struct NotACount;
#[acton_message]
struct Echo {
token: usize,
}
#[acton_message]
#[derive(PartialEq, Eq)]
struct Echoed {
token: usize,
}
impl Request for Echo {
type Response = Echoed;
}
async fn start_counter(
runtime: &mut ActorRuntime,
stored_request: Arc<Notify>,
) -> ActorHandle {
let mut actor = runtime.new_actor::<Counter>();
actor
.mutate_on::<Increment>(|actor, _ctx| {
actor.model.count += 1;
Reply::ready()
})
.mutate_on::<GetCount>(|actor, ctx| {
let reply = ctx.reply_envelope();
let value = actor.model.count;
Reply::pending(async move {
reply.send(Count { value }).await;
})
})
.mutate_on::<Ignored>(|actor, _ctx| {
actor.model.count += 1;
Reply::ready()
})
.mutate_on::<Panics>(|_actor, _ctx| {
panic!("this handler panics before it can reply");
})
.mutate_on::<Confused>(|_actor, ctx| {
let reply = ctx.reply_envelope();
Reply::pending(async move {
reply.send(NotACount).await;
})
})
.mutate_on::<Deferred>(move |actor, ctx| {
actor.model.deferred = Some(ctx.reply_envelope());
stored_request.notify_one();
Reply::ready()
})
.mutate_on::<Echo>(|_actor, ctx| {
let reply = ctx.reply_envelope();
let token = ctx.message().token;
Reply::pending(async move {
reply.send(Echoed { token }).await;
})
});
actor.start().await
}
fn unused_signal() -> Arc<Notify> {
Arc::new(Notify::new())
}
#[acton_test]
async fn ask_returns_the_reply_the_handler_sent() -> anyhow::Result<()> {
let mut runtime = ActonApp::launch_async().await;
let handle = start_counter(&mut runtime, unused_signal()).await;
handle.send(Increment).await;
handle.send(Increment).await;
let count = tokio::time::timeout(PATIENCE, handle.ask(GetCount))
.await
.expect("ask must resolve, not hang")?;
assert_eq!(count.value, 2, "the reply should carry the actor's state");
runtime.shutdown_all().await?;
Ok(())
}
#[acton_test]
async fn ask_proves_every_earlier_message_was_processed() -> anyhow::Result<()> {
let mut runtime = ActonApp::launch_async().await;
let handle = start_counter(&mut runtime, unused_signal()).await;
for _ in 0..500 {
handle.send(Increment).await;
}
let count = tokio::time::timeout(PATIENCE, handle.ask(GetCount))
.await
.expect("ask must resolve, not hang")?;
assert_eq!(
count.value, 500,
"ask must not resolve until everything queued ahead of it has been processed"
);
runtime.shutdown_all().await?;
Ok(())
}
#[acton_test]
async fn ask_reports_no_reply_when_the_handler_does_not_answer() -> anyhow::Result<()> {
let mut runtime = ActonApp::launch_async().await;
let handle = start_counter(&mut runtime, unused_signal()).await;
let outcome = tokio::time::timeout(PATIENCE, handle.ask(Ignored))
.await
.expect("a handler that never replies must not hang the caller");
assert_eq!(outcome, Err(AskError::NoReply));
let count = tokio::time::timeout(PATIENCE, handle.ask(GetCount))
.await
.expect("ask must resolve, not hang")?;
assert_eq!(count.value, 1, "the silent handler still did its work");
runtime.shutdown_all().await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn ask_reports_no_reply_when_the_handler_panics() -> anyhow::Result<()> {
let mut runtime = ActonApp::launch_async().await;
let handle = start_counter(&mut runtime, unused_signal()).await;
let outcome = tokio::time::timeout(PATIENCE, handle.ask(Panics))
.await
.expect("a panicking handler must not hang the caller");
assert_eq!(outcome, Err(AskError::NoReply));
runtime.shutdown_all().await?;
Ok(())
}
#[acton_test]
async fn ask_reports_no_reply_when_the_actor_stops_holding_the_request() -> anyhow::Result<()> {
let mut runtime = ActonApp::launch_async().await;
let stored = Arc::new(Notify::new());
let handle = start_counter(&mut runtime, Arc::clone(&stored)).await;
let asking = tokio::spawn({
let handle = handle.clone();
async move { handle.ask(Deferred).await }
});
tokio::time::timeout(PATIENCE, stored.notified())
.await
.expect("the actor must receive and store the request");
handle.stop().await?;
let outcome = tokio::time::timeout(PATIENCE, asking)
.await
.expect("a stopped actor must release anyone waiting on it")?;
assert_eq!(outcome, Err(AskError::NoReply));
runtime.shutdown_all().await?;
Ok(())
}
#[acton_test]
async fn ask_reports_undeliverable_when_the_actor_has_already_stopped() -> anyhow::Result<()> {
let mut runtime = ActonApp::launch_async().await;
let handle = start_counter(&mut runtime, unused_signal()).await;
handle.stop().await?;
let outcome = tokio::time::timeout(PATIENCE, handle.ask(GetCount))
.await
.expect("asking a stopped actor must fail promptly, not hang");
assert_eq!(outcome, Err(AskError::Undeliverable));
runtime.shutdown_all().await?;
Ok(())
}
#[acton_test]
async fn ask_reports_unexpected_reply_when_the_handler_answers_with_another_type(
) -> anyhow::Result<()> {
let mut runtime = ActonApp::launch_async().await;
let handle = start_counter(&mut runtime, unused_signal()).await;
let outcome = tokio::time::timeout(PATIENCE, handle.ask(Confused))
.await
.expect("ask must resolve, not hang");
match outcome {
Err(AskError::UnexpectedReply { expected, received }) => {
assert!(
expected.ends_with("Count"),
"expected type should name the declared reply, got `{expected}`"
);
assert!(
received.contains("NotACount"),
"the rendering should identify what was actually sent, got `{received}`"
);
}
other => panic!("expected UnexpectedReply, got {other:?}"),
}
runtime.shutdown_all().await?;
Ok(())
}
#[acton_test]
async fn ask_times_out_when_the_actor_holds_the_request_and_never_answers() -> anyhow::Result<()> {
let mut runtime = ActonApp::launch_async().await;
let handle = start_counter(&mut runtime, unused_signal()).await;
let outcome = tokio::time::timeout(
PATIENCE,
handle.ask_with_timeout(Deferred, Duration::from_millis(100)),
)
.await
.expect("the deadline must release the caller well inside PATIENCE");
assert_eq!(
outcome,
Err(AskError::TimedOut {
after: Duration::from_millis(100)
}),
"a held-but-unanswered request must time out, not resolve as NoReply"
);
runtime.shutdown_all().await?;
Ok(())
}
#[acton_test]
async fn a_dropped_reply_address_is_reported_without_waiting_for_the_deadline(
) -> anyhow::Result<()> {
let mut runtime = ActonApp::launch_async().await;
let handle = start_counter(&mut runtime, unused_signal()).await;
let outcome = tokio::time::timeout(
PATIENCE,
handle.ask_with_timeout(Ignored, DEFAULT_ASK_TIMEOUT),
)
.await
.expect("closure must report the failure without waiting out the deadline");
assert!(
PATIENCE < DEFAULT_ASK_TIMEOUT,
"this test only means something while the deadline outlasts our patience"
);
assert_eq!(outcome, Err(AskError::NoReply));
runtime.shutdown_all().await?;
Ok(())
}
#[acton_test]
async fn concurrent_asks_each_receive_their_own_reply() -> anyhow::Result<()> {
let mut runtime = ActonApp::launch_async().await;
let handle = start_counter(&mut runtime, unused_signal()).await;
let mut asks = Vec::new();
for token in 0..25 {
let handle = handle.clone();
asks.push(tokio::spawn(async move {
(token, handle.ask(Echo { token }).await)
}));
}
for ask in asks {
let (token, reply) = tokio::time::timeout(PATIENCE, ask)
.await
.expect("every concurrent ask must resolve")?;
let echoed = reply?;
assert_eq!(
echoed.token, token,
"each caller must receive the reply to its own request"
);
}
runtime.shutdown_all().await?;
Ok(())
}