Skip to content

[WSLC] Run daemon exec off the single worker thread + per-container single-flight Busy admission (non-critical) #805

Description

Description of the task

Make the per-user WSLc daemon run exec off the single SDK worker thread, and add
per-container single-flight admission (ErrKind::Busy) so a long-running command in one
sandbox cannot stall lifecycle operations on other sandboxes.

Current behavior (the limitation). The daemon confines all WSLc SDK calls to one
worker thread, which processes WorkerCommands serially (rx.blocking_recv() in
src/backends/wslc/daemon/src/session_manager.rs). When it handles a WorkerCommand::Exec
it calls worker.exec(config, container, sink), which blocks the worker thread for the
entire duration of the run
(until the container process exits or the exec times out).
While that run is in flight, every other queued command — Provision / Start / Stop
/ Deprovision and Exec for any other container — waits behind it (head-of-line
blocking).

Priority: non-critical. This is an availability / latency limitation, not a
correctness or security bug:

  • Results stay correct and container isolation is unaffected.
  • It is bounded by the exec's scriptTimeout — a blocked run is killed at timeout, so
    there is no unbounded hang.
  • The state-aware WSLc surface is experimental and not yet released.

When it bites (the scenario). It only manifests under concurrent multi-container
use of a single daemon:

  • Sandbox A starts a long-running exec (e.g. a build, a test suite, or a
    scriptTimeout set to minutes).
  • Meanwhile the caller (or another caller sharing the same per-user daemon) tries to
    provision / start / exec / stop / deprovision sandbox B.
  • Those B operations block until A's exec finishes (or times out), even though A and B
    are independent containers. To the B caller the daemon looks hung / unresponsive for up
    to A's full timeout window.

For the common one-sandbox-per-agent flow (a single container executing one command at a
time) this contention does not occur, which is why it is safe to defer.

Additional context

Proposed fix.

  • session_manager.rs: run the blocking worker.exec(...) off the single worker thread
    (e.g. hand the started process off to a dedicated per-exec thread / task) so the worker
    loop stays responsive to other WorkerCommands during a run. Preserve the existing
    atomic admission (validate + start in one step) and the live OutputSink streaming.
  • Per-container single-flight: admit at most one in-flight exec per sandbox_id; reject
    a second concurrent exec on the same container with ErrKind::Busy (a new typed
    admission error) rather than queuing it.
  • control_server.rs: wire the Busy admission into the client frame sequence (typed
    pre-admission error, same shape as the existing not-provisioned / not-started
    rejections).

Deferral trail. Deferred from PR 2a/2b during the #767 daemon-hardening review
(2026-08-07), and again while scoping PR 2c (live output streaming). Not appropriate for
PR 3/3, which is the TypeScript SDK surface (types only) — this is a Rust daemon
concurrency change and should land as its own follow-up PR.

Key code references.

  • src/backends/wslc/daemon/src/session_manager.rs — single worker loop (spawn
    rx.blocking_recv()), WorkerCommand::Exec arm, worker.exec(...).
  • src/backends/wslc/daemon/src/control_server.rs — exec admission → frame sequence
    (write_exec_result); where a Busy typed rejection would be surfaced.

Metadata

Metadata

Labels

Area-Build-RustRust build systemContainer-WSLCIssues relating to WSL ContainersIssue-FeatureIt's a new feature requestIssue-TaskIt's a taskOS-WindowsWindowsPriority3Moderate issue with limited impact; workaround available, fix as capacity permits

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions