fix(twincat): retract the fresh ADS notification when unsubscribe raced the replay (05/STAB-17)
This commit is contained in:
@@ -280,6 +280,54 @@ public sealed class TwinCATReconnectReplayTests
|
||||
await drv.ShutdownAsync(TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// STAB-17: unsubscribing WHILE a replay is mid-<c>AddNotificationAsync</c> must not leak a
|
||||
/// live ADS notification. After the fresh handle is installed, an ownership re-check finds the
|
||||
/// registration was unsubscribed and retracts (disposes) the fresh handle — no orphan.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task UnsubscribeDuringReplay_DisposesFreshHandle_NoOrphan()
|
||||
{
|
||||
var gate = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var build = 0;
|
||||
var factory = new FakeTwinCATClientFactory
|
||||
{
|
||||
Customise = () =>
|
||||
{
|
||||
var c = new FakeTwinCATClient();
|
||||
if (++build == 2) c.AddNotificationGate = gate; // client2's replay blocks on the gate
|
||||
return c;
|
||||
},
|
||||
};
|
||||
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions(Host)],
|
||||
Tags = [new TwinCATTagDefinition("X", Host, "MAIN.X", TwinCATDataType.DInt)],
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
UseNativeNotifications = true,
|
||||
}, "drv-1", factory);
|
||||
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
|
||||
|
||||
var handle = await drv.SubscribeAsync(["X"], TimeSpan.FromMilliseconds(250), TestContext.Current.CancellationToken);
|
||||
|
||||
// Drop the wire; a read reconnects onto client2, whose replay AddNotification blocks on the gate.
|
||||
factory.Clients[0].SimulateWireDrop();
|
||||
var readTask = Task.Run(() => drv.ReadAsync(["X"], CancellationToken.None));
|
||||
await WaitForAsync(() => factory.Clients.Count >= 2, TimeSpan.FromSeconds(2));
|
||||
await Task.Delay(50); // let the replay reach the blocked AddNotification
|
||||
|
||||
// Unsubscribe mid-replay (gate-free) — removes the registration from the device registry.
|
||||
await drv.UnsubscribeAsync(handle, TestContext.Current.CancellationToken);
|
||||
|
||||
// Release the gate: AddNotification completes, SwapHandle installs the fresh handle, and the
|
||||
// ownership re-check must retract it (no owner) rather than leave a live orphan notification.
|
||||
gate.SetResult();
|
||||
await readTask;
|
||||
|
||||
await WaitForAsync(() => factory.Clients[1].Notifications.Count == 0, TimeSpan.FromSeconds(2));
|
||||
factory.Clients[1].Notifications.ShouldBeEmpty("the fresh handle must be disposed — no orphan ADS notification");
|
||||
}
|
||||
|
||||
private static async Task WaitForAsync(Func<bool> condition, TimeSpan timeout)
|
||||
{
|
||||
var deadline = DateTime.UtcNow + timeout;
|
||||
|
||||
Reference in New Issue
Block a user