scheduler: Add spawn_dedicated for single-threaded actors with !Send state - #57609
Merged
Conversation
nathansobo
force-pushed
the
scheduler-spawn-dedicated
branch
from
May 24, 2026 23:33
c22bf8b to
3d91b8f
Compare
as-cii
force-pushed
the
scheduler-spawn-dedicated
branch
from
May 25, 2026 13:58
8243567 to
26ad1dd
Compare
as-cii
approved these changes
May 25, 2026
nathansobo
force-pushed
the
scheduler-spawn-dedicated
branch
from
May 26, 2026 03:56
9564757 to
f29e357
Compare
…wn_dedicated Adds coverage for properties the existing tests didn't exercise directly: - Dropping the root Task cancels the dedicated future (test scheduler and PlatformScheduler). - The dedicated thread tears down after its work completes, by observing the future's captured state being dropped on the dedicated thread. - Detached children spawned on the dedicated executor continue running to completion after the root has finished (test scheduler and PlatformScheduler). Release Notes: - N/A
This lets callers reach session-allocation through Arc<dyn Scheduler> without downcasting to the concrete scheduler type, which is needed to spawn dedicated sessions via spawn_dedicated_thread from code that only holds a trait object.
…appers Adds `Scheduler::spawn_dedicated` as a dyn-safe trait method and type-safe `LocalExecutor::spawn_dedicated` / `BackgroundExecutor::spawn_dedicated` wrappers around it. Callers reach for the wrappers; the trait method exists so both `PlatformScheduler` (real OS thread via `spawn_dedicated_thread`) and `TestScheduler` (fresh local session on the test loop) can satisfy it without `as_test()` branching at the call site. The trait stays object-safe by type-erasing the closure's output as `Box<dyn Any + Send + Sync>`. The wrappers recover the concrete type via `Task::downcast<T>`, which is a new method on `Task<Box<dyn Any + Send + Sync>>` that turns the boxed-any task into a `Task<T>` with no extra spawn / channel / fan-in. Cancellation, `detach`, and `fallible` semantics flow through the new `TaskState::Downcast` variant to the inner task. The inherent `spawn_dedicated` methods on `PlatformScheduler` and `TestScheduler` are removed; their tests now go through `BackgroundExecutor::spawn_dedicated` instead. The `Future` impls on `Task<T>` and `FallibleTask<T>` now require `T: 'static` to support the downcast. Every existing `Task<T>` already had a `'static` `T` in practice (`async_task::spawn` requires it), but two generic wrappers storing `Shared<Task<T>>` over an unconstrained `T` needed a one-character bound bump. Release Notes: - N/A
The `scheduler.clone()` passed to `BackgroundExecutor::new` was redundant -- nothing in the surrounding tests used `scheduler` again after constructing the executor. Inlined into a single `BackgroundExecutor::new(Arc::new(PlatformScheduler::new(...)))` expression per test. Release Notes: - N/A
…closures Reconciles main's retention-cycle fix (#57789) with the new dispatch-based LocalExecutor: foreground/local dispatch closures now hold a Weak reference to the scheduler and upgrade lazily, so a detached, stalled local task no longer keeps the scheduler alive.
ConradIrwin
force-pushed
the
scheduler-spawn-dedicated
branch
from
May 29, 2026 14:23
f29e357 to
4a3f1f7
Compare
ConradIrwin
marked this pull request as ready for review
May 29, 2026 15:25
ConradIrwin
enabled auto-merge
May 29, 2026 15:50
TomPlanche
pushed a commit
to TomPlanche/zed
that referenced
this pull request
Jun 2, 2026
…state (zed-industries#57609) Adds `scheduler::spawn_dedicated_thread` (and inherent `spawn_dedicated` methods on `PlatformScheduler` and `TestScheduler`) so single-threaded actors that own `!Send` state can run on their own OS thread and freely do blocking I/O without disturbing any other executor. ### Why A single-threaded actor that needs to do blocking syscalls is currently stuck: it can't run on the shared foreground executor (blocking would stall every other foreground session), and it can't move to the background pool because its state isn't `Send`. `spawn_dedicated` gives each such actor its own thread and its own `LocalExecutor`, while still participating in the same testable scheduler infrastructure as everything else. ### Shape - `pub fn spawn_dedicated_thread(session_id, scheduler, f) -> Task<_>` in `scheduler`. Owns the OS thread, the per-session runnable channel, and the `LocalExecutor` setup. - Inherent `spawn_dedicated` on `PlatformScheduler` (allocates its own `SessionId`, delegates to the free function). - Inherent `spawn_dedicated` on `TestScheduler` (no real thread — runs as a fresh local session driven by the test scheduler's run loop, so determinism under `many` is preserved). - Renames `Scheduler::schedule_foreground` → `schedule_local` and `scheduler::ForegroundExecutor` → `scheduler::LocalExecutor` to reflect that these are session-pinned queues rather than "the main thread" (a dedicated session runs on its own thread). GPUI's wrapper `gpui::ForegroundExecutor` and the `foreground_executor` field/method names are unchanged to keep blast radius small. - `LocalExecutor::new` now takes an explicit dispatch closure, so the routing decision (default session, dedicated thread, or something else) lives at the construction site. ### Tests - `TestScheduler` side: round-trip, `!Send` future, `Send` closure capturing shared state, inner `executor.spawn`, determinism under `many` seeds, drop-cancels-future, detached child runs after root completes. - `PlatformScheduler` side: real separate thread (blocking syscalls don't stall the test), `!Send` future output, drop-cancels-future, thread tears down after work completes, detached child outlives root. cc @as-cii Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra <me@as-cii.com> Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This was referenced Jun 10, 2026
This was referenced Jun 18, 2026
Closed
This was referenced Jul 1, 2026
This was referenced Jul 10, 2026
jonx
pushed a commit
to jonx/zed-aros
that referenced
this pull request
Jul 17, 2026
…state (zed-industries#57609) Adds `scheduler::spawn_dedicated_thread` (and inherent `spawn_dedicated` methods on `PlatformScheduler` and `TestScheduler`) so single-threaded actors that own `!Send` state can run on their own OS thread and freely do blocking I/O without disturbing any other executor. ### Why A single-threaded actor that needs to do blocking syscalls is currently stuck: it can't run on the shared foreground executor (blocking would stall every other foreground session), and it can't move to the background pool because its state isn't `Send`. `spawn_dedicated` gives each such actor its own thread and its own `LocalExecutor`, while still participating in the same testable scheduler infrastructure as everything else. ### Shape - `pub fn spawn_dedicated_thread(session_id, scheduler, f) -> Task<_>` in `scheduler`. Owns the OS thread, the per-session runnable channel, and the `LocalExecutor` setup. - Inherent `spawn_dedicated` on `PlatformScheduler` (allocates its own `SessionId`, delegates to the free function). - Inherent `spawn_dedicated` on `TestScheduler` (no real thread — runs as a fresh local session driven by the test scheduler's run loop, so determinism under `many` is preserved). - Renames `Scheduler::schedule_foreground` → `schedule_local` and `scheduler::ForegroundExecutor` → `scheduler::LocalExecutor` to reflect that these are session-pinned queues rather than "the main thread" (a dedicated session runs on its own thread). GPUI's wrapper `gpui::ForegroundExecutor` and the `foreground_executor` field/method names are unchanged to keep blast radius small. - `LocalExecutor::new` now takes an explicit dispatch closure, so the routing decision (default session, dedicated thread, or something else) lives at the construction site. ### Tests - `TestScheduler` side: round-trip, `!Send` future, `Send` closure capturing shared state, inner `executor.spawn`, determinism under `many` seeds, drop-cancels-future, detached child runs after root completes. - `PlatformScheduler` side: real separate thread (blocking syscalls don't stall the test), `!Send` future output, drop-cancels-future, thread tears down after work completes, detached child outlives root. cc @as-cii Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra <me@as-cii.com> Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
jolutz
pushed a commit
to jolutz/zed
that referenced
this pull request
Aug 8, 2026
…state (zed-industries#57609) Adds `scheduler::spawn_dedicated_thread` (and inherent `spawn_dedicated` methods on `PlatformScheduler` and `TestScheduler`) so single-threaded actors that own `!Send` state can run on their own OS thread and freely do blocking I/O without disturbing any other executor. ### Why A single-threaded actor that needs to do blocking syscalls is currently stuck: it can't run on the shared foreground executor (blocking would stall every other foreground session), and it can't move to the background pool because its state isn't `Send`. `spawn_dedicated` gives each such actor its own thread and its own `LocalExecutor`, while still participating in the same testable scheduler infrastructure as everything else. ### Shape - `pub fn spawn_dedicated_thread(session_id, scheduler, f) -> Task<_>` in `scheduler`. Owns the OS thread, the per-session runnable channel, and the `LocalExecutor` setup. - Inherent `spawn_dedicated` on `PlatformScheduler` (allocates its own `SessionId`, delegates to the free function). - Inherent `spawn_dedicated` on `TestScheduler` (no real thread — runs as a fresh local session driven by the test scheduler's run loop, so determinism under `many` is preserved). - Renames `Scheduler::schedule_foreground` → `schedule_local` and `scheduler::ForegroundExecutor` → `scheduler::LocalExecutor` to reflect that these are session-pinned queues rather than "the main thread" (a dedicated session runs on its own thread). GPUI's wrapper `gpui::ForegroundExecutor` and the `foreground_executor` field/method names are unchanged to keep blast radius small. - `LocalExecutor::new` now takes an explicit dispatch closure, so the routing decision (default session, dedicated thread, or something else) lives at the construction site. ### Tests - `TestScheduler` side: round-trip, `!Send` future, `Send` closure capturing shared state, inner `executor.spawn`, determinism under `many` seeds, drop-cancels-future, detached child runs after root completes. - `PlatformScheduler` side: real separate thread (blocking syscalls don't stall the test), `!Send` future output, drop-cancels-future, thread tears down after work completes, detached child outlives root. cc @as-cii Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra <me@as-cii.com> Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
scheduler::spawn_dedicated_thread(and inherentspawn_dedicatedmethods onPlatformSchedulerandTestScheduler) so single-threaded actors that own!Sendstate can run on their own OS thread and freely do blocking I/O without disturbing any other executor.Why
A single-threaded actor that needs to do blocking syscalls is currently stuck: it can't run on the shared foreground executor (blocking would stall every other foreground session), and it can't move to the background pool because its state isn't
Send.spawn_dedicatedgives each such actor its own thread and its ownLocalExecutor, while still participating in the same testable scheduler infrastructure as everything else.Shape
pub fn spawn_dedicated_thread(session_id, scheduler, f) -> Task<_>inscheduler. Owns the OS thread, the per-session runnable channel, and theLocalExecutorsetup.spawn_dedicatedonPlatformScheduler(allocates its ownSessionId, delegates to the free function).spawn_dedicatedonTestScheduler(no real thread — runs as a fresh local session driven by the test scheduler's run loop, so determinism undermanyis preserved).Scheduler::schedule_foreground→schedule_localandscheduler::ForegroundExecutor→scheduler::LocalExecutorto reflect that these are session-pinned queues rather than "the main thread" (a dedicated session runs on its own thread). GPUI's wrappergpui::ForegroundExecutorand theforeground_executorfield/method names are unchanged to keep blast radius small.LocalExecutor::newnow takes an explicit dispatch closure, so the routing decision (default session, dedicated thread, or something else) lives at the construction site.Tests
TestSchedulerside: round-trip,!Sendfuture,Sendclosure capturing shared state, innerexecutor.spawn, determinism undermanyseeds, drop-cancels-future, detached child runs after root completes.PlatformSchedulerside: real separate thread (blocking syscalls don't stall the test),!Sendfuture output, drop-cancels-future, thread tears down after work completes, detached child outlives root.cc @as-cii
Release Notes: