Files
histsdk/src/AVEVA.Historian.Client/StoreForward/OutboxJson.cs
T
Joseph Doherty dd2aec3b8b M4 R4.1: pragmatic store-and-forward durable outbox
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
2026-06-21 22:35:30 -04:00

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);
}
}
}