a5557375ee
The continuous-historization recorder awaited every outbox append (an fsync under PerEntry) on its mailbox, so ingestion was capped at 1/fsync_latency bursts per second while the mux kept telling into an unbounded, unmetered mailbox. GetStatus also suspended the mailbox on an unbounded outbox count. - Gated ingest bursts go to a bounded channel (64 bursts) drained by one ordered append loop, which commits every burst waiting behind a slow append in a single AppendBatchAsync (FIFO kept). Only a full queue holds the mailbox (backpressure, bounded memory). Counters are Interlocked; the loop nudges the drain. PostStop flushes queued bursts within the shared 5 s stop budget. - GetStatus reads counters on the actor thread and pipes a count bounded by a 10 s timeout (-1 when it does not answer); it no longer suspends the mailbox. RecorderStatus gains PendingAppends, MailboxDepth and AppendBackpressureWaits; two gauges publish pending appends and mailbox depth. - ContinuousHistorization:CommitMode defaults to Periodic (100 ms). The Host's appsettings.json, its unparseable-value fallback and docker-dev still say PerEntry: filed as a Host follow-up (outside this lane). Rebased from the unpushed remed/lane-H2 branch (originally authored 2026-10-22 in session_01Ub9hGmYLQvn9XETzw7CMsT) onto current master. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
119 lines
4.7 KiB
C#
119 lines
4.7 KiB
C#
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.Historian;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Historian;
|
|
|
|
/// <summary>
|
|
/// Shared doubles for the recorder tests added with the pass-flush / off-mailbox-append work: a FIFO outbox and a
|
|
/// pass-aware writer that log every call into one ordered event list, so a test can assert the ORDER of writes,
|
|
/// flushes and acks across both. Thread-safe — the recorder appends and drains off the actor thread.
|
|
/// </summary>
|
|
internal sealed class EventLog
|
|
{
|
|
private readonly Lock _gate = new();
|
|
private readonly List<string> _events = new();
|
|
|
|
public void Add(string e) { lock (_gate) _events.Add(e); }
|
|
|
|
public IReadOnlyList<string> Snapshot() { lock (_gate) return _events.ToArray(); }
|
|
}
|
|
|
|
/// <summary>FIFO outbox honouring the truncate-through contract; appends can be made to yield or block.</summary>
|
|
internal sealed class LoggingOutbox(EventLog log) : IHistorizationOutbox
|
|
{
|
|
private readonly Lock _gate = new();
|
|
private readonly List<HistorizationOutboxEntry> _entries = new();
|
|
private readonly List<HistorizationOutboxEntry> _appended = new();
|
|
private int _appendBatchCalls;
|
|
|
|
/// <summary>When set, every AppendBatchAsync waits on it (on a pool thread) before committing — a slow fsync.</summary>
|
|
public TaskCompletionSource? AppendGate { get; set; }
|
|
|
|
/// <summary>Completed by an append that has entered (before waiting on <see cref="AppendGate"/>).</summary>
|
|
public TaskCompletionSource AppendEntered { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
|
|
/// <summary>When set, CountAsync blocks on it — a hung outbox.</summary>
|
|
public TaskCompletionSource? CountGate { get; set; }
|
|
|
|
public int AppendBatchCalls { get { lock (_gate) return _appendBatchCalls; } }
|
|
|
|
public long DroppedCount => 0;
|
|
|
|
public IReadOnlyList<HistorizationOutboxEntry> Entries { get { lock (_gate) return _entries.ToArray(); } }
|
|
|
|
/// <summary>Every entry ever appended, in append order (acks do not remove from it).</summary>
|
|
public IReadOnlyList<HistorizationOutboxEntry> Appended { get { lock (_gate) return _appended.ToArray(); } }
|
|
|
|
public ValueTask AppendAsync(HistorizationOutboxEntry entry, CancellationToken ct) => AppendBatchAsync([entry], ct);
|
|
|
|
public async ValueTask AppendBatchAsync(IReadOnlyList<HistorizationOutboxEntry> entries, CancellationToken ct)
|
|
{
|
|
lock (_gate) _appendBatchCalls++;
|
|
AppendEntered.TrySetResult();
|
|
await Task.Delay(1, ct).ConfigureAwait(false); // complete on a pool thread, as a real fsync does
|
|
if (AppendGate is { } gate) await gate.Task.WaitAsync(ct).ConfigureAwait(false);
|
|
lock (_gate)
|
|
{
|
|
_entries.AddRange(entries);
|
|
_appended.AddRange(entries);
|
|
}
|
|
log.Add($"append:{entries.Count}");
|
|
}
|
|
|
|
public ValueTask<IReadOnlyList<HistorizationOutboxEntry>> PeekBatchAsync(int max, CancellationToken ct)
|
|
{
|
|
lock (_gate) return ValueTask.FromResult<IReadOnlyList<HistorizationOutboxEntry>>(_entries.Take(max).ToArray());
|
|
}
|
|
|
|
public ValueTask RemoveAsync(Guid id, CancellationToken ct)
|
|
{
|
|
lock (_gate)
|
|
{
|
|
var i = _entries.FindIndex(e => e.Id == id);
|
|
if (i >= 0) _entries.RemoveRange(0, i + 1);
|
|
}
|
|
log.Add("ack");
|
|
return ValueTask.CompletedTask;
|
|
}
|
|
|
|
public async ValueTask<int> CountAsync(CancellationToken ct)
|
|
{
|
|
if (CountGate is { } gate) await gate.Task.WaitAsync(ct).ConfigureAwait(false);
|
|
lock (_gate) return _entries.Count;
|
|
}
|
|
|
|
public void Dispose() { }
|
|
}
|
|
|
|
/// <summary>A pass-aware writer: logs every in-pass write and flush; plain writes are logged distinctly.</summary>
|
|
internal sealed class LoggingPassWriter(EventLog log) : IHistorianPassWriter
|
|
{
|
|
private readonly HashSet<string> _failTags = new(StringComparer.Ordinal);
|
|
|
|
public Exception? FlushThrows { get; set; }
|
|
|
|
public void Fail(string tag) { lock (_failTags) _failTags.Add(tag); }
|
|
|
|
public Task<bool> WriteLiveValuesAsync(string tag, IReadOnlyList<HistorizationValue> values, CancellationToken ct)
|
|
{
|
|
log.Add($"plain:{tag}");
|
|
return Task.FromResult(true);
|
|
}
|
|
|
|
public async Task<bool> WriteLiveValuesInPassAsync(string tag, IReadOnlyList<HistorizationValue> values, CancellationToken ct)
|
|
{
|
|
await Task.Delay(1, ct).ConfigureAwait(false);
|
|
bool fail;
|
|
lock (_failTags) fail = _failTags.Contains(tag);
|
|
log.Add($"write:{tag}");
|
|
return !fail;
|
|
}
|
|
|
|
public async Task FlushAsync(CancellationToken ct)
|
|
{
|
|
await Task.Delay(1, ct).ConfigureAwait(false);
|
|
log.Add("flush");
|
|
if (FlushThrows is { } ex) throw ex;
|
|
}
|
|
}
|