using System; using System.Collections.Generic; using System.Linq; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.Historian { /// /// Thread-safe, pure-logic endpoint picker for the Wonderware Historian cluster. Tracks which /// configured nodes are healthy, places failed nodes in a time-bounded cooldown, and hands /// out an ordered list of eligible candidates for the data source to try in sequence. /// internal sealed class HistorianClusterEndpointPicker { private readonly Func _clock; private readonly TimeSpan _cooldown; private readonly object _lock = new object(); private readonly List _nodes; public HistorianClusterEndpointPicker(HistorianConfiguration config) : this(config, () => DateTime.UtcNow) { } internal HistorianClusterEndpointPicker(HistorianConfiguration config, Func clock) { _clock = clock ?? throw new ArgumentNullException(nameof(clock)); _cooldown = TimeSpan.FromSeconds(Math.Max(0, config.FailureCooldownSeconds)); var names = (config.ServerNames != null && config.ServerNames.Count > 0) ? config.ServerNames : new List { config.ServerName }; _nodes = names .Where(n => !string.IsNullOrWhiteSpace(n)) .Select(n => n.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) .Select(n => new NodeEntry { Name = n }) .ToList(); } public int NodeCount { get { lock (_lock) return _nodes.Count; } } public IReadOnlyList GetHealthyNodes() { lock (_lock) { var now = _clock(); return _nodes.Where(n => IsHealthyAt(n, now)).Select(n => n.Name).ToList(); } } public int HealthyNodeCount { get { lock (_lock) { var now = _clock(); return _nodes.Count(n => IsHealthyAt(n, now)); } } } public void MarkFailed(string node, string? error) { lock (_lock) { var entry = FindEntry(node); if (entry == null) return; var now = _clock(); entry.FailureCount++; entry.LastError = error; entry.LastFailureTime = now; entry.CooldownUntil = _cooldown.TotalMilliseconds > 0 ? now + _cooldown : (DateTime?)null; } } public void MarkHealthy(string node) { lock (_lock) { var entry = FindEntry(node); if (entry == null) return; entry.CooldownUntil = null; } } public List SnapshotNodeStates() { lock (_lock) { var now = _clock(); return _nodes.Select(n => new HistorianClusterNodeState { Name = n.Name, IsHealthy = IsHealthyAt(n, now), CooldownUntil = IsHealthyAt(n, now) ? null : n.CooldownUntil, FailureCount = n.FailureCount, LastError = n.LastError, LastFailureTime = n.LastFailureTime }).ToList(); } } private static bool IsHealthyAt(NodeEntry entry, DateTime now) { return entry.CooldownUntil == null || entry.CooldownUntil <= now; } private NodeEntry? FindEntry(string node) { for (var i = 0; i < _nodes.Count; i++) if (string.Equals(_nodes[i].Name, node, StringComparison.OrdinalIgnoreCase)) return _nodes[i]; return null; } private sealed class NodeEntry { public string Name { get; set; } = ""; public DateTime? CooldownUntil { get; set; } public int FailureCount { get; set; } public string? LastError { get; set; } public DateTime? LastFailureTime { get; set; } } } }