< 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@0d738bf294e6281b936d031e1979d928007495ff
Line coverage
87%
Covered lines: 115
Uncovered lines: 16
Coverable lines: 131
Total lines: 327
Line coverage: 87.7%
Branch coverage
93%
Covered branches: 54
Total branches: 58
Branch coverage: 93.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 08/26/2025 - 14:53:17 Line coverage: 67% (63/94) Branch coverage: 67.3% (35/52) Total lines: 251 Tag: Kestrun/Kestrun@78d1e497d8ba989d121b57aa39aa3c6b22de743109/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@10d476bee71c71ad215bb8ab59f219887b5b4a5e11/19/2025 - 02:25:56 Line coverage: 72.5% (87/120) Branch coverage: 77.7% (42/54) Total lines: 296 Tag: Kestrun/Kestrun@98ff905e5605a920343154665980a71211a03c6d12/12/2025 - 17:27:19 Line coverage: 69.4% (91/131) Branch coverage: 74.1% (43/58) Total lines: 327 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/15/2025 - 02:23:46 Line coverage: 72.5% (95/131) Branch coverage: 75.8% (44/58) Total lines: 327 Tag: Kestrun/Kestrun@7a3839f4de2254e22daae81ab8dc7cb2f40c833012/15/2025 - 04:25:23 Line coverage: 74.8% (98/131) Branch coverage: 79.3% (46/58) Total lines: 327 Tag: Kestrun/Kestrun@e333660af9731cab5ae4c14a12f3bb84a8fabc7d12/15/2025 - 18:01:19 Line coverage: 72.5% (95/131) Branch coverage: 75.8% (44/58) Total lines: 327 Tag: Kestrun/Kestrun@7127e76ef631ac4d0965a9a37239d5089469e32812/15/2025 - 18:44:50 Line coverage: 90% (118/131) Branch coverage: 96.5% (56/58) Total lines: 327 Tag: Kestrun/Kestrun@6b9e56ea2de904fc3597033ef0f9bc7839d5d61812/18/2025 - 21:41:58 Line coverage: 87.7% (115/131) Branch coverage: 93.1% (54/58) Total lines: 327 Tag: Kestrun/Kestrun@0d738bf294e6281b936d031e1979d928007495ff 08/26/2025 - 14:53:17 Line coverage: 67% (63/94) Branch coverage: 67.3% (35/52) Total lines: 251 Tag: Kestrun/Kestrun@78d1e497d8ba989d121b57aa39aa3c6b22de743109/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@10d476bee71c71ad215bb8ab59f219887b5b4a5e11/19/2025 - 02:25:56 Line coverage: 72.5% (87/120) Branch coverage: 77.7% (42/54) Total lines: 296 Tag: Kestrun/Kestrun@98ff905e5605a920343154665980a71211a03c6d12/12/2025 - 17:27:19 Line coverage: 69.4% (91/131) Branch coverage: 74.1% (43/58) Total lines: 327 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/15/2025 - 02:23:46 Line coverage: 72.5% (95/131) Branch coverage: 75.8% (44/58) Total lines: 327 Tag: Kestrun/Kestrun@7a3839f4de2254e22daae81ab8dc7cb2f40c833012/15/2025 - 04:25:23 Line coverage: 74.8% (98/131) Branch coverage: 79.3% (46/58) Total lines: 327 Tag: Kestrun/Kestrun@e333660af9731cab5ae4c14a12f3bb84a8fabc7d12/15/2025 - 18:01:19 Line coverage: 72.5% (95/131) Branch coverage: 75.8% (44/58) Total lines: 327 Tag: Kestrun/Kestrun@7127e76ef631ac4d0965a9a37239d5089469e32812/15/2025 - 18:44:50 Line coverage: 90% (118/131) Branch coverage: 96.5% (56/58) Total lines: 327 Tag: Kestrun/Kestrun@6b9e56ea2de904fc3597033ef0f9bc7839d5d61812/18/2025 - 21:41:58 Line coverage: 87.7% (115/131) Branch coverage: 93.1% (54/58) Total lines: 327 Tag: Kestrun/Kestrun@0d738bf294e6281b936d031e1979d928007495ff

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%1212100%
get_Host()100%11100%
get_MinRunspaces()100%11100%
get_MaxRunspaces()100%11100%
get_OpenApiClassesPath()100%11100%
get_ThreadOptions()100%11100%
Acquire()90%111077.77%
AcquireAsync()90%101087.5%
Release(...)100%8663.15%
CreateRunspace()100%66100%
Dispose()85.71%141490.32%

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{
 14113    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    /// Track all runspaces ever created (for cleanup)
 20    /// </summary>
 21    private readonly ConcurrentDictionary<Runspace, byte> _all;
 22
 23    /// <summary>
 24    /// KestrunHost is needed for logging, config, etc.
 25    /// </summary>
 248926    public KestrunHost Host { get; private set; }
 27
 28    /// <summary>
 29    /// Gets the minimum number of runspaces maintained in the pool.
 30    /// </summary>
 231    public int MinRunspaces { get; }
 32    /// <summary>
 33    /// Gets the maximum number of runspaces allowed in the pool.
 34    /// </summary>
 10435    public int MaxRunspaces { get; }
 36
 37    /// <summary>
 38    /// Path to the OpenAPI class definitions to be injected into each runspace.
 39    /// </summary>
 21340    public string? OpenApiClassesPath { get; init; }
 41
 42    /// <summary>
 43    /// Thread‑affinity strategy for *future* runspaces.
 44    /// Default is <see cref="PSThreadOptions.ReuseThread"/>.
 45    /// </summary>
 58846    public PSThreadOptions ThreadOptions { get; set; } = PSThreadOptions.ReuseThread;
 47
 48    // ───────────────── constructor ──────────────────────────
 49    /// <summary>
 50    /// Initializes a new instance of the <see cref="KestrunRunspacePoolManager"/> class with the specified minimum and 
 51    /// </summary>
 52    /// <param name="host">The Kestrun host instance.</param>
 53    /// <param name="minRunspaces">The minimum number of runspaces to maintain in the pool.</param>
 54    /// <param name="maxRunspaces">The maximum number of runspaces allowed in the pool.</param>
 55    /// <param name="initialSessionState">The initial session state for each runspace (optional).</param>
 56    /// <param name="threadOptions">The thread affinity strategy for runspaces (optional).</param>
 57    /// <param name="openApiClassesPath">The file path to the OpenAPI class definitions to be injected into each runspac
 14158    public KestrunRunspacePoolManager(
 14159        KestrunHost host,
 14160        int minRunspaces,
 14161        int maxRunspaces,
 14162        InitialSessionState? initialSessionState = null,
 14163        PSThreadOptions threadOptions = PSThreadOptions.ReuseThread,
 14164        string? openApiClassesPath = null)
 65    {
 14166        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 67        {
 11868            host.Logger.Debug("Initializing RunspacePoolManager: Min={Min}, Max={Max}", minRunspaces, maxRunspaces);
 69        }
 70
 14071        ArgumentOutOfRangeException.ThrowIfNegative(minRunspaces);
 13972        ArgumentNullException.ThrowIfNull(host);
 13973        ArgumentOutOfRangeException.ThrowIfNegative(maxRunspaces);
 74        // sanity check
 13875        if (maxRunspaces < 1 || maxRunspaces < minRunspaces)
 76        {
 277            throw new ArgumentOutOfRangeException(nameof(maxRunspaces));
 78        }
 13679        _all = new();
 13680        Host = host;
 13681        MinRunspaces = minRunspaces;
 13682        MaxRunspaces = maxRunspaces;
 13683        _iss = initialSessionState ?? InitialSessionState.CreateDefault();
 13684        ThreadOptions = threadOptions;
 85
 86        // warm the stash
 53687        for (var i = 0; i < minRunspaces; i++)
 88        {
 13289            _stash.Add(CreateRunspace());
 90        }
 91
 13692        _count = minRunspaces;
 93
 94        // Store OpenAPI classes
 13695        OpenApiClassesPath = openApiClassesPath;
 13696        if (Host.Logger.IsEnabled(LogEventLevel.Debug))
 97        {
 11498            Host.Logger.Debug("Warm-started pool with {Count} runspaces", _count);
 99        }
 136100    }
 101
 102    // ───────────────── public API ────────────────────────────
 103    /// <summary>Borrow a runspace (creates one if under the cap).</summary>
 104    public Runspace Acquire()
 105    {
 67106        if (Host.Logger.IsEnabled(LogEventLevel.Debug))
 107        {
 67108            Host.Logger.Debug("Acquiring runspace from pool: CurrentCount={Count}, Max={Max}", _count, MaxRunspaces);
 109        }
 110
 67111        ObjectDisposedException.ThrowIf(_disposed, nameof(KestrunRunspacePoolManager));
 112
 66113        if (_stash.TryTake(out var rs))
 114        {
 43115            if (rs.RunspaceStateInfo.State != RunspaceState.Opened)
 116            {
 0117                Host.Logger.Warning("Runspace from stash is not opened: {State}. Discarding and acquiring a new one.", r
 118                // If the runspace is not open, we cannot use it.
 119                // Discard and try again
 0120                rs.Dispose();
 0121                _ = Interlocked.Decrement(ref _count);
 0122                return Acquire();
 123            }
 43124            if (Host.Logger.IsEnabled(LogEventLevel.Debug))
 125            {
 43126                Host.Logger.Debug("Reusing runspace from stash: StashCount={Count}", _stash.Count);
 127            }
 128
 43129            return rs;
 130        }
 131        // Need a new one?—but only if we haven’t reached max.
 23132        if (Interlocked.Increment(ref _count) <= MaxRunspaces)
 133        {
 22134            Host.Logger.Debug("Creating new runspace: TotalCount={Count}", _count);
 22135            return CreateRunspace();
 136        }
 137        // Overshot: roll back and complain.
 1138        _ = Interlocked.Decrement(ref _count);
 139
 1140        Host.Logger.Warning("Runspace limit reached: Max={Max}", MaxRunspaces);
 1141        throw new InvalidOperationException("Run-space limit reached.");
 142    }
 143
 144    /// <summary>
 145    /// Asynchronously acquires a runspace from the pool, creating a new one if under the cap, or waits until one become
 146    /// </summary>
 147    /// <param name="cancellationToken">A cancellation token to observe while waiting for a runspace.</param>
 148    /// <returns>A task that represents the asynchronous operation, containing the acquired <see cref="Runspace"/>.</ret
 149    public async Task<Runspace> AcquireAsync(CancellationToken cancellationToken = default)
 150    {
 7151        if (Host.Logger.IsEnabled(LogEventLevel.Debug))
 152        {
 4153            Host.Logger.Debug("Acquiring runspace (async) from pool: CurrentCount={Count}, Max={Max}", _count, MaxRunspa
 154        }
 155
 2156        while (true)
 157        {
 9158            ObjectDisposedException.ThrowIf(_disposed, nameof(KestrunRunspacePoolManager));
 159
 8160            if (_stash.TryTake(out var rs))
 161            {
 5162                if (Host.Logger.IsEnabled(LogEventLevel.Debug))
 163                {
 2164                    Host.Logger.Debug("Reusing runspace from stash (async): StashCount={Count}", _stash.Count);
 165                }
 166
 5167                return rs;
 168            }
 169            // Need a new one?—but only if we haven’t reached max.
 3170            if (Interlocked.Increment(ref _count) <= MaxRunspaces)
 171            {
 0172                Host.Logger.Debug("Creating new runspace (async): TotalCount={Count}", _count);
 173                // Runspace creation is synchronous, but we can offload to thread pool
 0174                return await Task.Run(CreateRunspace, cancellationToken).ConfigureAwait(false);
 175            }
 176            // Overshot: roll back and try again.
 3177            _ = Interlocked.Decrement(ref _count);
 178
 179            // Wait for a runspace to be returned
 3180            if (Host.Logger.IsEnabled(LogEventLevel.Debug))
 181            {
 3182                Host.Logger.Debug("Waiting for runspace to become available (async)");
 183            }
 184
 185            // Use a short delay to poll for availability
 3186            await Task.Delay(50, cancellationToken).ConfigureAwait(false);
 187        }
 5188    }
 189
 190    /// <summary>
 191    /// Returns a runspace to the pool for reuse, or disposes it if the pool has been disposed.
 192    /// </summary>
 193    /// <param name="rs">The <see cref="Runspace"/> to return to the pool.</param>
 194    public void Release(Runspace rs)
 195    {
 66196        if (Host.Logger.IsEnabled(LogEventLevel.Debug))
 197        {
 65198            Host.Logger.Debug("Release() called: Disposed={Disposed}", _disposed);
 199        }
 200
 66201        if (_disposed)
 202        {
 1203            Host.Logger.Warning("Pool disposed; disposing returned runspace");
 1204            rs.Dispose();
 1205            return;
 206        }
 207
 208        try
 209        {
 210            // Put the genie back in the bottle: variables, funcs, modules…
 211            // This returns the runspace to the InitialSessionState baseline.
 65212            rs.ResetRunspaceState();
 65213        }
 0214        catch (Exception ex)
 215        {
 0216            Host.Logger.Warning(ex, "ResetRunspaceState failed; disposing runspace instead");
 0217            try { rs.Close(); }
 0218            catch (Exception closeEx) { Host.Logger.Verbose(exception: closeEx, messageTemplate: "Failed to close runspa
 0219            rs.Dispose();
 0220            _ = Interlocked.Decrement(ref _count);
 0221            return;
 222        }
 65223        _stash.Add(rs);
 65224        if (Host.Logger.IsEnabled(LogEventLevel.Debug))
 225        {
 64226            Host.Logger.Debug("Runspace returned to stash: StashCount={Count}", _stash.Count);
 227        }
 228        // Note: we do not decrement _count here, as the pool size is fixed.
 229        // The pool will keep the runspace open for reuse.
 65230    }
 231
 232    // ───────────────── helpers ───────────────────────────────
 233    /// <summary>
 234    /// Creates a new PowerShell runspace with the configured initial session state and thread options.
 235    /// </summary>
 236    /// <returns>A new <see cref="Runspace"/> instance.</returns>
 237    private Runspace CreateRunspace()
 238    {
 154239        if (Host.Logger.IsEnabled(LogEventLevel.Debug))
 240        {
 132241            Host.Logger.Debug("CreateRunspace() - creating new runspace");
 242        }
 243
 154244        if (Host.Logger.IsEnabled(LogEventLevel.Debug))
 245        {
 132246            Host.Logger.Debug("Creating new runspace with InitialSessionState");
 247        }
 248        // Important: clone per runspace
 154249        var iss = _iss.Clone();
 154250        var rs = RunspaceFactory.CreateRunspace(iss);
 251
 252        // Apply the chosen thread‑affinity strategy **before** opening.
 154253        rs.ThreadOptions = ThreadOptions;
 154254        rs.ApartmentState = ApartmentState.MTA;     // always MTA
 154255        rs.Open();
 256
 154257        Host.Logger.Information("Opened new Runspace with ThreadOptions={ThreadOptions}", ThreadOptions);
 154258        if (Host.Logger.IsEnabled(LogEventLevel.Debug))
 259        {
 132260            Host.Logger.Debug("New runspace created: {Runspace}", rs);
 261        }
 262
 154263        _ = _all.TryAdd(rs, 0);
 154264        return rs;
 265    }
 266
 267    // ───────────────── cleanup ───────────────────────────────
 268    /// <summary>
 269    /// Disposes the runspace pool manager and all pooled runspaces.
 270    /// </summary>
 271    public void Dispose()
 272    {
 91273        if (Host.Logger.IsEnabled(LogEventLevel.Debug))
 274        {
 89275            Host.Logger.Debug("Disposing KestrunRunspacePoolManager: Disposed={Disposed}", _disposed);
 276        }
 277
 91278        if (_disposed)
 279        {
 20280            return;
 281        }
 282
 71283        if (!string.IsNullOrWhiteSpace(OpenApiClassesPath))
 284        {
 285            try
 286            {
 2287                File.Delete(OpenApiClassesPath);
 0288                if (Host.Logger.IsEnabled(LogEventLevel.Debug))
 289                {
 0290                    Host.Logger.Debug("Deleted temporary OpenAPI classes script: {Path}", OpenApiClassesPath);
 291                }
 0292            }
 2293            catch (Exception ex)
 294            {
 2295                Host.Logger.Warning(ex, "Failed to delete temporary OpenAPI classes script: {Path}", OpenApiClassesPath)
 2296            }
 297        }
 71298        _disposed = true;
 299
 71300        Host.Logger.Information("Disposing RunspacePoolManager and all pooled runspaces");
 301
 302        // Drain the stash
 157303        while (_stash.TryTake(out var rs))
 304        {
 86305            if (Host.Logger.IsEnabled(LogEventLevel.Debug))
 306            {
 84307                Host.Logger.Debug("Disposing runspace: {Runspace}", rs);
 308            }
 172309            try { rs.ResetRunspaceState(); } catch (Exception ex) { Host.Logger.Verbose(exception: ex, messageTemplate: 
 172310            try { rs.Close(); } catch (Exception ex) { Host.Logger.Verbose(exception: ex, messageTemplate: "Failed to cl
 86311            rs.Dispose();
 86312            _ = _all.TryRemove(rs, out _);
 86313            _ = Interlocked.Decrement(ref _count);
 86314        }
 315
 316        // Anything still checked out? Close them too.
 148317        foreach (var kv in _all.Keys)
 318        {
 6319            try { kv.ResetRunspaceState(); } catch (Exception ex) { Host.Logger.Verbose(exception: ex, messageTemplate: 
 6320            try { kv.Close(); } catch (Exception ex) { Host.Logger.Verbose(exception: ex, messageTemplate: "Failed to cl
 3321            kv.Dispose();
 3322            _ = _all.TryRemove(kv, out _);
 3323            _ = Interlocked.Decrement(ref _count);
 324        }
 71325        Host.Logger.Information("RunspacePoolManager disposed");
 71326    }
 327}