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