d1cc9e2cfc
DriverHostActor wraps the health publisher it hands its children in a ChildHealthLedger (last snapshot per driver id, forwarded to the real publisher), so GetDiagnostics now answers with each child's row id, authored name, last-published state, device counts (host statuses when the driver probes hosts, else one device) and last-change time — instead of Guid.Empty / "Spawned" / 0 / now. Entries are forgotten on StopChild and operator Restart. IFleetDiagnosticsClient gains its first consumer: the new /fleet/diagnostics page (node selector + ?node= query, revision, running-from-cache notice, per-driver table). FleetDiagnosticsClient logs the degrade-to-empty path at Warning. Review ref: 02/U1. Refs #731 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
74 lines
3.7 KiB
C#
74 lines
3.7 KiB
C#
using System.Collections.Concurrent;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
|
|
|
/// <summary>The last health snapshot a driver child published, as kept by <see cref="ChildHealthLedger"/>.</summary>
|
|
/// <param name="State">The driver's reported state.</param>
|
|
/// <param name="LastSuccessfulReadUtc">When the driver last read successfully, if ever.</param>
|
|
/// <param name="LastError">The driver's last error, if any.</param>
|
|
/// <param name="HostStatuses">Per-host connectivity for a multi-host driver, or null when the driver has none.</param>
|
|
/// <param name="PublishedUtc">When the snapshot was published — the child's dedup means this is the last CHANGE.</param>
|
|
public sealed record ChildHealthRecord(
|
|
DriverState State,
|
|
DateTime? LastSuccessfulReadUtc,
|
|
string? LastError,
|
|
IReadOnlyList<HostConnectivityStatus>? HostStatuses,
|
|
DateTime PublishedUtc);
|
|
|
|
/// <summary>
|
|
/// An <see cref="IDriverHealthPublisher"/> decorator that remembers the last snapshot per driver instance
|
|
/// and forwards every publish to the real publisher. <see cref="DriverHostActor"/> wraps the publisher it
|
|
/// hands its children in one of these so <c>GetDiagnostics</c> can answer from the children's own health
|
|
/// (state, host counts, last change) instead of a placeholder — the children publish on their own actor
|
|
/// threads, hence the concurrent map.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Deliberately NOT a message to the host: the children already publish through this seam on every
|
|
/// change, so a parallel <c>Tell</c> would double every health transition's traffic for one consumer.
|
|
/// Entries are removed by the host when it stops a child; a late publish from a stopping child may re-add
|
|
/// one, which the host filters against its live child set when it answers.
|
|
/// </remarks>
|
|
public sealed class ChildHealthLedger : IDriverHealthPublisher
|
|
{
|
|
private readonly IDriverHealthPublisher _inner;
|
|
private readonly ConcurrentDictionary<string, ChildHealthRecord> _last = new(StringComparer.Ordinal);
|
|
|
|
/// <summary>Wraps <paramref name="inner"/>.</summary>
|
|
/// <param name="inner">The publisher every snapshot is forwarded to.</param>
|
|
public ChildHealthLedger(IDriverHealthPublisher inner)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(inner);
|
|
_inner = inner;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void Publish(
|
|
string clusterId,
|
|
string driverInstanceId,
|
|
DriverHealth health,
|
|
int errorCount5Min,
|
|
DateTime? rediscoveryNeededUtc = null,
|
|
string? rediscoveryReason = null,
|
|
IReadOnlyList<HostConnectivityStatus>? hostStatuses = null,
|
|
SkippedTagReport? skippedTags = null,
|
|
IReadOnlyDictionary<string, string>? diagnostics = null)
|
|
{
|
|
_last[driverInstanceId] = new ChildHealthRecord(
|
|
health.State, health.LastSuccessfulRead, health.LastError, hostStatuses, DateTime.UtcNow);
|
|
_inner.Publish(
|
|
clusterId, driverInstanceId, health, errorCount5Min, rediscoveryNeededUtc, rediscoveryReason,
|
|
hostStatuses, skippedTags, diagnostics);
|
|
}
|
|
|
|
/// <summary>The last snapshot published for <paramref name="driverInstanceId"/>, or null if none yet.</summary>
|
|
/// <param name="driverInstanceId">The driver instance.</param>
|
|
/// <returns>The record, or null.</returns>
|
|
public ChildHealthRecord? TryGet(string driverInstanceId) =>
|
|
_last.TryGetValue(driverInstanceId, out var record) ? record : null;
|
|
|
|
/// <summary>Forgets <paramref name="driverInstanceId"/>'s snapshot (the child was stopped).</summary>
|
|
/// <param name="driverInstanceId">The driver instance.</param>
|
|
public void Forget(string driverInstanceId) => _last.TryRemove(driverInstanceId, out _);
|
|
}
|