Skip to content

scheduler: Add spawn_dedicated for single-threaded actors with !Send state - #57609

Merged
ConradIrwin merged 8 commits into
mainfrom
scheduler-spawn-dedicated
May 29, 2026
Merged

scheduler: Add spawn_dedicated for single-threaded actors with !Send state#57609
ConradIrwin merged 8 commits into
mainfrom
scheduler-spawn-dedicated

Conversation

@nathansobo

@nathansobo nathansobo commented May 24, 2026

Copy link
Copy Markdown
Contributor

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_foregroundschedule_local and scheduler::ForegroundExecutorscheduler::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

@cla-bot cla-bot Bot added the cla-signed The user has signed the Contributor License Agreement label May 24, 2026
@zed-community-bot zed-community-bot Bot added the staff Pull requests authored by a current member of Zed staff label May 24, 2026
@nathansobo
nathansobo force-pushed the scheduler-spawn-dedicated branch from c22bf8b to 3d91b8f Compare May 24, 2026 23:33
@nathansobo
nathansobo requested a review from as-cii May 25, 2026 01:22
@as-cii
as-cii force-pushed the scheduler-spawn-dedicated branch from 8243567 to 26ad1dd Compare May 25, 2026 13:58
@nathansobo
nathansobo force-pushed the scheduler-spawn-dedicated branch from 9564757 to f29e357 Compare May 26, 2026 03:56
nathansobo and others added 7 commits May 29, 2026 08:11
…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
ConradIrwin force-pushed the scheduler-spawn-dedicated branch from f29e357 to 4a3f1f7 Compare May 29, 2026 14:23
@ConradIrwin
ConradIrwin marked this pull request as ready for review May 29, 2026 15:25
@ConradIrwin
ConradIrwin enabled auto-merge May 29, 2026 15:50
@ConradIrwin
ConradIrwin added this pull request to the merge queue May 29, 2026
Merged via the queue into main with commit c30d18b May 29, 2026
32 checks passed
@ConradIrwin
ConradIrwin deleted the scheduler-spawn-dedicated branch May 29, 2026 16:01
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 18, 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed The user has signed the Contributor License Agreement staff Pull requests authored by a current member of Zed staff

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants