using Akka.Actor; using Akka.Cluster.Tools.PublishSubscribe; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using ZB.MOM.WW.OtOpcUa.Commons.Messages.Drivers; using ZB.MOM.WW.OtOpcUa.Core.Resilience; namespace ZB.MOM.WW.OtOpcUa.Host.Drivers; /// /// The operator-facing reader for . Runs on driver-role /// nodes (registered next to the tracker it reads) and every publishes the /// full tracker snapshot — one per (instance, host) /// — to the driver-resilience-status DistributedPubSub topic. The AdminUI /// DriverResilienceStatusBridge subscribes and feeds the resilience section of the driver /// status panel. Mirrors the driver-health flow; publishing the full set every tick self-heals /// late-subscribing admin nodes and gives the panel staleness detection for free. /// public sealed class DriverResilienceStatusPublisherService : BackgroundService { private static readonly TimeSpan DefaultInterval = TimeSpan.FromSeconds(5); private readonly DriverResilienceStatusTracker _tracker; private readonly Func _actorSystemAccessor; private readonly ILogger _logger; private readonly TimeSpan _interval; /// Initializes a new instance of . /// The process-singleton resilience-status tracker to read each tick. /// Lazy accessor for the Akka whose DPS mediator publishes the snapshots. /// Logger for publish diagnostics. /// Publish cadence; defaults to 5 s when null. public DriverResilienceStatusPublisherService( DriverResilienceStatusTracker tracker, Func actorSystemAccessor, ILogger logger, TimeSpan? interval = null) { _tracker = tracker; _actorSystemAccessor = actorSystemAccessor; _logger = logger; _interval = interval ?? DefaultInterval; } /// /// Map the tracker's current snapshot to one wire message per (instance, host) pair. /// Pure + unit-testable; the timer/publish shell in calls this each tick. /// /// The tracker to read. /// The publish timestamp stamped on every emitted message. /// One per tracked pair (empty when the tracker is empty). public static IReadOnlyList BuildMessages( DriverResilienceStatusTracker tracker, DateTime publishedUtc) => tracker.Snapshot() .Select(entry => new DriverResilienceStatusChanged( entry.DriverInstanceId, entry.HostName, entry.Snapshot.IsBreakerOpen, entry.Snapshot.ConsecutiveFailures, entry.Snapshot.CurrentInFlight, entry.Snapshot.LastBreakerOpenUtc, entry.Snapshot.LastSampledUtc, publishedUtc)) .ToList(); /// protected override async Task ExecuteAsync(CancellationToken stoppingToken) { using var timer = new PeriodicTimer(_interval); while (await timer.WaitForNextTickAsync(stoppingToken).ConfigureAwait(false)) { try { var messages = BuildMessages(_tracker, DateTime.UtcNow); if (messages.Count == 0) continue; var mediator = DistributedPubSub.Get(_actorSystemAccessor()).Mediator; foreach (var message in messages) mediator.Tell(new Publish(DriverResilienceStatusChanged.TopicName, message)); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { break; } catch (Exception ex) { // A transient DPS / actor-system hiccup must never kill the publisher — the next tick retries. _logger.LogWarning(ex, "DriverResilienceStatusPublisherService: publish tick failed; retrying next interval"); } } } }