130 lines
4.3 KiB
C#
130 lines
4.3 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.Historian
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
internal sealed class HistorianClusterEndpointPicker
|
|
{
|
|
private readonly Func<DateTime> _clock;
|
|
private readonly TimeSpan _cooldown;
|
|
private readonly object _lock = new object();
|
|
private readonly List<NodeEntry> _nodes;
|
|
|
|
public HistorianClusterEndpointPicker(HistorianConfiguration config)
|
|
: this(config, () => DateTime.UtcNow) { }
|
|
|
|
internal HistorianClusterEndpointPicker(HistorianConfiguration config, Func<DateTime> 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<string> { 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<string> 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<HistorianClusterNodeState> 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; }
|
|
}
|
|
}
|
|
}
|