fix(history): advertise HistoryServerCapabilities from the wired backend (#881)
v2-ci / unit-tests (push) Has been cancelled
v2-ci / unit-tests (push) Has been cancelled
Every Boolean under Server/ServerCapabilities/HistoryServerCapabilities read false (the SDK default) even though data and event history were served, so a spec-following client skipped HistoryRead entirely. Each backend now declares what it serves through its own interface: - IHistorianDataSource.ReadCapabilities (Data / Events / ServerTimestamps): Gateway Data|Events; SqlServer + QuestDb add ServerTimestamps (they store ServerTimeUtc, the Gateway stamps read time); Null declares None. A reflection guard requires every production source to declare it itself. - IHistorianDataUpdater.Capabilities (required member): SqlServer and QuestDb declare Insert/Replace/Update/DeleteRaw/DeleteAtTime; the Gateway provider has no updater. The node manager writes the flags on every source/updater assignment (AdvertisedHistoryCapabilities), and the same declarations drive dispatch: an update the updater does not declare is BadHistoryOperationUnsupported without reaching it, the HistoryWrite AccessLevel bit follows a non-empty declaration, and event-history notifier promotion follows the Events flag. Event and annotation update flags stay false (nothing dispatches them). Tests: booted-server wire test per backend + Null + a SqlServer->Gateway rewire pairing on one session; dispatch gating unit test; the #801 live gate now asserts the Gateway flags (not run here). Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// What a historian backend serves for OPC UA HistoryRead — declared by every <see cref="IHistorianDataSource"/>
|
||||
/// (<see cref="IHistorianDataSource.ReadCapabilities"/>) and advertised by the server under
|
||||
/// <c>Server/ServerCapabilities/HistoryServerCapabilities</c> (lmxopcua#881).
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum HistorianReadCapabilities
|
||||
{
|
||||
/// <summary>No history is served (the Null source).</summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>Data history (raw / modified / processed / at-time) — <c>AccessHistoryDataCapability</c>.</summary>
|
||||
Data = 1,
|
||||
|
||||
/// <summary>Event history on notifier folders — <c>AccessHistoryEventsCapability</c>.</summary>
|
||||
Events = 2,
|
||||
|
||||
/// <summary>
|
||||
/// The backend stores each sample's server timestamp and returns it on a raw read, rather than stamping the read
|
||||
/// time — <c>ServerTimestampSupported</c> (Part 11 §5.4.2).
|
||||
/// </summary>
|
||||
ServerTimestamps = 4,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Which HistoryUpdate operations a <see cref="IHistorianDataUpdater"/> performs — declared by the updater itself
|
||||
/// (<see cref="IHistorianDataUpdater.Capabilities"/>). The server both advertises these under
|
||||
/// <c>HistoryServerCapabilities</c> and refuses an undeclared operation with <c>BadHistoryOperationUnsupported</c>,
|
||||
/// so the advertisement cannot drift from dispatch (lmxopcua#881). There are no event / annotation members: the
|
||||
/// server dispatches no event or annotation updates to any backend.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum HistoryUpdateCapabilities
|
||||
{
|
||||
/// <summary>No update operation.</summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary><c>UpdateDataDetails</c> with <c>PerformInsertReplace = Insert</c> — <c>InsertDataCapability</c>.</summary>
|
||||
InsertData = 1,
|
||||
|
||||
/// <summary><c>UpdateDataDetails</c> with <c>PerformInsertReplace = Replace</c> — <c>ReplaceDataCapability</c>.</summary>
|
||||
ReplaceData = 2,
|
||||
|
||||
/// <summary><c>UpdateDataDetails</c> with <c>PerformInsertReplace = Update</c> — <c>UpdateDataCapability</c>.</summary>
|
||||
UpdateData = 4,
|
||||
|
||||
/// <summary><c>DeleteRawModifiedDetails</c> with <c>IsDeleteModified = false</c> — <c>DeleteRawCapability</c>.</summary>
|
||||
DeleteRaw = 8,
|
||||
|
||||
/// <summary><c>DeleteAtTimeDetails</c> — <c>DeleteAtTimeCapability</c>.</summary>
|
||||
DeleteAtTime = 16,
|
||||
|
||||
/// <summary>Every data update the server can dispatch.</summary>
|
||||
AllData = InsertData | ReplaceData | UpdateData | DeleteRaw | DeleteAtTime,
|
||||
}
|
||||
@@ -246,6 +246,19 @@ public interface IHistorianDataSource : IDisposable
|
||||
/// </remarks>
|
||||
IReadOnlySet<HistoryAggregateType> SupportedAggregates => HistoryAggregateTypes.All;
|
||||
|
||||
/// <summary>
|
||||
/// What this backend serves for HistoryRead — the authority for the <c>AccessHistoryDataCapability</c>,
|
||||
/// <c>AccessHistoryEventsCapability</c> and <c>ServerTimestampSupported</c> flags the server advertises under
|
||||
/// <c>HistoryServerCapabilities</c>, and (<see cref="HistorianReadCapabilities.Events"/>) for whether notifier folders
|
||||
/// carry the <c>HistoryRead</c> EventNotifier bit and accept HistoryRead-Events (lmxopcua#881).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The default (<see cref="HistorianReadCapabilities.Data"/> | <see cref="HistorianReadCapabilities.Events"/>) keeps a
|
||||
/// test fake or out-of-tree source serving as before. Every PRODUCTION backend declares it itself (a reflection
|
||||
/// guard pins that); the Null source declares <see cref="HistorianReadCapabilities.None"/>.
|
||||
/// </remarks>
|
||||
HistorianReadCapabilities ReadCapabilities => HistorianReadCapabilities.Data | HistorianReadCapabilities.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Read modified-value history — OPC UA HistoryReadRaw with <c>IsReadModified = true</c> (Part 11):
|
||||
/// every modification a HistoryUpdate applied to the tag inside the window, each carrying the PRIOR value.
|
||||
|
||||
@@ -32,6 +32,15 @@ public enum HistoryUpdateMode
|
||||
/// </remarks>
|
||||
public interface IHistorianDataUpdater
|
||||
{
|
||||
/// <summary>
|
||||
/// The update operations this backend performs — declared by the backend itself (no default, so it cannot be
|
||||
/// forgotten). The server advertises exactly these under <c>HistoryServerCapabilities</c>
|
||||
/// (Insert/Replace/UpdateData, DeleteRaw, DeleteAtTime) and answers an operation outside the set with
|
||||
/// <c>BadHistoryOperationUnsupported</c> without calling the backend (lmxopcua#881). Read when the updater is
|
||||
/// wired and on every update, so it must be cheap and stable.
|
||||
/// </summary>
|
||||
HistoryUpdateCapabilities Capabilities { get; }
|
||||
|
||||
/// <summary>Inserts, replaces or upserts samples for one tag.</summary>
|
||||
/// <param name="tag">Historian tag name.</param>
|
||||
/// <param name="mode">Update mode.</param>
|
||||
|
||||
@@ -87,6 +87,11 @@ public sealed class NullHistorianDataSource : IHistorianDataSource
|
||||
// historian misled capability discovery. Raw / AtTime / Events reads still return the empty GoodNoData result.
|
||||
public IReadOnlySet<HistoryAggregateType> SupportedAggregates => HistoryAggregateTypes.None;
|
||||
|
||||
/// <inheritdoc />
|
||||
// None: no history is served, so HistoryServerCapabilities advertises no data / event access (lmxopcua#881) and no
|
||||
// folder is promoted to an event-history notifier.
|
||||
public HistorianReadCapabilities ReadCapabilities => HistorianReadCapabilities.None;
|
||||
|
||||
/// <inheritdoc />
|
||||
public HistorianHealthSnapshot GetHealthSnapshot() => new(
|
||||
TotalQueries: 0,
|
||||
|
||||
Reference in New Issue
Block a user