fix(core-abstractions): resolve Low code-review findings (Core.Abstractions-004,005,006,007,008)

- Core.Abstractions-004: guard DriverTypeRegistry.Register with a Lock so
  concurrent registrations are atomic.
- Core.Abstractions-005: narrow PollGroupEngine catch blocks to non-fatal
  exceptions, add optional onError callback, tolerate disposed-CTS races.
- Core.Abstractions-006: document the deliberate int-vs-uint asymmetry on
  IHistoryProvider.ReadEventsAsync / IHistorianDataSource.ReadEventsAsync.
- Core.Abstractions-007: pin the gaps with PollGroupEngine + DriverHealth
  contract tests.
- Core.Abstractions-008: correct XML docs on DriverHealth.LastError and
  the optional / required asymmetry on the history-read surfaces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-23 05:37:54 -04:00
parent a02c0ffe36
commit ff2e75ab98
10 changed files with 422 additions and 33 deletions
@@ -6,6 +6,14 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
/// Galaxy (Wonderware Historian via the optional plugin), OPC UA Client (forward
/// to upstream server).
/// </summary>
/// <remarks>
/// <see cref="ReadAtTimeAsync"/> and <see cref="ReadEventsAsync"/> are C# default interface
/// methods that throw <see cref="NotSupportedException"/> — drivers opt in by overriding so
/// a raw-only driver compiles without forcing it to provide at-time / event surfaces it
/// has no backend for. The sibling server-side surface, <see cref="IHistorianDataSource"/>,
/// declares both methods as required because a registered historian owns the full read
/// surface; the asymmetry is intentional (Core.Abstractions-008).
/// </remarks>
public interface IHistoryProvider
{
/// <summary>
@@ -60,12 +68,24 @@ public interface IHistoryProvider
/// </param>
/// <param name="startUtc">Inclusive lower bound on <c>EventTimeUtc</c>.</param>
/// <param name="endUtc">Exclusive upper bound on <c>EventTimeUtc</c>.</param>
/// <param name="maxEvents">Upper cap on returned events — the driver's backend enforces this.</param>
/// <param name="maxEvents">
/// Upper cap on returned events — the driver's backend enforces this. The type is
/// <see cref="int"/> rather than <see cref="uint"/> (which the sibling raw / processed
/// reads use for <c>maxValuesPerNode</c>) because callers and downstream historian
/// adapters historically treat <c>maxEvents &lt;= 0</c> as a sentinel meaning
/// "use the backend's default cap" (see <c>WonderwareHistorianClient</c> /
/// <c>HistorianDataSource</c>). The asymmetry is intentional — Core.Abstractions-006.
/// </param>
/// <param name="cancellationToken">Request cancellation.</param>
/// <remarks>
/// Default implementation throws. Only drivers with an event historian (Galaxy via the
/// Wonderware Alarm &amp; Events log) override. Modbus / the OPC UA Client driver stay
/// with the default and let callers see <c>BadHistoryOperationUnsupported</c>.
///
/// Note the type asymmetry with <see cref="ReadRawAsync"/> /
/// <see cref="ReadProcessedAsync"/> (both use <c>uint maxValuesPerNode</c>): event
/// readers accept a signed <c>int maxEvents</c> so callers can pass 0 / negative as a
/// "use default cap" sentinel without an extra parameter or overload.
/// </remarks>
Task<HistoricalEventsResult> ReadEventsAsync(
string? sourceName,