0f1f8ad8fa
I1: /hosts eviction keyed off the AUTHORED DriverInstances, so a driver deleted in /raw but not yet deployed - still running, still publishing - vanished every tick. The stores' sweep is now EvictUnconfiguredStale(set, olderThanUtc): an entry goes only when unconfigured AND its PublishedUtc is older than 60 s. Tests cover unconfigured+fresh kept (positive control), unconfigured+stale evicted, configured+stale kept, and the deploy hand-off. M1: Test Connect falls back to the driver's documented ProbeTimeoutSeconds default (Galaxy 30, OpcUaClient 15, ...) when the key is absent. M2: the budget never undercuts the config's root timeoutMs (+1 s). M5: RawBrowseModal compares the open generation captured before the await, so a close-then-reopen mid-open closes the stale session. M6: /alarms-historian keeps a 30 s re-poll on 'not registered' (a driver node may still be starting) instead of stopping for good. M7: /scripts source fetch failure shows in the dialog, not the error boundary. M8: the export modal maps only its preview rows; the endpoint writes via Results.Text (no second byte[] copy). M9: /hosts config queries are ordered, so SequenceEqual change detection holds. N1 tooltip, N2 CRLF-safe regex, N3 throttled Warning for subscriber faults, N5 equipment-import help text now describes RFC 4180 quoting. H3: MonacoEditor documents that themes are page-global. Refs #745 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
97 lines
4.7 KiB
C#
97 lines
4.7 KiB
C#
using Akka.Actor;
|
|
using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.Historian;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Clients;
|
|
|
|
/// <summary>What one <c>/alarms-historian</c> poll learned.</summary>
|
|
public enum HistorianSinkAvailability
|
|
{
|
|
/// <summary>The sink answered; <see cref="HistorianSinkReading.Status"/> is current.</summary>
|
|
Available,
|
|
|
|
/// <summary>This node has no historian adapter actor registered — an admin-only node, or a driver node
|
|
/// that has not finished starting. Re-polled slowly (<see cref="HistorianSinkStatusReader.NotRegisteredPollInterval"/>).</summary>
|
|
NotOnThisRole,
|
|
|
|
/// <summary>The actor exists but did not answer (timeout or failure); the last good status, if any, is stale.</summary>
|
|
Unavailable,
|
|
}
|
|
|
|
/// <summary>The outcome of one status poll.</summary>
|
|
/// <param name="Availability">What the poll learned.</param>
|
|
/// <param name="Status">The sink status when <see cref="HistorianSinkAvailability.Available"/>; otherwise null.</param>
|
|
/// <param name="Message">Why the status is missing, for the page; null when available.</param>
|
|
public sealed record HistorianSinkReading(HistorianSinkAvailability Availability, HistorianSinkStatus? Status, string? Message);
|
|
|
|
/// <summary>
|
|
/// Reads the local historian sink status for <c>/alarms-historian</c> (R04 L3). The page used to wrap the
|
|
/// Ask in a bare <c>catch {}</c>, 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.
|
|
/// </summary>
|
|
public static class HistorianSinkStatusReader
|
|
{
|
|
/// <summary>The normal poll cadence.</summary>
|
|
public static readonly TimeSpan PollInterval = TimeSpan.FromSeconds(5);
|
|
|
|
/// <summary>The slow re-poll while the actor is not registered (admin-only node, or a driver node mid-startup).</summary>
|
|
public static readonly TimeSpan NotRegisteredPollInterval = TimeSpan.FromSeconds(30);
|
|
|
|
/// <summary>The poll interval after a reading with <paramref name="availability"/>.</summary>
|
|
/// <param name="availability">The last reading's availability, or null before the first.</param>
|
|
/// <returns>The interval until the next poll.</returns>
|
|
public static TimeSpan PollIntervalFor(HistorianSinkAvailability? availability) =>
|
|
availability == HistorianSinkAvailability.NotOnThisRole ? NotRegisteredPollInterval : PollInterval;
|
|
|
|
/// <summary>The per-poll Ask timeout.</summary>
|
|
public static readonly TimeSpan AskTimeout = TimeSpan.FromSeconds(2);
|
|
|
|
/// <summary>Asks the historian adapter actor for its status.</summary>
|
|
/// <param name="resolveActor">Resolves the actor; throws when this node registers none.</param>
|
|
/// <param name="timeout">The Ask timeout.</param>
|
|
/// <param name="ct">Cancellation (the page's lifetime); a cancellation propagates.</param>
|
|
/// <returns>The classified reading.</returns>
|
|
public static async Task<HistorianSinkReading> ReadAsync(
|
|
Func<IActorRef> 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<HistorianSinkStatus>(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}");
|
|
}
|
|
}
|
|
}
|