fix(data-connection-layer): resolve DataConnectionLayer-008,013 — O(1) unsubscribe via reverse index, atomic disconnect guard
This commit is contained in:
@@ -38,7 +38,12 @@ public class OpcUaDataConnection : IDataConnection
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private volatile bool _disconnectFired;
|
||||
// DataConnectionLayer-013: an int flag toggled with Interlocked.Exchange so the
|
||||
// "only the first caller fires Disconnected" guard in RaiseDisconnected is genuinely
|
||||
// atomic. A plain volatile bool gives visibility but not atomicity — two threads
|
||||
// (e.g. the keep-alive thread and a ReadAsync failure path) could both observe it
|
||||
// false and both raise the event. 0 = not fired, 1 = fired.
|
||||
private int _disconnectFired;
|
||||
|
||||
public ConnectionHealth Status => _status;
|
||||
public event Action? Disconnected;
|
||||
@@ -82,7 +87,7 @@ public class OpcUaDataConnection : IDataConnection
|
||||
await _client.ConnectAsync(_endpointUrl, options, cancellationToken);
|
||||
|
||||
_status = ConnectionHealth.Connected;
|
||||
_disconnectFired = false;
|
||||
Interlocked.Exchange(ref _disconnectFired, 0);
|
||||
_logger.LogInformation("OPC UA connected to {Endpoint}", _endpointUrl);
|
||||
|
||||
await StartHeartbeatMonitorAsync(config.Heartbeat, cancellationToken);
|
||||
@@ -285,12 +290,15 @@ public class OpcUaDataConnection : IDataConnection
|
||||
|
||||
/// <summary>
|
||||
/// Marks the connection as disconnected and fires the Disconnected event once.
|
||||
/// Thread-safe: only the first caller triggers the event.
|
||||
/// Thread-safe: the firing guard is an atomic compare-and-set
|
||||
/// (<see cref="Interlocked.Exchange(ref int, int)"/>), so when several threads race
|
||||
/// here — e.g. the keep-alive thread via <see cref="OnClientConnectionLost"/> and a
|
||||
/// <c>ReadAsync</c> failure path — exactly one of them observes the 0→1 transition
|
||||
/// and invokes <see cref="Disconnected"/>.
|
||||
/// </summary>
|
||||
private void RaiseDisconnected()
|
||||
{
|
||||
if (_disconnectFired) return;
|
||||
_disconnectFired = true;
|
||||
if (Interlocked.Exchange(ref _disconnectFired, 1) != 0) return;
|
||||
_status = ConnectionHealth.Disconnected;
|
||||
_logger.LogWarning("OPC UA connection to {Endpoint} lost", _endpointUrl);
|
||||
Disconnected?.Invoke();
|
||||
|
||||
@@ -24,7 +24,10 @@ public class RealOpcUaClient : IOpcUaClient
|
||||
// Clear() is undefined behaviour, so they must be ConcurrentDictionary.
|
||||
private readonly ConcurrentDictionary<string, MonitoredItem> _monitoredItems = new();
|
||||
private readonly ConcurrentDictionary<string, Action<string, object?, DateTime, uint>> _callbacks = new();
|
||||
private volatile bool _connectionLostFired;
|
||||
// DataConnectionLayer-013: int flag toggled with Interlocked.Exchange so the
|
||||
// once-only ConnectionLost guard in OnSessionKeepAlive is atomic, not just visible.
|
||||
// 0 = not fired, 1 = fired.
|
||||
private int _connectionLostFired;
|
||||
private OpcUaConnectionOptions _options = new();
|
||||
private readonly OpcUaGlobalOptions _globalOptions;
|
||||
private readonly ILogger<RealOpcUaClient> _logger;
|
||||
@@ -112,7 +115,7 @@ public class RealOpcUaClient : IOpcUaClient
|
||||
"ScadaLink-DCL-Session", (uint)opts.SessionTimeoutMs, userIdentity, null, cancellationToken);
|
||||
|
||||
// Detect server going offline via keep-alive failures
|
||||
_connectionLostFired = false;
|
||||
Interlocked.Exchange(ref _connectionLostFired, 0);
|
||||
_session.KeepAlive += OnSessionKeepAlive;
|
||||
|
||||
// Store options for monitored item creation
|
||||
@@ -243,14 +246,15 @@ public class RealOpcUaClient : IOpcUaClient
|
||||
|
||||
/// <summary>
|
||||
/// Called by the OPC UA SDK when a keep-alive response arrives (or fails).
|
||||
/// When CurrentState is bad, the server is unreachable.
|
||||
/// When CurrentState is bad, the server is unreachable. The once-only guard is an
|
||||
/// atomic compare-and-set, so a burst of failed keep-alives raises
|
||||
/// <see cref="ConnectionLost"/> exactly once.
|
||||
/// </summary>
|
||||
private void OnSessionKeepAlive(ISession session, KeepAliveEventArgs e)
|
||||
{
|
||||
if (ServiceResult.IsBad(e.Status))
|
||||
{
|
||||
if (_connectionLostFired) return;
|
||||
_connectionLostFired = true;
|
||||
if (Interlocked.Exchange(ref _connectionLostFired, 1) != 0) return;
|
||||
ConnectionLost?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user