use std::cell::RefCell;
use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll, Waker};
use std::time::Duration;
use super::{
Actor, ActorError, ActorMessage, DEFAULT_CALL_TIMEOUT, TAG_CALL, TAG_CAST, actor_factory,
decode_reply, next_ref,
};
use crate::native::native_process::{NativeContext, NativeHandler, NativeOutcome};
use crate::process::ExitReason;
use crate::scheduler::WasmScheduler;
use crate::term::Term;
const TIMEOUT_MARKER: i64 = -1;
enum CallSlot<R> {
Pending(Option<Waker>),
Ready(R),
TimedOut,
}
type SharedCallSlot<R> = Arc<Mutex<CallSlot<R>>>;
fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
mutex
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub struct CoopActorRef<A: Actor> {
pub pid: u64,
pub sender: CoopSenderHandle<A>,
}
impl<A: Actor> Clone for CoopActorRef<A> {
fn clone(&self) -> Self {
Self {
pid: self.pid,
sender: self.sender.clone(),
}
}
}
pub struct CoopSenderHandle<A: Actor> {
scheduler: Rc<RefCell<WasmScheduler>>,
pid: u64,
_marker: PhantomData<fn() -> A>,
}
impl<A: Actor> Clone for CoopSenderHandle<A> {
fn clone(&self) -> Self {
Self {
scheduler: Rc::clone(&self.scheduler),
pid: self.pid,
_marker: PhantomData,
}
}
}
impl<A: Actor> CoopSenderHandle<A> {
#[must_use]
pub fn attach(scheduler: &Rc<RefCell<WasmScheduler>>, pid: u64) -> Self {
Self {
scheduler: Rc::clone(scheduler),
pid,
_marker: PhantomData,
}
}
#[must_use]
pub const fn pid(&self) -> u64 {
self.pid
}
pub fn cast(&self, message: A::Cast) -> Result<(), ActorError> {
let target = self.pid;
self.scheduler
.borrow_mut()
.spawn_native_root(Box::new(move || {
Box::new(CoopCastClient::<A> {
target,
message: message.clone(),
sent: false,
_marker: PhantomData,
})
}));
Ok(())
}
pub fn call_async(&self, request: A::Call) -> CallFuture<A::Reply> {
self.call_async_timeout(request, DEFAULT_CALL_TIMEOUT)
}
pub fn call_async_timeout(&self, request: A::Call, timeout: Duration) -> CallFuture<A::Reply> {
let slot: SharedCallSlot<A::Reply> = Arc::new(Mutex::new(CallSlot::Pending(None)));
let target = self.pid;
let reference = next_ref();
let client_slot = Arc::clone(&slot);
self.scheduler
.borrow_mut()
.spawn_native_root(Box::new(move || {
Box::new(CoopCallClient::<A> {
target,
request: request.clone(),
reference,
timeout,
slot: Arc::clone(&client_slot),
sent: false,
})
}));
CallFuture { slot }
}
}
pub struct CallFuture<R> {
slot: SharedCallSlot<R>,
}
impl<R> Future for CallFuture<R> {
type Output = Result<R, ActorError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut guard = lock(&self.slot);
match std::mem::replace(&mut *guard, CallSlot::Pending(None)) {
CallSlot::Ready(reply) => Poll::Ready(Ok(reply)),
CallSlot::TimedOut => Poll::Ready(Err(ActorError::Timeout)),
CallSlot::Pending(_) => {
*guard = CallSlot::Pending(Some(cx.waker().clone()));
Poll::Pending
}
}
}
}
struct CoopCastClient<A: Actor> {
target: u64,
message: A::Cast,
sent: bool,
_marker: PhantomData<fn() -> A>,
}
impl<A: Actor> NativeHandler for CoopCastClient<A> {
fn handle(&mut self, ctx: &mut NativeContext<'_>) -> NativeOutcome {
if !self.sent {
self.sent = true;
if let Some(payload) = self.message.encode(ctx)
&& let Some(envelope) = ctx.alloc_tuple(&[Term::small_int(TAG_CAST), payload])
{
ctx.send(self.target, envelope);
}
}
NativeOutcome::Stop(ExitReason::Normal)
}
}
struct CoopCallClient<A: Actor> {
target: u64,
request: A::Call,
reference: u64,
timeout: Duration,
slot: SharedCallSlot<A::Reply>,
sent: bool,
}
impl<A: Actor> CoopCallClient<A> {
fn resolve(&self, value: CallSlot<A::Reply>) {
let waker = {
let mut guard = lock(&self.slot);
let CallSlot::Pending(_) = &*guard else {
return;
};
match std::mem::replace(&mut *guard, value) {
CallSlot::Pending(waker) => waker,
_ => None,
}
};
if let Some(waker) = waker {
waker.wake();
}
}
}
impl<A: Actor> NativeHandler for CoopCallClient<A> {
fn handle(&mut self, ctx: &mut NativeContext<'_>) -> NativeOutcome {
if !self.sent {
self.sent = true;
let reference = Term::try_small_int(self.reference.cast_signed());
let reply_to = i64::try_from(ctx.self_pid())
.ok()
.and_then(Term::try_small_int);
if let (Some(reference), Some(reply_to), Some(request)) =
(reference, reply_to, self.request.encode(ctx))
&& let Some(envelope) =
ctx.alloc_tuple(&[Term::small_int(TAG_CALL), reference, reply_to, request])
{
ctx.send(self.target, envelope);
}
let _timer = ctx.schedule(self.timeout, Term::small_int(TIMEOUT_MARKER));
return NativeOutcome::Wait;
}
while let Some(message) = ctx.recv() {
if let Some((reference, reply_term)) = decode_reply(message) {
if reference == self.reference.cast_signed()
&& let Some(reply) = A::Reply::decode(reply_term)
{
self.resolve(CallSlot::Ready(reply));
return NativeOutcome::Stop(ExitReason::Normal);
}
} else if message.as_small_int() == Some(TIMEOUT_MARKER) {
self.resolve(CallSlot::TimedOut);
return NativeOutcome::Stop(ExitReason::Normal);
}
}
NativeOutcome::Wait
}
}
pub fn spawn_actor_cooperative<A, F>(
scheduler: &Rc<RefCell<WasmScheduler>>,
factory: F,
) -> CoopActorRef<A>
where
A: Actor,
F: Fn() -> A + Send + Sync + 'static,
{
let pid = scheduler
.borrow_mut()
.spawn_native_root(actor_factory(factory));
CoopActorRef {
pid,
sender: CoopSenderHandle {
scheduler: Rc::clone(scheduler),
pid,
_marker: PhantomData,
},
}
}
#[cfg(test)]
#[path = "actor_cooperative_tests.rs"]
mod tests;