Skip to content

[WSLC] (2/2) Admit concurrent execs and refuse a busy container - #1428

Open
Soham Das (SohamDas2021) wants to merge 6 commits into
sohamdas2021-wslc-exec-off-workerfrom
sohamdas2021-wslc-single-flight-busy
Open

Soham Das (SohamDas2021) wants to merge 6 commits into
sohamdas2021-wslc-exec-off-workerfrom
sohamdas2021-wslc-single-flight-busy

Conversation

@SohamDas2021

@SohamDas2021 Soham Das (SohamDas2021) commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

📖 Description

A caller could not run two sandboxes at once. The daemon admitted a single exec stream, so a second exec — even against a different container — was refused while the first ran, and the concurrency the previous change unlocked was unreachable.

The daemon now admits up to eight exec streams, one per container. A second exec naming a container that already has one in flight is refused immediately as busy instead of waiting behind a workload of unknown duration. Lifecycle commands on that container still wait for the run, because deleting the container would free a handle the run is using.

The bound of eight comes from memory. A streaming exec's output lives only in its bounded live-output queue, so the daemon stays near 128 MB of live output against clients that never drain. A streaming run no longer also fills the capped capture buffers it never reads, which is what keeps that figure true.

This is 2 of 2 for the work item, and includes the test work originally scoped as a third change.

🔗 References

🔍 Validation

  • cargo test -p mxc-sdk --features wslc — 4406 passed, 0 failed
  • cargo clippy --workspace --all-targets -- -D warnings, cargo fmt --all -- --check — clean
  • WSL2 host (not reachable from CI): five #[ignore] daemon tests, four daemon_ipc pipe tests, and the full E2E suite — one-shot 35/35, state-aware 93/93
  • New coverage, over the real control pipe: two clients executing concurrently on their own sandboxes with no cross-talk and measured in-container overlap, and enough simultaneous clients to prove the cap refuses the excess before admission. At E2E level, a full provision/start/exec/stop lifecycle on one sandbox completes while a long exec runs on another
  • Each new test was checked against a reverted implementation and fails for the intended reason: zeroing the cancellation lane starves cancellation, dropping the slot frees capacity early, restoring unconditional capture doubles per-exec memory, and raising the cap above the client count admits every client

✅ Checklist

📋 Issue Type

  • Bug fix
  • Feature
  • Task
Microsoft Reviewers: Open in CodeFlow

@SohamDas2021
Soham Das (SohamDas2021) requested a review from a team as a code owner October 6, 2026 21:39
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@SohamDas2021
Soham Das (SohamDas2021) added this pull request to stack #1436 October 7, 2026 02:32
@SohamDas2021
Soham Das (SohamDas2021) force-pushed the sohamdas2021-wslc-single-flight-busy branch from 538bc36 to e2a5c84 Compare October 7, 2026 04:59
Copilot AI balanced review requested due to automatic review settings October 7, 2026 04:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Exec permits can be released while output remains buffered, exceeding the stated memory bound.

Review effort: Balanced
Findings: 1 High severity · 2 Medium severity

Open (3)
What changed in this PR

Enables concurrent WSLc execution across containers, building on the off-worker execution support from #1407.

Changes:

  • Raises exec capacity to eight and rejects same-container concurrent runs as busy.
  • Adds reserved cancellation capacity and removes duplicate streaming-output capture.
  • Expands concurrency tests and documents admission behavior.
File Description
tests/​scripts/​run_wslc_state_aware_tests.ps1 Adds asynchronous phase helpers and concurrency scenarios.
src/​mxc-sdk/​tests/​wslc_daemon_ipc.rs Tests concurrent pipe clients and capacity refusal.
src/​mxc-sdk/​src/​bin/​wslc_daemon/​session_manager.rs Adds busy rejection, slot ownership, and worker tests.
src/​mxc-sdk/​src/​bin/​wslc_daemon/​control_server.rs Raises admission limits and reserves cancellation capacity.
src/​mxc-sdk/​src/​backends/​wslc/​common/​container_steps.rs Bypasses capture buffers when streaming output.
docs/​backends/​wslc/​wslc-state-aware.md Documents concurrency, limits, and busy responses.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/mxc-sdk/src/bin/wslc_daemon/control_server.rs Outdated
Comment on lines +2831 to +2833
// Occupy the worker with a container creation, so the exec below is
// still queued when the cancellation lands. Spawned tasks are polled in
// order, so this provision reaches the worker first.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Leaving this open - I addressed the guarantee but not the way you suggested, so it's not fully closed.

You're right that provisioning establishes no barrier and the active_execs check only proves registration. Rather than add a test-only barrier to the worker, I injected the run starter (RunStarter on Worker) and wrote a deterministic unit test, a_cancelled_exec_never_reaches_the_runner, that sets the cancellation flag, dispatches, and asserts the runner was never invoked at all. No SDK, no race. It fails if you remove the pre-run cancellation check.

That covers the guarantee this live test exists to protect, and covers it more strongly - the old assertion could only observe that done reported Cancelled, not that the process was never created.

What I did not do is remove the race in the #[ignore] live test itself, so it can still flake on a slow host. It passed 5/5 across four runs here, and it's host-gated so it never runs in CI, but the window you describe is still there.

Comment thread tests/scripts/run_wslc_state_aware_tests.ps1

@MGudgin Gudge (MGudgin) 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.

Stack-only review summary

This comment-only review compares #1428 at e2a5c84 to its actual PR base 7ce1c8e (#1407's head). The merge-base equals that base, and the local six-file diff matches gh pr diff after newline normalization. Findings on #1407 alone are not charged to this PR. The new run-bound permit addresses #1407's handler-disconnect over-admission at this tip.

Verified clean with receipts: The worker now checks its per-container in-flight map before deciding whether to return pre-admission Busy or park lifecycle work. Streaming callbacks choose the sink instead of the capture buffers, and the new real-pipe overlap test compares stamped in-container run intervals. Test markers in control_server.rs increased 20 to 23, session_manager.rs 42 to 47, and wslc_daemon_ipc.rs 2 to 4. I inspected source and test assertions; I did not rerun WSL2 E2E tests in this filing.

Previously raised on this same head (confirmed, not duplicated inline)

High — Output-queue lifetime exceeds the exec-slot lifetime. introduced_by_change: moving the permit into InFlightExec releases it on run completion even when a connected non-reading client's handler still holds a full 16 MiB output queue. Up to 16 general handlers can retain about 256 MiB of queued payload, rather than the advertised eight-queue/~128 MiB ceiling. Keep an output-memory claim until the handler drains or drops the queue while retaining a run claim until process completion. This is already anchored in the existing Copilot review (ID 5437849230).

Medium — E2E lifecycle-overlap assertion timestamps A too late. introduced_by_change: J4 records $cEnded before calling Wait-StateAware for A, whose EndedAt is then set to collection time. The comparison therefore succeeds even if A finished before C began. Compare against A's actual Process.ExitTime or an asynchronously recorded completion. Already anchored in review 5437849230.

Low — Queued-cancellation test assumes task poll order. introduced_by_change: the new test spawns provisioning and exec independently and waits for client-side exec registration, not for provisioning to occupy the worker. A worker admission barrier would make the intended queued-before-cancelled case deterministic. Already anchored in review 5437849230.

Verified pre-existing — not attributed to #1428

  • The no-timeout exec continuing after its client disconnects was already possible in #1407. Counting that run against the new capacity is an intentional safety tradeoff, not a new orphaning defect here.
  • The global client limiter and its saturation under enough parked lifecycle requests existed at #1407. Its separate cancellation reserve is new here, and the distinct capacity issue in the inline comments is attributed to that new lane.
  • daemon_protocol.rs is byte-identical base to head. Its Busy variant's older doc was already incomplete for global capacity; the new third Busy case is instead retargeted at this PR's added two-case documentation table.

The remaining distinct findings are anchored below. The verdict is COMMENT, not a request for changes.

Comment thread src/mxc-sdk/src/bin/wslc_daemon/control_server.rs
let request: DaemonRequest = timeout(FIRST_FRAME_TIMEOUT, read_frame(&mut pipe))
.await
.context("timed out waiting for the client's first frame")??;
if cancel_only && !matches!(request, DaemonRequest::CancelExec(_)) {

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.

Medium (reliability, performance, correctness) — A cancellation slot can be held before the request is classified.

Attribution: introduced_by_change — the new cancellation fallback acquires a reserved permit before this cancel_only check runs.

With the 16 general slots occupied, eight same-owner connections can take all eight cancellation permits and stall while sending a first frame. The existing read_frame accepts a length up to 16 MiB and allocates the whole body before reading it; the first-frame deadline is 30 seconds. Thus these connections can hold ~128 MiB of input buffers and block a subsequent cancellation for that period. An exhausted lane drops the next connection without handling its request. This is an owner-only local liveness risk, not remote access.

Fix: Give lane requests a much smaller pre-allocation frame limit and shorter first-frame timeout, or identify cancellation via a separate compact endpoint before reserving its slots. Test cancellation under stalled partial-frame connections.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed with your first option: cancel-lane connections now get LANE_MAX_FRAME_BYTES = 1024 (sized off the 304 bytes a worst-case cancel_exec encodes to) and LANE_FIRST_FRAME_TIMEOUT = 2s, so your scenario drops from 8 × 16 MiB held 30s to 8 × 1 KiB held 2s. Covered by a_cancellation_survives_a_lane_saturated_by_stalled_connections, which fills every lane slot with partial-frame stalls and lands a real cancellation in 2.00s

Comment thread src/mxc-sdk/src/backends/wslc/common/container_steps.rs
Comment thread src/mxc-sdk/src/bin/wslc_daemon/session_manager.rs Outdated
Comment thread src/mxc-sdk/tests/wslc_daemon_ipc.rs Outdated
Comment thread tests/scripts/run_wslc_state_aware_tests.ps1 Outdated
Comment thread docs/backends/wslc/wslc-state-aware.md Outdated
Copilot AI balanced review requested due to automatic review settings October 7, 2026 21:18
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6b2cb15b-3b28-48b2-be73-f43c8951bc5c
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6b2cb15b-3b28-48b2-be73-f43c8951bc5c
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6b2cb15b-3b28-48b2-be73-f43c8951bc5c
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6b2cb15b-3b28-48b2-be73-f43c8951bc5c

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Worker unwind cleanup can release the session and SDK while an image pull still uses them.

2 open findings
2 resolved since last review

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread src/mxc-sdk/src/bin/wslc_daemon/session_manager.rs Outdated
Copilot AI balanced review requested due to automatic review settings October 7, 2026 21:27
@SohamDas2021
Soham Das (SohamDas2021) force-pushed the sohamdas2021-wslc-single-flight-busy branch from f51722b to 5b7f42c Compare October 7, 2026 21:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Concurrent native-handle ownership and Windows-only failure-containment paths warrant final human review.

4 open findings
2 resolved since last review

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment on lines +156 to +158
counting while its output is still queued. A run whose termination could not be
confirmed leaves its sandbox quarantined and keeps the slot until that sandbox
is deprovisioned, because the process may still be alive.

| Condition | Message | Retry |
| --- | --- | --- |
| The daemon's eight exec slots are all occupied | `WSLc daemon exec capacity is exhausted` | Succeeds once any exec finishes |
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6b2cb15b-3b28-48b2-be73-f43c8951bc5c
Copilot AI balanced review requested due to automatic review settings October 7, 2026 22:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The cancellation lane rejects valid escaped identifiers when general client capacity is exhausted.

5 open findings

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread src/mxc-sdk/src/bin/wslc_daemon/control_server.rs Outdated
…the lane

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6b2cb15b-3b28-48b2-be73-f43c8951bc5c
Copilot AI balanced review requested due to automatic review settings October 7, 2026 23:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Native SDK concurrency and handle ownership require final human review of the Windows/WSL2 validation.

4 open findings
2 resolved since last review

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment on lines +173 to +175
consume. A connection on that lane must send its request within two seconds and
in under 1 KB, so one that connects and stalls cannot hold a cancellation slot
for the general deadline.

This branch has not been deployed

No deployments
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.

3 participants