60b3673f01
Adds AVEVA.Historian.Client.Redundancy — HistorianRedundantClient orchestrates N single-historian members (IHistorianMember; default HistorianClientMember over HistorianClient) as one logical client. Pure client-side, no server-side redundancy protocol, no RE. - Reads fail over to the next member in priority order. Streaming reads only fail over BEFORE the first row is observed; a mid-stream failure propagates (failing over mid-stream would risk duplicated/skipped rows). - Writes fan out: WriteFanout AllMembers | PreferredOnly, with All | Any ack policy, returning a per-member HistorianRedundantWriteResult. - Per-member health: FailureThreshold demotes a failing member out of the preferred pool; a background watchdog (PeriodicTimer) + CheckHealthAsync re-probe and restore recovered members. GetStatus() snapshot + ActiveMember. - Composes with R4.1: back a member's writes with a HistorianStoreForwardWriter so a down member buffers and replays on recovery — the pragmatic client-side equivalent of native ReSyncTags. 14 unit tests (no server): failover order, mid-stream no-failover, all-fail aggregation, probe-any-up, fan-out ack policies, PreferredOnly, soft reject, health demotion + CheckHealthAsync restore, watchdog recovery. Full suite 307 green. Roadmap R4.4 marked shipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
79 lines
1.9 KiB
C#
79 lines
1.9 KiB
C#
namespace AVEVA.Historian.Client.Redundancy;
|
|
|
|
/// <summary>
|
|
/// Mutable per-member health used by <see cref="HistorianRedundantClient"/> to route reads and
|
|
/// fan out writes. Thread-safe: ops update it from multiple call sites and the watchdog loop.
|
|
/// </summary>
|
|
internal sealed class MemberState
|
|
{
|
|
private readonly Lock _lock = new();
|
|
private readonly int _failureThreshold;
|
|
|
|
private bool _isHealthy = true;
|
|
private int _consecutiveFailures;
|
|
private string? _lastError;
|
|
private DateTime? _lastSuccessUtc;
|
|
private DateTime? _lastCheckUtc;
|
|
|
|
public MemberState(IHistorianMember member, int failureThreshold)
|
|
{
|
|
Member = member;
|
|
_failureThreshold = failureThreshold;
|
|
}
|
|
|
|
public IHistorianMember Member { get; }
|
|
|
|
public bool IsHealthy
|
|
{
|
|
get
|
|
{
|
|
lock (_lock)
|
|
{
|
|
return _isHealthy;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void MarkSuccess(DateTime utc)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
_consecutiveFailures = 0;
|
|
_isHealthy = true;
|
|
_lastError = null;
|
|
_lastSuccessUtc = utc;
|
|
_lastCheckUtc = utc;
|
|
}
|
|
}
|
|
|
|
public void MarkFailure(string? error, DateTime utc)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
_consecutiveFailures++;
|
|
_lastError = error;
|
|
_lastCheckUtc = utc;
|
|
if (_consecutiveFailures >= _failureThreshold)
|
|
{
|
|
_isHealthy = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
public HistorianMemberStatus Snapshot()
|
|
{
|
|
lock (_lock)
|
|
{
|
|
return new HistorianMemberStatus
|
|
{
|
|
Name = Member.Name,
|
|
IsHealthy = _isHealthy,
|
|
ConsecutiveFailures = _consecutiveFailures,
|
|
LastError = _lastError,
|
|
LastSuccessUtc = _lastSuccessUtc,
|
|
LastCheckUtc = _lastCheckUtc,
|
|
};
|
|
}
|
|
}
|
|
}
|