fix(js): drive the event loop cancellation-safely to avoid isolate leaks

Several event-loop operations bounded their runtime with
tokio::time::timeout(budget, run_event_loop()) or a select! against a
timer. When the timer wins, tokio drops the run_event_loop future
mid-poll; deno_core enters the isolate while polling, so the drop leaves
the isolate entered on the OS thread. Under concurrent CDP work a second
page's execute_script then trips heap->isolate() == TryGetCurrent() and
aborts the process (#430).

Drive the loop with poll_event_loop inside a poll_fn instead. It creates
and drops its own isolate scope per call, so stopping between ticks never
leaves the isolate entered. Convert resolve_promises_until,
run_event_loop_bounded, resolve_promises, the mod_evaluate settle in
load_module and load_inline_module, and the two navigation settle loops
in page.rs. Add poll_event_loop_once as the cancellation-safe single-tick
primitive the settle loops use.

Keeps the obstacle course at 33/33. This is hardening toward #430; the
residual concurrency abort still needs the thread-per-runtime refactor
that v8_lock.rs calls the "Option 2" fix.

Refs #430.
This commit is contained in:
sycatle committed 2026-07-20 16:37:50 +02:00
1 parent 72d66bc5c0
commit 45df429d99
2 files changed
+168 -80

No files matched your search

+15 -13
View File
@@ -763,13 +763,13 @@ impl Page {
if tokio::time::Instant::now() >= deadline {
break;
}
let result = tokio::time::timeout(
tokio::time::Duration::from_millis(10),
js.run_event_loop(),
).await;
match result {
Ok(Ok(())) => {
// Cancellation-safe single tick: a per-poll `tokio::time::timeout`
// wrapping `run_event_loop` would drop that future mid-poll and
// leave this isolate entered on the thread, aborting the process
// under concurrent pages (#430). `poll_event_loop_once` exits the
// isolate before returning, so the tick is safe to bound.
match js.poll_event_loop_once().await {
Some(Ok(())) => {
if self.http_client.active_requests() == 0 {
idle_count += 1;
if idle_count >= 2 {
@@ -781,9 +781,10 @@ impl Page {
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
}
}
Ok(Err(_)) => break,
Err(_) => {
Some(Err(_)) => break,
None => {
idle_count = 0;
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
}
}
}
@@ -1226,10 +1227,11 @@ impl Page {
}
if let Some(js) = &mut self.js {
let _ = tokio::time::timeout(
tokio::time::Duration::from_millis(50),
js.run_event_loop(),
).await;
// Cancellation-safe tick (see #430): never drop a
// `run_event_loop` future mid-poll, which leaks the entered
// isolate. `poll_event_loop_once` exits the isolate per tick.
let _ = js.poll_event_loop_once().await;
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
} else {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}