@@ -436,10 +436,8 @@ pub struct JsRuntimeState {
436436 pub ( crate ) function_templates : Rc < RefCell < FunctionTemplateData > > ,
437437 pub ( crate ) callsite_prototype : RefCell < Option < v8:: Global < v8:: Object > > > ,
438438 waker : Arc < AtomicWaker > ,
439- /// Level-triggered flag set by the custom V8 platform when a foreground
440- /// task is posted. Checked and cleared each poll to ensure pumping even
441- /// if the edge-triggered AtomicWaker notification was lost.
442- has_foreground_task : Arc < std:: sync:: atomic:: AtomicBool > ,
439+ /// Guards the 100ms safety-net timer so at most one is active at a time.
440+ safety_net_active : Arc < std:: sync:: atomic:: AtomicBool > ,
443441 /// Accessed through [`JsRuntimeState::with_inspector`].
444442 inspector : RefCell < Option < Rc < JsRuntimeInspector > > > ,
445443 has_inspector : Cell < bool > ,
@@ -745,8 +743,6 @@ impl JsRuntime {
745743 // ...now let's set up ` JsRuntimeState`, we'll need to set some fields
746744 // later, after `JsRuntime` is all set up...
747745 let waker = op_state. waker . clone ( ) ;
748- let has_foreground_task =
749- Arc :: new ( std:: sync:: atomic:: AtomicBool :: new ( false ) ) ;
750746 let op_state = Rc :: new ( RefCell :: new ( op_state) ) ;
751747 let ( eval_context_get_code_cache_cb, eval_context_set_code_cache_cb) =
752748 options
@@ -769,7 +765,7 @@ impl JsRuntime {
769765 eval_context_set_code_cache_cb,
770766 ) ,
771767 waker : waker. clone ( ) ,
772- has_foreground_task : has_foreground_task . clone ( ) ,
768+ safety_net_active : Arc :: new ( std :: sync :: atomic :: AtomicBool :: new ( false ) ) ,
773769 // Some fields are initialized later after isolate is created
774770 inspector : None . into ( ) ,
775771 has_inspector : false . into ( ) ,
@@ -851,7 +847,6 @@ impl JsRuntime {
851847 setup:: register_isolate_waker (
852848 setup:: isolate_ptr_to_key ( isolate_ptr) ,
853849 waker. clone ( ) ,
854- has_foreground_task,
855850 ) ;
856851
857852 // ...isolate is fully set up, we can forward its pointer to the ops to finish
@@ -2113,14 +2108,7 @@ impl JsRuntime {
21132108 if has_inspector {
21142109 self . inspector ( ) . poll_sessions_from_event_loop ( cx) ;
21152110 }
2116- // Clear the foreground-task flag (set by the custom V8 platform callback).
2117- // If it was set, we must pump the message loop regardless of other state.
2118- let had_foreground_task = self
2119- . inner
2120- . state
2121- . has_foreground_task
2122- . swap ( false , std:: sync:: atomic:: Ordering :: Relaxed ) ;
2123- if poll_options. pump_v8_message_loop || had_foreground_task {
2111+ if poll_options. pump_v8_message_loop {
21242112 self . pump_v8_message_loop ( scope) ?;
21252113 }
21262114
@@ -2277,6 +2265,27 @@ impl JsRuntime {
22772265 scope. perform_microtask_checkpoint ( ) ;
22782266 }
22792267
2268+ // Safety net: if V8 has pending background tasks (e.g. module compilation),
2269+ // schedule a delayed wake to pump the message loop in case the platform
2270+ // callback was missed due to a race condition. Uses an OS thread (not
2271+ // tokio) to avoid depending on the async runtime being cooperative.
2272+ // The AtomicBool guard ensures at most one safety-net timer is active.
2273+ if pending_state. has_pending_background_tasks
2274+ && !self
2275+ . inner
2276+ . state
2277+ . safety_net_active
2278+ . swap ( true , std:: sync:: atomic:: Ordering :: Relaxed )
2279+ {
2280+ let waker = cx. waker ( ) . clone ( ) ;
2281+ let flag = self . inner . state . safety_net_active . clone ( ) ;
2282+ std:: thread:: spawn ( move || {
2283+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 100 ) ) ;
2284+ flag. store ( false , std:: sync:: atomic:: Ordering :: Relaxed ) ;
2285+ waker. wake_by_ref ( ) ;
2286+ } ) ;
2287+ }
2288+
22802289 // Re-wake logic for next iteration
22812290 #[ allow( clippy:: suspicious_else_formatting, clippy:: if_same_then_else) ]
22822291 {
0 commit comments