use std::cell::RefCell;
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
use std::sync::Arc;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
use std::time::{Duration, Instant};
use super::super::{ActorError, CoopSenderHandle, spawn_actor_cooperative};
use super::{DynActor, ReplyFn, WireTerm};
use crate::atom::{Atom, AtomTable};
use crate::ets::OwnedTerm;
use crate::module::ModuleRegistry;
use crate::native::BifRegistryImpl;
use crate::native::ProcessContext;
use crate::scheduler::WasmScheduler;
use crate::term::Term;
use crate::term::boxed::Tuple;
fn scheduler() -> Rc<RefCell<WasmScheduler>> {
let atom_table = Arc::new(AtomTable::with_common_atoms());
let modules = Arc::new(ModuleRegistry::new());
let bifs = Arc::new(BifRegistryImpl::new());
Rc::new(RefCell::new(WasmScheduler::new(atom_table, modules, bifs)))
}
fn noop_waker() -> Waker {
fn clone(_: *const ()) -> RawWaker {
RawWaker::new(std::ptr::null(), &VTABLE)
}
fn noop(_: *const ()) {}
static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, noop, noop, noop);
unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) }
}
fn poll_once<R>(future: &mut Pin<Box<super::super::CallFuture<R>>>) -> Poll<Result<R, ActorError>> {
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
future.as_mut().poll(&mut cx)
}
fn owned_tuple1(value: i64) -> OwnedTerm {
let mut context = ProcessContext::new();
let element = Term::try_small_int(value).expect("small int fits");
let tuple = context
.alloc_tuple(&[element])
.expect("tuple allocation succeeds");
context
.take_detached_result(tuple)
.expect("tuple is a detached heap allocation")
}
fn tuple1_value(owned: &OwnedTerm) -> Option<i64> {
let tuple = Tuple::new(owned.root())?;
tuple.get(0)?.as_small_int()
}
fn tuple_first(owned: &OwnedTerm) -> Option<i64> {
let tuple = Tuple::new(owned.root())?;
tuple.get(0)?.as_small_int()
}
fn increment_reply() -> ReplyFn {
Arc::new(|request: &OwnedTerm| {
let n = tuple1_value(request).unwrap_or(0);
let mut context = ProcessContext::new();
let incremented = Term::try_small_int(n + 1).unwrap_or(Term::small_int(0));
let ok = Term::atom(Atom::OK);
let reply = context
.alloc_tuple(&[incremented, ok])
.unwrap_or(Term::small_int(n + 1));
context
.take_detached_result(reply)
.unwrap_or_else(|| OwnedTerm::immediate(Term::small_int(n + 1)))
})
}
#[test]
fn dyn_actor_call_async_round_trips_an_opaque_term_graph() {
let scheduler = scheduler();
let actor =
spawn_actor_cooperative::<DynActor, _>(&scheduler, || DynActor::new(increment_reply()));
let mut future = Box::pin(actor.sender.call_async(WireTerm::new(owned_tuple1(41))));
assert!(
matches!(poll_once(&mut future), Poll::Pending),
"future is pending before any host turn runs"
);
let mut resolved = None;
for _ in 0..8 {
scheduler.borrow_mut().run_until_idle();
if let Poll::Ready(result) = poll_once(&mut future) {
resolved = Some(result);
break;
}
}
let reply = resolved
.expect("future resolved within the pumped turns")
.expect("the dynamic actor replied (no timeout)");
let tuple = Tuple::new(reply.owned().root()).expect("reply is a tuple graph");
assert_eq!(
tuple.arity(),
2,
"reply tuple has the transform's two fields"
);
assert_eq!(
tuple_first(reply.owned()),
Some(42),
"reply carries n + 1 from the host transform"
);
assert_eq!(
tuple.get(1).and_then(Term::as_atom),
Some(Atom::OK),
"reply carries the transform's `ok` atom"
);
}
#[test]
fn dyn_actor_call_async_rejects_on_timeout_when_no_reply_arrives() {
let scheduler = scheduler();
let target = scheduler
.borrow_mut()
.spawn_native_root(Box::new(|| Box::new(ParkForever)));
let handle = CoopSenderHandle::<DynActor>::attach(&scheduler, target);
let delay = Duration::from_secs(10);
let mut future = Box::pin(handle.call_async_timeout(WireTerm::new(owned_tuple1(7)), delay));
scheduler.borrow_mut().run_until_idle();
assert!(
matches!(poll_once(&mut future), Poll::Pending),
"future is pending while the request is outstanding and the timeout is armed"
);
let start = Instant::now();
let _early = scheduler
.borrow_mut()
.tick_native_timers_at(start + Duration::from_secs(5));
scheduler.borrow_mut().run_until_idle();
assert!(
matches!(poll_once(&mut future), Poll::Pending),
"future stays pending before the timeout deadline"
);
let _fired = scheduler
.borrow_mut()
.tick_native_timers_at(start + delay + Duration::from_secs(5));
let mut rejected = None;
for _ in 0..8 {
scheduler.borrow_mut().run_until_idle();
if let Poll::Ready(result) = poll_once(&mut future) {
rejected = Some(result);
break;
}
}
assert!(
matches!(rejected, Some(Err(ActorError::Timeout))),
"the dynamic call_async future rejected with Timeout when no reply arrived, got {:?}",
rejected.as_ref().map(Result::is_ok)
);
}
struct ParkForever;
impl crate::native::native_process::NativeHandler for ParkForever {
fn handle(
&mut self,
_ctx: &mut crate::native::native_process::NativeContext<'_>,
) -> crate::native::native_process::NativeOutcome {
crate::native::native_process::NativeOutcome::Wait
}
}