⚡ Sangsakala Proxy 🏠 Home
Skip to content

Give each runspace in a RunspacePool its own InitialSessionState - #28286

Open
Joseph "Ryan" Ries (ryanriesMSFT) wants to merge 1 commit into
PowerShell:masterfrom
ryanriesMSFT:fix-runspacepool-per-runspace-iss
Open

Joseph "Ryan" Ries (ryanriesMSFT) wants to merge 1 commit into
PowerShell:masterfrom
ryanriesMSFT:fix-runspacepool-per-runspace-iss

Conversation

@ryanriesMSFT

Copy link
Copy Markdown

PR Summary

Give each runspace in a local RunspacePool its own copy of the pool's InitialSessionState, instead of sharing a single instance across all runspaces in the pool.

Sharing the instance can make a pooled runspace silently skip a module's type and format data (TypesToProcess / FormatsToProcess). ETS members are then missing in that runspace. For example, the enum script properties that CDXML modules generate are missing, so (Get-NetConnectionProfile).NetworkCategory intermittently returns [uint32] 2 instead of DomainAuthenticated. The same happens with Get-Disk, Get-Partition, and other cmdlets whose output relies on module type data, whenever they run in a pooled runspace.

Changes:

  • RunspacePoolInternal.CreateRunspace() now creates runspaces with RunspaceFactory.CreateRunspace(host, _initialSessionState), which clones the InitialSessionState just like a standalone runspace.
  • The no-clone path is now unused, so it is removed: RunspaceFactory.CreateRunspaceFromSessionStateNoClone, the LocalRunspace(..., bool suppressClone) constructor, and the RunspaceBase(..., bool suppressClone) constructor. All three are internal.
  • A new CI test in RunspacePool.Tests.ps1 fails without this change and passes with it.

PR Context

RunspacePoolInternal.CreateRunspace() created every runspace with CreateRunspaceFromSessionStateNoClone, so all runspaces in a pool shared one InitialSessionState object. That object behaves as mutable, per-runspace state:

  • When a module is imported, InitialSessionState.Bind(..., updateOnly: true) → UpdateTypes / UpdateFormats loads the module's type and format files into the importing runspace's TypeTable and format database. It also appends them to context.InitialSessionState.Types / .Formats, which is the shared instance.
  • ModuleCmdletBase.LoadModuleManifest decides whether a manifest's TypesToProcess / FormatsToProcess file is already loaded by looking it up in Context.InitialSessionState.Types / .Formats. If the file is listed, the binding is skipped.

Each runspace still has its own TypeTable and format database, which are populated from the InitialSessionState when the runspace is opened. So a failure follows when both of these happen:

  1. Runspace B is opened, for example because runspace A was busy when another command was queued.
  2. Runspace A imports a module, which records the module's type and format files in the shared InitialSessionState.

When B then imports the same module, the files are already listed, so B never loads them. Member lookup falls back to the CIM/.NET adapter and returns the raw value. No error or warning is written.

The problem is intermittent and only shows up in some hosts because it requires all of the following:

  • a pool that has more than one runspace, as created by scripts and frameworks that parallelize work with RunspaceFactory.CreateRunspacePool(min, max, ...), by custom hosts, or (by code inspection) by remoting endpoints when the client opens a pool with more than one runspace;
  • a specific order of runspace creation and module import, which depends on load and timing;
  • runspaces that live long enough. The pool's idle cleanup timer periodically destroys runspaces, so affected runspaces come and go.

Code paths that create standalone runspaces already clone the InitialSessionState and are not affected. This includes RunspaceFactory.CreateRunspace(...), the console host, and ForEach-Object -Parallel.

Not sharing the instance also removes other cross-runspace effects of the shared object:

  • One runspace could enumerate Types / Formats (UpdateTypes uses Parallel.ForEach) while a sibling appends to them during Import-Module.
  • Remove-Module, Update-TypeData, and Remove-TypeData in one runspace clear and rebuild the shared lists.
  • For a pool created from an InitialSessionState with a shared TypeTable entry, only the first runspace actually shared the TypeTable, because UpdateTypes replaces that entry in the shared instance.

Cost: one InitialSessionState.Clone() per runspace that the pool creates. For InitialSessionState.CreateDefault() that is about 1 ms, compared with about 50 ms to open a runspace.

Windows repro. Without the fix, the second line typically shows 0 (UInt32) or the corresponding integer:

$pool = [runspacefactory]::CreateRunspacePool(1, 2)
$pool.Open()
$script = {
    param($delay)
    Start-Sleep -Seconds $delay          # the first command imports NetConnection at ~2s, the second at ~4s
    $p = Get-NetConnectionProfile | Select-Object -First 1
    Start-Sleep -Seconds (6 - $delay)    # keep the first runspace busy so the pool opens a second one up front
    '{0}: {1} ({2})' -f [runspace]::DefaultRunspace.Name, $p.NetworkCategory, $p.NetworkCategory.GetType().Name
}
$invocations = 2, 4 | ForEach-Object {
    $ps = [powershell]::Create().AddScript($script).AddArgument($_)
    $ps.RunspacePool = $pool
    [pscustomobject]@{ PowerShell = $ps; Handle = $ps.BeginInvoke() }
}
$invocations | ForEach-Object { $_.PowerShell.EndInvoke($_.Handle) }
$pool.Dispose()

The new cross-platform test reproduces the same ordering deterministically with a self-contained module that has a types file and a format file. It uses events instead of sleeps. Results:

  • Built from this branch: the test passes.
  • PowerShell 7.6.9 without this change: the test fails with Expected 42, because the type data should be loaded in the 'Second' runspace, but got $null.

Locally, I also ran the existing tests that use runspace pools, along with the Import-Module, Update-TypeData, Remove-TypeData, type-data, and runspace cmdlet tests. Only the WinRM-based remote pool test failed, because WinRM isn't configured on the test machine.

PR Checklist

@azure-pipelines

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

RunspacePoolInternal created every runspace with
CreateRunspaceFromSessionStateNoClone, so all runspaces in a pool shared a
single InitialSessionState instance. The InitialSessionState is updated when a
runspace is opened and when modules are imported into it (for example, the
type and format files that were loaded are recorded in its Types and Formats
collections), and Import-Module uses those collections to decide whether a
module's TypesToProcess and FormatsToProcess files are already loaded.

As a result, a runspace that was opened before another runspace in the same
pool imported a module skipped that module's type and format data when it
imported the module itself. ETS members such as the enum script properties
generated for CDXML modules (NetConnection, Storage, ...) were missing in that
runspace, so properties like NetworkCategory surfaced as raw integers. Whether
a given runspace was affected depended on runspace creation order and timing.

Create pooled runspaces with RunspaceFactory.CreateRunspace, which clones the
pool's InitialSessionState exactly like a standalone runspace, and remove the
now-unused no-clone code path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7411f14d-b619-4f89-9a9e-b0880f11ef33
@ryanriesMSFT
Joseph "Ryan" Ries (ryanriesMSFT) marked this pull request as ready for review October 29, 2026 20:39
Copilot AI balanced review requested due to automatic review settings October 29, 2026 20:39

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.

🟢 Approval recommended

No active review issues were identified for the change isolating each pooled runspace’s session state.

No open issues 🎉

What changed in this PR

This PR isolates each local pooled runspace’s InitialSessionState, preventing module imports in one runspace from causing another to skip type and format data.

Changes:

  • Use the standard cloning factory when creating pooled runspaces.
  • Remove the unused internal factory and constructors that bypassed cloning.
  • Add a deterministic, cross-platform regression test covering type and format data loading in two pooled runspaces.
File Description
test/​powershell/​engine/​Remoting/​RunspacePool.Tests.ps1 Adds event-coordinated regression coverage for per-runspace module data.
src/​System.Management.Automation/​engine/​hostifaces/​RunspacePoolInternal.cs Creates each runspace with its own session-state copy.
src/​System.Management.Automation/​engine/​hostifaces/​LocalConnection.cs Removes the constructor supporting clone suppression.
src/​System.Management.Automation/​engine/​hostifaces/​ConnectionFactory.cs Removes the unused no-clone factory method.
src/​System.Management.Automation/​engine/​hostifaces/​ConnectionBase.cs Removes the base constructor supporting clone suppression.

Review effort: Balanced

@SteveL-MSFT Steve Lee (SteveL-MSFT) added WG-NeedsReview Needs a review by the labeled Working Group WG-Engine core PowerShell engine, interpreter, and runtime labels Oct 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

WG-Engine core PowerShell engine, interpreter, and runtime WG-NeedsReview Needs a review by the labeled Working Group

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants