perf(dcl): coalesce MxGateway alarm stream reopens across a subscribe burst (review 2026-10-21 02/P3)
This commit is contained in:
+55
@@ -123,6 +123,61 @@ public class MxGatewaySessionScopedHandleTests
|
||||
Assert.Equal(1, Volatile.Read(ref delivered));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Review 2026-10-21 02/P3: a burst of alarm subscribes (a staggered deploy batch of N
|
||||
/// alarm-bound instances) is coalesced into ONE stream reopen on the union prefix instead of N
|
||||
/// close/reopen cycles, each with its own connection-wide snapshot replay. Every source in the
|
||||
/// burst is still covered by that one fresh snapshot (DC-5's standing-state guarantee).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task AlarmSubscribeBurst_IsCoalescedIntoOneStreamReopen()
|
||||
{
|
||||
var fake = new FakeMxGatewayClient();
|
||||
var adapter = new MxGatewayDataConnection(
|
||||
fake, NullLogger<MxGatewayDataConnection>.Instance,
|
||||
alarmStreamReopenDebounce: TimeSpan.FromMilliseconds(300));
|
||||
await adapter.ConnectAsync(Details());
|
||||
|
||||
// Spaced like actor-delivered subscribes, so an un-coalesced reopen really reaches the
|
||||
// gateway each time (a reopen superseded before its task runs never would).
|
||||
foreach (var source in new[] { "Area1.Tank1", "Area1.Tank2", "Area1.Tank3", "Area1.Tank4", "Area1.Tank5" })
|
||||
{
|
||||
await adapter.SubscribeAlarmsAsync(source, null, _ => { });
|
||||
await Task.Delay(20);
|
||||
}
|
||||
|
||||
await WaitUntil(() => { lock (fake.AlarmStreamPrefixes) return fake.AlarmStreamPrefixes.Count >= 1; });
|
||||
await Task.Delay(400); // well past the window: no trailing reopens
|
||||
|
||||
lock (fake.AlarmStreamPrefixes)
|
||||
{
|
||||
Assert.Single(fake.AlarmStreamPrefixes);
|
||||
Assert.Equal("Area1.Tank", fake.AlarmStreamPrefixes[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A subscribe arriving after the coalesced reopen has fired still gets its own fresh
|
||||
/// snapshot — coalescing never swallows a source.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task AlarmSubscribe_AfterTheWindow_StillReopens()
|
||||
{
|
||||
var fake = new FakeMxGatewayClient();
|
||||
var adapter = new MxGatewayDataConnection(
|
||||
fake, NullLogger<MxGatewayDataConnection>.Instance,
|
||||
alarmStreamReopenDebounce: TimeSpan.FromMilliseconds(50));
|
||||
await adapter.ConnectAsync(Details());
|
||||
|
||||
await adapter.SubscribeAlarmsAsync("Area1.Tank1", null, _ => { });
|
||||
await WaitUntil(() => { lock (fake.AlarmStreamPrefixes) return fake.AlarmStreamPrefixes.Count == 1; });
|
||||
await adapter.SubscribeAlarmsAsync("Area1.Tank2", null, _ => { });
|
||||
await WaitUntil(() => { lock (fake.AlarmStreamPrefixes) return fake.AlarmStreamPrefixes.Count == 2; });
|
||||
|
||||
lock (fake.AlarmStreamPrefixes)
|
||||
Assert.Equal("Area1.Tank", fake.AlarmStreamPrefixes[1]);
|
||||
}
|
||||
|
||||
private static async Task WaitUntil(Func<bool> condition, int timeoutMs = 30_000)
|
||||
{
|
||||
var deadline = DateTime.UtcNow.AddMilliseconds(timeoutMs);
|
||||
|
||||
+23
@@ -39,4 +39,27 @@ public class DataConnectionOptionsValidatorTests
|
||||
Assert.True(result.Failed);
|
||||
Assert.Contains("SeedReadMaxAttempts", result.FailureMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an out-of-range MxGateway alarm-reopen debounce is rejected (review 2026-10-21 02/P3).</summary>
|
||||
/// <param name="milliseconds">The debounce under test.</param>
|
||||
[Theory]
|
||||
[InlineData(-1)]
|
||||
[InlineData(5001)]
|
||||
public void OutOfRangeMxAlarmStreamReopenDebounce_IsRejected(int milliseconds)
|
||||
{
|
||||
var result = Validate(new DataConnectionOptions
|
||||
{
|
||||
MxAlarmStreamReopenDebounce = TimeSpan.FromMilliseconds(milliseconds)
|
||||
});
|
||||
Assert.True(result.Failed);
|
||||
Assert.Contains("MxAlarmStreamReopenDebounce", result.FailureMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a zero debounce (reopen per subscribe) is accepted.</summary>
|
||||
[Fact]
|
||||
public void ZeroMxAlarmStreamReopenDebounce_IsAccepted()
|
||||
{
|
||||
var result = Validate(new DataConnectionOptions { MxAlarmStreamReopenDebounce = TimeSpan.Zero });
|
||||
Assert.True(result.Succeeded, result.FailureMessage);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user