fix(dcl): cancel old event loop + dispose old MxGateway client on reconnect (S1)
This commit is contained in:
@@ -67,6 +67,27 @@ public class MxGatewayDataConnection : IDataConnection, IBrowsableDataConnection
|
||||
public async Task ConnectAsync(IDictionary<string, string> connectionDetails, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var cfg = MxGatewayEndpointConfigSerializer.FromFlatDict(connectionDetails);
|
||||
|
||||
// Reconnect reuses this adapter: cancel the previous event loop and dispose
|
||||
// the previous client so a flapping gateway cannot accumulate orphaned
|
||||
// streams/sockets, and the old loop's fault path cannot signal Disconnected
|
||||
// against the new session after the guard resets below. Keep this BEFORE the
|
||||
// _disconnectFired reset so a fault raised by the dying loop during teardown
|
||||
// is absorbed by the old guard cycle. Dispose is best-effort fire-and-forget.
|
||||
if (_eventLoopCts is { } previousLoopCts)
|
||||
{
|
||||
previousLoopCts.Cancel();
|
||||
previousLoopCts.Dispose();
|
||||
_eventLoopCts = null;
|
||||
}
|
||||
if (_client is { } previousClient)
|
||||
{
|
||||
_ = previousClient.DisposeAsync().AsTask().ContinueWith(
|
||||
t => _logger.LogWarning(t.Exception?.GetBaseException(),
|
||||
"Disposing previous MxGateway client failed"),
|
||||
TaskContinuationOptions.OnlyOnFaulted);
|
||||
}
|
||||
|
||||
Interlocked.Exchange(ref _disconnectFired, 0); // reset guard on (re)connect, like OPC UA
|
||||
_client = _clientFactory.Create();
|
||||
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests.Adapters;
|
||||
|
||||
/// <summary>
|
||||
/// DataConnectionLayer-S1 (MxGateway half): the adapter instance is reused across
|
||||
/// reconnects. Before the fix, <c>ConnectAsync</c> overwrote <c>_client</c> and
|
||||
/// <c>_eventLoopCts</c> without cancelling the previous event loop or disposing the
|
||||
/// previous client — every gateway flap accumulated an orphaned stream/socket, and
|
||||
/// the dying old loop's fault path could raise <c>Disconnected</c> against the new
|
||||
/// session after the guard was reset.
|
||||
/// </summary>
|
||||
[Collection("DataConnectionManagerActor")]
|
||||
public class MxGatewayDataConnectionReconnectTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Reconnect_DisposesPreviousClient_AndCancelsPreviousEventLoop()
|
||||
{
|
||||
var clients = new List<IMxGatewayClient>();
|
||||
var loopTokens = new List<CancellationToken>();
|
||||
var factory = Substitute.For<IMxGatewayClientFactory>();
|
||||
factory.Create().Returns(_ =>
|
||||
{
|
||||
var c = Substitute.For<IMxGatewayClient>();
|
||||
c.RunEventLoopAsync(Arg.Any<Action<MxValueUpdate>>(), Arg.Do<CancellationToken>(t => loopTokens.Add(t)))
|
||||
.Returns(Task.Delay(Timeout.Infinite));
|
||||
clients.Add(c);
|
||||
return c;
|
||||
});
|
||||
var adapter = new MxGatewayDataConnection(factory, NullLogger<MxGatewayDataConnection>.Instance);
|
||||
var details = new Dictionary<string, string> { ["Endpoint"] = "http://gw:5000", ["ApiKey"] = "k" };
|
||||
|
||||
// The event loop is pumped fire-and-forget (ConnectAsync → Task.Run →
|
||||
// RunEventLoopAsync), so the token is recorded on a thread-pool continuation.
|
||||
// Barrier on each recorded token before proceeding.
|
||||
await adapter.ConnectAsync(details);
|
||||
await WaitUntilAsync(() => loopTokens.Count == 1);
|
||||
|
||||
await adapter.ConnectAsync(details); // reconnect
|
||||
await WaitUntilAsync(() => loopTokens.Count == 2);
|
||||
|
||||
Assert.Equal(2, clients.Count);
|
||||
await clients[0].Received(1).DisposeAsync();
|
||||
Assert.True(loopTokens[0].IsCancellationRequested); // old loop cancelled
|
||||
Assert.False(loopTokens[1].IsCancellationRequested); // new loop live
|
||||
}
|
||||
|
||||
private static async Task WaitUntilAsync(Func<bool> condition)
|
||||
{
|
||||
for (var i = 0; i < 100 && !condition(); i++)
|
||||
await Task.Delay(50);
|
||||
Assert.True(condition(), "condition not met within timeout");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user