Skip to content

fix(core): always register isolate to prevent silent foreground task drop#35408

Merged
bartlomieju merged 2 commits into
denoland:mainfrom
haru0017:fix-foreground-task-silent-drop
Jun 23, 2026
Merged

fix(core): always register isolate to prevent silent foreground task drop#35408
bartlomieju merged 2 commits into
denoland:mainfrom
haru0017:fix-foreground-task-silent-drop

Conversation

@haru0017

Copy link
Copy Markdown
Contributor

Fixes #35345

Registration in ISOLATE_ENTRIES was conditional on Handle::try_current(), so it could be skipped. When skipped, queue_task and spawn_delayed_task silently dropped foreground tasks, causing the event loop to hang.

This change makes registration unconditional by making Handle optional. queue_task does not use the handle, so it works regardless. spawn_delayed_task falls back to immediate queuing when no handle is available, consistent with the PlatformImpl defaults.

A missing entry now unambiguously means the isolate was already unregistered during shutdown, so dropping is safe.

This PR was prepared with assistance from Claude Code. All changes were directed and reviewed by the author.

Make registration unconditional by making Handle optional.
This prevents foreground tasks from being silently dropped
when the isolate is not registered.
@deno-cla-assistant

deno-cla-assistant Bot commented Jun 21, 2026

Copy link
Copy Markdown

Deno Individual Contributor License Agreement

All contributors have signed the CLA. Thank you!

Re-run CLA check


This is an automated message from CLA Assistant

@bartlomieju

Copy link
Copy Markdown
Member

A couple of non-blocking notes from reviewing this:

  • The delay is dropped in the fallback path. When there's no tokio handle, spawn_delayed_task queues the task immediately, ignoring delay_in_seconds. That's a reasonable fallback (there's no executor to schedule a timer on), and it's strictly better than the previous silent drop. One thing to keep in mind: if a task reschedules itself with a delay, the fallback turns it into a busy-requeue loop instead of a paced retry. In the no-tokio contexts this targets (snapshot creation / unit tests) that's fine, and the doc comment already calls out the immediate-queuing behavior.

  • The test nicely exercises the real no-handle pathJsRuntime::new(...) is constructed outside block_on, so registration happens with handle == None, while the WASM compile (which posts foreground tasks from background compilation threads) runs inside the tokio context. That genuinely reproduces Event loop hangs for unregistered isolates #35345. It'd be worth a one-line comment explaining why the runtime is created outside block_on, since that placement is the crux of the test and currently looks like it could be incidental.

@bartlomieju bartlomieju left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@bartlomieju
bartlomieju merged commit aa90115 into denoland:main Jun 23, 2026
268 of 270 checks passed
@rlaskey

rlaskey commented Jun 29, 2026

Copy link
Copy Markdown

@haru0017 or anyone else here: is it possible that this change would have impacted deno lsp? When upgrading to 2.9.0, I see the CPU pegged. 2.8.3 is just fine.

I posted about this in Discord and a contributor there identified this PR as a potential root cause.

There are a number of things here I do not understand, and I do not want to be creating more noise. Apologies in advance :) With that said, Mistral suggested for me a following patch, dropping two of the inner statements around entry.handle.as_ref, such that it looks like this:

let Some(handle) = entry.handle.as_ref() else { return };

What do we think?

@haru0017

haru0017 commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for reporting this @rlaskey.

I believe this change is causing the issue. Looking into it, I found that JsRuntime::new() is called before the tokio runtime is created in run_tsc_thread (cli/lsp/tsc.rs), so the isolate is registered with handle: None. This causes spawn_delayed_task to fall back to immediate queuing. V8 internal delayed tasks that reschedule themselves then turn into a busy loop.

I've opened #35595 to fix the root cause by moving tokio runtime creation before JsRuntime::new() in run_tsc_thread.

For spawn_delayed_task, dropping the task when no handle is available risks a hang, while queuing immediately risks a busy loop as we saw here. I'm not sure which is the better fallback.

@bartlomieju I'd appreciate your thoughts on this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Event loop hangs for unregistered isolates

3 participants