M4 R4.4: client-side multi-historian redundancy
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
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user