#![cfg(feature = "ipc")]
use std::path::PathBuf;
use std::time::Duration;
use acton_reactive::ipc::{IpcClient, IpcConfig, IpcListenerHandle};
use acton_reactive::prelude::*;
use acton_test::prelude::*;
use tokio_util::sync::CancellationToken;
const SHORT_TIMEOUT: Duration = Duration::from_millis(500);
#[acton_message(ipc)]
struct GetCount;
#[acton_message(ipc)]
struct Count {
value: usize,
}
impl Request for GetCount {
type Response = Count;
}
impl RemoteRequest for GetCount {
const MESSAGE_TYPE: &'static str = "GetCount";
}
#[acton_message(ipc)]
struct AskNothing;
impl Request for AskNothing {
type Response = Count;
}
impl RemoteRequest for AskNothing {
const MESSAGE_TYPE: &'static str = "AskNothing";
}
#[acton_message(ipc)]
struct AskForever;
impl Request for AskForever {
type Response = Count;
}
impl RemoteRequest for AskForever {
const MESSAGE_TYPE: &'static str = "AskForever";
}
#[acton_actor]
struct CounterState {
count: usize,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Registrations {
Complete,
WithoutReplyType,
WithoutRequestType,
}
async fn start_server(
socket_path: PathBuf,
registrations: Registrations,
release_token: &CancellationToken,
) -> anyhow::Result<(ActorRuntime, IpcListenerHandle)> {
let mut runtime: ActorRuntime = ActonApp::launch_async().await;
let registry = runtime.ipc_registry();
if registrations != Registrations::WithoutRequestType {
registry.register::<GetCount>("GetCount");
}
registry.register::<AskNothing>("AskNothing");
registry.register::<AskForever>("AskForever");
if registrations != Registrations::WithoutReplyType {
registry.register::<Count>("Count");
}
let mut counter = runtime.new_actor_with_name::<CounterState>("counter".to_string());
counter.model.count = 7;
counter.act_on::<GetCount>(|actor, envelope| {
let reply = envelope.reply_envelope();
let value = actor.model.count;
Reply::pending(async move {
reply.send(Count { value }).await;
})
});
counter.act_on::<AskNothing>(|_actor, _envelope| Reply::ready());
let handler_token = release_token.clone();
counter.act_on::<AskForever>(move |_actor, envelope| {
let reply = envelope.reply_envelope();
let token = handler_token.clone();
Reply::pending(async move {
let _held_open = reply;
token.cancelled().await;
})
});
let handle = counter.start().await;
runtime
.ipc_expose("counter", handle)
.expect("IPC name should be unclaimed at startup");
let mut config = IpcConfig::default();
config.socket.path = Some(socket_path);
let listener = runtime.start_ipc_listener_with_config(config).await?;
Ok((runtime, listener))
}
struct Peer {
runtime: ActorRuntime,
listener: IpcListenerHandle,
client: IpcClient,
release_token: CancellationToken,
_dir: tempfile::TempDir,
}
impl Peer {
async fn start(registrations: Registrations) -> anyhow::Result<Self> {
let dir = tempfile::tempdir()?;
let socket_path = dir.path().join("ipc.sock");
let release_token = CancellationToken::new();
let (runtime, listener) =
start_server(socket_path.clone(), registrations, &release_token).await?;
let client = IpcClient::connect(&socket_path).await?;
Ok(Self {
runtime,
listener,
client,
release_token,
_dir: dir,
})
}
async fn shutdown(mut self) -> anyhow::Result<()> {
self.release_token.cancel();
self.client.disconnect().await?;
self.listener.stop();
self.runtime.shutdown_all().await?;
Ok(())
}
}
#[acton_test]
async fn asking_a_remote_actor_returns_the_declared_reply() -> anyhow::Result<()> {
let peer = Peer::start(Registrations::Complete).await?;
let count: Count = peer.client.actor("counter").ask(GetCount).await?;
assert_eq!(count.value, 7);
peer.shutdown().await
}
#[acton_test]
async fn a_handler_that_never_replies_is_reported_as_no_reply() -> anyhow::Result<()> {
let peer = Peer::start(Registrations::Complete).await?;
let outcome = peer.client.actor("counter").ask(AskNothing).await;
assert_eq!(outcome.err(), Some(AskError::NoReply));
peer.shutdown().await
}
#[acton_test]
async fn a_handler_that_holds_the_reply_open_is_ended_by_the_deadline() -> anyhow::Result<()> {
let peer = Peer::start(Registrations::Complete).await?;
let started = std::time::Instant::now();
let outcome = peer
.client
.actor("counter")
.ask_with_timeout(AskForever, SHORT_TIMEOUT)
.await;
let elapsed = started.elapsed();
assert!(
matches!(outcome, Err(AskError::TimedOut { .. })),
"expected TimedOut, got {outcome:?}"
);
assert!(
elapsed < Duration::from_secs(10),
"the caller's deadline should govern, but the ask took {elapsed:?}"
);
peer.shutdown().await
}
#[acton_test]
async fn asking_an_unknown_actor_is_refused_before_dispatch() -> anyhow::Result<()> {
let peer = Peer::start(Registrations::Complete).await?;
let outcome = peer.client.actor("nobody-here").ask(GetCount).await;
match outcome {
Err(AskError::PeerRejected { code, .. }) => {
assert_eq!(code.as_deref(), Some("ACTOR_NOT_FOUND"));
}
other => panic!("expected PeerRejected, got {other:?}"),
}
peer.shutdown().await
}
#[acton_test]
async fn an_unregistered_request_type_is_refused_before_dispatch() -> anyhow::Result<()> {
let peer = Peer::start(Registrations::WithoutRequestType).await?;
let outcome = peer.client.actor("counter").ask(GetCount).await;
match outcome {
Err(AskError::PeerRejected { code, .. }) => {
assert_eq!(code.as_deref(), Some("UNKNOWN_MESSAGE_TYPE"));
}
other => panic!("expected PeerRejected, got {other:?}"),
}
peer.shutdown().await
}
#[acton_test]
async fn an_unregistered_reply_type_surfaces_as_an_unexpected_reply() -> anyhow::Result<()> {
let peer = Peer::start(Registrations::WithoutReplyType).await?;
let outcome = peer.client.actor("counter").ask(GetCount).await;
match outcome {
Err(AskError::UnexpectedReply { received, .. }) => {
assert!(
received.contains("_ipc_fallback"),
"the payload should name the peer's fallback, got `{received}`"
);
}
other => panic!("expected UnexpectedReply, got {other:?}"),
}
peer.shutdown().await
}
#[acton_test]
async fn a_peer_that_never_answers_is_ended_by_the_callers_own_deadline() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
let socket_path = dir.path().join("silent.sock");
let listener = tokio::net::UnixListener::bind(&socket_path)?;
let accepted = tokio::spawn(async move {
let (stream, _addr) = listener.accept().await.expect("peer should accept");
std::future::pending::<()>().await;
drop(stream);
});
let client = IpcClient::connect(&socket_path).await?;
let started = std::time::Instant::now();
let outcome = client
.actor("counter")
.ask_with_timeout(GetCount, SHORT_TIMEOUT)
.await;
let elapsed = started.elapsed();
assert!(
matches!(outcome, Err(AskError::TimedOut { .. })),
"expected TimedOut, got {outcome:?}"
);
assert!(
elapsed < Duration::from_secs(10),
"the caller's deadline should end this, but the ask took {elapsed:?}"
);
accepted.abort();
Ok(())
}
#[acton_test]
async fn asking_over_a_closed_connection_reports_uncertain_delivery() -> anyhow::Result<()> {
let mut peer = Peer::start(Registrations::Complete).await?;
peer.client.disconnect().await?;
let outcome = peer.client.actor("counter").ask(GetCount).await;
assert!(
matches!(outcome, Err(AskError::TransportFailed { .. })),
"expected TransportFailed, got {outcome:?}"
);
peer.listener.stop();
peer.runtime.shutdown_all().await?;
Ok(())
}