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