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}");
}
}
}