docs(xmldoc): fill missing XML docs + strip tracking-ID comments across src
v2-ci / build (push) Failing after 41s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
v2-ci / build (push) Failing after 41s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Adds <summary>/<param>/<returns>/<inheritdoc> where missing and removes project bookkeeping IDs (task/tracking refs) from shipped code comments, so the docs read cleanly and CommentChecker is quiet except for known false positives (PLC/protocol terms, event/IEqualityComparer inheritdoc). Doc/comment-only; no logic changed; solution builds clean.
This commit is contained in:
@@ -4,8 +4,8 @@ namespace ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian;
|
||||
|
||||
/// <summary>
|
||||
/// The event shape the historian sink consumes — source-agnostic across scripted
|
||||
/// alarms + Galaxy-native + AB CIP ALMD + any future IAlarmSource per Phase 7 plan
|
||||
/// decision #15 (sink scope = all alarm sources, not just scripted). A per-alarm
|
||||
/// alarms + Galaxy-native + AB CIP ALMD + any future IAlarmSource (sink scope = all
|
||||
/// alarm sources, not just scripted). A per-alarm
|
||||
/// <c>HistorizeToAveva</c> toggle on the producer side gates which events flow.
|
||||
/// </summary>
|
||||
/// <param name="AlarmId">Stable condition identity.</param>
|
||||
|
||||
@@ -11,8 +11,8 @@ namespace ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian;
|
||||
/// <see cref="EnqueueAsync"/> is fire-and-forget from the engine's perspective —
|
||||
/// the sink MUST NOT block the emitting thread. Production implementations
|
||||
/// (<see cref="SqliteStoreAndForwardSink"/>) persist to a local SQLite queue
|
||||
/// first, then drain asynchronously to the actual historian. Per Phase 7 plan
|
||||
/// decision #16, failed downstream writes replay with exponential backoff;
|
||||
/// first, then drain asynchronously to the actual historian. Per the Phase 7 plan,
|
||||
/// failed downstream writes replay with exponential backoff;
|
||||
/// operator actions are never blocked waiting on the historian.
|
||||
/// </para>
|
||||
/// <para>
|
||||
@@ -25,9 +25,11 @@ public interface IAlarmHistorianSink
|
||||
/// <summary>Durably enqueue the event. Returns as soon as the queue row is committed.</summary>
|
||||
/// <param name="evt">The alarm historian event to enqueue.</param>
|
||||
/// <param name="cancellationToken">A cancellation token for async operations.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
Task EnqueueAsync(AlarmHistorianEvent evt, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>Snapshot of current queue depth + drain health.</summary>
|
||||
/// <returns>The current queue depth and drain health snapshot.</returns>
|
||||
HistorianSinkStatus GetStatus();
|
||||
}
|
||||
|
||||
@@ -96,6 +98,7 @@ public interface IAlarmHistorianWriter
|
||||
/// <summary>Push a batch of events to the historian. Returns one outcome per event, same order.</summary>
|
||||
/// <param name="batch">The batch of alarm historian events to write.</param>
|
||||
/// <param name="cancellationToken">A cancellation token for async operations.</param>
|
||||
/// <returns>One <see cref="HistorianWriteOutcome"/> per event, in the same order as <paramref name="batch"/>.</returns>
|
||||
Task<IReadOnlyList<HistorianWriteOutcome>> WriteBatchAsync(
|
||||
IReadOnlyList<AlarmHistorianEvent> batch, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -5,11 +5,10 @@ using Serilog;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian;
|
||||
|
||||
/// <summary>
|
||||
/// Phase 7 plan decisions #16–#17 implementation: durable SQLite queue on the node
|
||||
/// absorbs every qualifying alarm event, a drain worker batches rows to the
|
||||
/// Wonderware historian sidecar via <see cref="IAlarmHistorianWriter"/> on an
|
||||
/// exponential-backoff cadence, and operator acks never block on the historian
|
||||
/// being reachable.
|
||||
/// Durable SQLite queue on the node absorbs every qualifying alarm event, a drain
|
||||
/// worker batches rows to the Wonderware historian sidecar via
|
||||
/// <see cref="IAlarmHistorianWriter"/> on an exponential-backoff cadence, and
|
||||
/// operator acks never block on the historian being reachable.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
@@ -27,7 +26,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian;
|
||||
/// );
|
||||
/// </code>
|
||||
/// Dead-lettered rows stay in place for the configured retention window (default
|
||||
/// 30 days per Phase 7 plan decision #21) so operators can inspect + manually
|
||||
/// 30 days) so operators can inspect + manually
|
||||
/// retry before the sweeper purges them. Regular queue capacity is bounded —
|
||||
/// overflow evicts the oldest non-dead-lettered rows with a WARN log. The
|
||||
/// durability guarantee is therefore bounded by <see cref="DefaultCapacity"/>:
|
||||
@@ -77,29 +76,16 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable
|
||||
private TimeSpan _tickInterval;
|
||||
private volatile int _backoffIndex;
|
||||
// volatile: read by EnqueueAsync, DrainOnceAsync, RescheduleDrain, and StartDrainLoop
|
||||
// from different threads; Dispose() writes it from the owner's thread (Core.AlarmHistorian-013).
|
||||
// from different threads; Dispose() writes it from the owner's thread.
|
||||
private volatile bool _disposed;
|
||||
|
||||
// Core.AlarmHistorian-005: status fields written by the drain timer thread and
|
||||
// read concurrently by GetStatus() / health-check threads. Guard all reads and
|
||||
// writes with this lock so the Admin UI never observes a torn or stale value.
|
||||
private readonly object _statusLock = new();
|
||||
private DateTime? _lastDrainUtc;
|
||||
private DateTime? _lastSuccessUtc;
|
||||
private string? _lastError;
|
||||
private HistorianDrainState _drainState = HistorianDrainState.Idle;
|
||||
// Core.AlarmHistorian-009: lifetime counter of rows evicted due to capacity overflow.
|
||||
// Surfaces in HistorianSinkStatus so operators can see data-loss events without
|
||||
// having to scrape the WARN log.
|
||||
private long _evictedCount;
|
||||
|
||||
// Core.AlarmHistorian-008: keep an approximate in-memory count of non-dead-lettered
|
||||
// rows so EnqueueAsync does not need to run a SELECT COUNT(*) on every call. The
|
||||
// counter is seeded from storage at construction, kept current by every mutation
|
||||
// (Enqueue, Drain, RetryDeadLettered, PurgeAgedDeadLetters, EnforceCapacity), and
|
||||
// periodically re-synced from storage as a safety net against drift.
|
||||
// Mutations cross threads (EnqueueAsync is called from the emitting thread, drain
|
||||
// runs on the timer / drain thread) so it is updated via Interlocked.
|
||||
private long _queuedRowCount;
|
||||
// Probe counter — incremented every time we actually issue a real COUNT(*) for
|
||||
// capacity enforcement. Public for test instrumentation only.
|
||||
@@ -145,7 +131,7 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable
|
||||
// DefaultTimeout gives ADO.NET command-level retry; the PRAGMA busy_timeout
|
||||
// applied in OpenConnection backs it with SQLite's own busy-handler so an
|
||||
// enqueue/drain collision waits out the file lock instead of throwing
|
||||
// SQLITE_BUSY immediately (Core.AlarmHistorian-004).
|
||||
// SQLITE_BUSY immediately.
|
||||
_connectionString = new SqliteConnectionStringBuilder
|
||||
{
|
||||
DataSource = databasePath,
|
||||
@@ -153,8 +139,6 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable
|
||||
}.ToString();
|
||||
|
||||
InitializeSchema();
|
||||
// Core.AlarmHistorian-008: seed the in-memory counter from storage so the
|
||||
// perf-optimised EnqueueAsync path starts in sync with what's on disk.
|
||||
_queuedRowCount = ProbeQueuedRowCount();
|
||||
}
|
||||
|
||||
@@ -195,11 +179,11 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable
|
||||
/// <remarks>
|
||||
/// The worker is a self-rescheduling one-shot <see cref="Timer"/>: after each
|
||||
/// drain it sets its next due-time to <c>max(tickInterval, CurrentBackoff)</c>
|
||||
/// so a historian outage actually slows the cadence down the backoff ladder
|
||||
/// (Core.AlarmHistorian-002). The callback body is fully guarded — a fault in
|
||||
/// so a historian outage actually slows the cadence down the backoff ladder.
|
||||
/// The callback body is fully guarded — a fault in
|
||||
/// <see cref="DrainOnceAsync"/> is logged and recorded into
|
||||
/// <see cref="GetStatus"/> rather than being lost as an unobserved task
|
||||
/// exception (Core.AlarmHistorian-006).
|
||||
/// exception.
|
||||
/// </remarks>
|
||||
/// <param name="tickInterval">The base interval between drain attempts.</param>
|
||||
public void StartDrainLoop(TimeSpan tickInterval)
|
||||
@@ -251,19 +235,7 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable
|
||||
catch (ObjectDisposedException) { /* raced with Dispose — nothing to re-arm */ }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enqueues an alarm historian event asynchronously for forwarding to the historian.
|
||||
/// Respects the queue capacity and enforces eviction of oldest rows when full.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Core.AlarmHistorian-003: use async SQLite APIs so the emitting thread is not
|
||||
/// blocked waiting for a file-lock or disk write; honor the cancellationToken
|
||||
/// throughout. Microsoft.Data.Sqlite's async surface (OpenAsync /
|
||||
/// ExecuteNonQueryAsync) is a thin wrapper over the synchronous path, so the
|
||||
/// blocking still happens — but on a thread-pool thread, not the caller's thread.
|
||||
/// </remarks>
|
||||
/// <param name="evt">The alarm historian event to enqueue.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <inheritdoc />
|
||||
public async Task EnqueueAsync(AlarmHistorianEvent evt, CancellationToken cancellationToken)
|
||||
{
|
||||
if (evt is null) throw new ArgumentNullException(nameof(evt));
|
||||
@@ -273,10 +245,6 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable
|
||||
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
|
||||
await ApplyPragmasAsync(conn, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Core.AlarmHistorian-008: use the in-memory counter to short-circuit the
|
||||
// capacity check on every enqueue. The bare hot path is now one INSERT — no
|
||||
// SELECT COUNT(*). We fall back to a real probe only when the cached counter
|
||||
// says we're at or above capacity, or periodically to defend against drift.
|
||||
await EnforceCapacityFastPathAsync(conn, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
using var cmd = conn.CreateCommand();
|
||||
@@ -347,13 +315,8 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable
|
||||
/// on RetryPlease. Safe to call from multiple threads; the semaphore enforces
|
||||
/// serial execution.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Core.AlarmHistorian-008: every per-tick SQLite operation runs through a
|
||||
/// single shared connection (purge, read, corrupt-row dead-letter, and the
|
||||
/// outcome-applying transaction). Pre-fix the drain opened three independent
|
||||
/// connections per tick, each paying the open + PRAGMA cost.
|
||||
/// </remarks>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task DrainOnceAsync(CancellationToken ct)
|
||||
{
|
||||
if (_disposed) return;
|
||||
@@ -391,8 +354,6 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable
|
||||
foreach (var rowId in corruptRowIds)
|
||||
DeadLetterRow(conn, corruptTx, rowId, $"corrupt payload at {_clock():O}");
|
||||
corruptTx.Commit();
|
||||
// Each corrupt row leaves the non-dead-lettered queue — bookkeeping for
|
||||
// the in-memory counter (Core.AlarmHistorian-008).
|
||||
Interlocked.Add(ref _queuedRowCount, -corruptRowIds.Count);
|
||||
_logger.Warning(
|
||||
"Dead-lettered {Count} historian queue row(s) with un-deserializable payload",
|
||||
@@ -413,9 +374,6 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Core.AlarmHistorian-012: reset _drainState so the status surface does
|
||||
// not stay stuck at Draining after the cancellation unwinds the method.
|
||||
// The row stays queued; the next drain tick will retry it.
|
||||
lock (_statusLock) { _drainState = HistorianDrainState.BackingOff; }
|
||||
throw;
|
||||
}
|
||||
@@ -432,13 +390,6 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
// Core.AlarmHistorian-007: a cardinality mismatch is a writer contract
|
||||
// violation — potentially the events were already persisted. Rather than
|
||||
// throwing (which, pre -006 fix, was swallowed and left _drainState
|
||||
// stale), treat it as a transient batch failure so the rows stay queued
|
||||
// and the backoff surface becomes visible to the operator. A deterministic
|
||||
// mismatch will stall the row until an operator intervenes or the writer
|
||||
// is fixed — far safer than re-throwing into a fire-and-forget timer.
|
||||
if (outcomes.Count != events.Count)
|
||||
{
|
||||
var msg = $"Writer returned {outcomes.Count} outcomes for {events.Count} events — expected 1:1; treating as batch retry";
|
||||
@@ -491,7 +442,7 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable
|
||||
}
|
||||
// Ack-deleted + PermanentFail-dead-lettered rows both leave the
|
||||
// non-dead-lettered queue, as do RetryPlease rows that hit the max-attempts
|
||||
// cap (finding 002) — keep the counter aligned (Core.AlarmHistorian-008).
|
||||
// cap (finding 002) — keep the counter aligned.
|
||||
if (rowsLeavingQueue > 0)
|
||||
Interlocked.Add(ref _queuedRowCount, -rowsLeavingQueue);
|
||||
|
||||
@@ -517,14 +468,10 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets the current status of the historian sink including queue depth and drain state.</summary>
|
||||
/// <inheritdoc />
|
||||
public HistorianSinkStatus GetStatus()
|
||||
{
|
||||
if (_disposed) throw new ObjectDisposedException(nameof(SqliteStoreAndForwardSink));
|
||||
// Core.AlarmHistorian-008: read the non-dead-lettered count from the in-memory
|
||||
// counter so a busy Admin UI / health probe does not hammer the DB. Dead-letter
|
||||
// depth is rare-path only (it lives in the queue until retention) so a real
|
||||
// COUNT(*) on a single combined connection is fine.
|
||||
var queued = Interlocked.Read(ref _queuedRowCount);
|
||||
if (queued < 0) queued = 0;
|
||||
|
||||
@@ -536,8 +483,6 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable
|
||||
deadlettered = (long)(cmd.ExecuteScalar() ?? 0L);
|
||||
}
|
||||
|
||||
// Core.AlarmHistorian-005: snapshot status fields atomically under the lock
|
||||
// so the Admin UI never sees a torn DateTime? or stale DrainState.
|
||||
DateTime? lastDrain, lastSuccess;
|
||||
string? lastError;
|
||||
HistorianDrainState drainState;
|
||||
@@ -562,6 +507,7 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Operator action from Admin UI — retry every dead-lettered row. Non-cascading: they rejoin the regular queue + get a fresh backoff.</summary>
|
||||
/// <returns>The number of rows revived from the dead-letter state.</returns>
|
||||
public int RetryDeadLettered()
|
||||
{
|
||||
if (_disposed) throw new ObjectDisposedException(nameof(SqliteStoreAndForwardSink));
|
||||
@@ -570,7 +516,7 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable
|
||||
cmd.CommandText = "UPDATE Queue SET DeadLettered = 0, AttemptCount = 0, LastError = NULL WHERE DeadLettered = 1";
|
||||
var revived = cmd.ExecuteNonQuery();
|
||||
// Dead-lettered rows rejoin the non-dead-lettered queue — keep the in-memory
|
||||
// counter aligned (Core.AlarmHistorian-008).
|
||||
// counter aligned.
|
||||
if (revived > 0) Interlocked.Add(ref _queuedRowCount, revived);
|
||||
return revived;
|
||||
}
|
||||
@@ -652,10 +598,7 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
// Async variant used by EnqueueAsync (Core.AlarmHistorian-003).
|
||||
// Core.AlarmHistorian-008: the precise path — runs COUNT(*) to compute the exact
|
||||
// number of rows to evict. Reached only from the fast-path fallback when the
|
||||
// in-memory counter says we are at or above capacity.
|
||||
// Async variant used by EnqueueAsync.
|
||||
private async Task EnforceCapacityAsync(SqliteConnection conn, CancellationToken ct)
|
||||
{
|
||||
Interlocked.Increment(ref _capacityProbeCount);
|
||||
|
||||
Reference in New Issue
Block a user