revert(js): drop the "cancellation-safe" event loop pumping

The premise was wrong. deno_core 0.350's run_event_loop is:

    pub async fn run_event_loop(&mut self, opts) -> Result<(), CoreError> {
        poll_fn(|cx| self.poll_event_loop(cx, opts)).await
    }

so replacing `timeout(budget, run_event_loop())` with a hand-rolled
`poll_fn(poll_event_loop)` loop is the same code with the same cancellation
semantics, not safer ones. The mechanism I described cannot happen either:
`poll` is a synchronous `&mut self` call, a future can only be dropped between
polls, and the HandleScope/ContextScope live inside poll_event_loop as stack
locals that are destroyed before it returns. The isolate is exited between
polls both before and after that change.

What actually fixes #430 is confining each connection's isolates to their own
OS thread, which the next commits do. The regression test still reproduces the
abort on the pre-thread-per-connection tree and still passes here without this
commit, so nothing depended on it.

It was not merely a no-op. Two regressions it introduced:

- resolve_promises_until inverted the meaning of tick_ms. It was "pump the
  event loop for up to tick_ms, returning early on idle"; it became one single
  poll followed by an unconditional sleep(tick_ms). With the 1->50ms backoff
  that settles at 20 event-loop turns per second, so an awaited promise chain
  needing many turns crawls, and past the budget the caller silently reads a
  stale __obscura_await_meta.
- drive_module_eval replaced `e?` with `let _ = poll_event_loop(...)`,
  discarding event-loop errors. A module stalled on a top-level await used to
  exit immediately with a logged error; it instead spun for the whole budget
  logging nothing -- the exact #374 starvation the surrounding comment claims
  to prevent.

Keep the drive_module_eval extraction (it genuinely dedupes load_module and
load_inline_module) but restore the original select!-based body.

Refs #430.
This commit is contained in:
sycatle committed 2026-07-20 18:41:00 +02:00
1 parent 3015f7d8ad
commit 319c6034fe
2 files changed
+62 -136

No files matched your search

+13 -15
View File
@@ -763,13 +763,13 @@ impl Page {
if tokio::time::Instant::now() >= deadline {
break;
}
// 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(())) => {
let result = tokio::time::timeout(
tokio::time::Duration::from_millis(10),
js.run_event_loop(),
).await;
match result {
Ok(Ok(())) => {
if self.http_client.active_requests() == 0 {
idle_count += 1;
if idle_count >= 2 {
@@ -781,10 +781,9 @@ impl Page {
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
}
}
Some(Err(_)) => break,
None => {
Ok(Err(_)) => break,
Err(_) => {
idle_count = 0;
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
}
}
}
@@ -1227,11 +1226,10 @@ impl Page {
}
if let Some(js) = &mut self.js {
// 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;
let _ = tokio::time::timeout(
tokio::time::Duration::from_millis(50),
js.run_event_loop(),
).await;
} else {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}