fix(runtime): keep native-alarm feed across data re-subscribes; reconcile retries alarms (#662)
A data re-subscribe (every deploy's bulk SetDesiredSubscriptions push to a kept driver) dropped the prior data subscription through the full DetachSubscription, which also unhooked OnAlarmEvent and forgot the alarm handle. On any deploy that did not respawn the driver, the alarm subscription kept logging "established" while every event reached nobody until a reconnect or respawn. The data path now uses DetachDataSubscription; only a lost connection and PostStop detach both. The #488 health-poll reconcile only looked at data, so an alarm subscribe that failed on (re)connect gated the feed for the life of the connection. ReconcileAlarmSubscription now re-attaches a missing handler and re-subscribes a stale/missing alarm handle every tick. OpcUaClient removes a half-built alarm subscription from its session when SubscribeAlarmsAsync throws, so the new retries cannot accumulate. Tests: actor-level data-resubscribe + reconcile-retry tests and an end-to-end NativeAlarmFeedSurvivesDeployTests through DriverHostActor; all three fail against the unfixed actor. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -1710,6 +1710,30 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
?? throw new InvalidOperationException(
|
||||
"OpcUaClient session was lost before the alarm subscription could be created.");
|
||||
session.AddSubscription(subscription);
|
||||
try
|
||||
{
|
||||
await CreateAlarmEventItemAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// #662: the host now retries a failed alarm subscribe on every reconcile tick, so a half-built
|
||||
// subscription left on the session would accumulate one per retry for the length of an outage.
|
||||
try { await session.RemoveSubscriptionAsync(subscription, CancellationToken.None).ConfigureAwait(false); }
|
||||
catch { /* best-effort — the session may be the thing that failed */ }
|
||||
throw;
|
||||
}
|
||||
|
||||
// #619: a new event item only reports FUTURE transitions — a condition already active upstream would
|
||||
// stay invisible until it next changed. ConditionRefresh re-delivers every retained condition's
|
||||
// current state through the normal path (deduplicated against the last-told state).
|
||||
await TryConditionRefreshAsync(subscription, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally { _gate.Release(); }
|
||||
|
||||
return handle;
|
||||
|
||||
async Task CreateAlarmEventItemAsync()
|
||||
{
|
||||
await subscription.CreateAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var eventItem = new MonitoredItem(telemetry: null!, new MonitoredItemOptions
|
||||
@@ -1736,15 +1760,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
await subscription.CreateItemsAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_alarmSubscriptions[id] = new RemoteAlarmSubscription(subscription, handle, eventItem, notifHandler);
|
||||
|
||||
// #619: a new event item only reports FUTURE transitions — a condition already active upstream would
|
||||
// stay invisible until it next changed. ConditionRefresh re-delivers every retained condition's
|
||||
// current state through the normal path (deduplicated against the last-told state).
|
||||
await TryConditionRefreshAsync(subscription, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally { _gate.Release(); }
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
Reference in New Issue
Block a user