#[cfg(all(feature = "async-runtime", feature = "tokio_rt", not(feature = "noop")))]
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(not(feature = "noop"))]
use std::sync::OnceLock;
#[cfg(all(feature = "tokio_rt", not(feature = "noop")))]
use std::sync::{LazyLock, RwLock};
#[cfg(all(feature = "async-runtime", not(feature = "noop")))]
use std::sync::{Mutex, MutexGuard, PoisonError};
use std::{future::Future, marker::PhantomData};
#[cfg(feature = "async-runtime")]
use std::{
panic::{catch_unwind, AssertUnwindSafe},
pin::Pin,
task::{Context, Poll},
};
#[cfg(feature = "tokio_rt")]
use tokio::runtime::Runtime;
use crate::{bindgen_runtime::ToNapiValue, sys, Env, Error, Result};
#[cfg(not(feature = "noop"))]
use crate::{JsDeferred, SendableResolver, Unknown};
#[cfg(all(feature = "async-runtime", not(feature = "noop")))]
const DUPLICATE_RUNTIME_ERROR: &str =
"register_async_runtime was called more than once for the same addon image";
#[cfg(all(feature = "async-runtime", not(feature = "noop")))]
const LATE_RUNTIME_REGISTRATION_ERROR: &str = "register_async_runtime must be called before the first Node-API environment begins activation or an earlier runtime-backed operation commits a backend choice";
#[cfg(all(
feature = "async-runtime",
not(feature = "tokio_rt"),
not(feature = "noop")
))]
const MISSING_RUNTIME_BACKEND_ERROR: &str = "no AsyncRuntime backend is registered; call `register_async_runtime` from `#[module_init]` before invoking runtime-backed operations";
#[cfg(all(feature = "async-runtime", not(feature = "noop")))]
const TASK_CANCELLED_ERROR: &str = "task was cancelled before completion";
#[cfg(feature = "async-runtime")]
pub trait AsyncRuntimeGuard {}
#[cfg(feature = "async-runtime")]
impl AsyncRuntimeGuard for () {}
#[cfg(feature = "async-runtime")]
pub struct AsyncRuntimeRejection<T> {
work: T,
error: Error,
}
#[cfg(feature = "async-runtime")]
impl<T> AsyncRuntimeRejection<T> {
pub fn new(work: T, error: Error) -> Self {
Self { work, error }
}
pub fn error(&self) -> &Error {
&self.error
}
pub fn into_parts(self) -> (T, Error) {
(self.work, self.error)
}
}
#[cfg(all(feature = "async-runtime", not(feature = "noop")))]
type AsyncRuntimeTaskCancelCallback = Box<dyn FnOnce(Error) + Send + 'static>;
#[cfg(all(feature = "async-runtime", not(feature = "noop")))]
fn invoke_cancel_callback(on_cancel: AsyncRuntimeTaskCancelCallback, error: Error) {
if let Err(payload) = catch_unwind(AssertUnwindSafe(move || on_cancel(error))) {
drop_contained(payload);
}
}
#[cfg(all(feature = "async-runtime", not(feature = "noop")))]
pub struct AsyncRuntimeTask {
fut: Pin<Box<dyn Future<Output = ()> + Send + 'static>>,
on_cancel: Option<AsyncRuntimeTaskCancelCallback>,
}
#[cfg(all(feature = "async-runtime", not(feature = "noop")))]
impl AsyncRuntimeTask {
fn new(
fut: Pin<Box<dyn Future<Output = ()> + Send + 'static>>,
on_cancel: AsyncRuntimeTaskCancelCallback,
) -> Self {
Self {
fut,
on_cancel: Some(on_cancel),
}
}
fn reject_with(mut self, error: Error) {
if let Some(on_cancel) = self.on_cancel.take() {
invoke_cancel_callback(on_cancel, error);
}
}
}
#[cfg(all(feature = "async-runtime", not(feature = "noop")))]
impl Future for AsyncRuntimeTask {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
if this.on_cancel.is_none() {
return Poll::Ready(());
}
match catch_unwind(AssertUnwindSafe(|| this.fut.as_mut().poll(cx))) {
Ok(Poll::Pending) => Poll::Pending,
Ok(Poll::Ready(())) => {
this.on_cancel = None;
Poll::Ready(())
}
Err(panic_payload) => {
if let Some(on_cancel) = this.on_cancel.take() {
invoke_cancel_callback(on_cancel, async_task_panic_error(panic_payload));
}
Poll::Ready(())
}
}
}
}
#[cfg(all(feature = "async-runtime", not(feature = "noop")))]
impl Drop for AsyncRuntimeTask {
fn drop(&mut self) {
if let Some(on_cancel) = self.on_cancel.take() {
invoke_cancel_callback(
on_cancel,
Error::new(crate::Status::GenericFailure, TASK_CANCELLED_ERROR),
);
}
}
}
#[cfg(all(feature = "async-runtime", feature = "noop"))]
pub struct AsyncRuntimeTask {
_private: (),
}
#[cfg(all(feature = "async-runtime", feature = "noop"))]
impl Future for AsyncRuntimeTask {
type Output = ();
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
Poll::Ready(())
}
}
#[cfg(all(feature = "async-runtime", not(feature = "noop")))]
fn async_task_panic_error(panic_payload: Box<dyn std::any::Any + Send>) -> Error {
let reason = if let Some(reason) = panic_payload.downcast_ref::<&str>() {
(*reason).to_string()
} else if let Some(reason) = panic_payload.downcast_ref::<String>() {
reason.clone()
} else {
"Panic in async function".to_owned()
};
drop_contained(panic_payload);
Error::new(crate::Status::GenericFailure, reason)
}
#[cfg(feature = "async-runtime")]
pub unsafe trait AsyncRuntime: Send + Sync + 'static {
fn spawn(
&self,
task: AsyncRuntimeTask,
) -> std::result::Result<(), AsyncRuntimeRejection<AsyncRuntimeTask>>;
fn block_on(&self, future: Pin<&mut dyn Future<Output = ()>>) -> Result<()>;
fn enter(&self) -> Result<Box<dyn AsyncRuntimeGuard + '_>> {
Ok(Box::new(()))
}
fn start(&self) -> Result<()> {
Ok(())
}
fn shutdown(&self) -> Result<()>;
fn spawn_blocking(
&self,
work: Box<dyn FnOnce() + Send + 'static>,
) -> std::result::Result<(), AsyncRuntimeRejection<Box<dyn FnOnce() + Send + 'static>>> {
Err(AsyncRuntimeRejection::new(
work,
Error::new(
crate::Status::GenericFailure,
"The AsyncRuntime backend does not support blocking work",
),
))
}
}
#[cfg(all(feature = "async-runtime", not(feature = "noop")))]
struct AsyncRuntimeRegistry {
backend: OnceLock<Box<dyn AsyncRuntime>>,
state: Mutex<RegistryState>,
lifecycle: Mutex<()>,
deferred_registration_error: Mutex<Option<&'static str>>,
}
#[cfg(all(feature = "async-runtime", not(feature = "noop")))]
#[derive(Clone, Copy, PartialEq, Eq)]
enum LifecyclePhase {
Idle,
Starting,
Started,
}
#[cfg(all(feature = "async-runtime", not(feature = "noop")))]
struct RegistryState {
selection_frozen: bool,
phase: LifecyclePhase,
}
#[cfg(all(feature = "async-runtime", not(feature = "noop")))]
impl AsyncRuntimeRegistry {
const fn new() -> Self {
Self {
backend: OnceLock::new(),
state: Mutex::new(RegistryState {
selection_frozen: false,
phase: LifecyclePhase::Idle,
}),
lifecycle: Mutex::new(()),
deferred_registration_error: Mutex::new(None),
}
}
fn lock_state(&self) -> MutexGuard<'_, RegistryState> {
self.state.lock().unwrap_or_else(PoisonError::into_inner)
}
fn lock_lifecycle(&self) -> MutexGuard<'_, ()> {
self
.lifecycle
.lock()
.unwrap_or_else(PoisonError::into_inner)
}
fn try_register(
&self,
runtime: Box<dyn AsyncRuntime>,
) -> std::result::Result<(), (&'static str, Box<dyn AsyncRuntime>)> {
let state = self.lock_state();
if self.backend.get().is_some() {
return Err((DUPLICATE_RUNTIME_ERROR, runtime));
}
if state.selection_frozen {
return Err((LATE_RUNTIME_REGISTRATION_ERROR, runtime));
}
match self.backend.set(runtime) {
Ok(()) => Ok(()),
Err(rejected) => Err((DUPLICATE_RUNTIME_ERROR, rejected)),
}
}
fn record_registration_error(&self, reason: &'static str) {
if let Ok(mut slot) = self.deferred_registration_error.lock() {
if slot.is_none() {
*slot = Some(reason);
}
}
}
fn deferred_registration_error(&self) -> Option<&'static str> {
self
.deferred_registration_error
.lock()
.ok()
.and_then(|slot| *slot)
}
fn commit_selection(&self, fallback_commits: bool) -> Option<&dyn AsyncRuntime> {
let mut state = self.lock_state();
let backend = self.backend.get().map(|backend| backend.as_ref());
if backend.is_some() || fallback_commits {
state.selection_frozen = true;
}
backend
}
fn ensure_started(&self, backend: &dyn AsyncRuntime) {
{
let mut state = self.lock_state();
match state.phase {
LifecyclePhase::Starting | LifecyclePhase::Started => return,
LifecyclePhase::Idle => state.phase = LifecyclePhase::Starting,
}
}
self.run_claimed_start(backend);
}
#[cfg(all(
feature = "async-runtime",
not(feature = "tokio_rt"),
not(feature = "noop")
))]
fn within_runtime<F: FnOnce() -> T, T>(&self, f: F) -> T {
if let Some(backend) = self.commit_selection(false) {
self.ensure_started(backend);
let _guard = match catch_unwind(AssertUnwindSafe(|| backend.enter())) {
Ok(Ok(guard)) => Some(ContainedGuard(Some(guard))),
Ok(Err(error)) => {
drop_contained(error);
None
}
Err(payload) => {
drop_contained(payload);
None
}
};
return f();
}
f()
}
fn run_claimed_start(&self, backend: &dyn AsyncRuntime) {
let _lifecycle = self.lock_lifecycle();
if self.lock_state().phase != LifecyclePhase::Starting {
return;
}
let phase = if start_backend_with_rollback(backend) {
#[cfg(feature = "tokio_rt")]
refill_drained_tokio_runtime();
LifecyclePhase::Started
} else {
LifecyclePhase::Idle
};
self.lock_state().phase = phase;
}
fn activate(&self) -> bool {
let backend = {
let mut state = self.lock_state();
state.selection_frozen = true;
let Some(backend) = self.backend.get() else {
return false;
};
match state.phase {
LifecyclePhase::Starting | LifecyclePhase::Started => return true,
LifecyclePhase::Idle => state.phase = LifecyclePhase::Starting,
}
backend
};
self.run_claimed_start(backend.as_ref());
true
}
fn deactivate(&self) -> bool {
let Some(backend) = self.backend.get() else {
return false;
};
let _lifecycle = self.lock_lifecycle();
match catch_unwind(AssertUnwindSafe(|| backend.shutdown())) {
Ok(shutdown_result) => drop_contained(shutdown_result),
Err(payload) => {
std::mem::forget(payload);
std::process::abort();
}
}
self.lock_state().phase = LifecyclePhase::Idle;
true
}
}
#[cfg(feature = "async-runtime")]
fn drop_contained<T>(value: T) {
if let Err(second_payload) = catch_unwind(AssertUnwindSafe(move || drop(value))) {
std::mem::forget(second_payload);
}
}
#[cfg(all(
feature = "async-runtime",
not(feature = "tokio_rt"),
not(feature = "noop")
))]
struct ContainedGuard<'a>(Option<Box<dyn AsyncRuntimeGuard + 'a>>);
#[cfg(all(
feature = "async-runtime",
not(feature = "tokio_rt"),
not(feature = "noop")
))]
impl Drop for ContainedGuard<'_> {
fn drop(&mut self) {
if let Some(guard) = self.0.take() {
drop_contained(guard);
}
}
}
#[cfg(all(feature = "async-runtime", not(feature = "noop")))]
fn start_backend_with_rollback(backend: &dyn AsyncRuntime) -> bool {
match catch_unwind(AssertUnwindSafe(|| backend.start())) {
Ok(Ok(())) => return true,
Ok(Err(start_error)) => drop_contained(start_error),
Err(payload) => drop_contained(payload),
}
match catch_unwind(AssertUnwindSafe(|| backend.shutdown())) {
Ok(shutdown_result) => drop_contained(shutdown_result),
Err(payload) => {
std::mem::forget(payload);
std::process::abort();
}
}
false
}
#[cfg(all(feature = "async-runtime", not(feature = "noop")))]
static ASYNC_RUNTIME_REGISTRY: AsyncRuntimeRegistry = AsyncRuntimeRegistry::new();
#[cfg(feature = "async-runtime")]
fn retire_rejected_async_runtime(runtime: Box<dyn AsyncRuntime>) {
match catch_unwind(AssertUnwindSafe(|| runtime.shutdown())) {
Ok(shutdown_result) => drop_contained(shutdown_result),
Err(payload) => {
std::mem::forget(payload);
std::mem::forget(runtime);
std::process::abort();
}
}
if let Err(payload) = catch_unwind(AssertUnwindSafe(move || drop(runtime))) {
std::mem::forget(payload);
std::process::abort();
}
}
#[cfg(all(feature = "async-runtime", not(feature = "noop")))]
pub fn register_async_runtime<R: AsyncRuntime>(runtime: R) {
if let Err((reason, rejected)) = ASYNC_RUNTIME_REGISTRY.try_register(Box::new(runtime)) {
retire_rejected_async_runtime(rejected);
ASYNC_RUNTIME_REGISTRY.record_registration_error(reason);
}
}
#[cfg(all(feature = "async-runtime", not(feature = "noop")))]
pub fn try_register_async_runtime<R: AsyncRuntime>(runtime: R) -> Result<()> {
match ASYNC_RUNTIME_REGISTRY.try_register(Box::new(runtime)) {
Ok(()) => Ok(()),
Err((reason, rejected)) => {
retire_rejected_async_runtime(rejected);
Err(Error::new(crate::Status::GenericFailure, reason))
}
}
}
#[cfg(all(feature = "async-runtime", feature = "noop"))]
pub fn register_async_runtime<R: AsyncRuntime>(runtime: R) {
retire_rejected_async_runtime(Box::new(runtime));
}
#[cfg(all(feature = "async-runtime", feature = "noop"))]
pub fn try_register_async_runtime<R: AsyncRuntime>(runtime: R) -> Result<()> {
retire_rejected_async_runtime(Box::new(runtime));
Err(Error::new(
crate::Status::InvalidArg,
"The `noop` feature is enabled; no AsyncRuntime backend can be registered",
))
}
#[cfg(all(feature = "tokio_rt", not(feature = "noop")))]
fn create_runtime() -> Runtime {
if IS_USER_DEFINED_RT.get().copied().unwrap_or(false) {
if let Some(user_defined_rt) = USER_DEFINED_RT
.get()
.and_then(|rt| rt.write().ok().and_then(|mut rt| rt.take()))
{
return user_defined_rt;
}
}
#[cfg(any(
all(target_family = "wasm", tokio_unstable),
not(target_family = "wasm")
))]
{
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("Create tokio runtime failed")
}
#[cfg(all(target_family = "wasm", not(tokio_unstable)))]
{
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Create tokio runtime failed")
}
}
#[cfg(all(feature = "async-runtime", feature = "tokio_rt", not(feature = "noop")))]
static RT_CONSTRUCTED: AtomicBool = AtomicBool::new(false);
#[cfg(all(feature = "tokio_rt", not(feature = "noop")))]
static RT: LazyLock<RwLock<Option<Runtime>>> = LazyLock::new(|| {
#[cfg(feature = "async-runtime")]
RT_CONSTRUCTED.store(true, Ordering::SeqCst);
RwLock::new(Some(create_runtime()))
});
#[cfg(all(feature = "async-runtime", feature = "tokio_rt", not(feature = "noop")))]
fn refill_drained_tokio_runtime() {
if RT_CONSTRUCTED.load(Ordering::SeqCst) {
if let Ok(mut rt) = RT.write() {
if rt.is_none() {
*rt = Some(create_runtime());
}
}
}
}
#[cfg(all(feature = "tokio_rt", not(feature = "noop")))]
static USER_DEFINED_RT: OnceLock<RwLock<Option<Runtime>>> = OnceLock::new();
#[cfg(all(feature = "tokio_rt", not(feature = "noop")))]
static IS_USER_DEFINED_RT: OnceLock<bool> = OnceLock::new();
#[cfg(all(feature = "tokio_rt", not(feature = "noop")))]
pub fn create_custom_tokio_runtime(rt: Runtime) {
USER_DEFINED_RT.get_or_init(move || RwLock::new(Some(rt)));
IS_USER_DEFINED_RT.get_or_init(|| true);
}
#[cfg(all(feature = "tokio_rt", feature = "noop"))]
pub fn create_custom_tokio_runtime(_: Runtime) {}
#[cfg(not(feature = "noop"))]
pub fn start_async_runtime() {
#[cfg(feature = "async-runtime")]
if ASYNC_RUNTIME_REGISTRY.activate() {
#[cfg(feature = "tokio_rt")]
refill_drained_tokio_runtime();
#[cfg(feature = "tokio_rt")]
return;
}
#[cfg(feature = "tokio_rt")]
if let Ok(mut rt) = RT.write() {
if rt.is_none() {
*rt = Some(create_runtime());
}
}
}
#[cfg(not(feature = "noop"))]
pub fn shutdown_async_runtime() {
#[cfg(feature = "async-runtime")]
if ASYNC_RUNTIME_REGISTRY.deactivate() {
#[cfg(feature = "tokio_rt")]
if let Some(rt) = RT_CONSTRUCTED
.load(Ordering::SeqCst)
.then(|| RT.write().ok().and_then(|mut rt| rt.take()))
.flatten()
{
rt.shutdown_background();
}
#[cfg(feature = "tokio_rt")]
if let Some(user_rt) = USER_DEFINED_RT
.get()
.and_then(|rt| rt.write().ok().and_then(|mut rt| rt.take()))
{
user_rt.shutdown_background();
}
#[cfg(feature = "tokio_rt")]
return;
}
#[cfg(feature = "tokio_rt")]
if let Some(rt) = RT.write().ok().and_then(|mut rt| rt.take()) {
rt.shutdown_background();
}
}
#[cfg(all(feature = "tokio_rt", not(feature = "noop")))]
pub fn spawn<F>(fut: F) -> tokio::task::JoinHandle<F::Output>
where
F: 'static + Send + Future<Output = ()>,
{
RT.read()
.ok()
.and_then(|rt| rt.as_ref().map(|rt| rt.spawn(fut)))
.expect("Access tokio runtime failed in spawn")
}
#[cfg(all(feature = "tokio_rt", not(feature = "noop")))]
pub fn block_on<F: Future>(fut: F) -> F::Output {
RT.read()
.ok()
.and_then(|rt| rt.as_ref().map(|rt| rt.block_on(fut)))
.expect("Access tokio runtime failed in block_on")
}
#[cfg(all(feature = "tokio_rt", feature = "noop"))]
pub fn block_on<F: Future>(_: F) -> F::Output {
unreachable!("noop feature is enabled, block_on is not available")
}
#[cfg(all(feature = "tokio_rt", not(feature = "noop")))]
pub fn spawn_blocking<F, R>(func: F) -> tokio::task::JoinHandle<R>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
RT.read()
.ok()
.and_then(|rt| rt.as_ref().map(|rt| rt.spawn_blocking(func)))
.expect("Access tokio runtime failed in spawn_blocking")
}
#[cfg(all(feature = "tokio_rt", not(feature = "noop")))]
pub fn within_runtime_if_available<F: FnOnce() -> T, T>(f: F) -> T {
RT.read()
.ok()
.and_then(|rt| {
rt.as_ref().map(|rt| {
let rt_guard = rt.enter();
let ret = f();
drop(rt_guard);
ret
})
})
.expect("Access tokio runtime failed in within_runtime_if_available")
}
#[cfg(all(
feature = "async-runtime",
not(feature = "tokio_rt"),
not(feature = "noop")
))]
pub fn within_runtime_if_available<F: FnOnce() -> T, T>(f: F) -> T {
ASYNC_RUNTIME_REGISTRY.within_runtime(f)
}
#[cfg(feature = "noop")]
pub fn within_runtime_if_available<F: FnOnce() -> T, T>(f: F) -> T {
f()
}
#[cfg(feature = "noop")]
#[allow(unused)]
pub fn execute_tokio_future<
Data: 'static + Send,
Fut: 'static + Send + Future<Output = std::result::Result<Data, impl Into<Error>>>,
Resolver: 'static + FnOnce(sys::napi_env, Data) -> Result<sys::napi_value>,
>(
env: sys::napi_env,
fut: Fut,
resolver: Resolver,
) -> Result<sys::napi_value> {
Ok(std::ptr::null_mut())
}
#[cfg(not(feature = "noop"))]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub fn execute_tokio_future<
Data: 'static + Send,
Fut: 'static + Send + Future<Output = std::result::Result<Data, impl Into<Error>>>,
Resolver: 'static + FnOnce(sys::napi_env, Data) -> Result<sys::napi_value>,
>(
env: sys::napi_env,
fut: Fut,
resolver: Resolver,
) -> Result<sys::napi_value> {
execute_future_impl(env, fut, resolver, None)
}
#[cfg(not(feature = "noop"))]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
fn execute_future_impl<
Data: 'static + Send,
Fut: 'static + Send + Future<Output = std::result::Result<Data, impl Into<Error>>>,
Resolver: 'static + FnOnce(sys::napi_env, Data) -> Result<sys::napi_value>,
>(
env: sys::napi_env,
fut: Fut,
resolver: Resolver,
finalize_callback: Option<Box<dyn FnOnce(sys::napi_env)>>,
) -> Result<sys::napi_value> {
let env = Env::from_raw(env);
let (mut deferred, promise) = JsDeferred::new(&env)?;
deferred.set_finalize_callback(finalize_callback);
let promise_value = promise.0.value;
#[cfg(feature = "async-runtime")]
if let Some(reason) = ASYNC_RUNTIME_REGISTRY.deferred_registration_error() {
deferred.reject(Error::new(crate::Status::GenericFailure, reason));
return Ok(promise_value);
}
#[cfg(all(feature = "async-runtime", not(feature = "tokio_rt")))]
if ASYNC_RUNTIME_REGISTRY.commit_selection(false).is_none() {
deferred.reject(Error::new(
crate::Status::GenericFailure,
MISSING_RUNTIME_BACKEND_ERROR,
));
return Ok(promise_value);
}
#[cfg(feature = "async-runtime")]
let deferred_for_cancel = deferred.clone();
#[cfg(all(
feature = "tokio_rt",
any(
all(target_family = "wasm", tokio_unstable),
not(target_family = "wasm")
)
))]
let deferred_for_panic = deferred.clone();
#[cfg(all(feature = "tokio_rt", target_family = "wasm", not(tokio_unstable)))]
let deferred_for_threadless = deferred.clone();
let sendable_resolver = SendableResolver::new(resolver);
let inner = async move {
match fut.await {
Ok(v) => deferred.resolve(move |env| {
sendable_resolver
.resolve(env.raw(), v)
.map(|v| unsafe { Unknown::from_raw_unchecked(env.raw(), v) })
}),
Err(e) => deferred.reject(e.into()),
}
};
#[cfg(feature = "async-runtime")]
if let Some(backend) = ASYNC_RUNTIME_REGISTRY.commit_selection(cfg!(feature = "tokio_rt")) {
ASYNC_RUNTIME_REGISTRY.ensure_started(backend);
let task = AsyncRuntimeTask::new(
Box::pin(inner),
Box::new(move |error| deferred_for_cancel.reject(error)),
);
match catch_unwind(AssertUnwindSafe(|| backend.spawn(task))) {
Ok(Ok(())) => {}
Ok(Err(rejection)) => {
let (task, error) = rejection.into_parts();
task.reject_with(error);
}
Err(payload) => drop_contained(payload),
}
return Ok(promise_value);
}
#[cfg(feature = "tokio_rt")]
{
#[cfg(any(
all(target_family = "wasm", tokio_unstable),
not(target_family = "wasm")
))]
let jh = spawn(inner);
#[cfg(any(
all(target_family = "wasm", tokio_unstable),
not(target_family = "wasm")
))]
spawn(async move {
if let Err(err) = jh.await {
if let Ok(reason) = err.try_into_panic() {
if let Some(s) = reason.downcast_ref::<&str>() {
deferred_for_panic.reject(Error::new(crate::Status::GenericFailure, s));
} else {
deferred_for_panic.reject(Error::new(
crate::Status::GenericFailure,
"Panic in async function",
));
}
}
}
});
#[cfg(all(target_family = "wasm", not(tokio_unstable)))]
{
drop(inner);
deferred_for_threadless.reject(Error::new(
crate::Status::GenericFailure,
"Built-in Tokio async tasks require a threaded WASI target. Use wasm32-wasip1-threads, or enable async-runtime and register a custom AsyncRuntime backend for wasm32-wasip1.",
));
}
Ok(promise_value)
}
#[cfg(all(feature = "async-runtime", not(feature = "tokio_rt")))]
{
drop(inner);
Ok(promise_value)
}
}
#[doc(hidden)]
#[cfg(not(feature = "noop"))]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub fn execute_tokio_future_with_finalize_callback<
Data: 'static + Send,
Fut: 'static + Send + Future<Output = std::result::Result<Data, impl Into<Error>>>,
Resolver: 'static + FnOnce(sys::napi_env, Data) -> Result<sys::napi_value>,
>(
env: sys::napi_env,
fut: Fut,
resolver: Resolver,
finalize_callback: Option<Box<dyn FnOnce(sys::napi_env)>>,
) -> Result<sys::napi_value> {
execute_future_impl(env, fut, resolver, finalize_callback)
}
#[cfg(feature = "noop")]
#[doc(hidden)]
pub fn execute_tokio_future_with_finalize_callback<
Data: 'static + Send,
Fut: 'static + Send + Future<Output = std::result::Result<Data, impl Into<Error>>>,
Resolver: 'static + FnOnce(sys::napi_env, Data) -> Result<sys::napi_value>,
>(
_env: sys::napi_env,
_fut: Fut,
_resolver: Resolver,
_finalize_callback: Option<Box<dyn FnOnce(sys::napi_env)>>,
) -> Result<sys::napi_value> {
Ok(std::ptr::null_mut())
}
pub struct AsyncBlockBuilder<
V: Send + 'static,
F: Future<Output = Result<V>> + Send + 'static,
Dispose: FnOnce(Env) -> Result<()> + 'static = fn(Env) -> Result<()>,
> {
inner: F,
dispose: Option<Dispose>,
}
impl<V: ToNapiValue + Send + 'static, F: Future<Output = Result<V>> + Send + 'static>
AsyncBlockBuilder<V, F>
{
pub fn new(inner: F) -> Self {
Self {
inner,
dispose: None,
}
}
}
impl<
V: ToNapiValue + Send + 'static,
F: Future<Output = Result<V>> + Send + 'static,
Dispose: FnOnce(Env) -> Result<()> + 'static,
> AsyncBlockBuilder<V, F, Dispose>
{
pub fn with(inner: F) -> Self {
Self {
inner,
dispose: None,
}
}
pub fn with_dispose(mut self, dispose: Dispose) -> Self {
self.dispose = Some(dispose);
self
}
pub fn build(self, env: &Env) -> Result<AsyncBlock<V>> {
Ok(AsyncBlock {
inner: execute_tokio_future(env.0, self.inner, |env, v| unsafe {
if let Some(dispose) = self.dispose {
let env = Env::from_raw(env);
dispose(env)?;
}
V::to_napi_value(env, v)
})?,
_phantom: PhantomData,
})
}
}
impl<V: Send + 'static, F: Future<Output = Result<V>> + Send + 'static> AsyncBlockBuilder<V, F> {
pub fn build_with_map<T: ToNapiValue, Map: FnOnce(Env, V) -> Result<T> + 'static>(
env: &Env,
inner: F,
map: Map,
) -> Result<AsyncBlock<T>> {
Ok(AsyncBlock {
inner: execute_tokio_future(env.0, inner, |env, v| unsafe {
let v = map(Env::from_raw(env), v)?;
T::to_napi_value(env, v)
})?,
_phantom: PhantomData,
})
}
}
pub struct AsyncBlock<T: ToNapiValue + 'static> {
inner: sys::napi_value,
_phantom: PhantomData<T>,
}
impl<T: ToNapiValue + 'static> ToNapiValue for AsyncBlock<T> {
unsafe fn to_napi_value(_: napi_sys::napi_env, val: Self) -> Result<napi_sys::napi_value> {
Ok(val.inner)
}
}
#[cfg(all(test, feature = "async-runtime", not(feature = "noop")))]
mod spi_tests {
use std::{
sync::{
atomic::{AtomicBool as TestAtomicBool, AtomicUsize, Ordering as AtomicOrdering},
mpsc, Arc, Mutex,
},
time::Duration,
};
use super::*;
const BACKEND_STOPPED_ERROR: &str = "mock backend is stopped";
const HANG_PROTECTION: Duration = Duration::from_secs(30);
#[derive(Default)]
struct BackendProbe {
start_calls: AtomicUsize,
shutdown_calls: AtomicUsize,
spawn_calls: AtomicUsize,
spawns_before_start: AtomicUsize,
running: TestAtomicBool,
}
struct MockRuntime {
probe: Arc<BackendProbe>,
fail_start: bool,
}
impl MockRuntime {
fn new(probe: &Arc<BackendProbe>) -> Self {
Self {
probe: probe.clone(),
fail_start: false,
}
}
fn failing_start(probe: &Arc<BackendProbe>) -> Self {
Self {
probe: probe.clone(),
fail_start: true,
}
}
}
unsafe impl AsyncRuntime for MockRuntime {
fn spawn(
&self,
task: AsyncRuntimeTask,
) -> std::result::Result<(), AsyncRuntimeRejection<AsyncRuntimeTask>> {
if self.probe.start_calls.load(AtomicOrdering::SeqCst) == 0 {
self
.probe
.spawns_before_start
.fetch_add(1, AtomicOrdering::SeqCst);
}
self.probe.spawn_calls.fetch_add(1, AtomicOrdering::SeqCst);
if !self.probe.running.load(AtomicOrdering::SeqCst) {
return Err(AsyncRuntimeRejection::new(
task,
Error::new(crate::Status::GenericFailure, BACKEND_STOPPED_ERROR),
));
}
futures::executor::block_on(task);
Ok(())
}
fn block_on(&self, future: Pin<&mut dyn Future<Output = ()>>) -> Result<()> {
futures::executor::block_on(future);
Ok(())
}
fn start(&self) -> Result<()> {
self.probe.start_calls.fetch_add(1, AtomicOrdering::SeqCst);
if self.fail_start {
return Err(Error::new(crate::Status::GenericFailure, "start failed"));
}
self.probe.running.store(true, AtomicOrdering::SeqCst);
Ok(())
}
fn shutdown(&self) -> Result<()> {
self.probe.running.store(false, AtomicOrdering::SeqCst);
self
.probe
.shutdown_calls
.fetch_add(1, AtomicOrdering::SeqCst);
Ok(())
}
}
fn recording_cancel_callback() -> (
Arc<AtomicUsize>,
Arc<Mutex<Option<String>>>,
AsyncRuntimeTaskCancelCallback,
) {
let fired = Arc::new(AtomicUsize::new(0));
let message = Arc::new(Mutex::new(None));
let fired_in_callback = fired.clone();
let message_in_callback = message.clone();
let callback: AsyncRuntimeTaskCancelCallback = Box::new(move |error: Error| {
fired_in_callback.fetch_add(1, AtomicOrdering::SeqCst);
*message_in_callback.lock().unwrap() = Some(error.reason.to_string());
});
(fired, message, callback)
}
#[cfg(not(feature = "tokio_rt"))]
struct PanicOnEnterRuntime {
probe: Arc<BackendProbe>,
}
#[cfg(not(feature = "tokio_rt"))]
unsafe impl AsyncRuntime for PanicOnEnterRuntime {
fn spawn(
&self,
task: AsyncRuntimeTask,
) -> std::result::Result<(), AsyncRuntimeRejection<AsyncRuntimeTask>> {
futures::executor::block_on(task);
Ok(())
}
fn block_on(&self, future: Pin<&mut dyn Future<Output = ()>>) -> Result<()> {
futures::executor::block_on(future);
Ok(())
}
fn start(&self) -> Result<()> {
self.probe.start_calls.fetch_add(1, AtomicOrdering::SeqCst);
self.probe.running.store(true, AtomicOrdering::SeqCst);
Ok(())
}
fn enter(&self) -> Result<Box<dyn AsyncRuntimeGuard + '_>> {
panic!("enter hook panic");
}
fn shutdown(&self) -> Result<()> {
self.probe.running.store(false, AtomicOrdering::SeqCst);
self
.probe
.shutdown_calls
.fetch_add(1, AtomicOrdering::SeqCst);
Ok(())
}
}
#[cfg(not(feature = "tokio_rt"))]
#[test]
fn within_runtime_starts_dormant_backend_before_enter() {
let registry = AsyncRuntimeRegistry::new();
let probe = Arc::new(BackendProbe::default());
assert!(registry
.try_register(Box::new(MockRuntime::new(&probe)))
.is_ok());
assert_eq!(probe.start_calls.load(AtomicOrdering::SeqCst), 0);
let ran = registry.within_runtime(|| true);
assert!(ran, "the wrapped closure must run");
assert_eq!(
probe.start_calls.load(AtomicOrdering::SeqCst),
1,
"within_runtime must start the backend before entering its context"
);
}
#[cfg(not(feature = "tokio_rt"))]
#[test]
fn within_runtime_contains_panicking_enter_hook() {
let registry = AsyncRuntimeRegistry::new();
let probe = Arc::new(BackendProbe::default());
assert!(registry
.try_register(Box::new(PanicOnEnterRuntime {
probe: probe.clone()
}))
.is_ok());
let ran = registry.within_runtime(|| true);
assert!(
ran,
"the wrapped closure must still run after a contained enter() panic"
);
assert_eq!(
probe.start_calls.load(AtomicOrdering::SeqCst),
1,
"the backend is still started before the (panicking) enter()"
);
}
#[cfg(not(feature = "tokio_rt"))]
struct PanicOnDropGuard {
dropped: Arc<AtomicUsize>,
}
#[cfg(not(feature = "tokio_rt"))]
impl AsyncRuntimeGuard for PanicOnDropGuard {}
#[cfg(not(feature = "tokio_rt"))]
impl Drop for PanicOnDropGuard {
fn drop(&mut self) {
self.dropped.fetch_add(1, AtomicOrdering::SeqCst);
panic!("guard drop panic");
}
}
#[cfg(not(feature = "tokio_rt"))]
struct GuardDropPanicRuntime {
guard_dropped: Arc<AtomicUsize>,
}
#[cfg(not(feature = "tokio_rt"))]
unsafe impl AsyncRuntime for GuardDropPanicRuntime {
fn spawn(
&self,
task: AsyncRuntimeTask,
) -> std::result::Result<(), AsyncRuntimeRejection<AsyncRuntimeTask>> {
futures::executor::block_on(task);
Ok(())
}
fn block_on(&self, future: Pin<&mut dyn Future<Output = ()>>) -> Result<()> {
futures::executor::block_on(future);
Ok(())
}
fn start(&self) -> Result<()> {
Ok(())
}
fn enter(&self) -> Result<Box<dyn AsyncRuntimeGuard + '_>> {
Ok(Box::new(PanicOnDropGuard {
dropped: self.guard_dropped.clone(),
}))
}
fn shutdown(&self) -> Result<()> {
Ok(())
}
}
#[cfg(not(feature = "tokio_rt"))]
fn register_guard_drop_panic_backend(registry: &AsyncRuntimeRegistry) -> Arc<AtomicUsize> {
let guard_dropped = Arc::new(AtomicUsize::new(0));
assert!(registry
.try_register(Box::new(GuardDropPanicRuntime {
guard_dropped: guard_dropped.clone(),
}))
.is_ok());
guard_dropped
}
#[cfg(not(feature = "tokio_rt"))]
#[test]
fn within_runtime_contains_panicking_guard_drop() {
let registry = AsyncRuntimeRegistry::new();
let guard_dropped = register_guard_drop_panic_backend(®istry);
let value = registry.within_runtime(|| 7);
assert_eq!(
value, 7,
"the closure's return value must flow out unaffected"
);
assert_eq!(
guard_dropped.load(AtomicOrdering::SeqCst),
1,
"the panicking guard destructor must run (and be contained)"
);
}
#[cfg(not(feature = "tokio_rt"))]
#[test]
fn within_runtime_contains_guard_drop_panic_during_closure_unwind() {
let registry = AsyncRuntimeRegistry::new();
let guard_dropped = register_guard_drop_panic_backend(®istry);
let escaped = catch_unwind(AssertUnwindSafe(|| {
registry.within_runtime(|| panic!("closure panic"));
}));
assert!(
escaped.is_err(),
"the closure's own panic must still propagate (only the guard panic is contained)"
);
assert_eq!(
guard_dropped.load(AtomicOrdering::SeqCst),
1,
"the guard destructor ran during the unwind and was contained (no double-panic abort)"
);
}
#[test]
fn duplicate_registration_defers_error_and_retires_backend() {
let registry = AsyncRuntimeRegistry::new();
let first_probe = Arc::new(BackendProbe::default());
let second_probe = Arc::new(BackendProbe::default());
assert!(registry
.try_register(Box::new(MockRuntime::new(&first_probe)))
.is_ok());
let (reason, rejected) = registry
.try_register(Box::new(MockRuntime::new(&second_probe)))
.expect_err("second registration must be rejected");
assert_eq!(reason, DUPLICATE_RUNTIME_ERROR);
retire_rejected_async_runtime(rejected);
assert_eq!(second_probe.shutdown_calls.load(AtomicOrdering::SeqCst), 1);
assert!(registry.commit_selection(false).is_some());
assert_eq!(first_probe.shutdown_calls.load(AtomicOrdering::SeqCst), 0);
registry.record_registration_error(reason);
assert_eq!(
registry.deferred_registration_error(),
Some(DUPLICATE_RUNTIME_ERROR)
);
}
#[test]
fn late_registration_after_env_activation_rejected() {
let registry = AsyncRuntimeRegistry::new();
assert!(!registry.activate());
let probe = Arc::new(BackendProbe::default());
let (reason, _rejected) = registry
.try_register(Box::new(MockRuntime::new(&probe)))
.expect_err("registration after env activation must be rejected");
assert_eq!(reason, LATE_RUNTIME_REGISTRATION_ERROR);
}
#[test]
fn tokio_fallback_selection_commits_and_closes_registration() {
let registry = AsyncRuntimeRegistry::new();
assert!(registry.commit_selection(true).is_none());
let probe = Arc::new(BackendProbe::default());
let (reason, _rejected) = registry
.try_register(Box::new(MockRuntime::new(&probe)))
.expect_err("registration after a committed fallback choice must be rejected");
assert_eq!(reason, LATE_RUNTIME_REGISTRATION_ERROR);
}
#[test]
fn missing_backend_does_not_freeze_selection() {
let registry = AsyncRuntimeRegistry::new();
assert!(registry.commit_selection(false).is_none());
let probe = Arc::new(BackendProbe::default());
assert!(registry
.try_register(Box::new(MockRuntime::new(&probe)))
.is_ok());
assert!(registry.commit_selection(false).is_some());
}
#[test]
fn task_drop_fires_cancellation_exactly_once() {
let (fired, message, callback) = recording_cancel_callback();
let task = AsyncRuntimeTask::new(Box::pin(async {}), callback);
drop(task);
assert_eq!(fired.load(AtomicOrdering::SeqCst), 1);
assert_eq!(
message.lock().unwrap().as_deref(),
Some(TASK_CANCELLED_ERROR)
);
}
#[test]
fn task_completion_disarms_cancellation() {
let (fired, _message, callback) = recording_cancel_callback();
let task = AsyncRuntimeTask::new(Box::pin(async {}), callback);
futures::executor::block_on(task);
assert_eq!(fired.load(AtomicOrdering::SeqCst), 0);
}
#[test]
fn rejection_returns_task_untouched() {
let (fired, message, callback) = recording_cancel_callback();
let task = AsyncRuntimeTask::new(Box::pin(async {}), callback);
let rejection = AsyncRuntimeRejection::new(
task,
Error::new(crate::Status::GenericFailure, "queue is full"),
);
assert_eq!(rejection.error().reason, "queue is full");
let (task, error) = rejection.into_parts();
assert_eq!(fired.load(AtomicOrdering::SeqCst), 0);
task.reject_with(error);
assert_eq!(fired.load(AtomicOrdering::SeqCst), 1);
assert_eq!(message.lock().unwrap().as_deref(), Some("queue is full"));
}
#[test]
fn spawn_blocking_default_declines_with_work_returned() {
struct SpawnOnlyRuntime;
unsafe impl AsyncRuntime for SpawnOnlyRuntime {
fn spawn(
&self,
task: AsyncRuntimeTask,
) -> std::result::Result<(), AsyncRuntimeRejection<AsyncRuntimeTask>> {
futures::executor::block_on(task);
Ok(())
}
fn block_on(&self, future: Pin<&mut dyn Future<Output = ()>>) -> Result<()> {
futures::executor::block_on(future);
Ok(())
}
fn shutdown(&self) -> Result<()> {
Ok(())
}
}
let ran = Arc::new(AtomicUsize::new(0));
let ran_in_work = ran.clone();
let rejection = SpawnOnlyRuntime
.spawn_blocking(Box::new(move || {
ran_in_work.fetch_add(1, AtomicOrdering::SeqCst);
}))
.expect_err("the default spawn_blocking implementation must decline");
assert_eq!(rejection.error().status, crate::Status::GenericFailure);
let (work, _error) = rejection.into_parts();
assert_eq!(ran.load(AtomicOrdering::SeqCst), 0);
work();
assert_eq!(ran.load(AtomicOrdering::SeqCst), 1);
}
#[test]
fn panic_in_task_fires_cancellation_with_panic_message() {
let (fired, message, callback) = recording_cancel_callback();
let task = AsyncRuntimeTask::new(
Box::pin(async {
panic!("boom in async task");
}),
callback,
);
futures::executor::block_on(task);
assert_eq!(fired.load(AtomicOrdering::SeqCst), 1);
assert_eq!(
message.lock().unwrap().as_deref(),
Some("boom in async task")
);
}
#[test]
fn pre_activation_dispatch_starts_backend_before_first_spawn() {
let registry = AsyncRuntimeRegistry::new();
let probe = Arc::new(BackendProbe::default());
assert!(registry
.try_register(Box::new(MockRuntime::new(&probe)))
.is_ok());
let backend = registry
.commit_selection(cfg!(feature = "tokio_rt"))
.expect("custom backend must be selected");
registry.ensure_started(backend);
let (_fired, _message, callback) = recording_cancel_callback();
assert!(backend
.spawn(AsyncRuntimeTask::new(Box::pin(async {}), callback))
.is_ok());
assert_eq!(probe.spawn_calls.load(AtomicOrdering::SeqCst), 1);
assert_eq!(probe.spawns_before_start.load(AtomicOrdering::SeqCst), 0);
assert_eq!(probe.start_calls.load(AtomicOrdering::SeqCst), 1);
assert!(registry.activate());
assert_eq!(probe.start_calls.load(AtomicOrdering::SeqCst), 1);
assert!(registry.deactivate());
assert!(registry.activate());
assert_eq!(probe.start_calls.load(AtomicOrdering::SeqCst), 2);
}
#[test]
fn deactivate_waits_out_inflight_start() {
struct GatedStartRuntime {
events: Arc<Mutex<Vec<&'static str>>>,
start_entered: mpsc::Sender<()>,
release_start: Mutex<mpsc::Receiver<()>>,
}
unsafe impl AsyncRuntime for GatedStartRuntime {
fn spawn(
&self,
task: AsyncRuntimeTask,
) -> std::result::Result<(), AsyncRuntimeRejection<AsyncRuntimeTask>> {
futures::executor::block_on(task);
Ok(())
}
fn block_on(&self, future: Pin<&mut dyn Future<Output = ()>>) -> Result<()> {
futures::executor::block_on(future);
Ok(())
}
fn start(&self) -> Result<()> {
self.events.lock().unwrap().push("start:enter");
self
.start_entered
.send(())
.expect("test driver dropped the start-entered channel");
self
.release_start
.lock()
.unwrap()
.recv_timeout(HANG_PROTECTION)
.expect("test driver never released the gated start");
self.events.lock().unwrap().push("start:exit");
Ok(())
}
fn shutdown(&self) -> Result<()> {
self.events.lock().unwrap().push("shutdown");
Ok(())
}
}
let events: Arc<Mutex<Vec<&'static str>>> = Arc::new(Mutex::new(Vec::new()));
let (start_entered_tx, start_entered_rx) = mpsc::channel();
let (release_start_tx, release_start_rx) = mpsc::channel();
let (deactivate_called_tx, deactivate_called_rx) = mpsc::channel();
let registry = AsyncRuntimeRegistry::new();
assert!(registry
.try_register(Box::new(GatedStartRuntime {
events: events.clone(),
start_entered: start_entered_tx,
release_start: Mutex::new(release_start_rx),
}))
.is_ok());
std::thread::scope(|scope| {
let starter = scope.spawn(|| assert!(registry.activate()));
start_entered_rx
.recv_timeout(HANG_PROTECTION)
.expect("start was never entered");
let stopper = scope.spawn(|| {
deactivate_called_tx
.send(())
.expect("test driver dropped the deactivate-called channel");
assert!(registry.deactivate());
events.lock().unwrap().push("deactivate:returned");
});
deactivate_called_rx
.recv_timeout(HANG_PROTECTION)
.expect("deactivate was never called");
release_start_tx
.send(())
.expect("the gated start is no longer waiting for its release");
starter.join().expect("starter thread panicked");
stopper.join().expect("stopper thread panicked");
});
assert_eq!(
*events.lock().unwrap(),
[
"start:enter",
"start:exit",
"shutdown",
"deactivate:returned"
]
);
}
#[test]
fn completed_teardown_revokes_pending_start_claim() {
let registry = AsyncRuntimeRegistry::new();
let probe = Arc::new(BackendProbe::default());
assert!(registry
.try_register(Box::new(MockRuntime::new(&probe)))
.is_ok());
registry.lock_state().phase = LifecyclePhase::Starting;
assert!(registry.deactivate());
assert_eq!(probe.shutdown_calls.load(AtomicOrdering::SeqCst), 1);
let backend = registry
.commit_selection(cfg!(feature = "tokio_rt"))
.expect("custom backend must stay selected");
registry.run_claimed_start(backend);
assert_eq!(probe.start_calls.load(AtomicOrdering::SeqCst), 0);
let (fired, message, callback) = recording_cancel_callback();
let rejection = backend
.spawn(AsyncRuntimeTask::new(Box::pin(async {}), callback))
.expect_err("a stopped conforming backend must reject the spawn");
let (task, error) = rejection.into_parts();
task.reject_with(error);
assert_eq!(fired.load(AtomicOrdering::SeqCst), 1);
assert_eq!(
message.lock().unwrap().as_deref(),
Some(BACKEND_STOPPED_ERROR)
);
registry.ensure_started(backend);
assert_eq!(probe.start_calls.load(AtomicOrdering::SeqCst), 1);
}
#[test]
fn start_error_triggers_shutdown_rollback() {
let registry = AsyncRuntimeRegistry::new();
let probe = Arc::new(BackendProbe::default());
assert!(registry
.try_register(Box::new(MockRuntime::failing_start(&probe)))
.is_ok());
assert!(registry.activate());
assert_eq!(probe.start_calls.load(AtomicOrdering::SeqCst), 1);
assert_eq!(probe.shutdown_calls.load(AtomicOrdering::SeqCst), 1);
assert!(registry.deactivate());
assert_eq!(probe.shutdown_calls.load(AtomicOrdering::SeqCst), 2);
}
#[test]
fn panicking_payload_drop_from_start_is_contained() {
struct PanickingDropPayload {
drops: Arc<AtomicUsize>,
}
impl Drop for PanickingDropPayload {
fn drop(&mut self) {
self.drops.fetch_add(1, AtomicOrdering::SeqCst);
panic!("panic payload drop");
}
}
struct PanicOnFirstStartRuntime {
probe: Arc<BackendProbe>,
payload_drops: Arc<AtomicUsize>,
}
unsafe impl AsyncRuntime for PanicOnFirstStartRuntime {
fn spawn(
&self,
task: AsyncRuntimeTask,
) -> std::result::Result<(), AsyncRuntimeRejection<AsyncRuntimeTask>> {
futures::executor::block_on(task);
Ok(())
}
fn block_on(&self, future: Pin<&mut dyn Future<Output = ()>>) -> Result<()> {
futures::executor::block_on(future);
Ok(())
}
fn start(&self) -> Result<()> {
if self.probe.start_calls.fetch_add(1, AtomicOrdering::SeqCst) == 0 {
std::panic::panic_any(PanickingDropPayload {
drops: self.payload_drops.clone(),
});
}
self.probe.running.store(true, AtomicOrdering::SeqCst);
Ok(())
}
fn shutdown(&self) -> Result<()> {
self.probe.running.store(false, AtomicOrdering::SeqCst);
self
.probe
.shutdown_calls
.fetch_add(1, AtomicOrdering::SeqCst);
Ok(())
}
}
let registry = AsyncRuntimeRegistry::new();
let probe = Arc::new(BackendProbe::default());
let payload_drops = Arc::new(AtomicUsize::new(0));
assert!(registry
.try_register(Box::new(PanicOnFirstStartRuntime {
probe: probe.clone(),
payload_drops: payload_drops.clone(),
}))
.is_ok());
let activated = catch_unwind(AssertUnwindSafe(|| registry.activate()))
.expect("a panicking payload Drop must not unwind out of the activate path");
assert!(activated);
assert_eq!(payload_drops.load(AtomicOrdering::SeqCst), 1);
assert_eq!(probe.start_calls.load(AtomicOrdering::SeqCst), 1);
assert_eq!(probe.shutdown_calls.load(AtomicOrdering::SeqCst), 1);
assert!(registry.lock_state().phase == LifecyclePhase::Idle);
assert!(registry.activate());
assert_eq!(probe.start_calls.load(AtomicOrdering::SeqCst), 2);
assert!(probe.running.load(AtomicOrdering::SeqCst));
}
#[test]
fn public_registration_flow_is_first_writer_wins() {
let first_probe = Arc::new(BackendProbe::default());
let second_probe = Arc::new(BackendProbe::default());
register_async_runtime(MockRuntime::new(&first_probe));
assert_eq!(first_probe.shutdown_calls.load(AtomicOrdering::SeqCst), 0);
let error = try_register_async_runtime(MockRuntime::new(&second_probe))
.expect_err("duplicate registration must error");
assert_eq!(error.status, crate::Status::GenericFailure);
assert_eq!(error.reason, DUPLICATE_RUNTIME_ERROR);
assert_eq!(second_probe.shutdown_calls.load(AtomicOrdering::SeqCst), 1);
#[cfg(all(
feature = "tokio_rt",
any(
all(target_family = "wasm", tokio_unstable),
not(target_family = "wasm")
)
))]
{
start_async_runtime();
assert_eq!(first_probe.start_calls.load(AtomicOrdering::SeqCst), 1);
let (first_tx, first_rx) = mpsc::channel();
spawn(async move {
first_tx.send(()).expect("deliver the first task signal");
});
first_rx
.recv_timeout(HANG_PROTECTION)
.expect("the first spawned task must run");
shutdown_async_runtime();
assert_eq!(first_probe.shutdown_calls.load(AtomicOrdering::SeqCst), 1);
start_async_runtime();
assert_eq!(first_probe.start_calls.load(AtomicOrdering::SeqCst), 2);
let (second_tx, second_rx) = mpsc::channel();
spawn(async move {
second_tx.send(()).expect("deliver the second task signal");
});
second_rx
.recv_timeout(HANG_PROTECTION)
.expect("a spawn after re-activation must run instead of panicking");
shutdown_async_runtime();
assert_eq!(first_probe.shutdown_calls.load(AtomicOrdering::SeqCst), 2);
let backend = ASYNC_RUNTIME_REGISTRY
.commit_selection(cfg!(feature = "tokio_rt"))
.expect("custom backend must stay selected");
ASYNC_RUNTIME_REGISTRY.ensure_started(backend);
assert_eq!(first_probe.start_calls.load(AtomicOrdering::SeqCst), 3);
assert!(within_runtime_if_available(|| {
tokio::runtime::Handle::try_current().is_ok()
}));
}
}
}