< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Scripting.KestrunRunspacePoolManager
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Scripting/KestrunRunspacePoolManager.cs
Tag: Kestrun/Kestrun@2d87023b37eb91155071c91dd3d6a2eeb3004705
Line coverage
75%
Covered lines: 75
Uncovered lines: 24
Coverable lines: 99
Total lines: 262
Line coverage: 75.7%
Branch coverage
75%
Covered branches: 39
Total branches: 52
Branch coverage: 75%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 08/26/2025 - 01:25:22 Line coverage: 67% (63/94) Branch coverage: 67.3% (35/52) Total lines: 251 Tag: Kestrun/Kestrun@07f821172e5dc3657f1be7e6818f18d6721cf38a09/02/2025 - 17:53:35 Line coverage: 74.4% (70/94) Branch coverage: 75% (39/52) Total lines: 251 Tag: Kestrun/Kestrun@58c69eb1b344fcfa1f4b9c597030af85fac9e31b09/06/2025 - 18:30:33 Line coverage: 75.5% (71/94) Branch coverage: 76.9% (40/52) Total lines: 251 Tag: Kestrun/Kestrun@aeddbedb8a96e9137aac94c2d5edd011b57ac87110/13/2025 - 16:52:37 Line coverage: 75.7% (75/99) Branch coverage: 75% (39/52) Total lines: 262 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e 08/26/2025 - 01:25:22 Line coverage: 67% (63/94) Branch coverage: 67.3% (35/52) Total lines: 251 Tag: Kestrun/Kestrun@07f821172e5dc3657f1be7e6818f18d6721cf38a09/02/2025 - 17:53:35 Line coverage: 74.4% (70/94) Branch coverage: 75% (39/52) Total lines: 251 Tag: Kestrun/Kestrun@58c69eb1b344fcfa1f4b9c597030af85fac9e31b09/06/2025 - 18:30:33 Line coverage: 75.5% (71/94) Branch coverage: 76.9% (40/52) Total lines: 251 Tag: Kestrun/Kestrun@aeddbedb8a96e9137aac94c2d5edd011b57ac87110/13/2025 - 16:52:37 Line coverage: 75.7% (75/99) Branch coverage: 75% (39/52) Total lines: 262 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)83.33%121296%
get_Host()100%11100%
get_MinRunspaces()100%210%
get_MaxRunspaces()100%11100%
get_ThreadOptions()100%11100%
Acquire()60%271044.44%
AcquireAsync()40%281043.75%
Release(...)83.33%7670%
CreateRunspace()100%66100%
Dispose()100%88100%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Scripting/KestrunRunspacePoolManager.cs

#LineLine coverage
 1using System.Collections.Concurrent;
 2using System.Management.Automation.Runspaces;
 3using Kestrun.Hosting;
 4using Serilog.Events;
 5
 6namespace Kestrun.Scripting;
 7
 8/// <summary>
 9/// Manages a pool of PowerShell runspaces for efficient reuse and resource control.
 10/// </summary>
 11public sealed class KestrunRunspacePoolManager : IDisposable
 12{
 10213    private readonly ConcurrentBag<Runspace> _stash = [];
 14    private readonly InitialSessionState _iss;
 15    private int _count;        // total live runspaces
 16    private bool _disposed;
 17
 18    /// <summary>
 19    /// KestrunHost is needed for logging, config, etc.
 20    /// </summary>
 124021    public KestrunHost Host { get; private set; }
 22
 23    /// <summary>
 24    /// Gets the minimum number of runspaces maintained in the pool.
 25    /// </summary>
 026    public int MinRunspaces { get; }
 27    /// <summary>
 28    /// Gets the maximum number of runspaces allowed in the pool.
 29    /// </summary>
 1230    public int MaxRunspaces { get; }
 31
 32    /// <summary>
 33    /// Thread‑affinity strategy for *future* runspaces.
 34    /// Default is <see cref="PSThreadOptions.ReuseThread"/>.
 35    /// </summary>
 40636    public PSThreadOptions ThreadOptions { get; set; } = PSThreadOptions.ReuseThread;
 37
 38    // ───────────────── constructor ──────────────────────────
 39    /// <summary>
 40    /// Initializes a new instance of the <see cref="KestrunRunspacePoolManager"/> class with the specified minimum and 
 41    /// </summary>
 42    /// <param name="host">The Kestrun host instance.</param>
 43    /// <param name="minRunspaces">The minimum number of runspaces to maintain in the pool.</param>
 44    /// <param name="maxRunspaces">The maximum number of runspaces allowed in the pool.</param>
 45    /// <param name="initialSessionState">The initial session state for each runspace (optional).</param>
 46    /// <param name="threadOptions">The thread affinity strategy for runspaces (optional).</param>
 10247    public KestrunRunspacePoolManager(
 10248        KestrunHost host,
 10249        int minRunspaces,
 10250        int maxRunspaces,
 10251        InitialSessionState? initialSessionState = null,
 10252        PSThreadOptions threadOptions = PSThreadOptions.ReuseThread)
 53    {
 10254        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 55        {
 6556            host.Logger.Debug("Initializing RunspacePoolManager: Min={Min}, Max={Max}", minRunspaces, maxRunspaces);
 57        }
 58
 10259        ArgumentOutOfRangeException.ThrowIfNegative(minRunspaces);
 10260        ArgumentNullException.ThrowIfNull(host);
 10261        ArgumentOutOfRangeException.ThrowIfNegative(maxRunspaces);
 62        // sanity check
 10263        if (maxRunspaces < 1 || maxRunspaces < minRunspaces)
 64        {
 065            throw new ArgumentOutOfRangeException(nameof(maxRunspaces));
 66        }
 67
 10268        Host = host;
 10269        MinRunspaces = minRunspaces;
 10270        MaxRunspaces = maxRunspaces;
 10271        _iss = initialSessionState ?? InitialSessionState.CreateDefault();
 10272        ThreadOptions = threadOptions;
 73
 74        // warm the stash
 40675        for (var i = 0; i < minRunspaces; i++)
 76        {
 10177            _stash.Add(CreateRunspace());
 78        }
 79
 10280        _count = minRunspaces;
 10281        if (Host.Logger.IsEnabled(LogEventLevel.Debug))
 82        {
 6583            Host.Logger.Debug("Warm-started pool with {Count} runspaces", _count);
 84        }
 10285    }
 86
 87    // ───────────────── public API ────────────────────────────
 88    /// <summary>Borrow a runspace (creates one if under the cap).</summary>
 89    public Runspace Acquire()
 90    {
 1591        if (Host.Logger.IsEnabled(LogEventLevel.Debug))
 92        {
 993            Host.Logger.Debug("Acquiring runspace from pool: CurrentCount={Count}, Max={Max}", _count, MaxRunspaces);
 94        }
 95
 1596        ObjectDisposedException.ThrowIf(_disposed, nameof(KestrunRunspacePoolManager));
 97
 1598        if (_stash.TryTake(out var rs))
 99        {
 15100            if (rs.RunspaceStateInfo.State != RunspaceState.Opened)
 101            {
 0102                Host.Logger.Warning("Runspace from stash is not opened: {State}. Discarding and acquiring a new one.", r
 103                // If the runspace is not open, we cannot use it.
 104                // Discard and try again
 0105                rs.Dispose();
 0106                _ = Interlocked.Decrement(ref _count);
 0107                return Acquire();
 108            }
 15109            if (Host.Logger.IsEnabled(LogEventLevel.Debug))
 110            {
 9111                Host.Logger.Debug("Reusing runspace from stash: StashCount={Count}", _stash.Count);
 112            }
 113
 15114            return rs;
 115        }
 116        // Need a new one?—but only if we haven’t reached max.
 0117        if (Interlocked.Increment(ref _count) <= MaxRunspaces)
 118        {
 0119            Host.Logger.Debug("Creating new runspace: TotalCount={Count}", _count);
 0120            return CreateRunspace();
 121        }
 122        // Overshot: roll back and complain.
 0123        _ = Interlocked.Decrement(ref _count);
 124
 0125        Host.Logger.Warning("Runspace limit reached: Max={Max}", MaxRunspaces);
 0126        throw new InvalidOperationException("Run-space limit reached.");
 127    }
 128
 129    /// <summary>
 130    /// Asynchronously acquires a runspace from the pool, creating a new one if under the cap, or waits until one become
 131    /// </summary>
 132    /// <param name="cancellationToken">A cancellation token to observe while waiting for a runspace.</param>
 133    /// <returns>A task that represents the asynchronous operation, containing the acquired <see cref="Runspace"/>.</ret
 134    public async Task<Runspace> AcquireAsync(CancellationToken cancellationToken = default)
 135    {
 4136        if (Host.Logger.IsEnabled(LogEventLevel.Debug))
 137        {
 1138            Host.Logger.Debug("Acquiring runspace (async) from pool: CurrentCount={Count}, Max={Max}", _count, MaxRunspa
 139        }
 140
 0141        while (true)
 142        {
 4143            ObjectDisposedException.ThrowIf(_disposed, nameof(KestrunRunspacePoolManager));
 144
 3145            if (_stash.TryTake(out var rs))
 146            {
 3147                if (Host.Logger.IsEnabled(LogEventLevel.Debug))
 148                {
 0149                    Host.Logger.Debug("Reusing runspace from stash (async): StashCount={Count}", _stash.Count);
 150                }
 151
 3152                return rs;
 153            }
 154
 0155            if (Interlocked.Increment(ref _count) <= MaxRunspaces)
 156            {
 0157                Host.Logger.Debug("Creating new runspace (async): TotalCount={Count}", _count);
 158                // Runspace creation is synchronous, but we can offload to thread pool
 0159                return await Task.Run(CreateRunspace, cancellationToken).ConfigureAwait(false);
 160            }
 0161            _ = Interlocked.Decrement(ref _count);
 162
 163            // Wait for a runspace to be returned
 0164            if (Host.Logger.IsEnabled(LogEventLevel.Debug))
 165            {
 0166                Host.Logger.Debug("Waiting for runspace to become available (async)");
 167            }
 168
 169            // Use a short delay to poll for availability
 0170            await Task.Delay(50, cancellationToken).ConfigureAwait(false);
 171        }
 3172    }
 173
 174
 175    /// <summary>
 176    /// Returns a runspace to the pool for reuse, or disposes it if the pool has been disposed.
 177    /// </summary>
 178    /// <param name="rs">The <see cref="Runspace"/> to return to the pool.</param>
 179    public void Release(Runspace rs)
 180    {
 16181        if (Host.Logger.IsEnabled(LogEventLevel.Debug))
 182        {
 9183            Host.Logger.Debug("Release() called: Disposed={Disposed}", _disposed);
 184        }
 185
 16186        if (_disposed)
 187        {
 0188            Host.Logger.Warning("Pool disposed; disposing returned runspace");
 0189            rs.Dispose();
 0190            return;
 191        }
 192
 16193        _stash.Add(rs);
 16194        if (Host.Logger.IsEnabled(LogEventLevel.Debug))
 195        {
 9196            Host.Logger.Debug("Runspace returned to stash: StashCount={Count}", _stash.Count);
 197        }
 198        // Note: we do not decrement _count here, as the pool size is fixed.
 199        // The pool will keep the runspace open for reuse.
 16200    }
 201
 202
 203    // ───────────────── helpers ───────────────────────────────
 204    private Runspace CreateRunspace()
 205    {
 101206        if (Host.Logger.IsEnabled(LogEventLevel.Debug))
 207        {
 64208            Host.Logger.Debug("CreateRunspace() - creating new runspace");
 209        }
 210
 101211        if (Host.Logger.IsEnabled(LogEventLevel.Debug))
 212        {
 64213            Host.Logger.Debug("Creating new runspace with InitialSessionState");
 214        }
 215
 101216        var rs = RunspaceFactory.CreateRunspace(_iss);
 217
 218        // Apply the chosen thread‑affinity strategy **before** opening.
 101219        rs.ThreadOptions = ThreadOptions;
 101220        rs.ApartmentState = ApartmentState.MTA;     // always MTA
 221
 101222        rs.Open();
 101223        Host.Logger.Information("Opened new Runspace with ThreadOptions={ThreadOptions}", ThreadOptions);
 101224        if (Host.Logger.IsEnabled(LogEventLevel.Debug))
 225        {
 64226            Host.Logger.Debug("New runspace created: {Runspace}", rs);
 227        }
 228
 101229        return rs;
 230    }
 231
 232    // ───────────────── cleanup ───────────────────────────────
 233    /// <summary>
 234    /// Disposes the runspace pool manager and all pooled runspaces.
 235    /// </summary>
 236    public void Dispose()
 237    {
 59238        if (Host.Logger.IsEnabled(LogEventLevel.Debug))
 239        {
 25240            Host.Logger.Debug("Disposing KestrunRunspacePoolManager: Disposed={Disposed}", _disposed);
 241        }
 242
 59243        if (_disposed)
 244        {
 17245            return;
 246        }
 247
 42248        _disposed = true;
 42249        Host.Logger.Information("Disposing RunspacePoolManager and all pooled runspaces");
 83250        while (_stash.TryTake(out var rs))
 251        {
 41252            if (Host.Logger.IsEnabled(LogEventLevel.Debug))
 253            {
 24254                Host.Logger.Debug("Disposing runspace: {Runspace}", rs);
 255            }
 256
 82257            try { rs.Close(); } catch { /* ignore */ }
 41258            rs.Dispose();
 41259        }
 42260        Host.Logger.Information("RunspacePoolManager disposed");
 42261    }
 262}