513fa1aa16
Comment/XML-doc changes only: - add summaries to ~10k test methods, fixtures, fakes and helpers (+ <param> on theories, <returns> on async Task methods, <inheritdoc/> on interface implementations the checker can resolve) - strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N, #NN, Component-0NN) from comments, keeping the explanatory text; reword false positives in comments only - real docs on fakes implementing BCL/library interfaces (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime) - reword the last 5 src/ false positives (CWE-1236, failures-1, year-0001, M365) Only non-comment edits: four one-line nested classes split across lines so members can carry docs. Build 0 errors / 0 warnings.
112 lines
4.9 KiB
C#
112 lines
4.9 KiB
C#
using ZB.MOM.WW.ScadaBridge.SiteRuntime;
|
|
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Scripts;
|
|
|
|
/// <summary>
|
|
/// The blocking script pool is no longer a fixed 8 threads forever. It
|
|
/// scales with the number of running instances between a configured floor and ceiling, and it
|
|
/// is deliberately GROW-ONLY: undeploying instances leaves idle threads (which cost nothing
|
|
/// measurable) rather than paying for drain/steal complexity.
|
|
/// </summary>
|
|
public class ScriptPoolSizingTests
|
|
{
|
|
private static SiteRuntimeOptions Options(int floor = 8, int ceiling = 32) => new()
|
|
{
|
|
ScriptExecutionThreadCount = floor,
|
|
ScriptExecutionMaxThreadCount = ceiling
|
|
};
|
|
|
|
/// <summary>Verifies that the target thread count applies the floor, ratio and ceiling.</summary>
|
|
/// <param name="instances">The number of running instances.</param>
|
|
/// <param name="expected">The expected target thread count.</param>
|
|
[Theory]
|
|
// At or below floor * 8 instances the result is exactly the original fixed size —
|
|
// existing configurations are byte-for-byte unchanged in behaviour.
|
|
[InlineData(0, 8)]
|
|
[InlineData(1, 8)]
|
|
[InlineData(64, 8)]
|
|
// Past that, one thread per 8 instances, rounding up.
|
|
[InlineData(65, 9)]
|
|
[InlineData(72, 9)]
|
|
[InlineData(200, 25)]
|
|
// …clamped at the ceiling.
|
|
[InlineData(256, 32)]
|
|
[InlineData(10_000, 32)]
|
|
public void ComputeTargetThreads_AppliesFloorRatioAndCeiling(int instances, int expected)
|
|
=> Assert.Equal(expected, ScriptExecutionScheduler.ComputeTargetThreads(instances, Options()));
|
|
|
|
/// <summary>Verifies that an overridden floor and ceiling are honoured.</summary>
|
|
[Fact]
|
|
public void ComputeTargetThreads_HonoursAnOverriddenFloorAndCeiling()
|
|
{
|
|
var options = Options(floor: 2, ceiling: 4);
|
|
Assert.Equal(2, ScriptExecutionScheduler.ComputeTargetThreads(0, options));
|
|
Assert.Equal(2, ScriptExecutionScheduler.ComputeTargetThreads(16, options));
|
|
Assert.Equal(3, ScriptExecutionScheduler.ComputeTargetThreads(17, options));
|
|
Assert.Equal(4, ScriptExecutionScheduler.ComputeTargetThreads(1000, options));
|
|
}
|
|
|
|
/// <summary>Verifies that the target thread count is never less than one, even with a degenerate floor.</summary>
|
|
[Fact]
|
|
public void ComputeTargetThreads_NeverReturnsLessThanOne_EvenWithADegenerateFloor()
|
|
{
|
|
// The validator rejects these, but a directly-constructed options object must still
|
|
// not produce a zero-thread scheduler.
|
|
var options = new SiteRuntimeOptions { ScriptExecutionThreadCount = 0, ScriptExecutionMaxThreadCount = 0 };
|
|
Assert.Equal(1, ScriptExecutionScheduler.ComputeTargetThreads(0, options));
|
|
Assert.Equal(1, ScriptExecutionScheduler.ComputeTargetThreads(500, options));
|
|
}
|
|
|
|
/// <summary>Verifies that EnsureCapacity grows once, is idempotent and never shrinks.</summary>
|
|
[Fact]
|
|
public void EnsureCapacity_GrowsOnce_IsIdempotent_AndNeverShrinks()
|
|
{
|
|
using var scheduler = new ScriptExecutionScheduler(2);
|
|
Assert.Equal(2, scheduler.MaximumConcurrencyLevel);
|
|
|
|
Assert.Equal(5, scheduler.EnsureCapacity(5));
|
|
Assert.Equal(5, scheduler.MaximumConcurrencyLevel);
|
|
|
|
// Idempotent: asking for the same target again changes nothing.
|
|
Assert.Equal(5, scheduler.EnsureCapacity(5));
|
|
Assert.Equal(5, scheduler.MaximumConcurrencyLevel);
|
|
|
|
// Grow-only: a smaller target is a no-op, not a shrink.
|
|
Assert.Equal(5, scheduler.EnsureCapacity(1));
|
|
Assert.Equal(5, scheduler.MaximumConcurrencyLevel);
|
|
}
|
|
|
|
/// <summary>Verifies that EnsureCapacity widens the gauges so the whole pool is observable.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task EnsureCapacity_WidensTheGauges_SoTheWholePoolIsObservable()
|
|
{
|
|
using var scheduler = new ScriptExecutionScheduler(1);
|
|
scheduler.EnsureCapacity(3);
|
|
|
|
using var gate = new ManualResetEventSlim(false);
|
|
var blocking = Enumerable.Range(0, 3)
|
|
.Select(_ => Task.Factory.StartNew(() => gate.Wait(),
|
|
CancellationToken.None, TaskCreationOptions.None, scheduler))
|
|
.ToArray();
|
|
|
|
// All three grown workers report busy — the bookkeeping widened with the pool.
|
|
await WaitUntilAsync(() => scheduler.BusyThreadCount == 3);
|
|
Assert.Equal(0, scheduler.QueueDepth);
|
|
Assert.NotNull(scheduler.OldestBusyAge);
|
|
|
|
gate.Set();
|
|
await Task.WhenAll(blocking);
|
|
await WaitUntilAsync(() => scheduler.BusyThreadCount == 0);
|
|
Assert.Null(scheduler.OldestBusyAge);
|
|
}
|
|
|
|
private static async Task WaitUntilAsync(Func<bool> condition)
|
|
{
|
|
for (var i = 0; i < 200 && !condition(); i++)
|
|
await Task.Delay(25);
|
|
Assert.True(condition(), "condition not met within timeout");
|
|
}
|
|
}
|