using Akka.Actor; using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian; using ZB.MOM.WW.OtOpcUa.Runtime.Historian; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Clients; /// What one /alarms-historian poll learned. public enum HistorianSinkAvailability { /// The sink answered; is current. Available, /// This node has no historian adapter actor registered — an admin-only node, or a driver node /// that has not finished starting. Re-polled slowly (). NotOnThisRole, /// The actor exists but did not answer (timeout or failure); the last good status, if any, is stale. Unavailable, } /// The outcome of one status poll. /// What the poll learned. /// The sink status when ; otherwise null. /// Why the status is missing, for the page; null when available. public sealed record HistorianSinkReading(HistorianSinkAvailability Availability, HistorianSinkStatus? Status, string? Message); /// /// Reads the local historian sink status for /alarms-historian (R04 L3). The page used to wrap the /// Ask in a bare catch {}, so every failure — including "this role has no historian actor" — left /// it on "Loading…" forever. This classifies the failure so the page can say which it is. /// public static class HistorianSinkStatusReader { /// The normal poll cadence. public static readonly TimeSpan PollInterval = TimeSpan.FromSeconds(5); /// The slow re-poll while the actor is not registered (admin-only node, or a driver node mid-startup). public static readonly TimeSpan NotRegisteredPollInterval = TimeSpan.FromSeconds(30); /// The poll interval after a reading with . /// The last reading's availability, or null before the first. /// The interval until the next poll. public static TimeSpan PollIntervalFor(HistorianSinkAvailability? availability) => availability == HistorianSinkAvailability.NotOnThisRole ? NotRegisteredPollInterval : PollInterval; /// The per-poll Ask timeout. public static readonly TimeSpan AskTimeout = TimeSpan.FromSeconds(2); /// Asks the historian adapter actor for its status. /// Resolves the actor; throws when this node registers none. /// The Ask timeout. /// Cancellation (the page's lifetime); a cancellation propagates. /// The classified reading. public static async Task ReadAsync( Func resolveActor, TimeSpan timeout, CancellationToken ct) { IActorRef actor; try { actor = resolveActor(); } catch (Exception ex) when (ex is not OperationCanceledException) { // Akka.Hosting throws when the registry has no entry for the key: the actor is spawned only on // driver-role nodes, so an admin-only node never has one. return new HistorianSinkReading(HistorianSinkAvailability.NotOnThisRole, null, "This node does not run the alarm historian sink (it has no driver role). Open this page on a driver node."); } if (actor.IsNobody()) { return new HistorianSinkReading(HistorianSinkAvailability.NotOnThisRole, null, "This node does not run the alarm historian sink (it has no driver role). Open this page on a driver node."); } try { var status = await actor.Ask(HistorianAdapterActor.GetStatus.Instance, timeout, ct); return new HistorianSinkReading(HistorianSinkAvailability.Available, status, null); } catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; } catch (AskTimeoutException) { return new HistorianSinkReading(HistorianSinkAvailability.Unavailable, null, $"The historian sink did not answer within {timeout.TotalSeconds:0} s."); } catch (Exception ex) { return new HistorianSinkReading(HistorianSinkAvailability.Unavailable, null, $"The historian sink status could not be read: {ex.Message}"); } } }