Files
histsdk/src/AVEVA.Historian.Client/Redundancy/HistorianClientMember.cs
T
Joseph Doherty 60b3673f01 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
2026-06-21 22:46:10 -04:00

50 lines
2.6 KiB
C#

using AVEVA.Historian.Client.Models;
namespace AVEVA.Historian.Client.Redundancy;
/// <summary>
/// Default <see cref="IHistorianMember"/> adapter over a <see cref="HistorianClient"/>. For durable
/// redundant writes, pass a member whose write methods enqueue to an R4.1
/// <c>HistorianStoreForwardWriter</c> instead — then a member that is down buffers its writes and
/// replays them on recovery (the pragmatic client-side equivalent of native ReSyncTags).
/// </summary>
public sealed class HistorianClientMember : IHistorianMember
{
private readonly HistorianClient _client;
public HistorianClientMember(string name, HistorianClient client)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
Name = name;
_client = client ?? throw new ArgumentNullException(nameof(client));
}
public string Name { get; }
public Task<bool> ProbeAsync(CancellationToken cancellationToken) => _client.ProbeAsync(cancellationToken);
public IAsyncEnumerable<HistorianSample> ReadRawAsync(string tag, DateTime startUtc, DateTime endUtc, int maxValues, CancellationToken cancellationToken) =>
_client.ReadRawAsync(tag, startUtc, endUtc, maxValues, cancellationToken);
public IAsyncEnumerable<HistorianAggregateSample> ReadAggregateAsync(string tag, DateTime startUtc, DateTime endUtc, RetrievalMode mode, TimeSpan interval, CancellationToken cancellationToken) =>
_client.ReadAggregateAsync(tag, startUtc, endUtc, mode, interval, cancellationToken);
public Task<IReadOnlyList<HistorianSample>> ReadAtTimeAsync(string tag, IReadOnlyList<DateTime> timestampsUtc, CancellationToken cancellationToken) =>
_client.ReadAtTimeAsync(tag, timestampsUtc, cancellationToken);
public IAsyncEnumerable<HistorianEvent> ReadEventsAsync(DateTime startUtc, DateTime endUtc, CancellationToken cancellationToken) =>
_client.ReadEventsAsync(startUtc, endUtc, cancellationToken);
public IAsyncEnumerable<string> BrowseTagNamesAsync(string filter, CancellationToken cancellationToken) =>
_client.BrowseTagNamesAsync(filter, cancellationToken);
public Task<HistorianTagMetadata?> GetTagMetadataAsync(string tag, CancellationToken cancellationToken) =>
_client.GetTagMetadataAsync(tag, cancellationToken);
public Task<bool> AddHistoricalValuesAsync(string tag, IReadOnlyList<HistorianHistoricalValue> values, CancellationToken cancellationToken) =>
_client.AddHistoricalValuesAsync(tag, values, cancellationToken);
public Task<bool> SendEventAsync(HistorianEvent historianEvent, CancellationToken cancellationToken) =>
_client.SendEventAsync(historianEvent, cancellationToken);
}