using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Core.Resilience; /// /// Wraps the three mutating surfaces of /// (, , /// ) through so the /// Phase 6.1 resilience pipeline runs — retry semantics match /// (retries by default) and /// (does NOT retry per decision #143). /// /// /// Multi-host dispatch: when the driver implements , /// each source-node-id is resolved individually + grouped by host so a dead PLC inside a /// multi-device driver doesn't poison the sibling hosts' breakers. Drivers with a single /// host fall back to as the single-host key. /// /// Why this lives here + not on : alarm surfaces have a /// handle-returning shape (SubscribeAlarmsAsync returns ) /// + a per-call fan-out (AcknowledgeAsync gets a batch of /// s that may span multiple hosts). Keeping the fan-out /// logic here keeps the invoker's execute-overloads narrow. /// public sealed class AlarmSurfaceInvoker { private readonly CapabilityInvoker _invoker; private readonly IAlarmSource _alarmSource; private readonly IPerCallHostResolver? _hostResolver; private readonly string _defaultHost; public AlarmSurfaceInvoker( CapabilityInvoker invoker, IAlarmSource alarmSource, string defaultHost, IPerCallHostResolver? hostResolver = null) { ArgumentNullException.ThrowIfNull(invoker); ArgumentNullException.ThrowIfNull(alarmSource); ArgumentException.ThrowIfNullOrWhiteSpace(defaultHost); _invoker = invoker; _alarmSource = alarmSource; _defaultHost = defaultHost; _hostResolver = hostResolver; } /// /// Subscribe to alarm events for a set of source node ids, fanning out by resolved host /// so per-host breakers / bulkheads apply. Returns one handle per host — callers that /// don't care about per-host separation may concatenate them. /// public async Task> SubscribeAsync( IReadOnlyList sourceNodeIds, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(sourceNodeIds); if (sourceNodeIds.Count == 0) return []; var byHost = GroupByHost(sourceNodeIds); var handles = new List(byHost.Count); foreach (var (host, ids) in byHost) { var handle = await _invoker.ExecuteAsync( DriverCapability.AlarmSubscribe, host, async ct => await _alarmSource.SubscribeAlarmsAsync(ids, ct).ConfigureAwait(false), cancellationToken).ConfigureAwait(false); handles.Add(handle); } return handles; } /// Cancel an alarm subscription. Routes through the AlarmSubscribe pipeline for parity. public ValueTask UnsubscribeAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(handle); return _invoker.ExecuteAsync( DriverCapability.AlarmSubscribe, _defaultHost, async ct => await _alarmSource.UnsubscribeAlarmsAsync(handle, ct).ConfigureAwait(false), cancellationToken); } /// /// Acknowledge alarms. Fans out by resolved host; each host's batch runs through the /// AlarmAcknowledge pipeline (no-retry per decision #143 — an alarm-ack is not idempotent /// at the plant-floor acknowledgement level even if the OPC UA spec permits re-issue). /// public async Task AcknowledgeAsync( IReadOnlyList acknowledgements, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(acknowledgements); if (acknowledgements.Count == 0) return; var byHost = _hostResolver is null ? new Dictionary> { [_defaultHost] = acknowledgements.ToList() } : acknowledgements .GroupBy(a => _hostResolver.ResolveHost(a.SourceNodeId)) .ToDictionary(g => g.Key, g => g.ToList()); foreach (var (host, batch) in byHost) { var batchSnapshot = batch; // capture for the lambda await _invoker.ExecuteAsync( DriverCapability.AlarmAcknowledge, host, async ct => await _alarmSource.AcknowledgeAsync(batchSnapshot, ct).ConfigureAwait(false), cancellationToken).ConfigureAwait(false); } } private Dictionary> GroupByHost(IReadOnlyList sourceNodeIds) { if (_hostResolver is null) return new Dictionary> { [_defaultHost] = sourceNodeIds.ToList() }; var result = new Dictionary>(StringComparer.Ordinal); foreach (var id in sourceNodeIds) { var host = _hostResolver.ResolveHost(id); if (!result.TryGetValue(host, out var list)) result[host] = list = new List(); list.Add(id); } return result; } }