1use std::{future::Future, pin::Pin, ptr};
11
12use napi::bindgen_prelude::{
13 AsyncRuntime, AsyncRuntimeRejection, AsyncRuntimeTask, FnArgs, Promise, Unknown,
14 register_async_runtime,
15};
16use napi::threadsafe_function::ThreadsafeFunctionCallMode;
17use napi_derive::napi;
18
19use crate::MAX_ASYNC_RUNTIME_WORKER_THREADS;
20use crate::async_runtime::{
21 CurrentThreadTaskDelivery, CurrentThreadTaskDriver, CurrentThreadTaskDriverId, RuntimeFlavor,
22 RuntimeMetricsSnapshot, RuntimeOptions, RuntimeOptionsPatch, TimerDriver, TimerDriverId, TimerId,
23 acknowledge_current_thread_task_delivery, configure, configure_partial, configured_options,
24 drive_current_thread_tasks, fail_current_thread_task_delivery, metrics,
25 register_current_thread_task_driver, register_timer_driver, request_current_thread_task_drain,
26 reset_metrics, shutdown, start, try_block_on_dyn, try_spawn, try_spawn_blocking,
27 try_spawn_detached, unregister_current_thread_task_driver, unregister_timer_driver,
28};
29use crate::js_callback::{InvalidReturnValue, JsCallback};
30
31struct SharedAsyncRuntime;
32
33unsafe impl AsyncRuntime for SharedAsyncRuntime {
39 fn spawn(
40 &self,
41 task: AsyncRuntimeTask,
42 ) -> std::result::Result<(), AsyncRuntimeRejection<AsyncRuntimeTask>> {
43 match try_spawn(task) {
44 Ok(handle) => {
45 handle.detach();
46 Ok(())
47 }
48 Err((error, task)) => Err(AsyncRuntimeRejection::new(
49 task,
50 napi::Error::from_reason(error.to_string()),
51 )),
52 }
53 }
54
55 fn block_on(&self, future: Pin<&mut dyn Future<Output = ()>>) -> napi::Result<()> {
56 try_block_on_dyn(future).map_err(|error| napi::Error::from_reason(error.to_string()))
57 }
58
59 fn spawn_blocking(
60 &self,
61 work: Box<dyn FnOnce() + Send + 'static>,
62 ) -> std::result::Result<(), AsyncRuntimeRejection<Box<dyn FnOnce() + Send + 'static>>> {
63 match try_spawn_blocking(work) {
66 Ok(handle) => {
67 handle.detach();
68 Ok(())
69 }
70 Err((error, work)) => Err(AsyncRuntimeRejection::new(
71 work,
72 napi::Error::from_reason(error.to_string()),
73 )),
74 }
75 }
76
77 fn start(&self) -> napi::Result<()> {
78 start().map_err(|error| napi::Error::from_reason(error.to_string()))
79 }
80
81 fn shutdown(&self) -> napi::Result<()> {
82 shutdown().map_err(|error| napi::Error::from_reason(error.to_string()))
83 }
84}
85
86#[napi(string_enum)]
87#[derive(Clone, Copy)]
88pub enum BindingRuntimeFlavor {
89 CurrentThread,
90 MultiThread,
91}
92
93impl From<BindingRuntimeFlavor> for RuntimeFlavor {
94 fn from(value: BindingRuntimeFlavor) -> Self {
95 match value {
96 BindingRuntimeFlavor::CurrentThread => Self::CurrentThread,
97 BindingRuntimeFlavor::MultiThread => Self::MultiThread,
98 }
99 }
100}
101
102impl From<RuntimeFlavor> for BindingRuntimeFlavor {
103 fn from(value: RuntimeFlavor) -> Self {
104 match value {
105 RuntimeFlavor::CurrentThread => Self::CurrentThread,
106 RuntimeFlavor::MultiThread => Self::MultiThread,
107 }
108 }
109}
110
111#[napi(object)]
112pub struct BindingRuntimeOptions {
113 pub flavor: Option<BindingRuntimeFlavor>,
114 pub worker_threads: Option<f64>,
116 pub max_blocking_tasks: Option<f64>,
118}
119
120impl TryFrom<BindingRuntimeOptions> for RuntimeOptionsPatch {
121 type Error = napi::Error;
122
123 fn try_from(value: BindingRuntimeOptions) -> Result<Self, Self::Error> {
124 Ok(Self {
125 flavor: value.flavor.map(Into::into),
126 worker_threads: validate_binding_thread_count(
127 "workerThreads",
128 value.worker_threads,
129 MAX_ASYNC_RUNTIME_WORKER_THREADS,
130 )?,
131 max_blocking_tasks: validate_binding_thread_count(
132 "maxBlockingTasks",
133 value.max_blocking_tasks,
134 MAX_ASYNC_RUNTIME_WORKER_THREADS,
135 )?,
136 })
137 }
138}
139
140fn validate_binding_thread_count(
141 field: &str,
142 value: Option<f64>,
143 maximum: usize,
144) -> napi::Result<Option<usize>> {
145 value
146 .map(|count| {
147 #[expect(
148 clippy::cast_precision_loss,
149 reason = "the small production thread limit is exactly representable as f64"
150 )]
151 let maximum_as_f64 = maximum as f64;
152 if !count.is_finite() || count < 1.0 || count.fract() != 0.0 || count > maximum_as_f64 {
153 return Err(napi::Error::from_reason(format!(
154 "`{field}` must be a positive integer no greater than {maximum}"
155 )));
156 }
157
158 #[expect(
159 clippy::cast_possible_truncation,
160 clippy::cast_sign_loss,
161 reason = "the finite positive integer was range-checked against the usize-backed limit"
162 )]
163 let count = count as usize;
164 Ok(count)
165 })
166 .transpose()
167}
168
169#[napi(object)]
170pub struct BindingRuntimeConfig {
171 pub flavor: BindingRuntimeFlavor,
172 pub worker_threads: u32,
173 pub max_blocking_tasks: u32,
174}
175
176impl From<RuntimeOptions> for BindingRuntimeConfig {
177 fn from(value: RuntimeOptions) -> Self {
178 Self {
179 flavor: value.flavor.into(),
180 worker_threads: saturating_u32(value.worker_threads as u64),
181 max_blocking_tasks: saturating_u32(value.max_blocking_tasks as u64),
182 }
183 }
184}
185
186#[napi(object)]
187pub struct BindingRuntimeMetrics {
188 pub flavor: BindingRuntimeFlavor,
189 pub worker_threads: u32,
190 pub max_blocking_tasks: u32,
191 pub tasks_spawned: f64,
192 pub tasks_completed: f64,
193 pub tasks_panicked: f64,
194 pub runnable_schedules: f64,
195 pub runnable_polls: f64,
196 pub queued_runnables: f64,
197 pub max_queued_runnables: f64,
198 pub active_runnables: f64,
199 pub max_active_runnables: f64,
200 pub blocking_tasks_started: f64,
201 pub blocking_tasks_completed: f64,
202 pub active_blocking_tasks: f64,
203 pub max_active_blocking_tasks: f64,
204}
205
206impl From<RuntimeMetricsSnapshot> for BindingRuntimeMetrics {
207 fn from(value: RuntimeMetricsSnapshot) -> Self {
208 Self {
209 flavor: value.flavor.into(),
210 worker_threads: saturating_u32(value.worker_threads as u64),
211 max_blocking_tasks: saturating_u32(value.max_blocking_tasks as u64),
212 tasks_spawned: safe_js_number(value.tasks_spawned),
213 tasks_completed: safe_js_number(value.tasks_completed),
214 tasks_panicked: safe_js_number(value.tasks_panicked),
215 runnable_schedules: safe_js_number(value.runnable_schedules),
216 runnable_polls: safe_js_number(value.runnable_polls),
217 queued_runnables: safe_js_number(value.queued_runnables),
218 max_queued_runnables: safe_js_number(value.max_queued_runnables),
219 active_runnables: safe_js_number(value.active_runnables),
220 max_active_runnables: safe_js_number(value.max_active_runnables),
221 blocking_tasks_started: safe_js_number(value.blocking_tasks_started),
222 blocking_tasks_completed: safe_js_number(value.blocking_tasks_completed),
223 active_blocking_tasks: safe_js_number(value.active_blocking_tasks),
224 max_active_blocking_tasks: safe_js_number(value.max_active_blocking_tasks),
225 }
226 }
227}
228
229fn saturating_u32(value: u64) -> u32 {
230 u32::try_from(value).unwrap_or(u32::MAX)
231}
232
233const MAX_SAFE_JS_INTEGER: u64 = (1_u64 << 53) - 1;
234
235#[expect(
236 clippy::cast_precision_loss,
237 reason = "the value is clamped to JavaScript's exactly representable integer range"
238)]
239fn safe_js_number(value: u64) -> f64 {
240 value.min(MAX_SAFE_JS_INTEGER) as f64
241}
242
243#[napi]
244pub fn configure_async_runtime(options: BindingRuntimeOptions) -> napi::Result<()> {
248 configure_partial(options.try_into()?)
249 .map_err(|error| napi::Error::from_reason(error.to_string()))
250}
251
252#[napi]
253pub fn get_async_runtime_config() -> BindingRuntimeConfig {
259 configured_options().into()
260}
261
262#[napi]
263pub fn get_async_runtime_metrics() -> BindingRuntimeMetrics {
265 metrics().into()
266}
267
268#[napi]
269pub fn reset_async_runtime_metrics() {
274 reset_metrics();
275}
276
277#[napi(object)]
278pub struct BindingHostRegistration {
279 pub high: u32,
280 pub low: u32,
281}
282
283impl BindingHostRegistration {
284 fn from_id(id: u64) -> Self {
285 Self {
286 high: (id >> 32) as u32,
287 low: u32::try_from(id & u64::from(u32::MAX))
288 .expect("the masked host registration low word must fit in u32"),
289 }
290 }
291
292 fn id(high: u32, low: u32) -> u64 {
293 (u64::from(high) << 32) | u64::from(low)
294 }
295}
296
297static NEXT_HOST_REGISTRATION_ID: std::sync::atomic::AtomicU64 =
298 std::sync::atomic::AtomicU64::new(1);
299
300static RESERVED_HOST_REGISTRATIONS: std::sync::LazyLock<
301 std::sync::Mutex<rustc_hash::FxHashSet<u64>>,
302> = std::sync::LazyLock::new(|| std::sync::Mutex::new(rustc_hash::FxHashSet::default()));
303
304fn reserve_host_registration_id() -> napi::Result<u64> {
305 let id = NEXT_HOST_REGISTRATION_ID
306 .fetch_update(
307 std::sync::atomic::Ordering::SeqCst,
308 std::sync::atomic::Ordering::SeqCst,
309 |current| current.checked_add(1),
310 )
311 .map_err(|_| {
312 napi::Error::new(
313 napi::Status::GenericFailure,
314 "JavaScript host registration id space exhausted",
315 )
316 })?;
317 RESERVED_HOST_REGISTRATIONS
318 .lock()
319 .unwrap_or_else(std::sync::PoisonError::into_inner)
320 .insert(id);
321 Ok(id)
322}
323
324fn claim_host_registration_id(registration_high: u32, registration_low: u32) -> napi::Result<u64> {
325 let id = BindingHostRegistration::id(registration_high, registration_low);
326 if RESERVED_HOST_REGISTRATIONS
327 .lock()
328 .unwrap_or_else(std::sync::PoisonError::into_inner)
329 .remove(&id)
330 {
331 Ok(id)
332 } else {
333 Err(napi::Error::new(
334 napi::Status::InvalidArg,
335 "CurrentThread host registration was not reserved or was already consumed",
336 ))
337 }
338}
339
340fn release_host_registration_id(id: u64) {
341 RESERVED_HOST_REGISTRATIONS
342 .lock()
343 .unwrap_or_else(std::sync::PoisonError::into_inner)
344 .remove(&id);
345}
346
347#[napi]
348pub fn reserve_current_thread_host_registration() -> napi::Result<BindingHostRegistration> {
352 reserve_host_registration_id().map(BindingHostRegistration::from_id)
353}
354
355fn current_thread_task_host_napi_result(
356 status: napi::sys::napi_status,
357 context: &'static str,
358) -> napi::Result<()> {
359 if status == napi::sys::Status::napi_ok {
360 Ok(())
361 } else {
362 Err(napi::Error::new(napi::Status::from(status), context))
363 }
364}
365
366fn contain_current_thread_task_host_unwind<T>(operation: impl FnOnce() -> T) -> Option<T> {
367 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(operation)) {
368 Ok(value) => Some(value),
369 Err(payload) => {
370 if let Err(nested) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| drop(payload)))
371 {
372 std::mem::forget(nested);
373 }
374 None
375 }
376 }
377}
378
379#[cfg(test)]
380thread_local! {
381 static NATIVE_TASK_HOST_AFTER_DRIVE_TEST_HOOK:
382 std::cell::RefCell<Option<Box<dyn FnOnce()>>> =
383 const { std::cell::RefCell::new(None) };
384 static NATIVE_TASK_HOST_AFTER_PAYLOAD_DROP_TEST_HOOK:
385 std::cell::RefCell<Option<Box<dyn FnOnce()>>> =
386 const { std::cell::RefCell::new(None) };
387}
388
389#[cfg(test)]
390fn run_native_task_host_after_drive_test_hook() {
391 if let Some(hook) = NATIVE_TASK_HOST_AFTER_DRIVE_TEST_HOOK.with(|slot| slot.borrow_mut().take()) {
392 hook();
393 }
394}
395
396#[cfg(test)]
397fn run_native_task_host_after_payload_drop_test_hook() {
398 if let Some(hook) =
399 NATIVE_TASK_HOST_AFTER_PAYLOAD_DROP_TEST_HOOK.with(|slot| slot.borrow_mut().take())
400 {
401 hook();
402 }
403}
404
405unsafe extern "C" fn finalize_native_current_thread_task_host(
406 _env: napi::sys::napi_env,
407 finalize_data: *mut std::ffi::c_void,
408 _finalize_hint: *mut std::ffi::c_void,
409) {
410 if finalize_data.is_null() {
411 return;
412 }
413 let _ = contain_current_thread_task_host_unwind(|| {
414 let weak = unsafe {
415 std::sync::Weak::<NativeCurrentThreadTaskHostInner>::from_raw(finalize_data.cast())
416 };
417 if let Some(inner) = weak.upgrade() {
418 inner.finalized();
419 }
420 });
421}
422
423unsafe extern "C" fn call_native_current_thread_task_host(
424 env: napi::sys::napi_env,
425 _js_callback: napi::sys::napi_value,
426 _context: *mut std::ffi::c_void,
427 data: *mut std::ffi::c_void,
428) {
429 if data.is_null() {
430 return;
431 }
432
433 let payload = unsafe { Box::<NativeCurrentThreadTaskHostPayload>::from_raw(data.cast()) };
434 let delivery = payload.delivery;
435 let callback_lease = if env.is_null() {
436 None
437 } else {
438 contain_current_thread_task_host_unwind(|| {
439 let lease = drive_current_thread_tasks(delivery.capability());
440 #[cfg(test)]
441 if lease.is_some() {
442 run_native_task_host_after_drive_test_hook();
443 }
444 lease
445 })
446 .flatten()
447 };
448 let claimed = callback_lease.is_some();
449 let completed = contain_current_thread_task_host_unwind(|| {
450 if claimed {
451 acknowledge_current_thread_task_delivery(delivery);
452 } else {
453 fail_current_thread_task_delivery(delivery);
454 }
455 });
456 if completed.is_none() {
457 let _ = contain_current_thread_task_host_unwind(|| {
458 fail_current_thread_task_delivery(delivery);
459 });
460 }
461 let _ = contain_current_thread_task_host_unwind(|| drop(payload));
462 #[cfg(test)]
463 run_native_task_host_after_payload_drop_test_hook();
464 let _ = contain_current_thread_task_host_unwind(|| drop(callback_lease));
465}
466
467struct NativeCurrentThreadTaskHostPayload {
468 delivery: CurrentThreadTaskDelivery,
469 #[cfg(test)]
470 drop_observer: Option<std::sync::Arc<std::sync::atomic::AtomicUsize>>,
471}
472
473impl NativeCurrentThreadTaskHostPayload {
474 fn new(delivery: CurrentThreadTaskDelivery) -> Self {
475 Self {
476 delivery,
477 #[cfg(test)]
478 drop_observer: None,
479 }
480 }
481
482 #[cfg(test)]
483 fn with_drop_observer(
484 delivery: CurrentThreadTaskDelivery,
485 drop_observer: std::sync::Arc<std::sync::atomic::AtomicUsize>,
486 ) -> Self {
487 Self {
488 delivery,
489 drop_observer: Some(drop_observer),
490 }
491 }
492}
493
494#[cfg(test)]
495impl Drop for NativeCurrentThreadTaskHostPayload {
496 fn drop(&mut self) {
497 if let Some(observer) = &self.drop_observer {
498 observer.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
499 }
500 }
501}
502
503struct NativeCurrentThreadTaskHost {
505 inner: std::sync::Arc<NativeCurrentThreadTaskHostInner>,
506}
507
508struct NativeCurrentThreadTaskHostInner {
509 threadsafe_function: std::sync::Mutex<Option<usize>>,
510 dead: std::sync::atomic::AtomicBool,
511 environment_closing: std::sync::atomic::AtomicBool,
512 host_registration: u64,
513 registration: std::sync::Mutex<Option<CurrentThreadTaskDriverId>>,
514}
515
516static NATIVE_CURRENT_THREAD_TASK_HOSTS: std::sync::LazyLock<
517 std::sync::Mutex<rustc_hash::FxHashMap<u64, std::sync::Weak<NativeCurrentThreadTaskHostInner>>>,
518> = std::sync::LazyLock::new(|| std::sync::Mutex::new(rustc_hash::FxHashMap::default()));
519
520fn registered_current_thread_task_host(
521 id: u64,
522) -> Option<std::sync::Arc<NativeCurrentThreadTaskHostInner>> {
523 let mut registrations = NATIVE_CURRENT_THREAD_TASK_HOSTS
524 .lock()
525 .unwrap_or_else(std::sync::PoisonError::into_inner);
526 let inner = registrations.get(&id).and_then(std::sync::Weak::upgrade);
527 if inner.is_none() {
528 registrations.remove(&id);
529 }
530 inner
531}
532
533impl NativeCurrentThreadTaskHostInner {
534 fn new(env: &napi::Env, host_registration: u64) -> napi::Result<std::sync::Arc<Self>> {
535 const ASYNC_RESOURCE_NAME: &[u8] = b"napi_async_runtime_current_thread_task_host";
536 let name_len = isize::try_from(ASYNC_RESOURCE_NAME.len())
537 .expect("the CurrentThread task-host resource name length must fit");
538 let mut async_resource_name = ptr::null_mut();
539 current_thread_task_host_napi_result(
540 unsafe {
541 napi::sys::napi_create_string_utf8(
542 env.raw(),
543 ASYNC_RESOURCE_NAME.as_ptr().cast(),
544 name_len,
545 &raw mut async_resource_name,
546 )
547 },
548 "Failed to create the CurrentThread task-host async resource name",
549 )?;
550
551 let inner = std::sync::Arc::new(Self {
552 threadsafe_function: std::sync::Mutex::default(),
553 dead: std::sync::atomic::AtomicBool::new(false),
554 environment_closing: std::sync::atomic::AtomicBool::new(false),
555 host_registration,
556 registration: std::sync::Mutex::default(),
557 });
558 let finalize_data = std::sync::Weak::into_raw(std::sync::Arc::downgrade(&inner))
559 .cast_mut()
560 .cast();
561 let mut threadsafe_function = ptr::null_mut();
562 let create_status = unsafe {
563 napi::sys::napi_create_threadsafe_function(
564 env.raw(),
565 ptr::null_mut(),
566 ptr::null_mut(),
567 async_resource_name,
568 1,
569 1,
570 finalize_data,
571 Some(finalize_native_current_thread_task_host),
572 ptr::null_mut(),
573 Some(call_native_current_thread_task_host),
574 &raw mut threadsafe_function,
575 )
576 };
577 if create_status != napi::sys::Status::napi_ok {
578 unsafe {
579 drop(std::sync::Weak::<Self>::from_raw(finalize_data.cast()));
580 }
581 return Err(napi::Error::new(
582 napi::Status::from(create_status),
583 "Failed to create the native CurrentThread task host",
584 ));
585 }
586 *inner
587 .threadsafe_function
588 .lock()
589 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(threadsafe_function as usize);
590
591 if let Err(error) = current_thread_task_host_napi_result(
592 unsafe { napi::sys::napi_unref_threadsafe_function(env.raw(), threadsafe_function) },
593 "Failed to unref the native CurrentThread task host",
594 ) {
595 inner.release_threadsafe_function(napi::sys::ThreadsafeFunctionReleaseMode::abort);
596 return Err(error);
597 }
598 Ok(inner)
599 }
600
601 fn is_live(&self) -> bool {
602 !self.dead.load(std::sync::atomic::Ordering::SeqCst)
603 && !self
604 .environment_closing
605 .load(std::sync::atomic::Ordering::SeqCst)
606 && self
607 .threadsafe_function
608 .lock()
609 .unwrap_or_else(std::sync::PoisonError::into_inner)
610 .is_some()
611 }
612
613 fn call_threadsafe_function_with(
614 &self,
615 data: *mut std::ffi::c_void,
616 call: impl FnOnce(
617 napi::sys::napi_threadsafe_function,
618 *mut std::ffi::c_void,
619 ) -> napi::sys::napi_status,
620 ) -> Option<napi::sys::napi_status> {
621 let mut slot = self
622 .threadsafe_function
623 .lock()
624 .unwrap_or_else(std::sync::PoisonError::into_inner);
625 if self.dead.load(std::sync::atomic::Ordering::SeqCst)
626 || self
627 .environment_closing
628 .load(std::sync::atomic::Ordering::SeqCst)
629 {
630 return None;
631 }
632 let threadsafe_function = (*slot)? as napi::sys::napi_threadsafe_function;
633 let status = call(threadsafe_function, data);
634 if status == napi::sys::Status::napi_closing {
635 self
638 .environment_closing
639 .store(true, std::sync::atomic::Ordering::SeqCst);
640 slot.take();
641 }
642 Some(status)
643 }
644
645 fn dispatch(&self, delivery: CurrentThreadTaskDelivery) -> bool {
646 let data: *mut std::ffi::c_void =
647 Box::into_raw(Box::new(NativeCurrentThreadTaskHostPayload::new(delivery))).cast();
648 let Some(status) =
649 self.call_threadsafe_function_with(data, |threadsafe_function, data| unsafe {
650 napi::sys::napi_call_threadsafe_function(
651 threadsafe_function,
652 data,
653 napi::sys::ThreadsafeFunctionCallMode::nonblocking,
654 )
655 })
656 else {
657 unsafe {
658 drop(Box::<NativeCurrentThreadTaskHostPayload>::from_raw(
659 data.cast(),
660 ));
661 }
662 return false;
663 };
664 if status != napi::sys::Status::napi_ok {
665 unsafe {
666 drop(Box::<NativeCurrentThreadTaskHostPayload>::from_raw(
667 data.cast(),
668 ));
669 }
670 return false;
671 }
672 true
673 }
674
675 fn finalized(&self) {
676 self
677 .environment_closing
678 .store(true, std::sync::atomic::Ordering::SeqCst);
679 self
682 .threadsafe_function
683 .lock()
684 .unwrap_or_else(std::sync::PoisonError::into_inner)
685 .take();
686 self.evict_inner(true, false);
687 }
688
689 fn environment_cleanup(&self) {
690 self
691 .environment_closing
692 .store(true, std::sync::atomic::Ordering::SeqCst);
693 let _ = contain_current_thread_task_host_unwind(|| self.evict_inner(true, false));
697 self.release_threadsafe_function(napi::sys::ThreadsafeFunctionReleaseMode::release);
698 }
699
700 fn evict_after_sweep(&self) {
701 let abort = !self
702 .environment_closing
703 .load(std::sync::atomic::Ordering::SeqCst);
704 self.evict_inner(false, abort);
705 }
706
707 fn rollback(&self) {
708 self.evict_inner(true, true);
709 }
710
711 fn evict_inner(&self, request_redispatch: bool, abort: bool) {
712 self.dead.store(true, std::sync::atomic::Ordering::SeqCst);
713 NATIVE_CURRENT_THREAD_TASK_HOSTS
714 .lock()
715 .unwrap_or_else(std::sync::PoisonError::into_inner)
716 .remove(&self.host_registration);
717 let registration = self
718 .registration
719 .lock()
720 .unwrap_or_else(std::sync::PoisonError::into_inner)
721 .take();
722 if let Some(id) = registration {
723 unregister_current_thread_task_driver(id);
724 if request_redispatch {
725 request_current_thread_task_drain();
729 }
730 }
731 if abort {
732 self.release_threadsafe_function(napi::sys::ThreadsafeFunctionReleaseMode::abort);
733 }
734 }
735
736 fn release_threadsafe_function(&self, mode: napi::sys::napi_threadsafe_function_release_mode) {
737 self.release_threadsafe_function_with(mode, |threadsafe_function, mode| unsafe {
738 napi::sys::napi_release_threadsafe_function(threadsafe_function, mode)
739 });
740 }
741
742 fn release_threadsafe_function_with(
743 &self,
744 mode: napi::sys::napi_threadsafe_function_release_mode,
745 release: impl FnOnce(
746 napi::sys::napi_threadsafe_function,
747 napi::sys::napi_threadsafe_function_release_mode,
748 ) -> napi::sys::napi_status,
749 ) {
750 let threadsafe_function = self
751 .threadsafe_function
752 .lock()
753 .unwrap_or_else(std::sync::PoisonError::into_inner)
754 .take();
755 let Some(threadsafe_function) = threadsafe_function else {
756 return;
757 };
758 let status = release(
759 threadsafe_function as napi::sys::napi_threadsafe_function,
760 mode,
761 );
762 if status == napi::sys::Status::napi_closing {
763 self
764 .environment_closing
765 .store(true, std::sync::atomic::Ordering::SeqCst);
766 }
767 }
768}
769
770impl CurrentThreadTaskDriver for NativeCurrentThreadTaskHost {
771 fn dispatch(&self, delivery: CurrentThreadTaskDelivery) -> bool {
772 self.inner.dispatch(delivery)
773 }
774
775 fn is_live(&self) -> bool {
776 self.inner.is_live()
777 }
778
779 fn on_swept(&self) {
780 self.inner.evict_after_sweep();
783 }
784}
785
786fn reject_current_thread_task_host_callback(dispatch: Option<Unknown<'_>>) -> napi::Result<()> {
787 if dispatch.is_none() {
788 Ok(())
789 } else {
790 Err(napi::Error::new(
791 napi::Status::InvalidArg,
792 "registerCurrentThreadTaskHost does not accept a JavaScript callback",
793 ))
794 }
795}
796
797const CURRENT_THREAD_TASK_HOST_CONTRACT_VERSION: u32 = 4;
798
799#[napi]
800pub fn get_current_thread_task_host_contract_version() -> u32 {
805 CURRENT_THREAD_TASK_HOST_CONTRACT_VERSION
806}
807
808#[napi]
809pub fn is_current_thread_host_registration_active(
814 registration_high: u32,
815 registration_low: u32,
816) -> bool {
817 let id = BindingHostRegistration::id(registration_high, registration_low);
818 registered_current_thread_task_host(id).is_some_and(|inner| inner.is_live())
819 || registered_timer_host(id).is_some_and(|inner| inner.is_live())
820}
821
822fn install_cleanup_hook_or_rollback<T>(
823 install: impl FnOnce() -> napi::Result<T>,
824 rollback: impl FnOnce(),
825) -> napi::Result<()> {
826 match install() {
827 Ok(_) => Ok(()),
828 Err(error) => {
829 rollback();
830 Err(error)
831 }
832 }
833}
834
835fn install_host_driver_registration<T>(
836 dead: &std::sync::atomic::AtomicBool,
837 registration: &std::sync::Mutex<Option<T>>,
838 install: impl FnOnce() -> T,
839 publish: impl FnOnce(),
840 rollback: impl FnOnce(T),
841) -> bool {
842 let mut installed = Some(install());
846 {
847 let mut slot = registration
848 .lock()
849 .unwrap_or_else(std::sync::PoisonError::into_inner);
850 if !dead.load(std::sync::atomic::Ordering::SeqCst) {
851 *slot = installed.take();
852 publish();
855 }
856 }
857 if let Some(installed) = installed {
858 rollback(installed);
859 false
860 } else {
861 true
862 }
863}
864
865#[napi(ts_args_type = "registrationHigh: number, registrationLow: number, dispatch?: never")]
866pub fn register_current_thread_task_host(
870 env: &napi::Env,
871 registration_high: u32,
872 registration_low: u32,
873 dispatch: Option<Unknown<'_>>,
874) -> napi::Result<()> {
875 reject_current_thread_task_host_callback(dispatch)?;
876 let host_registration = claim_host_registration_id(registration_high, registration_low)?;
877 let inner = NativeCurrentThreadTaskHostInner::new(env, host_registration)?;
878 {
879 let mut slot = inner
880 .registration
881 .lock()
882 .unwrap_or_else(std::sync::PoisonError::into_inner);
883 *slot = Some(register_current_thread_task_driver(std::sync::Arc::new(
884 NativeCurrentThreadTaskHost {
885 inner: std::sync::Arc::clone(&inner),
886 },
887 )));
888 }
889 NATIVE_CURRENT_THREAD_TASK_HOSTS
890 .lock()
891 .unwrap_or_else(std::sync::PoisonError::into_inner)
892 .insert(host_registration, std::sync::Arc::downgrade(&inner));
893 request_current_thread_task_drain();
894 let hook_inner = std::sync::Arc::clone(&inner);
895 install_cleanup_hook_or_rollback(
896 || {
897 env.add_env_cleanup_hook(hook_inner, |inner| {
898 let _ = contain_current_thread_task_host_unwind(|| inner.environment_cleanup());
899 })
900 },
901 || inner.rollback(),
902 )?;
903 Ok(())
904}
905
906#[napi]
907pub fn unregister_current_thread_task_host(registration_high: u32, registration_low: u32) {
911 let id = BindingHostRegistration::id(registration_high, registration_low);
912 release_host_registration_id(id);
913 if let Some(inner) = registered_current_thread_task_host(id) {
914 inner.rollback();
915 }
916}
917
918struct JsTimerHost {
944 inner: std::sync::Arc<JsTimerHostInner>,
945}
946
947#[derive(Default)]
948struct RelayIdAllocator {
949 next: std::sync::atomic::AtomicU64,
950}
951
952impl RelayIdAllocator {
953 fn reserve(&self) -> Result<u32, RelayIdExhausted> {
954 self
955 .next
956 .fetch_update(
957 std::sync::atomic::Ordering::Relaxed,
958 std::sync::atomic::Ordering::Relaxed,
959 |next| u32::try_from(next).is_ok().then_some(next + 1),
960 )
961 .map(|id| u32::try_from(id).expect("the checked relay id must fit in u32"))
962 .map_err(|_| RelayIdExhausted)
963 }
964}
965
966#[derive(Clone, Copy, Debug, PartialEq, Eq)]
967struct RelayIdExhausted;
968
969#[derive(Clone, Copy, Debug, PartialEq, Eq)]
970#[repr(u8)]
971enum RelayCancellationAccounting {
972 HostHealth = 0,
973 CleanupOnly = 1,
974}
975
976struct HostTimerRelayHealth {
977 cancellation_accounting: std::sync::atomic::AtomicU8,
978 failure_recorded: std::sync::atomic::AtomicBool,
979}
980
981impl Default for HostTimerRelayHealth {
982 fn default() -> Self {
983 Self {
984 cancellation_accounting: std::sync::atomic::AtomicU8::new(
985 RelayCancellationAccounting::HostHealth as u8,
986 ),
987 failure_recorded: std::sync::atomic::AtomicBool::new(false),
988 }
989 }
990}
991
992impl HostTimerRelayHealth {
993 fn set_cancellation_accounting(&self, accounting: RelayCancellationAccounting) {
994 self
995 .cancellation_accounting
996 .store(accounting as u8, std::sync::atomic::Ordering::Release);
997 }
998
999 fn cancellation_accounting(&self) -> RelayCancellationAccounting {
1000 match self
1001 .cancellation_accounting
1002 .load(std::sync::atomic::Ordering::Acquire)
1003 {
1004 value if value == RelayCancellationAccounting::CleanupOnly as u8 => {
1005 RelayCancellationAccounting::CleanupOnly
1006 }
1007 _ => RelayCancellationAccounting::HostHealth,
1008 }
1009 }
1010
1011 fn claim_failure(&self) -> bool {
1012 self
1013 .failure_recorded
1014 .compare_exchange(
1015 false,
1016 true,
1017 std::sync::atomic::Ordering::AcqRel,
1018 std::sync::atomic::Ordering::Acquire,
1019 )
1020 .is_ok()
1021 }
1022
1023 fn failure_recorded(&self) -> bool {
1024 self
1025 .failure_recorded
1026 .load(std::sync::atomic::Ordering::Acquire)
1027 }
1028}
1029
1030const HOST_TIMER_MAX_TRANSIENT_FAILURES: u32 = 3;
1040
1041fn should_evict_for_relay_error(status: napi::Status, host_is_live: bool) -> bool {
1069 status == napi::Status::Closing || !host_is_live
1070}
1071
1072#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1073enum HostTimerFailureAction {
1074 Duplicate,
1075 EvictHost,
1076 EvictHostAfterStrikes(u32),
1077 Retry(u32),
1078}
1079
1080fn record_host_timer_failure(
1081 transient_failures: &std::sync::atomic::AtomicU32,
1082 relay_health: &HostTimerRelayHealth,
1083 status: napi::Status,
1084 host_is_live: bool,
1085) -> HostTimerFailureAction {
1086 if !relay_health.claim_failure() {
1087 return HostTimerFailureAction::Duplicate;
1088 }
1089 if should_evict_for_relay_error(status, host_is_live) {
1090 return HostTimerFailureAction::EvictHost;
1091 }
1092 let strikes = transient_failures.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1;
1093 if strikes >= HOST_TIMER_MAX_TRANSIENT_FAILURES {
1094 HostTimerFailureAction::EvictHostAfterStrikes(strikes)
1095 } else {
1096 HostTimerFailureAction::Retry(strikes)
1097 }
1098}
1099
1100fn reset_host_timer_failures_after_success(
1101 transient_failures: &std::sync::atomic::AtomicU32,
1102 relay_health: &HostTimerRelayHealth,
1103) {
1104 if !relay_health.failure_recorded() {
1105 transient_failures.store(0, std::sync::atomic::Ordering::SeqCst);
1106 }
1107}
1108
1109struct JsTimerHostInner {
1110 callback: JsCallback<FnArgs<(u32, f64)>, Promise<()>>,
1111 cancel_callback: JsCallback<FnArgs<(u32,)>, ()>,
1112 pending: std::sync::Mutex<rustc_hash::FxHashMap<TimerId, PendingHostTimer>>,
1113 relay_ids: RelayIdAllocator,
1114 dead: std::sync::atomic::AtomicBool,
1117 host_registration: u64,
1120 registration: std::sync::Mutex<Option<TimerDriverId>>,
1125 transient_failures: std::sync::atomic::AtomicU32,
1128}
1129
1130static JS_TIMER_HOSTS: std::sync::LazyLock<
1131 std::sync::Mutex<rustc_hash::FxHashMap<u64, std::sync::Weak<JsTimerHostInner>>>,
1132> = std::sync::LazyLock::new(|| std::sync::Mutex::new(rustc_hash::FxHashMap::default()));
1133
1134fn registered_timer_host(id: u64) -> Option<std::sync::Arc<JsTimerHostInner>> {
1135 let mut registrations = JS_TIMER_HOSTS
1136 .lock()
1137 .unwrap_or_else(std::sync::PoisonError::into_inner);
1138 let inner = registrations.get(&id).and_then(std::sync::Weak::upgrade);
1139 if inner.is_none() {
1140 registrations.remove(&id);
1141 }
1142 inner
1143}
1144
1145struct PendingHostTimer {
1146 cancellation: Option<futures::channel::oneshot::Sender<()>>,
1147 relay_health: std::sync::Arc<HostTimerRelayHealth>,
1148 relay_id: u32,
1149 waker: std::task::Waker,
1150 schedule_state: RelayScheduleState,
1151}
1152
1153impl PendingHostTimer {
1154 fn signal_native_cancellation(&mut self) {
1155 if let Some(sender) = self.cancellation.take() {
1156 let _ = sender.send(());
1157 }
1158 }
1159}
1160
1161#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1162enum RelayScheduleState {
1163 AwaitingCallback,
1164 CallbackComplete,
1165}
1166
1167trait PendingRelayState: Send + Sync + 'static {
1168 fn take_pending_relay(
1169 &self,
1170 id: TimerId,
1171 relay_id: u32,
1172 accounting: RelayCancellationAccounting,
1173 ) -> Option<PendingHostTimer>;
1174 fn mark_relay_callback_complete(&self, id: TimerId, relay_id: u32) -> bool;
1178 fn cancel_relay(
1179 self: &std::sync::Arc<Self>,
1180 relay_id: u32,
1181 relay_health: std::sync::Arc<HostTimerRelayHealth>,
1182 );
1183}
1184
1185struct PendingRelayDropGuard<T: PendingRelayState> {
1186 state: std::sync::Arc<T>,
1187 id: TimerId,
1188 relay_id: u32,
1189 cleanup_on_drop: bool,
1190}
1191
1192impl<T: PendingRelayState> PendingRelayDropGuard<T> {
1193 fn new(state: std::sync::Arc<T>, id: TimerId, relay_id: u32) -> Self {
1194 Self {
1195 state,
1196 id,
1197 relay_id,
1198 cleanup_on_drop: true,
1199 }
1200 }
1201
1202 fn disarm(&mut self) {
1203 self.cleanup_on_drop = false;
1204 }
1205}
1206
1207impl<T: PendingRelayState> Drop for PendingRelayDropGuard<T> {
1208 fn drop(&mut self) {
1209 if !self.cleanup_on_drop {
1210 return;
1211 }
1212 let Some(pending) = self.state.take_pending_relay(
1213 self.id,
1214 self.relay_id,
1215 RelayCancellationAccounting::HostHealth,
1216 ) else {
1217 return;
1218 };
1219 retire_pending_relay(&self.state, pending);
1223 }
1224}
1225
1226enum PendingHostTimerRegistration {
1227 Refreshed(std::task::Waker),
1228 Armed {
1229 relay_id: u32,
1230 cancellation: futures::channel::oneshot::Receiver<()>,
1231 relay_health: std::sync::Arc<HostTimerRelayHealth>,
1232 },
1233 Exhausted(std::task::Waker),
1234}
1235
1236fn register_pending_host_timer_locked(
1237 pending: &mut rustc_hash::FxHashMap<TimerId, PendingHostTimer>,
1238 relay_ids: &RelayIdAllocator,
1239 id: TimerId,
1240 waker: std::task::Waker,
1241) -> PendingHostTimerRegistration {
1242 if let Some(slot) = pending.get_mut(&id) {
1243 return PendingHostTimerRegistration::Refreshed(std::mem::replace(&mut slot.waker, waker));
1244 }
1245 let Ok(relay_id) = relay_ids.reserve() else {
1246 return PendingHostTimerRegistration::Exhausted(waker);
1247 };
1248 let (cancellation_sender, cancellation) = futures::channel::oneshot::channel();
1249 let relay_health = std::sync::Arc::new(HostTimerRelayHealth::default());
1250 pending.insert(
1251 id,
1252 PendingHostTimer {
1253 cancellation: Some(cancellation_sender),
1254 relay_health: std::sync::Arc::clone(&relay_health),
1255 relay_id,
1256 waker,
1257 schedule_state: RelayScheduleState::AwaitingCallback,
1258 },
1259 );
1260 PendingHostTimerRegistration::Armed {
1261 relay_id,
1262 cancellation,
1263 relay_health,
1264 }
1265}
1266
1267#[cfg(test)]
1268fn register_pending_host_timer(
1269 pending: &std::sync::Mutex<rustc_hash::FxHashMap<TimerId, PendingHostTimer>>,
1270 relay_ids: &RelayIdAllocator,
1271 id: TimerId,
1272 waker: std::task::Waker,
1273) -> PendingHostTimerRegistration {
1274 let mut pending = pending
1275 .lock()
1276 .unwrap_or_else(std::sync::PoisonError::into_inner);
1277 register_pending_host_timer_locked(&mut pending, relay_ids, id, waker)
1278}
1279
1280fn register_pending_host_timer_if_live(
1281 pending: &std::sync::Mutex<rustc_hash::FxHashMap<TimerId, PendingHostTimer>>,
1282 relay_ids: &RelayIdAllocator,
1283 id: TimerId,
1284 waker: std::task::Waker,
1285 is_live: impl FnOnce() -> bool,
1286) -> Result<PendingHostTimerRegistration, std::task::Waker> {
1287 let mut pending = pending
1288 .lock()
1289 .unwrap_or_else(std::sync::PoisonError::into_inner);
1290 if !is_live() {
1291 return Err(waker);
1292 }
1293 Ok(register_pending_host_timer_locked(
1294 &mut pending,
1295 relay_ids,
1296 id,
1297 waker,
1298 ))
1299}
1300
1301fn take_pending_host_timers(
1302 pending: &std::sync::Mutex<rustc_hash::FxHashMap<TimerId, PendingHostTimer>>,
1303 accounting: RelayCancellationAccounting,
1304) -> rustc_hash::FxHashMap<TimerId, PendingHostTimer> {
1305 let mut pending = pending
1306 .lock()
1307 .unwrap_or_else(std::sync::PoisonError::into_inner);
1308 for timer in pending.values() {
1309 timer.relay_health.set_cancellation_accounting(accounting);
1310 }
1311 std::mem::take(&mut *pending)
1312}
1313
1314fn take_pending_host_timer(
1315 pending: &std::sync::Mutex<rustc_hash::FxHashMap<TimerId, PendingHostTimer>>,
1316 id: TimerId,
1317 relay_id: u32,
1318 accounting: RelayCancellationAccounting,
1319) -> Option<PendingHostTimer> {
1320 let mut pending = pending
1321 .lock()
1322 .unwrap_or_else(std::sync::PoisonError::into_inner);
1323 if pending
1324 .get(&id)
1325 .is_some_and(|slot| slot.relay_id == relay_id)
1326 {
1327 pending
1328 .get(&id)
1329 .expect("the matching pending relay must remain present")
1330 .relay_health
1331 .set_cancellation_accounting(accounting);
1332 pending.remove(&id)
1333 } else {
1334 None
1335 }
1336}
1337
1338fn run_host_timer_cleanup_safely(cleanup: impl FnOnce()) {
1339 if let Err(payload) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(cleanup))
1340 && let Err(nested_payload) =
1341 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| drop(payload)))
1342 {
1343 std::mem::forget(nested_payload);
1347 }
1348}
1349
1350fn retire_pending_relay<T: PendingRelayState>(
1351 state: &std::sync::Arc<T>,
1352 mut pending: PendingHostTimer,
1353) {
1354 pending.signal_native_cancellation();
1358 if pending.schedule_state == RelayScheduleState::CallbackComplete {
1359 let relay_health = std::sync::Arc::clone(&pending.relay_health);
1360 run_host_timer_cleanup_safely(|| state.cancel_relay(pending.relay_id, relay_health));
1361 }
1362 wake_host_timer_safely(pending.waker);
1363}
1364
1365fn retire_pending_relays<T: PendingRelayState>(
1366 state: &std::sync::Arc<T>,
1367 pending: rustc_hash::FxHashMap<TimerId, PendingHostTimer>,
1368) {
1369 for (_, pending) in pending {
1370 retire_pending_relay(state, pending);
1371 }
1372}
1373
1374fn complete_relay_schedule_callback<T: PendingRelayState, R>(
1375 state: &std::sync::Arc<T>,
1376 id: TimerId,
1377 relay_id: u32,
1378 relay_health: &std::sync::Arc<HostTimerRelayHealth>,
1379 result: R,
1380 deliver: impl FnOnce(R),
1381) {
1382 run_host_timer_cleanup_safely(|| {
1387 if state.mark_relay_callback_complete(id, relay_id) {
1388 state.cancel_relay(relay_id, std::sync::Arc::clone(relay_health));
1389 }
1390 });
1391 run_host_timer_cleanup_safely(|| deliver(result));
1392}
1393
1394fn normalize_timer_schedule_result(
1395 result: napi::Result<napi::Either<Promise<()>, InvalidReturnValue>>,
1396) -> napi::Result<Promise<()>> {
1397 match result {
1398 Ok(napi::Either::A(promise)) => Ok(promise),
1399 Ok(napi::Either::B(invalid)) => Err(napi::Error::new(
1400 napi::Status::InvalidArg,
1401 format!(
1402 "The function returned `{}`, but expected `object`.",
1403 invalid.value_type.to_string().to_ascii_lowercase()
1404 ),
1405 )),
1406 Err(error) => Err(error),
1407 }
1408}
1409
1410fn wake_host_timer_safely(waker: std::task::Waker) {
1411 run_host_timer_cleanup_safely(|| waker.wake_by_ref());
1416 run_host_timer_cleanup_safely(|| drop(waker));
1417}
1418
1419fn drop_host_timer_waker_safely(waker: std::task::Waker) {
1420 run_host_timer_cleanup_safely(|| drop(waker));
1421}
1422
1423fn recover_host_timer_failure(recover: impl FnOnce(), diagnostic: std::fmt::Arguments<'_>) {
1424 recover();
1425 run_host_timer_cleanup_safely(|| {
1426 use std::io::Write as _;
1427 let _ = writeln!(std::io::stderr().lock(), "{diagnostic}");
1428 });
1429}
1430
1431impl JsTimerHostInner {
1432 fn lock_pending(
1433 &self,
1434 ) -> std::sync::MutexGuard<'_, rustc_hash::FxHashMap<TimerId, PendingHostTimer>> {
1435 self
1436 .pending
1437 .lock()
1438 .unwrap_or_else(std::sync::PoisonError::into_inner)
1439 }
1440
1441 fn register_pending(
1442 &self,
1443 id: TimerId,
1444 waker: std::task::Waker,
1445 ) -> Result<PendingHostTimerRegistration, std::task::Waker> {
1446 register_pending_host_timer_if_live(&self.pending, &self.relay_ids, id, waker, || {
1450 self.is_live()
1451 })
1452 }
1453
1454 fn is_live(&self) -> bool {
1458 !self.dead.load(std::sync::atomic::Ordering::SeqCst)
1459 && !self.callback.aborted()
1460 && !self.cancel_callback.aborted()
1461 }
1462
1463 fn handle_cancellation_failure(
1464 self: &std::sync::Arc<Self>,
1465 relay_id: u32,
1466 relay_health: &HostTimerRelayHealth,
1467 error: napi::Error,
1468 queue_failure: bool,
1469 ) {
1470 let operation = if queue_failure {
1471 "could not be queued"
1472 } else {
1473 "failed"
1474 };
1475 if relay_health.cancellation_accounting() == RelayCancellationAccounting::CleanupOnly {
1476 recover_host_timer_failure(
1477 || {},
1478 format_args!(
1479 "napi-async-runtime: host timer cancellation callback {operation} for relay {relay_id} during \
1480 cleanup: {error}"
1481 ),
1482 );
1483 return;
1484 }
1485
1486 match record_host_timer_failure(
1487 &self.transient_failures,
1488 relay_health,
1489 error.status,
1490 self.is_live(),
1491 ) {
1492 HostTimerFailureAction::Duplicate => {
1493 recover_host_timer_failure(
1494 || {},
1495 format_args!(
1496 "napi-async-runtime: host timer cancellation callback {operation} for relay {relay_id} after \
1497 this relay failure was already accounted: {error}"
1498 ),
1499 );
1500 }
1501 HostTimerFailureAction::EvictHost => {
1502 recover_host_timer_failure(
1503 || self.evict(),
1504 format_args!(
1505 "napi-async-runtime: host timer cancellation callback {operation} for relay {relay_id} (host \
1506 gone, evicting): {error}"
1507 ),
1508 );
1509 }
1510 HostTimerFailureAction::EvictHostAfterStrikes(strikes) => {
1511 recover_host_timer_failure(
1512 || self.evict(),
1513 format_args!(
1514 "napi-async-runtime: host timer cancellation callback {operation} {strikes} times in a row, \
1515 evicting this timer host (relay {relay_id}): {error}"
1516 ),
1517 );
1518 }
1519 HostTimerFailureAction::Retry(strikes) => {
1520 recover_host_timer_failure(
1521 || {},
1522 format_args!(
1523 "napi-async-runtime: host timer cancellation callback {operation} for relay {relay_id} \
1524 ({strikes}/{HOST_TIMER_MAX_TRANSIENT_FAILURES} before eviction): {error}"
1525 ),
1526 );
1527 }
1528 }
1529 }
1530
1531 fn cancel_relay(
1532 self: &std::sync::Arc<Self>,
1533 relay_id: u32,
1534 relay_health: std::sync::Arc<HostTimerRelayHealth>,
1535 ) {
1536 let callback_inner = std::sync::Arc::clone(self);
1537 let callback_health = std::sync::Arc::clone(&relay_health);
1538 let status = self.cancel_callback.call_with_return_value(
1539 FnArgs { data: (relay_id,) },
1540 ThreadsafeFunctionCallMode::NonBlocking,
1541 move |result, _| {
1542 let error = match result {
1543 Ok(napi::Either::A(())) => {
1544 if callback_health.cancellation_accounting() == RelayCancellationAccounting::HostHealth
1545 {
1546 reset_host_timer_failures_after_success(
1547 &callback_inner.transient_failures,
1548 &callback_health,
1549 );
1550 }
1551 return Ok(());
1552 }
1553 Ok(napi::Either::B(invalid)) => napi::Error::new(
1554 napi::Status::InvalidArg,
1555 format!(
1556 "The timer cancellation callback returned `{}`, but expected `undefined`.",
1557 invalid.value_type.to_string().to_ascii_lowercase()
1558 ),
1559 ),
1560 Err(error) => error,
1561 };
1562 callback_inner.handle_cancellation_failure(relay_id, &callback_health, error, false);
1563 Ok(())
1564 },
1565 );
1566 if status != napi::Status::Ok {
1567 let error = napi::Error::new(status, "Threadsafe timer cancellation callback call failed");
1568 self.handle_cancellation_failure(relay_id, &relay_health, error, true);
1569 }
1570 }
1571
1572 fn mark_relay_callback_complete(&self, id: TimerId, relay_id: u32) -> bool {
1573 let mut pending = self.lock_pending();
1574 match pending.get_mut(&id) {
1575 Some(slot) if slot.relay_id == relay_id => {
1576 slot.schedule_state = RelayScheduleState::CallbackComplete;
1577 false
1578 }
1579 _ => true,
1580 }
1581 }
1582
1583 fn take_pending_relay(
1584 &self,
1585 id: TimerId,
1586 relay_id: u32,
1587 accounting: RelayCancellationAccounting,
1588 ) -> Option<PendingHostTimer> {
1589 take_pending_host_timer(&self.pending, id, relay_id, accounting)
1590 }
1591
1592 async fn invoke_schedule_callback(
1593 self: &std::sync::Arc<Self>,
1594 id: TimerId,
1595 relay_id: u32,
1596 ms: f64,
1597 relay_health: std::sync::Arc<HostTimerRelayHealth>,
1598 ) -> napi::Result<Promise<()>> {
1599 let (sender, receiver) = futures::channel::oneshot::channel();
1600 let callback_inner = std::sync::Arc::clone(self);
1601 let status = self.callback.call_with_return_value(
1602 FnArgs {
1603 data: (relay_id, ms),
1604 },
1605 ThreadsafeFunctionCallMode::NonBlocking,
1606 move |result, _| {
1607 complete_relay_schedule_callback(
1608 &callback_inner,
1609 id,
1610 relay_id,
1611 &relay_health,
1612 result,
1613 move |result| {
1614 let _ = sender.send(normalize_timer_schedule_result(result));
1615 },
1616 );
1617 Ok(())
1618 },
1619 );
1620 if status != napi::Status::Ok {
1621 return Err(napi::Error::new(
1622 status,
1623 "Threadsafe function call_async_catch failed",
1624 ));
1625 }
1626 receiver.await.map_err(|_| {
1627 napi::Error::new(
1628 napi::Status::GenericFailure,
1629 "Receive value from threadsafe function sender failed",
1630 )
1631 })?
1632 }
1633
1634 fn evict(self: &std::sync::Arc<Self>) {
1641 self.dead.store(true, std::sync::atomic::Ordering::SeqCst);
1642 let registration = self
1643 .registration
1644 .lock()
1645 .unwrap_or_else(std::sync::PoisonError::into_inner)
1646 .take();
1647 JS_TIMER_HOSTS
1648 .lock()
1649 .unwrap_or_else(std::sync::PoisonError::into_inner)
1650 .remove(&self.host_registration);
1651 if let Some(id) = registration {
1652 unregister_timer_driver(id);
1653 }
1654 let pending = take_pending_host_timers(&self.pending, RelayCancellationAccounting::CleanupOnly);
1655 retire_pending_relays(self, pending);
1656 }
1657}
1658
1659impl PendingRelayState for JsTimerHostInner {
1660 fn take_pending_relay(
1661 &self,
1662 id: TimerId,
1663 relay_id: u32,
1664 accounting: RelayCancellationAccounting,
1665 ) -> Option<PendingHostTimer> {
1666 JsTimerHostInner::take_pending_relay(self, id, relay_id, accounting)
1667 }
1668
1669 fn mark_relay_callback_complete(&self, id: TimerId, relay_id: u32) -> bool {
1670 JsTimerHostInner::mark_relay_callback_complete(self, id, relay_id)
1671 }
1672
1673 fn cancel_relay(
1674 self: &std::sync::Arc<Self>,
1675 relay_id: u32,
1676 relay_health: std::sync::Arc<HostTimerRelayHealth>,
1677 ) {
1678 JsTimerHostInner::cancel_relay(self, relay_id, relay_health);
1679 }
1680}
1681
1682impl TimerDriver for JsTimerHost {
1683 fn register(&self, id: TimerId, deadline: std::time::Instant, waker: std::task::Waker) {
1684 if !self.inner.is_live() {
1685 self.inner.evict();
1689 wake_host_timer_safely(waker);
1690 return;
1691 }
1692 let (relay_id, cancellation, relay_health) = match self.inner.register_pending(id, waker) {
1693 Err(waker) => {
1694 self.inner.evict();
1695 wake_host_timer_safely(waker);
1696 return;
1697 }
1698 Ok(PendingHostTimerRegistration::Refreshed(replaced_waker)) => {
1699 drop_host_timer_waker_safely(replaced_waker);
1702 return;
1703 }
1704 Ok(PendingHostTimerRegistration::Armed {
1705 relay_id,
1706 cancellation,
1707 relay_health,
1708 }) => (relay_id, cancellation, relay_health),
1709 Ok(PendingHostTimerRegistration::Exhausted(waker)) => {
1710 recover_host_timer_failure(
1711 || {
1712 self.inner.evict();
1713 wake_host_timer_safely(waker);
1714 },
1715 format_args!(
1716 "napi-async-runtime: host timer relay id space exhausted, evicting this timer host"
1717 ),
1718 );
1719 return;
1720 }
1721 };
1722 let ms = deadline
1723 .saturating_duration_since(std::time::Instant::now())
1724 .as_secs_f64()
1725 * 1000.0;
1726 let inner = std::sync::Arc::clone(&self.inner);
1727 let relay_drop_guard = PendingRelayDropGuard::new(std::sync::Arc::clone(&inner), id, relay_id);
1730 let submission = try_spawn_detached(async move {
1731 let mut relay_drop_guard = relay_drop_guard;
1732 let schedule_started = std::sync::atomic::AtomicBool::new(false);
1733 let schedule = async {
1734 schedule_started.store(true, std::sync::atomic::Ordering::Release);
1735 match inner
1736 .invoke_schedule_callback(id, relay_id, ms, std::sync::Arc::clone(&relay_health))
1737 .await
1738 {
1739 Ok(promise) => promise.await,
1740 Err(error) => Err(error),
1741 }
1742 };
1743 futures::pin_mut!(cancellation);
1744 futures::pin_mut!(schedule);
1745 let result = match futures::future::select(cancellation, schedule).await {
1746 futures::future::Either::Left((_cancelled, schedule)) => {
1747 relay_drop_guard.disarm();
1751 if !schedule_started.load(std::sync::atomic::Ordering::Acquire) {
1752 return;
1757 }
1758 schedule.await
1768 }
1769 futures::future::Either::Right((result, _cancellation)) => result,
1770 };
1771 match result {
1772 Ok(()) => {
1773 reset_host_timer_failures_after_success(&inner.transient_failures, &relay_health);
1774 if let Some(pending) =
1775 inner.take_pending_relay(id, relay_id, RelayCancellationAccounting::CleanupOnly)
1776 {
1777 wake_host_timer_safely(pending.waker);
1778 }
1779 relay_drop_guard.disarm();
1780 }
1781 Err(error) => {
1782 let action = record_host_timer_failure(
1783 &inner.transient_failures,
1784 &relay_health,
1785 error.status,
1786 inner.is_live(),
1787 );
1788 match action {
1789 HostTimerFailureAction::EvictHost => {
1790 recover_host_timer_failure(
1791 || inner.evict(),
1792 format_args!(
1793 "napi-async-runtime: host timer callback failed (host gone, evicting): {error}"
1794 ),
1795 );
1796 }
1797 HostTimerFailureAction::EvictHostAfterStrikes(strikes) => {
1798 recover_host_timer_failure(
1799 || inner.evict(),
1800 format_args!(
1801 "napi-async-runtime: host timer callback failed {strikes} times in a row, evicting this \
1802 timer host: {error}"
1803 ),
1804 );
1805 }
1806 HostTimerFailureAction::Retry(strikes) => {
1807 recover_host_timer_failure(
1808 || {
1809 if let Some(pending) =
1810 inner.take_pending_relay(id, relay_id, RelayCancellationAccounting::CleanupOnly)
1811 {
1812 retire_pending_relay(&inner, pending);
1817 }
1818 },
1819 format_args!(
1820 "napi-async-runtime: host timer callback failed \
1821 ({strikes}/{HOST_TIMER_MAX_TRANSIENT_FAILURES} before eviction): {error}"
1822 ),
1823 );
1824 }
1825 HostTimerFailureAction::Duplicate => {
1826 recover_host_timer_failure(
1827 || {
1828 if let Some(pending) =
1829 inner.take_pending_relay(id, relay_id, RelayCancellationAccounting::CleanupOnly)
1830 {
1831 retire_pending_relay(&inner, pending);
1832 }
1833 },
1834 format_args!(
1835 "napi-async-runtime: host timer callback failed after this relay failure was already \
1836 accounted: {error}"
1837 ),
1838 );
1839 }
1840 }
1841 relay_drop_guard.disarm();
1842 }
1843 }
1844 });
1845 if let Err(rejected_relay) = submission {
1846 drop(rejected_relay);
1849 }
1850 }
1851
1852 fn cancel(&self, id: TimerId) {
1853 let pending = {
1854 let mut pending = self.inner.lock_pending();
1855 if let Some(timer) = pending.get(&id) {
1856 timer
1857 .relay_health
1858 .set_cancellation_accounting(RelayCancellationAccounting::HostHealth);
1859 }
1860 pending.remove(&id)
1861 };
1862 if let Some(mut pending) = pending {
1863 pending.signal_native_cancellation();
1864 if pending.schedule_state == RelayScheduleState::CallbackComplete {
1865 let relay_health = std::sync::Arc::clone(&pending.relay_health);
1866 self.inner.cancel_relay(pending.relay_id, relay_health);
1867 }
1868 drop_host_timer_waker_safely(pending.waker);
1869 }
1870 }
1871
1872 fn is_live(&self) -> bool {
1873 self.inner.is_live()
1874 }
1875
1876 fn on_swept(&self) {
1877 self.inner.evict();
1882 }
1883}
1884
1885#[napi(
1886 ts_args_type = "registrationHigh: number, registrationLow: number, schedule: (id: number, ms: number) => Promise<void>, cancel: (id: number) => void"
1887)]
1888pub fn register_timer_host(
1894 env: &napi::Env,
1895 registration_high: u32,
1896 registration_low: u32,
1897 schedule: JsCallback<FnArgs<(u32, f64)>, Promise<()>>,
1898 cancel: JsCallback<FnArgs<(u32,)>, ()>,
1899) -> napi::Result<()> {
1900 let host_registration = claim_host_registration_id(registration_high, registration_low)?;
1901 let inner = std::sync::Arc::new(JsTimerHostInner {
1902 callback: schedule,
1903 cancel_callback: cancel,
1904 pending: std::sync::Mutex::default(),
1905 relay_ids: RelayIdAllocator::default(),
1906 dead: std::sync::atomic::AtomicBool::new(false),
1907 host_registration,
1908 registration: std::sync::Mutex::default(),
1909 transient_failures: std::sync::atomic::AtomicU32::new(0),
1910 });
1911 if !install_host_driver_registration(
1912 &inner.dead,
1913 &inner.registration,
1914 || {
1915 register_timer_driver(std::sync::Arc::new(JsTimerHost {
1916 inner: std::sync::Arc::clone(&inner),
1917 }))
1918 },
1919 || {
1920 JS_TIMER_HOSTS
1921 .lock()
1922 .unwrap_or_else(std::sync::PoisonError::into_inner)
1923 .insert(host_registration, std::sync::Arc::downgrade(&inner));
1924 },
1925 unregister_timer_driver,
1926 ) {
1927 return Err(napi::Error::new(
1928 napi::Status::GenericFailure,
1929 "The CurrentThread timer host was evicted during registration",
1930 ));
1931 }
1932 let hook_inner = std::sync::Arc::clone(&inner);
1936 install_cleanup_hook_or_rollback(
1937 || {
1938 env.add_env_cleanup_hook(hook_inner, |inner| {
1939 run_host_timer_cleanup_safely(|| inner.evict());
1940 })
1941 },
1942 || run_host_timer_cleanup_safely(|| inner.evict()),
1943 )?;
1944 Ok(())
1945}
1946
1947#[napi]
1948pub fn unregister_timer_host(registration_high: u32, registration_low: u32) {
1951 let id = BindingHostRegistration::id(registration_high, registration_low);
1952 release_host_registration_id(id);
1953 if let Some(inner) = registered_timer_host(id) {
1954 inner.evict();
1955 }
1956}
1957
1958pub fn install(options: RuntimeOptions) -> napi::Result<()> {
1968 configure(options).map_err(|error| napi::Error::from_reason(error.to_string()))?;
1969 register_async_runtime(SharedAsyncRuntime);
1970 Ok(())
1971}
1972
1973pub fn resolve_thread_count(raw: Option<String>, default: usize, maximum: usize) -> usize {
1981 assert!(default > 0, "the default thread count must be positive");
1982 assert!(maximum > 0, "the maximum thread count must be positive");
1983 raw
1984 .and_then(|value| value.parse::<usize>().ok())
1985 .filter(|&value| value != 0)
1986 .unwrap_or(default)
1987 .min(maximum)
1988}
1989
1990#[cfg(test)]
1991mod tests {
1992 use super::{
1993 BindingHostRegistration, BindingRuntimeFlavor, BindingRuntimeOptions,
1994 HOST_TIMER_MAX_TRANSIENT_FAILURES, HostTimerFailureAction, HostTimerRelayHealth,
1995 MAX_SAFE_JS_INTEGER, NATIVE_TASK_HOST_AFTER_DRIVE_TEST_HOOK,
1996 NATIVE_TASK_HOST_AFTER_PAYLOAD_DROP_TEST_HOOK, NativeCurrentThreadTaskHostInner,
1997 NativeCurrentThreadTaskHostPayload, PendingHostTimer, PendingHostTimerRegistration,
1998 PendingRelayDropGuard, PendingRelayState, RelayCancellationAccounting, RelayIdAllocator,
1999 RelayScheduleState, SharedAsyncRuntime, call_native_current_thread_task_host,
2000 claim_host_registration_id, complete_relay_schedule_callback,
2001 get_current_thread_task_host_contract_version, install_cleanup_hook_or_rollback,
2002 install_host_driver_registration, record_host_timer_failure, recover_host_timer_failure,
2003 register_pending_host_timer, register_pending_host_timer_if_live,
2004 reserve_current_thread_host_registration, reset_host_timer_failures_after_success,
2005 resolve_thread_count, retire_pending_relay, retire_pending_relays, safe_js_number,
2006 take_pending_host_timers, unregister_current_thread_task_host, wake_host_timer_safely,
2007 };
2008
2009 #[derive(Default)]
2010 struct TestPendingRelayState {
2011 pending:
2012 std::sync::Mutex<rustc_hash::FxHashMap<crate::async_runtime::TimerId, PendingHostTimer>>,
2013 relay_ids: RelayIdAllocator,
2014 cancelled_relays: std::sync::Mutex<Vec<u32>>,
2015 panic_on_cancel: std::sync::atomic::AtomicBool,
2016 }
2017
2018 impl PendingRelayState for TestPendingRelayState {
2019 fn take_pending_relay(
2020 &self,
2021 id: crate::async_runtime::TimerId,
2022 relay_id: u32,
2023 accounting: RelayCancellationAccounting,
2024 ) -> Option<PendingHostTimer> {
2025 let mut pending = self
2026 .pending
2027 .lock()
2028 .unwrap_or_else(std::sync::PoisonError::into_inner);
2029 if pending
2030 .get(&id)
2031 .is_some_and(|slot| slot.relay_id == relay_id)
2032 {
2033 pending
2034 .get(&id)
2035 .expect("the matching test relay must remain present")
2036 .relay_health
2037 .set_cancellation_accounting(accounting);
2038 pending.remove(&id)
2039 } else {
2040 None
2041 }
2042 }
2043
2044 fn mark_relay_callback_complete(
2045 &self,
2046 id: crate::async_runtime::TimerId,
2047 relay_id: u32,
2048 ) -> bool {
2049 let mut pending = self
2050 .pending
2051 .lock()
2052 .unwrap_or_else(std::sync::PoisonError::into_inner);
2053 match pending.get_mut(&id) {
2054 Some(slot) if slot.relay_id == relay_id => {
2055 slot.schedule_state = RelayScheduleState::CallbackComplete;
2056 false
2057 }
2058 _ => true,
2059 }
2060 }
2061
2062 fn cancel_relay(
2063 self: &std::sync::Arc<Self>,
2064 relay_id: u32,
2065 _relay_health: std::sync::Arc<HostTimerRelayHealth>,
2066 ) {
2067 self
2068 .cancelled_relays
2069 .lock()
2070 .unwrap_or_else(std::sync::PoisonError::into_inner)
2071 .push(relay_id);
2072 assert!(
2073 !self
2074 .panic_on_cancel
2075 .load(std::sync::atomic::Ordering::SeqCst),
2076 "intentional relay cancellation panic"
2077 );
2078 }
2079 }
2080
2081 fn test_pending_relay_guard(
2082 state: &std::sync::Arc<TestPendingRelayState>,
2083 id: crate::async_runtime::TimerId,
2084 waker: std::task::Waker,
2085 schedule_state: RelayScheduleState,
2086 ) -> (
2087 u32,
2088 std::sync::Arc<HostTimerRelayHealth>,
2089 PendingRelayDropGuard<TestPendingRelayState>,
2090 ) {
2091 let relay_id = state
2092 .relay_ids
2093 .reserve()
2094 .expect("the test relay id must be available");
2095 let relay_health = std::sync::Arc::new(HostTimerRelayHealth::default());
2096 let replaced = state
2097 .pending
2098 .lock()
2099 .unwrap_or_else(std::sync::PoisonError::into_inner)
2100 .insert(
2101 id,
2102 PendingHostTimer {
2103 cancellation: None,
2104 relay_health: std::sync::Arc::clone(&relay_health),
2105 relay_id,
2106 waker,
2107 schedule_state,
2108 },
2109 );
2110 assert!(replaced.is_none(), "the test timer id must be vacant");
2111 (
2112 relay_id,
2113 relay_health,
2114 PendingRelayDropGuard::new(std::sync::Arc::clone(state), id, relay_id),
2115 )
2116 }
2117
2118 struct BudgetBoundaryTimerWait {
2119 state: std::sync::Arc<TestPendingRelayState>,
2120 timer_id: crate::async_runtime::TimerId,
2121 schedule_state: RelayScheduleState,
2122 registered: bool,
2123 }
2124
2125 impl std::future::Future for BudgetBoundaryTimerWait {
2126 type Output = ();
2127
2128 fn poll(
2129 mut self: std::pin::Pin<&mut Self>,
2130 cx: &mut std::task::Context<'_>,
2131 ) -> std::task::Poll<Self::Output> {
2132 assert!(
2133 !self.registered,
2134 "the timer wait must be cancelled before a second poll"
2135 );
2136 self.registered = true;
2137 let state = std::sync::Arc::clone(&self.state);
2138 let timer_id = self.timer_id;
2139 let (_, _, relay_drop_guard) =
2140 test_pending_relay_guard(&state, timer_id, cx.waker().clone(), self.schedule_state);
2141 crate::async_runtime::spawn_detached(async move {
2142 let mut relay_drop_guard = relay_drop_guard;
2143 std::future::pending::<()>().await;
2144 relay_drop_guard.disarm();
2145 });
2146 std::task::Poll::Pending
2147 }
2148 }
2149
2150 #[test]
2151 fn current_thread_task_host_contract_version_is_stable() {
2152 assert_eq!(get_current_thread_task_host_contract_version(), 4);
2153 }
2154
2155 #[test]
2156 fn host_registration_reservations_are_exact_and_single_use() {
2157 let claimed = reserve_current_thread_host_registration().unwrap();
2158 let claimed_id = BindingHostRegistration::id(claimed.high, claimed.low);
2159 assert_eq!(
2160 claim_host_registration_id(claimed.high, claimed.low).unwrap(),
2161 claimed_id,
2162 "the exact reserved capability must be claimable once"
2163 );
2164 assert!(
2165 claim_host_registration_id(claimed.high, claimed.low).is_err(),
2166 "a consumed registration capability must not be reusable"
2167 );
2168
2169 let released = reserve_current_thread_host_registration().unwrap();
2170 unregister_current_thread_task_host(released.high, released.low);
2171 assert!(
2172 claim_host_registration_id(released.high, released.low).is_err(),
2173 "unregister must release a reservation before installation"
2174 );
2175 }
2176
2177 fn native_task_host_with_raw_owner(raw: usize) -> NativeCurrentThreadTaskHostInner {
2178 NativeCurrentThreadTaskHostInner {
2179 threadsafe_function: std::sync::Mutex::new(Some(raw)),
2180 dead: std::sync::atomic::AtomicBool::new(false),
2181 environment_closing: std::sync::atomic::AtomicBool::new(false),
2182 host_registration: u64::MAX,
2183 registration: std::sync::Mutex::default(),
2184 }
2185 }
2186
2187 #[test]
2188 fn native_task_host_competing_release_paths_retire_the_raw_owner_once() {
2189 let inner = std::sync::Arc::new(native_task_host_with_raw_owner(1));
2190 let releases = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
2191 let barrier = std::sync::Arc::new(std::sync::Barrier::new(3));
2192 let mut threads = Vec::new();
2193
2194 for mode in [
2195 napi::sys::ThreadsafeFunctionReleaseMode::release,
2196 napi::sys::ThreadsafeFunctionReleaseMode::abort,
2197 ] {
2198 let inner = std::sync::Arc::clone(&inner);
2199 let releases = std::sync::Arc::clone(&releases);
2200 let barrier = std::sync::Arc::clone(&barrier);
2201 threads.push(std::thread::spawn(move || {
2202 barrier.wait();
2203 inner.release_threadsafe_function_with(mode, |threadsafe_function, _| {
2204 assert_eq!(threadsafe_function as usize, 1);
2205 releases.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2206 napi::sys::Status::napi_ok
2207 });
2208 }));
2209 }
2210
2211 barrier.wait();
2212 for thread in threads {
2213 thread.join().unwrap();
2214 }
2215 assert_eq!(releases.load(std::sync::atomic::Ordering::SeqCst), 1);
2216 assert!(
2217 inner
2218 .threadsafe_function
2219 .lock()
2220 .unwrap_or_else(std::sync::PoisonError::into_inner)
2221 .is_none()
2222 );
2223 }
2224
2225 #[test]
2226 fn native_task_host_closing_call_retires_owner_without_a_second_release() {
2227 let inner = native_task_host_with_raw_owner(1);
2228 let status = inner
2229 .call_threadsafe_function_with(std::ptr::null_mut(), |threadsafe_function, _| {
2230 assert_eq!(threadsafe_function as usize, 1);
2231 napi::sys::Status::napi_closing
2232 })
2233 .expect("the fake TSFN owner must be callable");
2234
2235 assert_eq!(status, napi::sys::Status::napi_closing);
2236 assert!(
2237 inner
2238 .environment_closing
2239 .load(std::sync::atomic::Ordering::SeqCst)
2240 );
2241 let releases = std::sync::atomic::AtomicUsize::new(0);
2242 inner.release_threadsafe_function_with(
2243 napi::sys::ThreadsafeFunctionReleaseMode::release,
2244 |_, _| {
2245 releases.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2246 napi::sys::Status::napi_ok
2247 },
2248 );
2249 assert_eq!(
2250 releases.load(std::sync::atomic::Ordering::SeqCst),
2251 0,
2252 "napi_closing already retired the initial acquisition and invalidated the pointer"
2253 );
2254 }
2255
2256 #[test]
2257 fn host_registration_capability_round_trips_without_precision_loss() {
2258 for id in [1, u64::from(u32::MAX), u64::from(u32::MAX) + 1, u64::MAX] {
2259 let registration = BindingHostRegistration::from_id(id);
2260 assert_eq!(
2261 BindingHostRegistration::id(registration.high, registration.low),
2262 id
2263 );
2264 }
2265 }
2266
2267 #[test]
2268 fn binding_runtime_options_convert_to_a_partial_core_patch() {
2269 use crate::async_runtime::{RuntimeFlavor, RuntimeOptionsPatch};
2270
2271 let patch = RuntimeOptionsPatch::try_from(BindingRuntimeOptions {
2272 flavor: Some(BindingRuntimeFlavor::CurrentThread),
2273 worker_threads: None,
2274 max_blocking_tasks: Some(7.0),
2275 })
2276 .expect("valid binding options must convert");
2277
2278 assert!(matches!(patch.flavor, Some(RuntimeFlavor::CurrentThread)));
2279 assert_eq!(patch.worker_threads, None);
2280 assert_eq!(patch.max_blocking_tasks, Some(7));
2281 }
2282
2283 #[test]
2284 fn binding_runtime_options_reject_unsafe_thread_counts() {
2285 use crate::async_runtime::RuntimeOptionsPatch;
2286
2287 for value in [
2288 -1.0,
2289 0.0,
2290 1.5,
2291 f64::NAN,
2292 f64::INFINITY,
2293 257.0,
2294 f64::from(u32::MAX) + 1.0,
2295 ] {
2296 let error = RuntimeOptionsPatch::try_from(BindingRuntimeOptions {
2297 flavor: None,
2298 worker_threads: Some(value),
2299 max_blocking_tasks: None,
2300 })
2301 .expect_err("invalid worker thread counts must be rejected");
2302 assert!(
2303 error
2304 .reason
2305 .contains("`workerThreads` must be a positive integer"),
2306 "unexpected validation error: {}",
2307 error.reason
2308 );
2309 }
2310
2311 for value in [-1.0, 257.0] {
2312 let error = RuntimeOptionsPatch::try_from(BindingRuntimeOptions {
2313 flavor: None,
2314 worker_threads: None,
2315 max_blocking_tasks: Some(value),
2316 })
2317 .expect_err("invalid blocking task counts must be rejected");
2318 assert!(
2319 error
2320 .reason
2321 .contains("`maxBlockingTasks` must be a positive integer"),
2322 "unexpected validation error: {}",
2323 error.reason
2324 );
2325 }
2326 }
2327
2328 #[test]
2329 fn runtime_metrics_preserve_safe_javascript_integer_range() {
2330 const BEYOND_U32_JS_NUMBER: f64 = 4_294_967_296.0;
2331 const MAX_SAFE_JS_NUMBER: f64 = 9_007_199_254_740_991.0;
2332
2333 let beyond_u32 = u64::from(u32::MAX) + 1;
2334 assert_eq!(
2335 safe_js_number(beyond_u32).to_bits(),
2336 BEYOND_U32_JS_NUMBER.to_bits()
2337 );
2338 assert_eq!(
2339 safe_js_number(MAX_SAFE_JS_INTEGER).to_bits(),
2340 MAX_SAFE_JS_NUMBER.to_bits()
2341 );
2342 assert_eq!(
2343 safe_js_number(MAX_SAFE_JS_INTEGER + 1).to_bits(),
2344 MAX_SAFE_JS_NUMBER.to_bits(),
2345 "metrics must remain exact JavaScript numbers instead of silently losing integer precision"
2346 );
2347 }
2348
2349 #[test]
2350 fn relay_ids_fail_closed_without_reuse_at_u32_exhaustion() {
2351 let allocator = RelayIdAllocator::default();
2352 allocator
2353 .next
2354 .store(u64::from(u32::MAX), std::sync::atomic::Ordering::Relaxed);
2355
2356 assert_eq!(allocator.reserve(), Ok(u32::MAX));
2357 assert!(
2358 allocator.reserve().is_err(),
2359 "the allocator must fail instead of wrapping to a reusable id"
2360 );
2361 assert!(
2362 allocator.reserve().is_err(),
2363 "exhaustion must remain stable instead of spinning or advancing toward wraparound"
2364 );
2365
2366 let pending = std::sync::Mutex::new(rustc_hash::FxHashMap::default());
2367 let waker = futures::task::noop_waker();
2368 let exhausted_waker = match register_pending_host_timer(&pending, &allocator, 1, waker.clone())
2369 {
2370 PendingHostTimerRegistration::Exhausted(waker) => waker,
2371 PendingHostTimerRegistration::Refreshed(_) | PendingHostTimerRegistration::Armed { .. } => {
2372 panic!("an exhausted allocator must reject a new pending timer")
2373 }
2374 };
2375 assert!(
2376 exhausted_waker.will_wake(&waker),
2377 "allocator failure must return the attempted timer's waker intact"
2378 );
2379 assert!(
2380 pending
2381 .lock()
2382 .unwrap_or_else(std::sync::PoisonError::into_inner)
2383 .is_empty(),
2384 "an exhausted allocator must not publish a pending timer without a relay capability"
2385 );
2386 }
2387
2388 #[test]
2389 fn host_timer_wake_and_drop_panics_are_contained_separately() {
2390 struct PanickingWakeAndDrop;
2391
2392 impl std::task::Wake for PanickingWakeAndDrop {
2393 fn wake(self: std::sync::Arc<Self>) {
2394 panic!("intentional host timer waker panic");
2395 }
2396
2397 fn wake_by_ref(self: &std::sync::Arc<Self>) {
2398 panic!("intentional host timer borrowed waker panic");
2399 }
2400 }
2401
2402 impl Drop for PanickingWakeAndDrop {
2403 fn drop(&mut self) {
2404 panic!("intentional host timer waker destructor panic");
2405 }
2406 }
2407
2408 let waker = std::task::Waker::from(std::sync::Arc::new(PanickingWakeAndDrop));
2409 wake_host_timer_safely(waker);
2410 }
2411
2412 #[test]
2413 fn relay_drop_guard_cleans_only_its_matching_pending_relay() {
2414 struct CountingWake(std::sync::Arc<std::sync::atomic::AtomicUsize>);
2415
2416 impl std::task::Wake for CountingWake {
2417 fn wake(self: std::sync::Arc<Self>) {
2418 self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2419 }
2420 }
2421
2422 let state = std::sync::Arc::new(TestPendingRelayState::default());
2423 let wakes = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
2424 let (relay_id, _, guard) = test_pending_relay_guard(
2425 &state,
2426 1,
2427 std::task::Waker::from(std::sync::Arc::new(CountingWake(std::sync::Arc::clone(
2428 &wakes,
2429 )))),
2430 RelayScheduleState::CallbackComplete,
2431 );
2432 drop(guard);
2433
2434 assert!(
2435 state
2436 .pending
2437 .lock()
2438 .unwrap_or_else(std::sync::PoisonError::into_inner)
2439 .is_empty()
2440 );
2441 assert_eq!(
2442 *state
2443 .cancelled_relays
2444 .lock()
2445 .unwrap_or_else(std::sync::PoisonError::into_inner),
2446 [relay_id],
2447 "an armed relay must be cancelled before its waiter is woken"
2448 );
2449 assert_eq!(
2450 wakes.load(std::sync::atomic::Ordering::SeqCst),
2451 1,
2452 "terminal relay cleanup must wake the pending timer"
2453 );
2454
2455 let stale_wakes = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
2456 let (stale_relay_id, _, stale_guard) = test_pending_relay_guard(
2457 &state,
2458 2,
2459 std::task::Waker::from(std::sync::Arc::new(CountingWake(std::sync::Arc::clone(
2460 &stale_wakes,
2461 )))),
2462 RelayScheduleState::AwaitingCallback,
2463 );
2464 let replacement_relay_id = state
2465 .relay_ids
2466 .reserve()
2467 .expect("the replacement relay id must be available");
2468 assert_ne!(
2469 replacement_relay_id, stale_relay_id,
2470 "relay ids must never be reused within one host"
2471 );
2472 let replaced = state
2473 .pending
2474 .lock()
2475 .unwrap_or_else(std::sync::PoisonError::into_inner)
2476 .insert(
2477 2,
2478 PendingHostTimer {
2479 cancellation: None,
2480 relay_health: std::sync::Arc::new(HostTimerRelayHealth::default()),
2481 relay_id: replacement_relay_id,
2482 waker: futures::task::noop_waker(),
2483 schedule_state: RelayScheduleState::CallbackComplete,
2484 },
2485 )
2486 .expect("the original relay must still be pending");
2487 drop(replaced);
2488 drop(stale_guard);
2489
2490 let replacement = state
2491 .pending
2492 .lock()
2493 .unwrap_or_else(std::sync::PoisonError::into_inner)
2494 .remove(&2)
2495 .expect("a stale guard must preserve the replacement relay");
2496 assert_eq!(replacement.relay_id, replacement_relay_id);
2497 assert_eq!(stale_wakes.load(std::sync::atomic::Ordering::SeqCst), 0);
2498 assert_eq!(
2499 *state
2500 .cancelled_relays
2501 .lock()
2502 .unwrap_or_else(std::sync::PoisonError::into_inner),
2503 [relay_id],
2504 "a stale guard must not cancel a replacement relay"
2505 );
2506 drop(replacement);
2507
2508 let disarmed_wakes = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
2509 let (disarmed_relay_id, _, mut disarmed_guard) = test_pending_relay_guard(
2510 &state,
2511 3,
2512 std::task::Waker::from(std::sync::Arc::new(CountingWake(std::sync::Arc::clone(
2513 &disarmed_wakes,
2514 )))),
2515 RelayScheduleState::CallbackComplete,
2516 );
2517 disarmed_guard.disarm();
2518 drop(disarmed_guard);
2519 let disarmed_pending = state
2520 .pending
2521 .lock()
2522 .unwrap_or_else(std::sync::PoisonError::into_inner)
2523 .remove(&3)
2524 .expect("normal relay completion must retain ownership of pending cleanup");
2525 assert_eq!(disarmed_pending.relay_id, disarmed_relay_id);
2526 assert_eq!(disarmed_wakes.load(std::sync::atomic::Ordering::SeqCst), 0);
2527 assert_eq!(
2528 *state
2529 .cancelled_relays
2530 .lock()
2531 .unwrap_or_else(std::sync::PoisonError::into_inner),
2532 [relay_id],
2533 "a disarmed guard must not change normal relay cleanup semantics"
2534 );
2535 drop(disarmed_pending);
2536 }
2537
2538 #[test]
2539 fn relay_callback_completion_precedes_delivery_and_closes_terminal_drop_window() {
2540 struct CountingWake(std::sync::Arc<std::sync::atomic::AtomicUsize>);
2541
2542 impl std::task::Wake for CountingWake {
2543 fn wake(self: std::sync::Arc<Self>) {
2544 self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2545 }
2546 }
2547
2548 let state = std::sync::Arc::new(TestPendingRelayState::default());
2549 let wakes = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
2550 let (relay_id, relay_health, guard) = test_pending_relay_guard(
2551 &state,
2552 1,
2553 std::task::Waker::from(std::sync::Arc::new(CountingWake(std::sync::Arc::clone(
2554 &wakes,
2555 )))),
2556 RelayScheduleState::AwaitingCallback,
2557 );
2558
2559 complete_relay_schedule_callback(&state, 1, relay_id, &relay_health, guard, drop);
2562
2563 assert!(
2564 state
2565 .pending
2566 .lock()
2567 .unwrap_or_else(std::sync::PoisonError::into_inner)
2568 .is_empty()
2569 );
2570 assert_eq!(
2571 *state
2572 .cancelled_relays
2573 .lock()
2574 .unwrap_or_else(std::sync::PoisonError::into_inner),
2575 [relay_id],
2576 "terminal drop during return-value delivery must cancel the synchronously armed timeout"
2577 );
2578 assert_eq!(wakes.load(std::sync::atomic::Ordering::SeqCst), 1);
2579 }
2580
2581 #[test]
2582 fn relay_callback_completion_finishes_cleanup_that_won_before_js_returned() {
2583 struct CountingWake(std::sync::Arc<std::sync::atomic::AtomicUsize>);
2584
2585 impl std::task::Wake for CountingWake {
2586 fn wake(self: std::sync::Arc<Self>) {
2587 self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2588 }
2589 }
2590
2591 let state = std::sync::Arc::new(TestPendingRelayState::default());
2592 let wakes = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
2593 let (relay_id, relay_health, guard) = test_pending_relay_guard(
2594 &state,
2595 1,
2596 std::task::Waker::from(std::sync::Arc::new(CountingWake(std::sync::Arc::clone(
2597 &wakes,
2598 )))),
2599 RelayScheduleState::AwaitingCallback,
2600 );
2601
2602 drop(guard);
2603 assert!(
2604 state
2605 .cancelled_relays
2606 .lock()
2607 .unwrap_or_else(std::sync::PoisonError::into_inner)
2608 .is_empty(),
2609 "cancellation must not overtake a schedule callback that has not returned"
2610 );
2611
2612 complete_relay_schedule_callback(&state, 1, relay_id, &relay_health, (), drop);
2613 assert_eq!(
2614 *state
2615 .cancelled_relays
2616 .lock()
2617 .unwrap_or_else(std::sync::PoisonError::into_inner),
2618 [relay_id],
2619 "the callback return handshake must finish exact cancellation after timer creation"
2620 );
2621 assert_eq!(wakes.load(std::sync::atomic::Ordering::SeqCst), 1);
2622 }
2623
2624 #[test]
2625 fn relay_terminal_cleanup_contains_cancel_panics_and_still_wakes() {
2626 struct CountingWake(std::sync::Arc<std::sync::atomic::AtomicUsize>);
2627
2628 impl std::task::Wake for CountingWake {
2629 fn wake(self: std::sync::Arc<Self>) {
2630 self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2631 }
2632 }
2633
2634 let state = std::sync::Arc::new(TestPendingRelayState::default());
2635 state
2636 .panic_on_cancel
2637 .store(true, std::sync::atomic::Ordering::SeqCst);
2638 let wakes = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
2639 let (relay_id, _, guard) = test_pending_relay_guard(
2640 &state,
2641 1,
2642 std::task::Waker::from(std::sync::Arc::new(CountingWake(std::sync::Arc::clone(
2643 &wakes,
2644 )))),
2645 RelayScheduleState::CallbackComplete,
2646 );
2647
2648 drop(guard);
2649
2650 assert_eq!(
2651 *state
2652 .cancelled_relays
2653 .lock()
2654 .unwrap_or_else(std::sync::PoisonError::into_inner),
2655 [relay_id]
2656 );
2657 assert_eq!(
2658 wakes.load(std::sync::atomic::Ordering::SeqCst),
2659 1,
2660 "a panicking cancellation path must not suppress the pending timer wake"
2661 );
2662 }
2663
2664 #[test]
2665 fn relay_native_cancellation_settles_before_fallible_host_cancellation() {
2666 use futures::FutureExt as _;
2667
2668 let state = std::sync::Arc::new(TestPendingRelayState::default());
2669 state
2670 .panic_on_cancel
2671 .store(true, std::sync::atomic::Ordering::SeqCst);
2672 let registration = register_pending_host_timer(
2673 &state.pending,
2674 &state.relay_ids,
2675 1,
2676 futures::task::noop_waker(),
2677 );
2678 let cancellation = match registration {
2679 PendingHostTimerRegistration::Armed { cancellation, .. } => cancellation,
2680 PendingHostTimerRegistration::Refreshed(_) | PendingHostTimerRegistration::Exhausted(_) => {
2681 panic!("the first timer registration must reserve a native cancellation relay")
2682 }
2683 };
2684 let mut pending = state
2685 .pending
2686 .lock()
2687 .unwrap_or_else(std::sync::PoisonError::into_inner)
2688 .remove(&1)
2689 .expect("the registered timer must remain pending");
2690 pending.schedule_state = RelayScheduleState::CallbackComplete;
2691
2692 retire_pending_relay(&state, pending);
2693
2694 assert!(
2695 matches!(cancellation.now_or_never(), Some(Ok(()))),
2696 "native relay retirement must publish before the contained host cancellation panic"
2697 );
2698 }
2699
2700 #[test]
2701 fn relay_bulk_eviction_contains_each_cancel_panic_and_wakes_every_timer() {
2702 struct CountingWake(std::sync::Arc<std::sync::atomic::AtomicUsize>);
2703
2704 impl std::task::Wake for CountingWake {
2705 fn wake(self: std::sync::Arc<Self>) {
2706 self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2707 }
2708 }
2709
2710 let state = std::sync::Arc::new(TestPendingRelayState::default());
2711 state
2712 .panic_on_cancel
2713 .store(true, std::sync::atomic::Ordering::SeqCst);
2714 let wakes = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
2715 let pending = rustc_hash::FxHashMap::from_iter([7, 8].map(|relay_id| {
2716 (
2717 u64::from(relay_id),
2718 PendingHostTimer {
2719 cancellation: None,
2720 relay_health: std::sync::Arc::new(HostTimerRelayHealth::default()),
2721 relay_id,
2722 waker: std::task::Waker::from(std::sync::Arc::new(CountingWake(std::sync::Arc::clone(
2723 &wakes,
2724 )))),
2725 schedule_state: RelayScheduleState::CallbackComplete,
2726 },
2727 )
2728 }));
2729
2730 retire_pending_relays(&state, pending);
2731
2732 let mut cancelled = state
2733 .cancelled_relays
2734 .lock()
2735 .unwrap_or_else(std::sync::PoisonError::into_inner)
2736 .clone();
2737 cancelled.sort_unstable();
2738 assert_eq!(cancelled, [7, 8]);
2739 assert_eq!(
2740 wakes.load(std::sync::atomic::Ordering::SeqCst),
2741 2,
2742 "one panicking cancellation must not suppress any later pending timer wake"
2743 );
2744 }
2745
2746 #[test]
2747 fn failed_relay_retirement_cancels_only_after_the_schedule_callback_returns() {
2748 struct CountingWake(std::sync::Arc<std::sync::atomic::AtomicUsize>);
2749
2750 impl std::task::Wake for CountingWake {
2751 fn wake(self: std::sync::Arc<Self>) {
2752 self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2753 }
2754 }
2755
2756 let state = std::sync::Arc::new(TestPendingRelayState::default());
2757 let wakes = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
2758 retire_pending_relay(
2759 &state,
2760 PendingHostTimer {
2761 cancellation: None,
2762 relay_health: std::sync::Arc::new(HostTimerRelayHealth::default()),
2763 relay_id: 7,
2764 waker: std::task::Waker::from(std::sync::Arc::new(CountingWake(std::sync::Arc::clone(
2765 &wakes,
2766 )))),
2767 schedule_state: RelayScheduleState::CallbackComplete,
2768 },
2769 );
2770 retire_pending_relay(
2771 &state,
2772 PendingHostTimer {
2773 cancellation: None,
2774 relay_health: std::sync::Arc::new(HostTimerRelayHealth::default()),
2775 relay_id: 8,
2776 waker: std::task::Waker::from(std::sync::Arc::new(CountingWake(std::sync::Arc::clone(
2777 &wakes,
2778 )))),
2779 schedule_state: RelayScheduleState::AwaitingCallback,
2780 },
2781 );
2782
2783 assert_eq!(
2784 *state
2785 .cancelled_relays
2786 .lock()
2787 .unwrap_or_else(std::sync::PoisonError::into_inner),
2788 [7],
2789 "a callback-complete failure may have armed a timeout, while a failed TSFN submission did not"
2790 );
2791 assert_eq!(
2792 wakes.load(std::sync::atomic::Ordering::SeqCst),
2793 2,
2794 "both failed relays must wake their timer operations"
2795 );
2796 }
2797
2798 #[test]
2799 fn host_timer_cleanup_contains_panicking_payload_drop_across_ffi() {
2800 const CHILD_ENV: &str = "NAPI_RUNTIME_TEST_HOST_TIMER_PANIC_PAYLOAD_CHILD";
2801
2802 if std::env::var_os(CHILD_ENV).is_some() {
2803 struct PanicOnDrop;
2804
2805 impl Drop for PanicOnDrop {
2806 fn drop(&mut self) {
2807 panic!("intentional host timer panic payload destructor panic");
2808 }
2809 }
2810
2811 struct PanickingWake;
2812
2813 impl std::task::Wake for PanickingWake {
2814 fn wake(self: std::sync::Arc<Self>) {
2815 std::panic::panic_any(PanicOnDrop);
2816 }
2817 }
2818
2819 extern "C" fn cleanup_shim() {
2820 let waker = std::task::Waker::from(std::sync::Arc::new(PanickingWake));
2821 wake_host_timer_safely(waker);
2822 }
2823
2824 cleanup_shim();
2825 return;
2826 }
2827
2828 let output = std::process::Command::new(std::env::current_exe().unwrap())
2829 .arg("--exact")
2830 .arg("adapter::tests::host_timer_cleanup_contains_panicking_payload_drop_across_ffi")
2831 .arg("--nocapture")
2832 .env(CHILD_ENV, "1")
2833 .output()
2834 .expect("the host timer cleanup subprocess must start");
2835 assert!(
2836 output.status.success(),
2837 "host timer cleanup must contain panic-payload destruction across the C ABI; status={:?}\nstdout={}\nstderr={}",
2838 output.status.code(),
2839 String::from_utf8_lossy(&output.stdout),
2840 String::from_utf8_lossy(&output.stderr)
2841 );
2842 }
2843
2844 #[test]
2845 fn host_timer_failure_recovery_precedes_and_survives_diagnostic_panics() {
2846 struct PanickingDiagnostic;
2847
2848 impl std::fmt::Display for PanickingDiagnostic {
2849 fn fmt(&self, _formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2850 panic!("intentional host timer diagnostic panic");
2851 }
2852 }
2853
2854 let recovered = std::sync::atomic::AtomicBool::new(false);
2855 let diagnostic = PanickingDiagnostic;
2856 recover_host_timer_failure(
2857 || recovered.store(true, std::sync::atomic::Ordering::SeqCst),
2858 format_args!("{diagnostic}"),
2859 );
2860 assert!(
2861 recovered.load(std::sync::atomic::Ordering::SeqCst),
2862 "timer recovery must complete before fallible diagnostics run"
2863 );
2864 }
2865
2866 #[test]
2867 fn host_timer_registry_drops_wakers_after_unlock() {
2868 struct LockCheckingDrop {
2869 pending: std::sync::Weak<
2870 std::sync::Mutex<rustc_hash::FxHashMap<crate::async_runtime::TimerId, PendingHostTimer>>,
2871 >,
2872 dropped: std::sync::Arc<std::sync::atomic::AtomicBool>,
2873 }
2874
2875 #[expect(
2876 clippy::manual_noop_waker,
2877 reason = "the Arc-backed no-op waker exists to exercise its re-entrant destructor"
2878 )]
2879 impl std::task::Wake for LockCheckingDrop {
2880 fn wake(self: std::sync::Arc<Self>) {}
2881 }
2882
2883 impl Drop for LockCheckingDrop {
2884 fn drop(&mut self) {
2885 let pending = self
2886 .pending
2887 .upgrade()
2888 .expect("the pending registry must outlive its waker");
2889 let _guard = pending.try_lock().unwrap_or_else(|_| {
2890 panic!("a host-timer waker was dropped while the pending registry was locked")
2891 });
2892 self
2893 .dropped
2894 .store(true, std::sync::atomic::Ordering::SeqCst);
2895 }
2896 }
2897
2898 let pending = std::sync::Arc::new(std::sync::Mutex::new(rustc_hash::FxHashMap::default()));
2899 let relay_ids = RelayIdAllocator::default();
2900 let dropped = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
2901 let old_waker = std::task::Waker::from(std::sync::Arc::new(LockCheckingDrop {
2902 pending: std::sync::Arc::downgrade(&pending),
2903 dropped: std::sync::Arc::clone(&dropped),
2904 }));
2905 assert!(matches!(
2906 register_pending_host_timer(&pending, &relay_ids, 1, old_waker),
2907 PendingHostTimerRegistration::Armed { .. }
2908 ));
2909
2910 let replaced_waker =
2911 match register_pending_host_timer(&pending, &relay_ids, 1, futures::task::noop_waker()) {
2912 PendingHostTimerRegistration::Refreshed(waker) => waker,
2913 PendingHostTimerRegistration::Armed { .. } => {
2914 panic!("a re-poll must refresh the existing timer")
2915 }
2916 PendingHostTimerRegistration::Exhausted(_) => {
2917 panic!("the test allocator must not be exhausted")
2918 }
2919 };
2920 assert!(
2921 !dropped.load(std::sync::atomic::Ordering::SeqCst),
2922 "the helper must return ownership instead of dropping under its lock"
2923 );
2924 drop(replaced_waker);
2925 assert!(dropped.load(std::sync::atomic::Ordering::SeqCst));
2926
2927 let evicted_waker_dropped = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
2928 let evicted_waker = std::task::Waker::from(std::sync::Arc::new(LockCheckingDrop {
2929 pending: std::sync::Arc::downgrade(&pending),
2930 dropped: std::sync::Arc::clone(&evicted_waker_dropped),
2931 }));
2932 assert!(matches!(
2933 register_pending_host_timer(&pending, &relay_ids, 2, evicted_waker),
2934 PendingHostTimerRegistration::Armed { .. }
2935 ));
2936 let evicted = take_pending_host_timers(&pending, RelayCancellationAccounting::CleanupOnly);
2937 assert!(
2938 !evicted_waker_dropped.load(std::sync::atomic::Ordering::SeqCst),
2939 "bulk eviction must move wakers out instead of dropping under its lock"
2940 );
2941 drop(evicted);
2942 assert!(evicted_waker_dropped.load(std::sync::atomic::Ordering::SeqCst));
2943 }
2944
2945 #[test]
2946 fn host_timer_eviction_prevents_pending_registration_after_its_drain() {
2947 let pending = std::sync::Arc::new(std::sync::Mutex::new(rustc_hash::FxHashMap::default()));
2948 let relay_ids = std::sync::Arc::new(RelayIdAllocator::default());
2949 let dead = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
2950 let mut pending_guard = pending
2951 .lock()
2952 .unwrap_or_else(std::sync::PoisonError::into_inner);
2953 let (started_tx, started_rx) = std::sync::mpsc::sync_channel(1);
2954 let registration_pending = std::sync::Arc::clone(&pending);
2955 let registration_relay_ids = std::sync::Arc::clone(&relay_ids);
2956 let registration_dead = std::sync::Arc::clone(&dead);
2957 let registration = std::thread::spawn(move || {
2958 started_tx.send(()).unwrap();
2959 register_pending_host_timer_if_live(
2960 ®istration_pending,
2961 ®istration_relay_ids,
2962 1,
2963 futures::task::noop_waker(),
2964 || !registration_dead.load(std::sync::atomic::Ordering::SeqCst),
2965 )
2966 });
2967
2968 started_rx.recv().unwrap();
2969 dead.store(true, std::sync::atomic::Ordering::SeqCst);
2970 let drained = std::mem::take(&mut *pending_guard);
2971 drop(pending_guard);
2972 drop(drained);
2973
2974 assert!(
2975 registration.join().unwrap().is_err(),
2976 "registration arriving after death publication and the pending drain must be rejected"
2977 );
2978 assert!(
2979 pending
2980 .lock()
2981 .unwrap_or_else(std::sync::PoisonError::into_inner)
2982 .is_empty(),
2983 "a stale selected driver must not publish a new pending arm after eviction drained the map"
2984 );
2985 }
2986
2987 #[test]
2988 fn cleanup_hook_registration_failure_rolls_back_host_registration() {
2989 let rollback_calls = std::sync::atomic::AtomicUsize::new(0);
2990 let result = install_cleanup_hook_or_rollback(
2991 || -> napi::Result<()> {
2992 Err(napi::Error::new(
2993 napi::Status::GenericFailure,
2994 "intentional cleanup-hook registration failure",
2995 ))
2996 },
2997 || {
2998 rollback_calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2999 },
3000 );
3001
3002 assert!(result.is_err());
3003 assert_eq!(rollback_calls.load(std::sync::atomic::Ordering::SeqCst), 1);
3004 }
3005
3006 #[test]
3007 fn host_driver_installation_allows_reentrant_eviction_before_publication() {
3008 let dead = std::sync::atomic::AtomicBool::new(false);
3009 let registration = std::sync::Mutex::new(None);
3010 let published = std::sync::atomic::AtomicBool::new(false);
3011 let rolled_back = std::sync::atomic::AtomicUsize::new(0);
3012
3013 let installed = install_host_driver_registration(
3014 &dead,
3015 ®istration,
3016 || {
3017 dead.store(true, std::sync::atomic::Ordering::SeqCst);
3018 let removed = registration
3019 .try_lock()
3020 .expect("the arbitrary installation callback must run outside the registration mutex")
3021 .take();
3022 assert!(
3023 removed.is_none(),
3024 "reentrant eviction must see no half-published registration"
3025 );
3026 17_u64
3027 },
3028 || {
3029 published.store(true, std::sync::atomic::Ordering::SeqCst);
3030 },
3031 |id| {
3032 assert_eq!(id, 17);
3033 let guard = registration
3034 .try_lock()
3035 .expect("rollback must run outside the registration mutex");
3036 drop(guard);
3037 rolled_back.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
3038 },
3039 );
3040
3041 assert!(
3042 !installed,
3043 "reentrant eviction must reject the exported registration"
3044 );
3045 assert!(!published.load(std::sync::atomic::Ordering::SeqCst));
3046 assert_eq!(rolled_back.load(std::sync::atomic::Ordering::SeqCst), 1);
3047 assert!(registration.lock().unwrap().is_none());
3048 }
3049
3050 #[test]
3051 fn shared_runtime_rejects_after_shutdown_and_accepts_after_restart() {
3052 use std::{
3053 sync::{
3054 Arc,
3055 atomic::{AtomicBool, Ordering},
3056 mpsc,
3057 },
3058 time::Duration,
3059 };
3060
3061 napi::bindgen_prelude::AsyncRuntime::start(&SharedAsyncRuntime)
3062 .expect("the adapter runtime must start");
3063 crate::async_runtime::reset_metrics();
3064 let (first_tx, first_rx) = mpsc::channel();
3065 napi::bindgen_prelude::AsyncRuntime::spawn_blocking(
3066 &SharedAsyncRuntime,
3067 Box::new(move || {
3068 first_tx
3069 .send(())
3070 .expect("test receiver must still be listening");
3071 }),
3072 )
3073 .unwrap_or_else(|_| panic!("the shared blocking lane must accept napi work"));
3074 first_rx
3075 .recv_timeout(Duration::from_secs(5))
3076 .expect("the shared blocking lane must execute accepted napi work");
3077
3078 napi::bindgen_prelude::AsyncRuntime::shutdown(&SharedAsyncRuntime)
3079 .expect("the adapter runtime must shut down");
3080 let block_on_ran = Arc::new(AtomicBool::new(false));
3081 let block_on_ran_future = Arc::clone(&block_on_ran);
3082 let mut block_on_future = std::pin::pin!(async move {
3083 block_on_ran_future.store(true, Ordering::SeqCst);
3084 });
3085 let block_on_error =
3086 napi::bindgen_prelude::AsyncRuntime::block_on(&SharedAsyncRuntime, block_on_future.as_mut())
3087 .expect_err("the adapter must reject block_on while stopped");
3088 assert_eq!(
3089 block_on_error.reason,
3090 "the async runtime is stopped; call start before submitting work"
3091 );
3092 assert!(
3093 !block_on_ran.load(Ordering::SeqCst),
3094 "rejected block_on must not poll its future"
3095 );
3096
3097 let rejected_ran = Arc::new(AtomicBool::new(false));
3098 let rejected_ran_work = Arc::clone(&rejected_ran);
3099 let (rejected_retry_tx, rejected_retry_rx) = mpsc::channel();
3100 let rejected = napi::bindgen_prelude::AsyncRuntime::spawn_blocking(
3101 &SharedAsyncRuntime,
3102 Box::new(move || {
3103 rejected_ran_work.store(true, Ordering::SeqCst);
3104 rejected_retry_tx
3105 .send(())
3106 .expect("test receiver must still be listening");
3107 }),
3108 )
3109 .expect_err("the adapter must reject work while stopped");
3110 assert!(!rejected_ran.load(Ordering::SeqCst));
3111 let (rejected, error) = rejected.into_parts();
3112 assert_eq!(
3113 error.reason,
3114 "the async runtime is stopped; call start before submitting work"
3115 );
3116
3117 napi::bindgen_prelude::AsyncRuntime::start(&SharedAsyncRuntime)
3118 .expect("the adapter runtime must restart");
3119 napi::bindgen_prelude::AsyncRuntime::block_on(&SharedAsyncRuntime, block_on_future.as_mut())
3120 .expect("the restarted adapter runtime must accept the retained future");
3121 assert!(block_on_ran.load(Ordering::SeqCst));
3122
3123 napi::bindgen_prelude::AsyncRuntime::spawn_blocking(&SharedAsyncRuntime, rejected)
3124 .unwrap_or_else(|_| panic!("the restarted runtime must accept the retained napi work"));
3125 rejected_retry_rx
3126 .recv_timeout(Duration::from_secs(5))
3127 .expect("the restarted runtime must execute the retained napi work");
3128 assert!(
3129 rejected_ran.load(Ordering::SeqCst),
3130 "rejected work must be returned intact"
3131 );
3132 assert!(
3133 crate::async_runtime::metrics().blocking_tasks_started >= 2,
3134 "both accepted adapter submissions should reach the shared blocking scheduler"
3135 );
3136 }
3137
3138 #[cfg(not(target_family = "wasm"))]
3139 #[test]
3140 fn native_task_host_null_env_retires_payload_and_recovers_exact_delivery() {
3141 const CHILD_ENV: &str = "NAPI_RUNTIME_TEST_NATIVE_TASK_HOST_NULL_ENV_CHILD";
3142
3143 if std::env::var_os(CHILD_ENV).is_some() {
3144 use std::sync::{Arc, mpsc};
3145 use std::time::Duration;
3146
3147 use crate::async_runtime::{
3148 CurrentThreadTaskDelivery, CurrentThreadTaskDriver, RuntimeFlavor, RuntimeOptions,
3149 configure, register_current_thread_task_driver, shutdown, spawn_detached, start,
3150 unregister_current_thread_task_driver,
3151 };
3152
3153 struct RecordingTaskDriver {
3154 dispatches: mpsc::Sender<CurrentThreadTaskDelivery>,
3155 }
3156
3157 impl CurrentThreadTaskDriver for RecordingTaskDriver {
3158 fn dispatch(&self, delivery: CurrentThreadTaskDelivery) -> bool {
3159 self.dispatches.send(delivery).is_ok()
3160 }
3161 }
3162
3163 configure(RuntimeOptions {
3164 flavor: RuntimeFlavor::CurrentThread,
3165 worker_threads: 1,
3166 max_blocking_tasks: 1,
3167 ..RuntimeOptions::default()
3168 })
3169 .expect("the isolated runtime must accept CurrentThread configuration");
3170 start().expect("the isolated CurrentThread runtime must start");
3171
3172 let (dispatch_tx, dispatch_rx) = mpsc::channel();
3173 let driver_id = register_current_thread_task_driver(Arc::new(RecordingTaskDriver {
3174 dispatches: dispatch_tx,
3175 }));
3176 let (completed_tx, completed_rx) = mpsc::channel();
3177 spawn_detached(async move {
3178 completed_tx
3179 .send(())
3180 .expect("the completion observer must remain live");
3181 });
3182
3183 let payload_drops = Arc::new(std::sync::atomic::AtomicUsize::new(0));
3184 let first_delivery = dispatch_rx
3185 .recv_timeout(Duration::from_secs(2))
3186 .expect("the queued task must publish its first host delivery");
3187 let first_payload = Box::into_raw(Box::new(
3188 NativeCurrentThreadTaskHostPayload::with_drop_observer(
3189 first_delivery,
3190 Arc::clone(&payload_drops),
3191 ),
3192 ))
3193 .cast();
3194 unsafe {
3195 call_native_current_thread_task_host(
3196 std::ptr::null_mut(),
3197 std::ptr::null_mut(),
3198 std::ptr::null_mut(),
3199 first_payload,
3200 );
3201 }
3202 assert_eq!(
3203 payload_drops.load(std::sync::atomic::Ordering::SeqCst),
3204 1,
3205 "the null-env callback must destroy its queued payload exactly once"
3206 );
3207
3208 let replacement_delivery = dispatch_rx
3209 .recv_timeout(Duration::from_secs(2))
3210 .expect("the exact failed delivery must publish one replacement");
3211 assert_ne!(
3212 replacement_delivery, first_delivery,
3213 "recovery must use a fresh registration-scoped delivery capability"
3214 );
3215 let replacement_payload = Box::into_raw(Box::new(
3216 NativeCurrentThreadTaskHostPayload::with_drop_observer(
3217 replacement_delivery,
3218 Arc::clone(&payload_drops),
3219 ),
3220 ))
3221 .cast();
3222 let fake_env = std::ptr::NonNull::<std::ffi::c_void>::dangling()
3223 .as_ptr()
3224 .cast();
3225 unsafe {
3226 call_native_current_thread_task_host(
3227 fake_env,
3228 std::ptr::null_mut(),
3229 std::ptr::null_mut(),
3230 replacement_payload,
3231 );
3232 }
3233
3234 completed_rx
3235 .recv_timeout(Duration::from_secs(2))
3236 .expect("the replacement host delivery must run the queued task");
3237 assert_eq!(
3238 payload_drops.load(std::sync::atomic::Ordering::SeqCst),
3239 2,
3240 "successful recovery must also destroy its queued payload exactly once"
3241 );
3242 assert!(
3243 matches!(dispatch_rx.try_recv(), Err(mpsc::TryRecvError::Empty)),
3244 "successful replacement acknowledgement must not publish another delivery"
3245 );
3246
3247 unregister_current_thread_task_driver(driver_id);
3248 shutdown().expect("the isolated CurrentThread runtime must shut down cleanly");
3249 return;
3250 }
3251
3252 let output = std::process::Command::new(std::env::current_exe().unwrap())
3253 .arg("--exact")
3254 .arg("adapter::tests::native_task_host_null_env_retires_payload_and_recovers_exact_delivery")
3255 .arg("--nocapture")
3256 .env(CHILD_ENV, "1")
3257 .output()
3258 .expect("the null-env task-host subprocess must start");
3259 assert!(
3260 output.status.success(),
3261 "the null-env task-host regression failed; status={:?}\nstdout={}\nstderr={}",
3262 output.status.code(),
3263 String::from_utf8_lossy(&output.stdout),
3264 String::from_utf8_lossy(&output.stderr)
3265 );
3266 }
3267
3268 #[cfg(not(target_family = "wasm"))]
3269 #[test]
3270 fn native_task_host_callback_lease_covers_ack_payload_and_restart() {
3271 const CHILD_ENV: &str = "NAPI_RUNTIME_TEST_NATIVE_TASK_HOST_CALLBACK_LEASE_CHILD";
3272
3273 if std::env::var_os(CHILD_ENV).is_some() {
3274 use std::sync::{Arc, mpsc};
3275 use std::time::{Duration, Instant};
3276
3277 use crate::async_runtime::{
3278 CurrentThreadTaskDelivery, CurrentThreadTaskDriver, RuntimeFlavor, RuntimeOptions,
3279 configure, register_current_thread_task_driver, shutdown, spawn_detached, start,
3280 try_spawn_detached, unregister_current_thread_task_driver,
3281 };
3282
3283 struct RecordingTaskDriver {
3284 dispatches: mpsc::Sender<CurrentThreadTaskDelivery>,
3285 }
3286
3287 impl CurrentThreadTaskDriver for RecordingTaskDriver {
3288 fn dispatch(&self, delivery: CurrentThreadTaskDelivery) -> bool {
3289 self.dispatches.send(delivery).is_ok()
3290 }
3291 }
3292
3293 configure(RuntimeOptions {
3294 flavor: RuntimeFlavor::CurrentThread,
3295 worker_threads: 1,
3296 max_blocking_tasks: 1,
3297 ..RuntimeOptions::default()
3298 })
3299 .expect("the isolated runtime must accept CurrentThread configuration");
3300 start().expect("the isolated CurrentThread runtime must start");
3301
3302 let (dispatch_tx, dispatch_rx) = mpsc::channel();
3303 let driver_id = register_current_thread_task_driver(Arc::new(RecordingTaskDriver {
3304 dispatches: dispatch_tx,
3305 }));
3306 let (task_completed_tx, task_completed_rx) = mpsc::channel();
3307 spawn_detached(async move {
3308 task_completed_tx
3309 .send(())
3310 .expect("the task completion observer must remain live");
3311 });
3312 let delivery = dispatch_rx
3313 .recv_timeout(Duration::from_secs(2))
3314 .expect("the queued task must publish one native host delivery");
3315
3316 let payload_drops = Arc::new(std::sync::atomic::AtomicUsize::new(0));
3317 let payload = Box::into_raw(Box::new(
3318 NativeCurrentThreadTaskHostPayload::with_drop_observer(
3319 delivery,
3320 Arc::clone(&payload_drops),
3321 ),
3322 ))
3323 .cast::<std::ffi::c_void>() as usize;
3324 let (after_drive_tx, after_drive_rx) = mpsc::channel();
3325 let (release_after_drive_tx, release_after_drive_rx) = mpsc::channel();
3326 let (after_payload_tx, after_payload_rx) = mpsc::channel();
3327 let (release_after_payload_tx, release_after_payload_rx) = mpsc::channel();
3328 let callback = std::thread::spawn(move || {
3329 NATIVE_TASK_HOST_AFTER_DRIVE_TEST_HOOK.with(|slot| {
3330 *slot.borrow_mut() = Some(Box::new(move || {
3331 after_drive_tx.send(()).unwrap();
3332 release_after_drive_rx.recv().unwrap();
3333 }));
3334 });
3335 NATIVE_TASK_HOST_AFTER_PAYLOAD_DROP_TEST_HOOK.with(|slot| {
3336 *slot.borrow_mut() = Some(Box::new(move || {
3337 after_payload_tx.send(()).unwrap();
3338 release_after_payload_rx.recv().unwrap();
3339 }));
3340 });
3341 let fake_env = std::ptr::NonNull::<std::ffi::c_void>::dangling()
3342 .as_ptr()
3343 .cast();
3344 unsafe {
3345 call_native_current_thread_task_host(
3346 fake_env,
3347 std::ptr::null_mut(),
3348 std::ptr::null_mut(),
3349 payload as *mut std::ffi::c_void,
3350 );
3351 }
3352 });
3353
3354 after_drive_rx
3355 .recv_timeout(Duration::from_secs(2))
3356 .expect("the native callback must pause after driving the host turn");
3357 task_completed_rx
3358 .recv_timeout(Duration::from_secs(2))
3359 .expect("the host turn must run the queued task before acknowledgement");
3360
3361 let (shutdown_tx, shutdown_rx) = mpsc::channel();
3362 let shutdown_thread = std::thread::spawn(move || {
3363 shutdown_tx.send(shutdown()).unwrap();
3364 });
3365 let stopping_deadline = Instant::now() + Duration::from_secs(10);
3368 loop {
3369 match try_spawn_detached(async {}) {
3370 Ok(()) => {
3371 assert!(
3372 Instant::now() < stopping_deadline,
3373 "shutdown did not publish the stopping lifecycle"
3374 );
3375 std::thread::yield_now();
3376 }
3377 Err(future) => {
3378 drop(future);
3379 break;
3380 }
3381 }
3382 }
3383 assert!(
3384 shutdown_rx
3385 .recv_timeout(Duration::from_millis(200))
3386 .is_err(),
3387 "shutdown must wait after drive until delivery acknowledgement and payload destruction"
3388 );
3389
3390 let (restart_tx, restart_rx) = mpsc::channel();
3391 let restart_thread = std::thread::spawn(move || {
3392 start().expect("the runtime must restart after the old callback retires");
3393 restart_tx.send(()).unwrap();
3394 });
3395 assert!(
3396 restart_rx.recv_timeout(Duration::from_millis(200)).is_err(),
3397 "restart must not overlap the old callback before acknowledgement"
3398 );
3399
3400 release_after_drive_tx.send(()).unwrap();
3401 after_payload_rx
3402 .recv_timeout(Duration::from_secs(2))
3403 .expect("the callback must pause after acknowledging and destroying its payload");
3404 assert_eq!(
3405 payload_drops.load(std::sync::atomic::Ordering::SeqCst),
3406 1,
3407 "the acknowledged callback must destroy its payload before releasing the lease"
3408 );
3409 assert!(
3410 shutdown_rx
3411 .recv_timeout(Duration::from_millis(200))
3412 .is_err(),
3413 "shutdown must remain blocked after acknowledgement and payload destruction"
3414 );
3415 assert!(
3416 restart_rx.recv_timeout(Duration::from_millis(200)).is_err(),
3417 "restart must remain blocked until the callback lease retires"
3418 );
3419
3420 release_after_payload_tx.send(()).unwrap();
3421 callback.join().unwrap();
3422 shutdown_rx
3423 .recv_timeout(Duration::from_secs(2))
3424 .expect("shutdown must finish after the callback lease retires")
3425 .unwrap();
3426 restart_rx
3427 .recv_timeout(Duration::from_secs(2))
3428 .expect("restart must finish after old-generation shutdown");
3429 shutdown_thread.join().unwrap();
3430 restart_thread.join().unwrap();
3431
3432 unregister_current_thread_task_driver(driver_id);
3433 shutdown().expect("the replacement CurrentThread runtime must shut down cleanly");
3434 return;
3435 }
3436
3437 let output = std::process::Command::new(std::env::current_exe().unwrap())
3438 .arg("--exact")
3439 .arg("adapter::tests::native_task_host_callback_lease_covers_ack_payload_and_restart")
3440 .arg("--nocapture")
3441 .env(CHILD_ENV, "1")
3442 .output()
3443 .expect("the native task-host callback lease subprocess must start");
3444 assert!(
3445 output.status.success(),
3446 "the native task-host callback lease regression failed; status={:?}\nstdout={}\nstderr={}",
3447 output.status.code(),
3448 String::from_utf8_lossy(&output.stdout),
3449 String::from_utf8_lossy(&output.stderr)
3450 );
3451 }
3452
3453 #[cfg(not(target_family = "wasm"))]
3454 #[test]
3455 fn timer_relay_terminal_drop_at_host_turn_budget_settles_without_later_work() {
3456 const CHILD_ENV: &str = "NAPI_RUNTIME_TEST_TIMER_RELAY_TERMINAL_DROP_CHILD";
3457 const HOST_TURN_RUNNABLE_BUDGET: usize = 64;
3458
3459 if let Some(relay_state) = std::env::var_os(CHILD_ENV) {
3460 use std::sync::{Arc, mpsc};
3461 use std::time::Duration;
3462
3463 use crate::async_runtime::{
3464 CurrentThreadTaskDelivery, CurrentThreadTaskDriver, RuntimeFlavor, RuntimeOptions,
3465 acknowledge_current_thread_task_delivery, cancel_current_thread_task_dispatch, configure,
3466 drive_current_thread_tasks, metrics, register_current_thread_task_driver, reset_metrics,
3467 shutdown, spawn, spawn_detached, start, unregister_current_thread_task_driver,
3468 };
3469 use futures::FutureExt as _;
3470
3471 struct RecordingTaskDriver {
3472 dispatches: mpsc::Sender<CurrentThreadTaskDelivery>,
3473 }
3474
3475 impl CurrentThreadTaskDriver for RecordingTaskDriver {
3476 fn dispatch(&self, delivery: CurrentThreadTaskDelivery) -> bool {
3477 self.dispatches.send(delivery).is_ok()
3478 }
3479 }
3480
3481 let schedule_state = if relay_state == "complete" {
3482 RelayScheduleState::CallbackComplete
3483 } else {
3484 RelayScheduleState::AwaitingCallback
3485 };
3486
3487 let options = RuntimeOptions {
3488 flavor: RuntimeFlavor::CurrentThread,
3489 worker_threads: 1,
3490 max_blocking_tasks: 1,
3491 ..RuntimeOptions::default()
3492 };
3493 configure(options).expect("the isolated runtime must accept CurrentThread configuration");
3494 start().expect("the isolated CurrentThread runtime must start");
3495 reset_metrics();
3496
3497 let (dispatch_tx, dispatch_rx) = mpsc::channel();
3498 let driver_id = register_current_thread_task_driver(Arc::new(RecordingTaskDriver {
3499 dispatches: dispatch_tx,
3500 }));
3501
3502 for _ in 0..HOST_TURN_RUNNABLE_BUDGET - 1 {
3503 spawn_detached(async {});
3504 }
3505 let state = Arc::new(TestPendingRelayState::default());
3506 let timer_task = spawn(BudgetBoundaryTimerWait {
3507 state: Arc::clone(&state),
3508 timer_id: 1,
3509 schedule_state,
3510 registered: false,
3511 });
3512 let first_dispatch = dispatch_rx
3513 .recv_timeout(Duration::from_secs(2))
3514 .expect("the initial host turn must be published");
3515 assert!(
3516 matches!(dispatch_rx.try_recv(), Err(mpsc::TryRecvError::Empty)),
3517 "all 64 initial runnables must coalesce behind one host turn"
3518 );
3519
3520 let callback_lease = drive_current_thread_tasks(first_dispatch.capability())
3521 .expect("the exact host delivery must be admitted");
3522 acknowledge_current_thread_task_delivery(first_dispatch);
3523 drop(callback_lease);
3524 assert_eq!(
3525 metrics().runnable_polls,
3526 HOST_TURN_RUNNABLE_BUDGET as u64,
3527 "the timer operation must occupy the last poll in the host-turn budget"
3528 );
3529 assert_eq!(
3530 state
3531 .pending
3532 .lock()
3533 .unwrap_or_else(std::sync::PoisonError::into_inner)
3534 .len(),
3535 1,
3536 "the timer poll must register its pending relay"
3537 );
3538 assert_eq!(
3539 metrics().queued_runnables,
3540 1,
3541 "the unpolled relay must be the sole runnable beyond the budget boundary"
3542 );
3543
3544 let failed_dispatch = dispatch_rx
3545 .recv_timeout(Duration::from_secs(2))
3546 .expect("budget exhaustion must publish a fresh relay turn");
3547 cancel_current_thread_task_dispatch(failed_dispatch.capability());
3548 let replacement_dispatch = dispatch_rx
3549 .recv_timeout(Duration::from_secs(2))
3550 .expect("the first scheduler failure must publish one replacement");
3551 assert_ne!(replacement_dispatch, failed_dispatch);
3552 cancel_current_thread_task_dispatch(replacement_dispatch.capability());
3553
3554 assert!(
3555 state
3556 .pending
3557 .lock()
3558 .unwrap_or_else(std::sync::PoisonError::into_inner)
3559 .is_empty(),
3560 "terminal queue destruction must retire the matching pending relay"
3561 );
3562 let cancelled_relays = state
3563 .cancelled_relays
3564 .lock()
3565 .unwrap_or_else(std::sync::PoisonError::into_inner);
3566 if schedule_state == RelayScheduleState::CallbackComplete {
3567 assert_eq!(
3568 cancelled_relays.len(),
3569 1,
3570 "a callback-complete relay must queue exactly one host cancellation"
3571 );
3572 } else {
3573 assert!(
3574 cancelled_relays.is_empty(),
3575 "cleanup must not overtake a schedule callback that has not returned"
3576 );
3577 }
3578 drop(cancelled_relays);
3579 assert_eq!(metrics().queued_runnables, 0);
3580 assert!(
3581 matches!(dispatch_rx.try_recv(), Err(mpsc::TryRecvError::Empty)),
3582 "terminal recovery must stay bounded after the replacement failure"
3583 );
3584 let task_result = timer_task
3585 .now_or_never()
3586 .expect("the original timer operation must settle immediately");
3587 assert_eq!(
3588 task_result
3589 .expect_err("terminal recovery must cancel the pending timer operation")
3590 .to_string(),
3591 "async runtime stopped before the task completed"
3592 );
3593
3594 unregister_current_thread_task_driver(driver_id);
3595 shutdown().expect("the isolated CurrentThread runtime must shut down cleanly");
3596 return;
3597 }
3598
3599 for relay_state in ["awaiting", "complete"] {
3600 let output = std::process::Command::new(std::env::current_exe().unwrap())
3601 .arg("--exact")
3602 .arg(
3603 "adapter::tests::timer_relay_terminal_drop_at_host_turn_budget_settles_without_later_work",
3604 )
3605 .arg("--nocapture")
3606 .env(CHILD_ENV, relay_state)
3607 .output()
3608 .expect("the timer relay regression subprocess must start");
3609 assert!(
3610 output.status.success(),
3611 "{relay_state} timer relay terminal-drop regression failed; status={:?}\nstdout={}\nstderr={}",
3612 output.status.code(),
3613 String::from_utf8_lossy(&output.stdout),
3614 String::from_utf8_lossy(&output.stderr)
3615 );
3616 }
3617 }
3618
3619 #[test]
3626 fn relay_eviction_is_decided_by_status_and_probe_never_by_message() {
3627 use napi::Status;
3628
3629 use super::should_evict_for_relay_error;
3630
3631 assert!(
3636 should_evict_for_relay_error(Status::Closing, true),
3637 "Closing overrides a live probe"
3638 );
3639 assert!(should_evict_for_relay_error(Status::Closing, false));
3640
3641 assert!(should_evict_for_relay_error(Status::GenericFailure, false));
3645 assert!(should_evict_for_relay_error(
3646 Status::PendingException,
3647 false
3648 ));
3649
3650 assert!(
3656 !should_evict_for_relay_error(Status::GenericFailure, true),
3657 "a live host's GenericFailure -- e.g. a forged 'oneshot canceled' rejection -- must strike"
3658 );
3659 assert!(
3660 !should_evict_for_relay_error(Status::PendingException, true),
3661 "a JS throw on a live host must strike"
3662 );
3663 assert!(
3664 !should_evict_for_relay_error(Status::InvalidArg, true),
3665 "a wrong return type on a live host must strike"
3666 );
3667 assert!(
3668 !should_evict_for_relay_error(Status::QueueFull, true),
3669 "queue pressure on a live host must strike"
3670 );
3671 }
3672
3673 #[test]
3674 fn cancellation_failures_share_the_three_strike_budget_without_cleanup_double_counting() {
3675 use std::sync::atomic::{AtomicU32, Ordering};
3676
3677 use napi::Status;
3678
3679 let failures = AtomicU32::new(0);
3680 for expected_strikes in 1..HOST_TIMER_MAX_TRANSIENT_FAILURES {
3681 let relay_health = HostTimerRelayHealth::default();
3682 assert_eq!(
3683 record_host_timer_failure(&failures, &relay_health, Status::GenericFailure, true),
3684 HostTimerFailureAction::Retry(expected_strikes),
3685 );
3686 assert_eq!(
3687 record_host_timer_failure(&failures, &relay_health, Status::GenericFailure, true),
3688 HostTimerFailureAction::Duplicate,
3689 "one relay failure must consume at most one strike"
3690 );
3691 }
3692
3693 let cleanup_health = HostTimerRelayHealth::default();
3694 cleanup_health.set_cancellation_accounting(RelayCancellationAccounting::CleanupOnly);
3695 assert_eq!(
3696 cleanup_health.cancellation_accounting(),
3697 RelayCancellationAccounting::CleanupOnly
3698 );
3699 assert_eq!(
3700 failures.load(Ordering::SeqCst),
3701 HOST_TIMER_MAX_TRANSIENT_FAILURES - 1,
3702 "schedule-failure and eviction cleanup must not mutate host health"
3703 );
3704
3705 let final_health = HostTimerRelayHealth::default();
3706 assert_eq!(
3707 record_host_timer_failure(&failures, &final_health, Status::GenericFailure, true),
3708 HostTimerFailureAction::EvictHostAfterStrikes(HOST_TIMER_MAX_TRANSIENT_FAILURES),
3709 );
3710 }
3711
3712 #[test]
3713 fn a_same_relay_success_cannot_erase_its_recorded_failure() {
3714 use std::sync::atomic::{AtomicU32, Ordering};
3715
3716 use napi::Status;
3717
3718 let failures = AtomicU32::new(1);
3719 let healthy_relay = HostTimerRelayHealth::default();
3720 reset_host_timer_failures_after_success(&failures, &healthy_relay);
3721 assert_eq!(failures.load(Ordering::SeqCst), 0);
3722
3723 failures.store(1, Ordering::SeqCst);
3724 let failed_relay = HostTimerRelayHealth::default();
3725 assert_eq!(
3726 record_host_timer_failure(&failures, &failed_relay, Status::GenericFailure, true),
3727 HostTimerFailureAction::Retry(2),
3728 );
3729 reset_host_timer_failures_after_success(&failures, &failed_relay);
3730 assert_eq!(
3731 failures.load(Ordering::SeqCst),
3732 2,
3733 "a later success from the same relay must not hide its cancellation failure"
3734 );
3735 }
3736
3737 #[test]
3738 fn resolve_thread_count_handles_env_inputs() {
3739 const DEFAULT: usize = 8;
3740 const MAXIMUM: usize = 16;
3741 assert_eq!(
3745 resolve_thread_count(Some("0".to_string()), DEFAULT, MAXIMUM),
3746 DEFAULT
3747 );
3748 assert_eq!(
3750 resolve_thread_count(Some("4".to_string()), DEFAULT, MAXIMUM),
3751 4
3752 );
3753 assert_eq!(
3755 resolve_thread_count(Some("1000".to_string()), DEFAULT, MAXIMUM),
3756 MAXIMUM
3757 );
3758 assert_eq!(
3759 resolve_thread_count(Some(usize::MAX.to_string()), DEFAULT, MAXIMUM),
3760 MAXIMUM
3761 );
3762 assert_eq!(
3764 resolve_thread_count(Some("18446744073709551616".to_string()), DEFAULT, MAXIMUM,),
3765 DEFAULT
3766 );
3767 assert_eq!(
3769 resolve_thread_count(Some("abc".to_string()), DEFAULT, MAXIMUM),
3770 DEFAULT
3771 );
3772 assert_eq!(resolve_thread_count(None, DEFAULT, MAXIMUM), DEFAULT);
3774 assert_eq!(resolve_thread_count(None, 32, MAXIMUM), MAXIMUM);
3776 }
3777
3778 #[test]
3779 #[should_panic(expected = "the default thread count must be positive")]
3780 fn resolve_thread_count_rejects_an_invalid_default() {
3781 let _ = resolve_thread_count(None, 0, 1);
3782 }
3783}