fix(driver-opcuaclient): resolve Low code-review findings (Driver.OpcUaClient-011,014)
- Driver.OpcUaClient-011: rewrote the ValueRank comment with the OPC UA Part 3 constants and an explicit scalar/array boundary at valueRank >= 0. - Driver.OpcUaClient-014: track every MonitoredItem.Notification handler in a MonitoredItemNotificationHandle record; UnsubscribeAsync / UnsubscribeAlarmsAsync / ShutdownAsync detach the handler before Subscription.DeleteAsync so the SDK's invocation list no longer keeps the driver alive. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -494,9 +494,12 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
// Tear down remote subscriptions first — otherwise Session.Close will try and may fail
|
||||
// with BadSubscriptionIdInvalid noise in the upstream log. _subscriptions is cleared
|
||||
// whether or not the wire-side delete succeeds since the local handles are useless
|
||||
// after close anyway.
|
||||
// after close anyway. Before deleting each subscription we detach the Notification
|
||||
// handlers we attached at subscribe time so the SDK's invocation list no longer
|
||||
// holds the driver instance through the closure (Driver.OpcUaClient-014).
|
||||
foreach (var rs in _subscriptions.Values)
|
||||
{
|
||||
DetachNotificationHandlers(rs.ItemHandlers);
|
||||
try { await rs.Subscription.DeleteAsync(silent: true, cancellationToken).ConfigureAwait(false); }
|
||||
catch { /* best-effort */ }
|
||||
}
|
||||
@@ -504,6 +507,8 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
|
||||
foreach (var ras in _alarmSubscriptions.Values)
|
||||
{
|
||||
try { ras.EventItem.Notification -= ras.Handler; }
|
||||
catch { /* best-effort */ }
|
||||
try { await ras.Subscription.DeleteAsync(silent: true, cancellationToken).ConfigureAwait(false); }
|
||||
catch { /* best-effort */ }
|
||||
}
|
||||
@@ -1005,7 +1010,14 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
? MapUpstreamDataType(dtId)
|
||||
: DriverDataType.Int32;
|
||||
var valueRank = StatusCode.IsGood(valueRankDv.StatusCode) && valueRankDv.Value is int vr ? vr : -1;
|
||||
var isArray = valueRank >= 0; // -1 = scalar; 1+ = array dimensions; 0 = one-dimensional array
|
||||
// OPC UA Part 3 ValueRank constants: -3 = ScalarOrOneDimension, -2 = Any,
|
||||
// -1 = Scalar, 0 = OneOrMoreDimensions, 1 = OneDimension, >1 = N specific dimensions.
|
||||
// Deliberate choice: treat anything >= 0 as an array (the spec guarantees -3/-2/-1
|
||||
// are the only negative values, and any non-negative rank denotes at least one
|
||||
// array dimension). -3 ScalarOrOneDimension and -2 Any are conservatively treated
|
||||
// as scalar — array-of-one is exposed as scalar to the local address space until
|
||||
// the upstream variable carries a concrete dimensioned rank.
|
||||
var isArray = valueRank >= 0;
|
||||
var access = StatusCode.IsGood(accessDv.StatusCode) && accessDv.Value is byte ab ? ab : (byte)0;
|
||||
var securityClass = MapAccessLevelToSecurityClass(access);
|
||||
var historizing = StatusCode.IsGood(histDv.StatusCode) && histDv.Value is bool b && b;
|
||||
@@ -1110,6 +1122,11 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
session.AddSubscription(subscription);
|
||||
await subscription.CreateAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Track each (MonitoredItem, handler) pair so UnsubscribeAsync / ShutdownAsync
|
||||
// can detach the Notification delegate before disposing the session
|
||||
// (Driver.OpcUaClient-014). The lambda captures `handle`, so we must hold the
|
||||
// exact delegate instance returned by `+=` to be able to remove it.
|
||||
var itemHandlers = new List<MonitoredItemNotificationHandle>();
|
||||
foreach (var fullRef in fullReferences)
|
||||
{
|
||||
if (!TryParseNodeId(session, fullRef, out var nodeId)) continue;
|
||||
@@ -1128,12 +1145,15 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
{
|
||||
Handle = fullRef,
|
||||
};
|
||||
item.Notification += (mi, args) => OnMonitoredItemNotification(handle, mi, args);
|
||||
MonitoredItemNotificationEventHandler notifHandler = (mi, args) =>
|
||||
OnMonitoredItemNotification(handle, mi, args);
|
||||
item.Notification += notifHandler;
|
||||
itemHandlers.Add(new MonitoredItemNotificationHandle(item, notifHandler));
|
||||
subscription.AddItem(item);
|
||||
}
|
||||
|
||||
await subscription.CreateItemsAsync(cancellationToken).ConfigureAwait(false);
|
||||
_subscriptions[id] = new RemoteSubscription(subscription, handle);
|
||||
_subscriptions[id] = new RemoteSubscription(subscription, handle, itemHandlers);
|
||||
}
|
||||
finally { _gate.Release(); }
|
||||
|
||||
@@ -1148,12 +1168,28 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
// Detach Notification handlers BEFORE deleting the subscription so the SDK's
|
||||
// MonitoredItem.Notification multicast invocation list no longer holds a
|
||||
// closure that captures the driver instance (Driver.OpcUaClient-014). The
|
||||
// delegate stored on RemoteSubscription is the exact instance that was added,
|
||||
// so `-=` removes it cleanly.
|
||||
DetachNotificationHandlers(rs.ItemHandlers);
|
||||
try { await rs.Subscription.DeleteAsync(silent: true, cancellationToken).ConfigureAwait(false); }
|
||||
catch { /* best-effort — the subscription may already be gone on reconnect */ }
|
||||
}
|
||||
finally { _gate.Release(); }
|
||||
}
|
||||
|
||||
private static void DetachNotificationHandlers(IReadOnlyList<MonitoredItemNotificationHandle> items)
|
||||
{
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
var pair = items[i];
|
||||
try { pair.Item.Notification -= pair.Handler; }
|
||||
catch { /* best-effort — SDK may have already cleared its invocation list on session loss */ }
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMonitoredItemNotification(OpcUaSubscriptionHandle handle, MonitoredItem item, MonitoredItemNotificationEventArgs args)
|
||||
{
|
||||
// args.NotificationValue arrives as a MonitoredItemNotification for value-change
|
||||
@@ -1170,7 +1206,28 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
OnDataChange?.Invoke(this, new DataChangeEventArgs(handle, fullRef, snapshot));
|
||||
}
|
||||
|
||||
private sealed record RemoteSubscription(Subscription Subscription, OpcUaSubscriptionHandle Handle);
|
||||
/// <summary>
|
||||
/// Live data-change subscription bookkeeping. Holds the SDK <see cref="Subscription"/>,
|
||||
/// the local handle, and the per-MonitoredItem (item, handler) pairs so
|
||||
/// <see cref="UnsubscribeAsync"/> / <see cref="ShutdownAsync"/> can detach the
|
||||
/// Notification delegates before the SDK disposes the subscription
|
||||
/// (Driver.OpcUaClient-014).
|
||||
/// </summary>
|
||||
private sealed record RemoteSubscription(
|
||||
Subscription Subscription,
|
||||
OpcUaSubscriptionHandle Handle,
|
||||
IReadOnlyList<MonitoredItemNotificationHandle> ItemHandlers);
|
||||
|
||||
/// <summary>
|
||||
/// One (MonitoredItem, handler-delegate-instance) pair captured at subscribe time so
|
||||
/// the same delegate instance can be `-=` removed at unsubscribe time. The lambda
|
||||
/// captures the local <c>OpcUaSubscriptionHandle</c>, which is what makes detach
|
||||
/// necessary — without it the SDK's multicast invocation list holds the driver
|
||||
/// through the closure until the session itself is disposed.
|
||||
/// </summary>
|
||||
private sealed record MonitoredItemNotificationHandle(
|
||||
MonitoredItem Item,
|
||||
MonitoredItemNotificationEventHandler Handler);
|
||||
|
||||
private sealed record OpcUaSubscriptionHandle(long Id) : ISubscriptionHandle
|
||||
{
|
||||
@@ -1259,11 +1316,17 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
{
|
||||
Handle = handle,
|
||||
};
|
||||
eventItem.Notification += (mi, args) => OnEventNotification(handle, sourceFilter, mi, args);
|
||||
// Capture the exact delegate instance so UnsubscribeAlarmsAsync / ShutdownAsync
|
||||
// can `-=` it later (Driver.OpcUaClient-014). The lambda captures `handle` and
|
||||
// `sourceFilter`, so without the explicit detach the SDK's invocation list keeps
|
||||
// the driver instance alive until the session itself is disposed.
|
||||
MonitoredItemNotificationEventHandler notifHandler = (mi, args) =>
|
||||
OnEventNotification(handle, sourceFilter, mi, args);
|
||||
eventItem.Notification += notifHandler;
|
||||
subscription.AddItem(eventItem);
|
||||
await subscription.CreateItemsAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_alarmSubscriptions[id] = new RemoteAlarmSubscription(subscription, handle);
|
||||
_alarmSubscriptions[id] = new RemoteAlarmSubscription(subscription, handle, eventItem, notifHandler);
|
||||
}
|
||||
finally { _gate.Release(); }
|
||||
|
||||
@@ -1278,6 +1341,11 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
// Detach the Notification handler before deleting the subscription so the SDK's
|
||||
// multicast invocation list no longer holds the driver instance through the
|
||||
// closure (Driver.OpcUaClient-014).
|
||||
try { rs.EventItem.Notification -= rs.Handler; }
|
||||
catch { /* best-effort */ }
|
||||
try { await rs.Subscription.DeleteAsync(silent: true, cancellationToken).ConfigureAwait(false); }
|
||||
catch { /* best-effort — session may already be gone across a reconnect */ }
|
||||
}
|
||||
@@ -1405,7 +1473,17 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
_ => AlarmSeverity.Critical,
|
||||
};
|
||||
|
||||
private sealed record RemoteAlarmSubscription(Subscription Subscription, OpcUaAlarmSubscriptionHandle Handle);
|
||||
/// <summary>
|
||||
/// Live alarm-event subscription bookkeeping. Holds the SDK <see cref="Subscription"/>,
|
||||
/// the local handle, the single event-MonitoredItem (`Server/Events`), and the exact
|
||||
/// handler delegate instance so unsubscribe / shutdown can detach the Notification
|
||||
/// event before the SDK disposes the subscription (Driver.OpcUaClient-014).
|
||||
/// </summary>
|
||||
private sealed record RemoteAlarmSubscription(
|
||||
Subscription Subscription,
|
||||
OpcUaAlarmSubscriptionHandle Handle,
|
||||
MonitoredItem EventItem,
|
||||
MonitoredItemNotificationEventHandler Handler);
|
||||
|
||||
private sealed record OpcUaAlarmSubscriptionHandle(long Id) : IAlarmSubscriptionHandle
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user