feat(driver-galaxy): consume the gateway's session-less alarm model
The mxaccessgw updated alarms to a session-less central monitor: AcknowledgeAlarm dropped SessionId and alarm transitions now come from the session-less StreamAlarms feed instead of the per-session worker StreamEvents stream. The GalaxyDriver no longer compiled against the updated client. - GatewayGalaxyAlarmAcknowledger: session-less rewrite — no GalaxyMxSession; outcome read from ProtocolStatus (throw) and Hresult (warn). - New IGalaxyAlarmFeed seam + GatewayGalaxyAlarmFeed: background consumer of StreamAlarms that decodes the active-alarm snapshot plus live transitions into GalaxyAlarmTransition and reopens the stream on transport faults. - EventPump: drop the dead per-session OnAlarmTransition path; the per-session stream no longer carries alarms. - GalaxyDriver: bridge the feed onto IAlarmSource.OnAlarmEvent; the feed starts on SubscribeAlarmsAsync, independent of data subscriptions. - Tests: replace EventPumpAlarmTests with GatewayGalaxyAlarmFeedTests; move the driver alarm-source tests onto the IGalaxyAlarmFeed seam. Browse needed no change — GatewayGalaxyHierarchySource consumes the unchanged DiscoverHierarchy contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -63,14 +63,18 @@ public sealed class GalaxyDriver
|
||||
private EventPump? _eventPump;
|
||||
private readonly Lock _pumpLock = new();
|
||||
|
||||
// PR B.2 — IAlarmSource implementation. Production-side acks route through
|
||||
// GatewayGalaxyAlarmAcknowledger which calls MxGatewayClient.AcknowledgeAlarmAsync
|
||||
// (PR E.2 SDK). Tests inject IGalaxyAlarmAcknowledger via the internal ctor to
|
||||
// exercise the wiring without a running gateway. The alarm event stream is
|
||||
// delivered by EventPump.OnAlarmTransition (PR B.1) — this driver is the
|
||||
// consumer that bridges it onto IAlarmSource.OnAlarmEvent.
|
||||
// IAlarmSource implementation. Production-side acks route through
|
||||
// GatewayGalaxyAlarmAcknowledger which calls the session-less
|
||||
// MxGatewayClient.AcknowledgeAlarmAsync RPC; alarm transitions arrive on the
|
||||
// gateway's session-less StreamAlarms feed via GatewayGalaxyAlarmFeed. Tests inject
|
||||
// IGalaxyAlarmAcknowledger + IGalaxyAlarmFeed via the internal ctor to exercise the
|
||||
// wiring without a running gateway. This driver bridges the feed's OnAlarmTransition
|
||||
// onto IAlarmSource.OnAlarmEvent.
|
||||
private IGalaxyAlarmAcknowledger? _alarmAcknowledger;
|
||||
private IGalaxyAlarmFeed? _alarmFeed;
|
||||
private readonly Lock _alarmHandlersLock = new();
|
||||
private readonly Lock _alarmFeedLock = new();
|
||||
private bool _alarmFeedWired;
|
||||
private readonly HashSet<GalaxyAlarmSubscriptionHandle> _alarmSubscriptions = new();
|
||||
|
||||
// PR 4.W — production runtime owned by InitializeAsync. The driver builds these
|
||||
@@ -118,7 +122,7 @@ public sealed class GalaxyDriver
|
||||
ILogger<GalaxyDriver>? logger = null)
|
||||
: this(driverInstanceId, options,
|
||||
hierarchySource: null, dataReader: null, dataWriter: null, subscriber: null,
|
||||
alarmAcknowledger: null, logger)
|
||||
alarmAcknowledger: null, alarmFeed: null, logger)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -136,6 +140,7 @@ public sealed class GalaxyDriver
|
||||
IGalaxyDataWriter? dataWriter = null,
|
||||
IGalaxySubscriber? subscriber = null,
|
||||
IGalaxyAlarmAcknowledger? alarmAcknowledger = null,
|
||||
IGalaxyAlarmFeed? alarmFeed = null,
|
||||
ILogger<GalaxyDriver>? logger = null)
|
||||
{
|
||||
_driverInstanceId = !string.IsNullOrWhiteSpace(driverInstanceId)
|
||||
@@ -148,6 +153,7 @@ public sealed class GalaxyDriver
|
||||
_dataWriter = dataWriter;
|
||||
_subscriber = subscriber;
|
||||
_alarmAcknowledger = alarmAcknowledger;
|
||||
_alarmFeed = alarmFeed;
|
||||
|
||||
// Forward the aggregator's transitions through IHostConnectivityProbe.
|
||||
_hostStatuses.OnHostStatusChanged += (_, args) => OnHostStatusChanged?.Invoke(this, args);
|
||||
@@ -230,8 +236,12 @@ public sealed class GalaxyDriver
|
||||
_subscriber, _hostStatuses, _logger,
|
||||
bufferedUpdateIntervalMs: _options.MxAccess.PublishingIntervalMs);
|
||||
|
||||
// PR B.2 — wire the alarm acknowledger to the live gateway client.
|
||||
_alarmAcknowledger ??= new GatewayGalaxyAlarmAcknowledger(_ownedMxClient, _ownedMxSession, _logger);
|
||||
// Wire the alarm acknowledger + feed to the live gateway client. Both are
|
||||
// session-less — the gateway serves alarms from an always-on central monitor —
|
||||
// so they hang off the owned MxGatewayClient, not the worker session.
|
||||
_alarmAcknowledger ??= new GatewayGalaxyAlarmAcknowledger(_ownedMxClient, _logger);
|
||||
_alarmFeed ??= new GatewayGalaxyAlarmFeed(
|
||||
_ownedMxClient.StreamAlarmsAsync, _logger, _options.MxAccess.ClientName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -724,13 +734,34 @@ public sealed class GalaxyDriver
|
||||
channelCapacity: _options.MxAccess.EventPumpChannelCapacity,
|
||||
clientName: _options.MxAccess.ClientName);
|
||||
_eventPump.OnDataChange += OnPumpDataChange;
|
||||
_eventPump.OnAlarmTransition += OnPumpAlarmTransition;
|
||||
_eventPump.Start();
|
||||
return _eventPump;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== IAlarmSource (PR B.2) =====
|
||||
// ===== IAlarmSource =====
|
||||
|
||||
/// <summary>
|
||||
/// Start the gateway alarm feed (idempotent) and wire its transitions onto this
|
||||
/// driver's <see cref="OnAlarmEvent"/> bridge. The feed is session-less — it does
|
||||
/// not depend on a data subscription or the <see cref="EventPump"/>.
|
||||
/// </summary>
|
||||
private void EnsureAlarmFeedStarted()
|
||||
{
|
||||
lock (_alarmFeedLock)
|
||||
{
|
||||
if (_alarmFeed is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"GalaxyDriver alarm feed is not wired. InitializeAsync must run (or a feed " +
|
||||
"seam must be injected via the internal ctor) before subscribing to alarms.");
|
||||
}
|
||||
if (_alarmFeedWired) return;
|
||||
_alarmFeed.OnAlarmTransition += OnAlarmFeedTransition;
|
||||
_alarmFeed.Start();
|
||||
_alarmFeedWired = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IAlarmSubscriptionHandle> SubscribeAlarmsAsync(
|
||||
@@ -740,12 +771,11 @@ public sealed class GalaxyDriver
|
||||
ArgumentNullException.ThrowIfNull(sourceNodeIds);
|
||||
|
||||
// The driver doesn't multiplex alarm subscriptions per source-node-id today —
|
||||
// alarm events arrive on the same gateway StreamEvents channel as data-change
|
||||
// events once the gateway emits the new family (PRs A.2 + A.3). The
|
||||
// subscription handle is a sentinel the server uses for symmetric Unsubscribe;
|
||||
// every active handle receives every alarm transition, and the server filters
|
||||
// by source node before raising Part 9 conditions. Same shape AbCip uses.
|
||||
EnsureEventPumpStarted();
|
||||
// every active handle receives every transition off the gateway's session-less
|
||||
// StreamAlarms feed, and the server filters by source node before raising Part 9
|
||||
// conditions. The subscription handle is a sentinel the server uses for
|
||||
// symmetric Unsubscribe. Same shape AbCip uses.
|
||||
EnsureAlarmFeedStarted();
|
||||
var handle = new GalaxyAlarmSubscriptionHandle(Guid.NewGuid().ToString("N"));
|
||||
lock (_alarmHandlersLock)
|
||||
{
|
||||
@@ -809,13 +839,13 @@ public sealed class GalaxyDriver
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Receives <see cref="GalaxyAlarmTransition"/> events from the EventPump and
|
||||
/// reshapes them into <see cref="AlarmEventArgs"/> for OPC UA-side consumers.
|
||||
/// Fires <see cref="OnAlarmEvent"/> only when at least one alarm subscription is
|
||||
/// active so a server that hasn't called <see cref="SubscribeAlarmsAsync"/> yet
|
||||
/// doesn't surface untracked transitions.
|
||||
/// Receives <see cref="GalaxyAlarmTransition"/> events from the gateway alarm
|
||||
/// feed and reshapes them into <see cref="AlarmEventArgs"/> for OPC UA-side
|
||||
/// consumers. Fires <see cref="OnAlarmEvent"/> only when at least one alarm
|
||||
/// subscription is active so a server that hasn't called
|
||||
/// <see cref="SubscribeAlarmsAsync"/> yet doesn't surface untracked transitions.
|
||||
/// </summary>
|
||||
private void OnPumpAlarmTransition(object? sender, GalaxyAlarmTransition transition)
|
||||
private void OnAlarmFeedTransition(object? sender, GalaxyAlarmTransition transition)
|
||||
{
|
||||
GalaxyAlarmSubscriptionHandle? handle;
|
||||
lock (_alarmHandlersLock)
|
||||
@@ -921,6 +951,11 @@ public sealed class GalaxyDriver
|
||||
lock (_pumpLock) { pump = _eventPump; _eventPump = null; }
|
||||
pump?.DisposeAsync().AsTask().GetAwaiter().GetResult();
|
||||
|
||||
IGalaxyAlarmFeed? alarmFeed;
|
||||
lock (_alarmFeedLock) { alarmFeed = _alarmFeed; _alarmFeed = null; }
|
||||
try { alarmFeed?.DisposeAsync().AsTask().GetAwaiter().GetResult(); }
|
||||
catch (Exception ex) { _logger.LogWarning(ex, "Alarm feed dispose failed"); }
|
||||
|
||||
_ownedMxSession?.DisposeAsync().AsTask().GetAwaiter().GetResult();
|
||||
_ownedMxSession = null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user