wslc: idle-terminate per-user session VMs when inactive#40781
wslc: idle-terminate per-user session VMs when inactive#40781benhillis wants to merge 21 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds on-demand creation and idle-termination of per-user WSLC session VMs (for sessions with persistent storage), so memory can be reclaimed while keeping the session object and storage intact. It also introduces VM-liveness/activity bookkeeping to prevent teardown during in-flight operations and adds new E2E coverage around VM lifecycle behavior.
Changes:
- Implement lazy VM bring-up and idle shutdown in
wslcsessionvia an idle worker, activity counting/tokens, and aVmLeaseused by VM-requiring operations. - Add client-side “operation keep-alive” usage in
wslc.execontainer operations to prevent VM teardown betweenOpenContainerand subsequent calls/streaming. - Add a new E2E test suite validating lazy start, idle stop, persistence across restarts, keep-alive for root-namespace processes, and teardown/recreate races.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| test/windows/wslc/e2e/WSLCE2EVmIdleTests.cpp | New E2E tests covering lazy VM start, idle stop, persistence, keep-alive, and race scenarios. |
| test/windows/wslc/e2e/WSLCE2EHelpers.h | Exposes the underlying IWSLCSession* for diagnostics/test-only calls. |
| src/windows/wslcsession/WSLCSession.h | Adds VM lifecycle state, idle worker/tokens/lease declarations, and new session methods. |
| src/windows/wslcsession/WSLCSession.cpp | Implements lazy VM creation, idle teardown, activity tokens, and VM diagnostics reporting. |
| src/windows/wslcsession/WSLCProcessControl.cpp | Preserves a real exit code when signaling container release, only synthesizing SIGKILL when needed. |
| src/windows/wslcsession/WSLCProcess.h | Stores a keep-alive token on root-namespace processes to keep the VM alive for their lifetime. |
| src/windows/wslcsession/WSLCContainer.cpp | Signals idle re-checks on terminal container transitions; holds a VM lease during delete. |
| src/windows/wslcsession/IORelay.h | Adds IsRelayThread() to safely avoid destroying the relay on its own thread. |
| src/windows/wslcsession/IORelay.cpp | Co-initializes the relay thread into the MTA; implements IsRelayThread(). |
| src/windows/wslc/services/SessionModel.h | Adds a helper to acquire/hold a keep-alive token for client-side container operations. |
| src/windows/wslc/services/ContainerService.cpp | Uses the keep-alive token across container operations (attach/start/stop/kill/delete/exec/etc.). |
| src/windows/service/inc/wslc.idl | Adds VM diagnostics type + new session methods for diagnostics and operation keep-alive. |
| src/windows/service/exe/WSLCSessionManager.cpp | Updates comments to reflect on-demand VM creation and recreation after idle termination. |
c12d7e1 to
fa2eb47
Compare
fa2eb47 to
ea2254c
Compare
b870044 to
4bcd87f
Compare
4bcd87f to
348e2e1
Compare
benhillis
left a comment
There was a problem hiding this comment.
Reviewed VM-related comments - all have been addressed:
Comment on WSLCSession.h:84: Already correct - lines 77-78 say "IWSLCVirtualMachineFactory" and "lazily on first use"
Comment on WSLCSession.cpp:571: Fixed by AddRef/Release activity tracking - idle worker checks ActivityCount (line 779), and container proxies increment it on AddRef 1→2 transition. VM will not idle-terminate while clients hold container proxies. See lines 672-674 comment.
Comment on WSLCSession.cpp:380: Already has exception handling - IdleWorker() is wrapped in CATCH_LOG() at lines 375-379
Comment on Session.cpp:65: Already correct - wil::unique_threadpool_wait (line 56) calls WaitForThreadpoolWaitCallbacks in destructor automatically
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy the termination reason/details into the session-visible fields before SetEvent() so a waiter woken by the terminated event cannot observe the default Unknown reason. Restores the pre-refactor ordering. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…factories Add a test-only TriggerIdleTermination COM method to force VM idle teardown on demand, with three TAEF tests that hammer the VM idle-termination race paths. Refactor WSLCContainer construction to take WSLCSessionRuntime& instead of five individual runtime members. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The TriggerIdleTerminationRecoversRunningContainer test left the persisted container in the shared default session on early-exit or if the recovered handle's best-effort delete failed, making later ListContainers-based tests order-dependent. Add a scope_exit that force-deletes the container by name regardless of exit path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…dle-terminate-vm # Conflicts: # src/windows/wslcsession/WSLCContainer.cpp # src/windows/wslcsession/WSLCSession.h
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Session-level WSLC plugin hooks (OnSessionCreated/OnSessionStopping) assumed session == running VM. With idle termination (PR #40781) the VM is created lazily, torn down when idle, and transparently recreated while the session persists, so plugins had no notification of actual VM lifecycle. Add VM-level hooks OnWslcVmStarted/OnWslcVmStopping that fire on every VM (re)start and teardown, in addition to the once-per-session hooks. Both are best-effort (errors logged and ignored). OnWslcVmStarted fires after releasing the runtime exclusive lock so a plugin may reentrantly call back into the session (e.g. WSLCCreateProcess) without deadlocking; OnWslcVmStopping fires under the lock during teardown, gated on a fire-once flag so a failed bring-up emits no spurious stopping. Adds a PluginTests::WslcVmRestart case that drives first start, forced idle teardown (TriggerIdleTermination), and lazy restart, asserting the hook sequence and proving reentrancy is deadlock-free. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f1c36ca0-19d0-46a7-82b3-10f1e1ee3e4c
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s). |
| m_dockerClient(runtime.Docker()), | ||
| m_eventTracker(runtime.Events()), | ||
| m_ioRelay(*runtime.Relay()), | ||
| m_containerEvents(runtime.Events().RegisterContainerStateUpdates( |
There was a problem hiding this comment.
m_containerEvents keeps a reference to the DockerEventTracker it was created with, but if that object goes away, it will operate on a dangling pointer.
The easiest way I can think of to solve this would be to keep the same event tracker object between different VM's, and have logic to "restart" it when a new VM starts (other wise, all containers & processes would need to re-register with the new event tracker)
| // the lock, so the handler must not call back into the session. | ||
| if (m_vmStartNotified.exchange(false) && m_hooks.OnVmStopping) | ||
| { | ||
| m_hooks.OnVmStopping(); |
There was a problem hiding this comment.
I think there's a potential deadlock here. If a plugin receives a "VM stopping" notification and runs an operation that can try to acquire a VM lease, we could get stuck on m_lock in VmLease's constructor
| // Wait for dockerd to be ready before starting the event tracker. | ||
| THROW_WIN32_IF_MSG( | ||
| ERROR_TIMEOUT, !m_dockerdReadyEvent.wait(Settings->BootTimeoutMs), "Timed out waiting for dockerd to start"); | ||
| m_containers.clear(); |
There was a problem hiding this comment.
I don't think we can do this unfortunately, because this will break any COM references that callers might have to containers
|
|
||
| // Tear down the VM (if running) and all VM-scoped state, capturing the termination reason. | ||
| // This mirrors the soft teardown used for idle shutdown, but here it is permanent. | ||
| TearDownVmLockHeld(/* CaptureTerminationReason */ true); |
There was a problem hiding this comment.
The locking is confusing me a bit here, because we're calling TearDownVmLockHeld, but we don't actually hold m_lock, only the session lock.
I think the locking still works because StopVmLockHeld uses an std::atomic to transition the VM to stopped, but I think we should make the locking a bit more clear here
| ULONG Id{}; | ||
| std::wstring DisplayName; | ||
| const std::atomic<bool>* Terminating{}; | ||
| wil::unique_event* SessionTerminatingEvent{}; |
There was a problem hiding this comment.
nit: We could use wil::shared_event to avoid having to deal with pointers here
| return S_OK; | ||
| } | ||
|
|
||
| g_logfile << "WSLC VM stopping, session=" << Session->SessionId << std::endl; |
There was a problem hiding this comment.
I recommend launching a process inside the VM & removing a mount here, to validate that we handle these calls correctly
Fixes #40961
Summary
Idle-terminates a per-user WSLC session's backing VM when it has been inactive, freeing memory while the session object (and its persistent storage) lives on. The VM is transparently recreated on the next operation.
Builds on #40770 (IWSLCVirtualMachineFactory).
Behavior
VM plugin hooks
Idle-terminate breaks the old "session == running VM" assumption (the VM is lazily created, idle-terminated, and transparently recreated while the session lives on), so plugins gain VM-level hooks that fire on every VM (re)start/teardown, alongside the existing per-session
OnSessionCreated/OnSessionStoppinghooks:WSLPluginAPI_OnWslcVmStarted/WSLPluginAPI_OnWslcVmStopping(WslPluginApi.h) withWslcVmStarted/WslcVmStoppingfields inWSLPluginHooksV1.IWSLCPluginNotifier::OnVmStarted()/OnVmStopping()(new methods, no new IID — service and per-user session ship together).OnWslcVmStartedfires after releasingm_lockinEnsureVmRunning(the sole caller holds an activity reference across the gap, so idle teardown can't race the VM down; firing under the lock could deadlock on a reentrantWSLCCreateProcess).OnWslcVmStoppingfires under the lock inTearDownVmLockHeld, with fire-once pairing viastd::atomic<bool> m_vmStartNotifiedso a failed bring-up emits no spurious stopping.Testing
WSLCE2EVmIdleTestsE2E suite (5 tests) includingWSLCE2E_VmIdle_RootProcessKeepsVmAlive.PluginTests::WslcVmRestartasserting the hook sequence: Session created -> VM started -> VM stopping -> VM started -> VM stopping -> Session stopping.WSLCTests::CreateRootNamespaceProcessstill passes.Notes / follow-ups (deferred)
Note
Draft for early review.