< 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@9d3a582b2d63930269564a7591aa77ef297cadeb
Line coverage
75%
Covered lines: 71
Uncovered lines: 23
Coverable lines: 94
Total lines: 251
Line coverage: 75.5%
Branch coverage
76%
Covered branches: 40
Total branches: 52
Branch coverage: 76.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)83.33%121295.23%
get_MinRunspaces()100%210%
get_MaxRunspaces()100%11100%
get_ThreadOptions()100%11100%
Acquire()60%271044.44%
AcquireAsync()50%231050%
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 Serilog;
 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{
 5013    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    /// Gets the minimum number of runspaces maintained in the pool.
 20    /// </summary>
 021    public int MinRunspaces { get; }
 22    /// <summary>
 23    /// Gets the maximum number of runspaces allowed in the pool.
 24    /// </summary>
 1625    public int MaxRunspaces { get; }
 26
 27    /// <summary>
 28    /// Thread‑affinity strategy for *future* runspaces.
 29    /// Default is <see cref="PSThreadOptions.ReuseThread"/>.
 30    /// </summary>
 19831    public PSThreadOptions ThreadOptions { get; set; } = PSThreadOptions.ReuseThread;
 32
 33    // ───────────────── constructor ──────────────────────────
 34    /// <summary>
 35    /// Initializes a new instance of the <see cref="KestrunRunspacePoolManager"/> class with the specified minimum and 
 36    /// </summary>
 37    /// <param name="minRunspaces">The minimum number of runspaces to maintain in the pool.</param>
 38    /// <param name="maxRunspaces">The maximum number of runspaces allowed in the pool.</param>
 39    /// <param name="initialSessionState">The initial session state for each runspace (optional).</param>
 40    /// <param name="threadOptions">The thread affinity strategy for runspaces (optional).</param>
 5041    public KestrunRunspacePoolManager(
 5042        int minRunspaces,
 5043        int maxRunspaces,
 5044        InitialSessionState? initialSessionState = null,
 5045        PSThreadOptions threadOptions = PSThreadOptions.ReuseThread)
 46    {
 5047        if (Log.IsEnabled(LogEventLevel.Debug))
 48        {
 5049            Log.Debug("Initializing RunspacePoolManager: Min={Min}, Max={Max}", minRunspaces, maxRunspaces);
 50        }
 51
 5052        ArgumentOutOfRangeException.ThrowIfNegative(minRunspaces);
 5053        if (maxRunspaces < 1 || maxRunspaces < minRunspaces)
 54        {
 055            throw new ArgumentOutOfRangeException(nameof(maxRunspaces));
 56        }
 57
 5058        MinRunspaces = minRunspaces;
 5059        MaxRunspaces = maxRunspaces;
 5060        _iss = initialSessionState ?? InitialSessionState.CreateDefault();
 5061        ThreadOptions = threadOptions;
 62
 63        // warm the stash
 19864        for (var i = 0; i < minRunspaces; i++)
 65        {
 4966            _stash.Add(CreateRunspace());
 67        }
 68
 5069        _count = minRunspaces;
 5070        if (Log.IsEnabled(LogEventLevel.Debug))
 71        {
 5072            Log.Debug("Warm-started pool with {Count} runspaces", _count);
 73        }
 5074    }
 75
 76    // ───────────────── public API ────────────────────────────
 77    /// <summary>Borrow a runspace (creates one if under the cap).</summary>
 78    public Runspace Acquire()
 79    {
 1180        if (Log.IsEnabled(LogEventLevel.Debug))
 81        {
 1182            Log.Debug("Acquiring runspace from pool: CurrentCount={Count}, Max={Max}", _count, MaxRunspaces);
 83        }
 84
 1185        ObjectDisposedException.ThrowIf(_disposed, nameof(KestrunRunspacePoolManager));
 86
 1187        if (_stash.TryTake(out var rs))
 88        {
 1189            if (rs.RunspaceStateInfo.State != RunspaceState.Opened)
 90            {
 091                Log.Warning("Runspace from stash is not opened: {State}. Discarding and acquiring a new one.", rs.Runspa
 92                // If the runspace is not open, we cannot use it.
 93                // Discard and try again
 094                rs.Dispose();
 095                _ = Interlocked.Decrement(ref _count);
 096                return Acquire();
 97            }
 1198            if (Log.IsEnabled(LogEventLevel.Debug))
 99            {
 11100                Log.Debug("Reusing runspace from stash: StashCount={Count}", _stash.Count);
 101            }
 102
 11103            return rs;
 104        }
 105        // Need a new one?—but only if we haven’t reached max.
 0106        if (Interlocked.Increment(ref _count) <= MaxRunspaces)
 107        {
 0108            Log.Debug("Creating new runspace: TotalCount={Count}", _count);
 0109            return CreateRunspace();
 110        }
 111        // Overshot: roll back and complain.
 0112        _ = Interlocked.Decrement(ref _count);
 113
 0114        Log.Warning("Runspace limit reached: Max={Max}", MaxRunspaces);
 0115        throw new InvalidOperationException("Run-space limit reached.");
 116    }
 117
 118    /// <summary>
 119    /// Asynchronously acquires a runspace from the pool, creating a new one if under the cap, or waits until one become
 120    /// </summary>
 121    /// <param name="cancellationToken">A cancellation token to observe while waiting for a runspace.</param>
 122    /// <returns>A task that represents the asynchronous operation, containing the acquired <see cref="Runspace"/>.</ret
 123    public async Task<Runspace> AcquireAsync(CancellationToken cancellationToken = default)
 124    {
 4125        if (Log.IsEnabled(LogEventLevel.Debug))
 126        {
 4127            Log.Debug("Acquiring runspace (async) from pool: CurrentCount={Count}, Max={Max}", _count, MaxRunspaces);
 128        }
 129
 0130        while (true)
 131        {
 4132            ObjectDisposedException.ThrowIf(_disposed, nameof(KestrunRunspacePoolManager));
 133
 3134            if (_stash.TryTake(out var rs))
 135            {
 3136                if (Log.IsEnabled(LogEventLevel.Debug))
 137                {
 3138                    Log.Debug("Reusing runspace from stash (async): StashCount={Count}", _stash.Count);
 139                }
 140
 3141                return rs;
 142            }
 143
 0144            if (Interlocked.Increment(ref _count) <= MaxRunspaces)
 145            {
 0146                Log.Debug("Creating new runspace (async): TotalCount={Count}", _count);
 147                // Runspace creation is synchronous, but we can offload to thread pool
 0148                return await Task.Run(CreateRunspace, cancellationToken).ConfigureAwait(false);
 149            }
 0150            _ = Interlocked.Decrement(ref _count);
 151
 152            // Wait for a runspace to be returned
 0153            if (Log.IsEnabled(LogEventLevel.Debug))
 154            {
 0155                Log.Debug("Waiting for runspace to become available (async)");
 156            }
 157
 158            // Use a short delay to poll for availability
 0159            await Task.Delay(50, cancellationToken).ConfigureAwait(false);
 160        }
 3161    }
 162
 163
 164    /// <summary>
 165    /// Returns a runspace to the pool for reuse, or disposes it if the pool has been disposed.
 166    /// </summary>
 167    /// <param name="rs">The <see cref="Runspace"/> to return to the pool.</param>
 168    public void Release(Runspace rs)
 169    {
 14170        if (Log.IsEnabled(LogEventLevel.Debug))
 171        {
 14172            Log.Debug("Release() called: Disposed={Disposed}", _disposed);
 173        }
 174
 14175        if (_disposed)
 176        {
 0177            Log.Warning("Pool disposed; disposing returned runspace");
 0178            rs.Dispose();
 0179            return;
 180        }
 181
 14182        _stash.Add(rs);
 14183        if (Log.IsEnabled(LogEventLevel.Debug))
 184        {
 14185            Log.Debug("Runspace returned to stash: StashCount={Count}", _stash.Count);
 186        }
 187        // Note: we do not decrement _count here, as the pool size is fixed.
 188        // The pool will keep the runspace open for reuse.
 14189    }
 190
 191
 192    // ───────────────── helpers ───────────────────────────────
 193    private Runspace CreateRunspace()
 194    {
 49195        if (Log.IsEnabled(LogEventLevel.Debug))
 196        {
 49197            Log.Debug("CreateRunspace() - creating new runspace");
 198        }
 199
 49200        if (Log.IsEnabled(LogEventLevel.Debug))
 201        {
 49202            Log.Debug("Creating new runspace with InitialSessionState: {Iss}", _iss);
 203        }
 204
 49205        var rs = RunspaceFactory.CreateRunspace(_iss);
 206
 207        // Apply the chosen thread‑affinity strategy **before** opening.
 49208        rs.ThreadOptions = ThreadOptions;
 49209        rs.ApartmentState = ApartmentState.MTA;     // always MTA
 210
 49211        rs.Open();
 49212        Log.Information("Opened new Runspace with ThreadOptions={ThreadOptions}", ThreadOptions);
 49213        if (Log.IsEnabled(LogEventLevel.Debug))
 214        {
 49215            Log.Debug("New runspace created: {Runspace}", rs);
 216        }
 217
 49218        return rs;
 219    }
 220
 221    // ───────────────── cleanup ───────────────────────────────
 222    /// <summary>
 223    /// Disposes the runspace pool manager and all pooled runspaces.
 224    /// </summary>
 225    public void Dispose()
 226    {
 38227        if (Log.IsEnabled(LogEventLevel.Debug))
 228        {
 38229            Log.Debug("Disposing KestrunRunspacePoolManager: Disposed={Disposed}", _disposed);
 230        }
 231
 38232        if (_disposed)
 233        {
 17234            return;
 235        }
 236
 21237        _disposed = true;
 21238        Log.Information("Disposing RunspacePoolManager and all pooled runspaces");
 41239        while (_stash.TryTake(out var rs))
 240        {
 20241            if (Log.IsEnabled(LogEventLevel.Debug))
 242            {
 20243                Log.Debug("Disposing runspace: {Runspace}", rs);
 244            }
 245
 40246            try { rs.Close(); } catch { /* ignore */ }
 20247            rs.Dispose();
 20248        }
 21249        Log.Information("RunspacePoolManager disposed");
 21250    }
 251}