dd2aec3b8b
Adds AVEVA.Historian.Client.StoreForward — a client-side store-and-forward layer over the historian write surface (AddHistoricalValuesAsync / SendEventAsync). Producers enqueue writes; the writer persists them and replays on reconnect so a transient disconnect never drops data. This is the roadmap's recommended pragmatic outbox, NOT a bit-faithful reimplementation of AVEVA's native SF cache (that stays deferred) — pure managed, no RE. - HistorianOutboxEntry / HistorianOutboxEntryKind: buffered-write envelope - IHistorianOutboxStore + InMemoryHistorianOutboxStore (tests) + FileHistorianOutboxStore (crash-durable: atomic temp+move JSON per entry, FIFO by filename sequence that resumes past on-disk max, corrupt-file quarantine). OutboxJson normalizes event object? properties off JsonElement. - IHistorianWriteSink + HistorianClientWriteSink (HistorianClient-backed) - HistorianStoreForwardWriter: enqueue, single-flight FIFO FlushAsync with head-of-line blocking, optional MaxDeliveryAttempts dead-lettering, DropOldest/Reject overflow policy, background drain loop (retry on reconnect), GetStatusAsync snapshot mirroring server SF Pending/Storing/ErrorOccurred. 12 unit tests (no server): durability-across-restart, reconnect-drain, FIFO order/head-of-line, dead-letter, overflow policies, background auto-drain. Full suite 293 green. Roadmap R4.1 marked shipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
64 lines
2.3 KiB
C#
64 lines
2.3 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace AVEVA.Historian.Client.StoreForward;
|
|
|
|
/// <summary>
|
|
/// Shared JSON settings for persisting <see cref="HistorianOutboxEntry"/> envelopes. The custom
|
|
/// <see cref="ObjectConverter"/> normalizes <c>HistorianEvent.Properties</c> values (typed
|
|
/// <see cref="object"/>) back to plain CLR scalars on read instead of leaving raw
|
|
/// <see cref="JsonElement"/>s, so a rehydrated event matches what the producer enqueued.
|
|
/// </summary>
|
|
internal static class OutboxJson
|
|
{
|
|
public static readonly JsonSerializerOptions Options = CreateOptions();
|
|
|
|
private static JsonSerializerOptions CreateOptions()
|
|
{
|
|
var options = new JsonSerializerOptions
|
|
{
|
|
WriteIndented = false,
|
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
|
};
|
|
options.Converters.Add(new ObjectConverter());
|
|
return options;
|
|
}
|
|
|
|
private sealed class ObjectConverter : JsonConverter<object?>
|
|
{
|
|
public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
{
|
|
switch (reader.TokenType)
|
|
{
|
|
case JsonTokenType.Null:
|
|
return null;
|
|
case JsonTokenType.String:
|
|
return reader.GetString();
|
|
case JsonTokenType.True:
|
|
return true;
|
|
case JsonTokenType.False:
|
|
return false;
|
|
case JsonTokenType.Number:
|
|
return reader.TryGetInt64(out long l) ? l : reader.GetDouble();
|
|
default:
|
|
using (JsonDocument doc = JsonDocument.ParseValue(ref reader))
|
|
{
|
|
return doc.RootElement.GetRawText();
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options)
|
|
{
|
|
if (value is null)
|
|
{
|
|
writer.WriteNullValue();
|
|
return;
|
|
}
|
|
|
|
// Serialize by runtime type so scalars use their own converters (no re-entry into this one).
|
|
JsonSerializer.Serialize(writer, value, value.GetType(), options);
|
|
}
|
|
}
|
|
}
|