use std::marker::PhantomData;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use crate::native::native_process::{NativeContext, NativeHandler, NativeOutcome};
use crate::scheduler::Scheduler;
use crate::term::Term;
use crate::term::boxed::Tuple;
const TAG_CAST: i64 = 0;
const TAG_CALL: i64 = 1;
const TAG_REPLY: i64 = 2;
const DEFAULT_CALL_TIMEOUT: Duration = Duration::from_secs(5);
static NEXT_REF: AtomicU64 = AtomicU64::new(1);
fn next_ref() -> u64 {
NEXT_REF.fetch_add(1, Ordering::Relaxed)
}
pub trait ActorMessage: Clone + Send + Sync + 'static {
fn encode(&self, ctx: &mut NativeContext<'_>) -> Option<Term>;
fn decode(term: Term) -> Option<Self>;
}
impl ActorMessage for i64 {
fn encode(&self, _ctx: &mut NativeContext<'_>) -> Option<Term> {
Term::try_small_int(*self)
}
fn decode(term: Term) -> Option<Self> {
term.as_small_int()
}
}
pub trait Actor: Send + 'static {
type Call: ActorMessage;
type Reply: ActorMessage;
type Cast: ActorMessage;
fn handle_call(&mut self, request: Self::Call, ctx: &mut ActorContext<'_, '_>) -> Self::Reply;
fn handle_cast(&mut self, request: Self::Cast, ctx: &mut ActorContext<'_, '_>);
}
pub struct ActorContext<'ctx, 'slice> {
inner: &'ctx mut NativeContext<'slice>,
}
impl<'ctx, 'slice> ActorContext<'ctx, 'slice> {
fn new(inner: &'ctx mut NativeContext<'slice>) -> Self {
Self { inner }
}
#[must_use]
pub fn self_pid(&self) -> u64 {
self.inner.self_pid()
}
pub fn cast<M: ActorMessage>(&mut self, target_pid: u64, message: &M) {
if let Some(payload) = message.encode(self.inner)
&& let Some(envelope) = self
.inner
.alloc_tuple(&[Term::small_int(TAG_CAST), payload])
{
self.inner.send(target_pid, envelope);
}
}
pub fn spawn_child<C, F>(&mut self, factory: F) -> Option<u64>
where
C: Actor,
F: Fn() -> C + Send + Sync + 'static,
{
let self_pid = self.inner.self_pid();
self.inner
.spawn_native(actor_factory(factory), Some(self_pid))
.ok()
}
}
struct ActorHandler<A: Actor> {
actor: A,
}
impl<A: Actor> NativeHandler for ActorHandler<A> {
fn handle(&mut self, ctx: &mut NativeContext<'_>) -> NativeOutcome {
let Some(message) = ctx.recv() else {
return NativeOutcome::Wait;
};
match Incoming::decode(message) {
Some(Incoming::Call {
reference,
reply_to,
request,
}) => {
if let Some(request) = A::Call::decode(request) {
let reply = {
let mut actor_ctx = ActorContext::new(ctx);
self.actor.handle_call(request, &mut actor_ctx)
};
if let Some(reply_term) = reply.encode(ctx)
&& let Some(envelope) = ctx.alloc_tuple(&[
Term::small_int(TAG_REPLY),
Term::small_int(reference),
reply_term,
])
{
ctx.send(reply_to, envelope);
}
}
}
Some(Incoming::Cast { request }) => {
if let Some(request) = A::Cast::decode(request) {
let mut actor_ctx = ActorContext::new(ctx);
self.actor.handle_cast(request, &mut actor_ctx);
}
}
None => {
}
}
if ctx.has_messages() {
NativeOutcome::Continue
} else {
NativeOutcome::Wait
}
}
}
enum Incoming {
Call {
reference: i64,
reply_to: u64,
request: Term,
},
Cast {
request: Term,
},
}
impl Incoming {
fn decode(term: Term) -> Option<Self> {
let tuple = Tuple::new(term)?;
match tuple.get(0)?.as_small_int()? {
TAG_CAST => Some(Self::Cast {
request: tuple.get(1)?,
}),
TAG_CALL => Some(Self::Call {
reference: tuple.get(1)?.as_small_int()?,
reply_to: u64::try_from(tuple.get(2)?.as_small_int()?).ok()?,
request: tuple.get(3)?,
}),
_ => None,
}
}
}
fn decode_reply(term: Term) -> Option<(i64, Term)> {
let tuple = Tuple::new(term)?;
if tuple.get(0)?.as_small_int()? != TAG_REPLY {
return None;
}
Some((tuple.get(1)?.as_small_int()?, tuple.get(2)?))
}
fn actor_factory<A, F>(factory: F) -> crate::native::native_process::NativeHandlerFactory
where
A: Actor,
F: Fn() -> A + Send + Sync + 'static,
{
Box::new(move || Box::new(ActorHandler { actor: factory() }))
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ActorError {
Spawn,
Timeout,
}
impl std::fmt::Display for ActorError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Spawn => f.write_str("actor spawn refused by scheduler"),
Self::Timeout => f.write_str("actor call timed out awaiting reply"),
}
}
}
impl std::error::Error for ActorError {}
pub struct ActorRef<A: Actor> {
pub pid: u64,
pub sender: SenderHandle<A>,
}
impl<A: Actor> Clone for ActorRef<A> {
fn clone(&self) -> Self {
Self {
pid: self.pid,
sender: self.sender.clone(),
}
}
}
pub struct SenderHandle<A: Actor> {
scheduler: Arc<Scheduler>,
pid: u64,
_marker: PhantomData<fn() -> A>,
}
impl<A: Actor> Clone for SenderHandle<A> {
fn clone(&self) -> Self {
Self {
scheduler: Arc::clone(&self.scheduler),
pid: self.pid,
_marker: PhantomData,
}
}
}
impl<A: Actor> SenderHandle<A> {
#[must_use]
pub fn attach(scheduler: &Arc<Scheduler>, pid: u64) -> Self {
Self {
scheduler: Arc::clone(scheduler),
pid,
_marker: PhantomData,
}
}
#[must_use]
pub fn pid(&self) -> u64 {
self.pid
}
pub fn cast(&self, message: A::Cast) -> Result<(), ActorError> {
let target = self.pid;
self.scheduler
.spawn_native(Box::new(move || {
clients::cast_handler::<A>(target, message.clone())
}))
.map(|_pid| ())
.map_err(|_| ActorError::Spawn)
}
pub fn call(&self, request: A::Call) -> Result<A::Reply, ActorError> {
self.call_timeout(request, DEFAULT_CALL_TIMEOUT)
}
pub fn call_timeout(
&self,
request: A::Call,
timeout: Duration,
) -> Result<A::Reply, ActorError> {
let (reply_tx, reply_rx) = crossbeam_channel::bounded::<A::Reply>(1);
let target = self.pid;
let reference = next_ref();
self.scheduler
.spawn_native(Box::new(move || {
clients::call_handler::<A>(target, request.clone(), reference, reply_tx.clone())
}))
.map_err(|_| ActorError::Spawn)?;
reply_rx
.recv_timeout(timeout)
.map_err(|_| ActorError::Timeout)
}
}
pub fn spawn_actor<A, F>(scheduler: &Arc<Scheduler>, factory: F) -> Result<ActorRef<A>, ActorError>
where
A: Actor,
F: Fn() -> A + Send + Sync + 'static,
{
let pid = scheduler
.spawn_native(actor_factory(factory))
.map_err(|_| ActorError::Spawn)?;
Ok(ActorRef {
pid,
sender: SenderHandle {
scheduler: Arc::clone(scheduler),
pid,
_marker: PhantomData,
},
})
}
#[path = "actor_clients.rs"]
mod clients;
#[cfg(test)]
#[path = "actor_tests.rs"]
mod tests;