Skip to content

Historian: aggregate-filter revision, processed-read aggregate gate, initial-value priming and Historize() forcing Historizing=true #4400

Description

@romanett

Summary

The native historian (IHistorianProvider, HistorianDispatcher, UseHistorian(), Historize()) covers the history read/update services, but four pieces of Part 11 / Part 13 behaviour that an archive-backed server needs still have to be implemented as AsyncCustomNodeManager overrides, and one fluent helper actively fights the provider. The Workshop/HistoricalAccess sample in UA-.NETStandard-Samples already registers its provider through Server.UseHistorian().UseProvider(...).RegisterForNamespace(...), yet 8 of its 15 hand-written members remain because of these gaps. The historian subsystem is unchanged between 2.0.0-preview.2 and master, so nothing on master closes them.

The four gaps

1. ReviseAggregateFilterAsync ignores the resolved historian

AsyncCustomNodeManager.ReviseAggregateFilterAsync (~6883) clamps ProcessingInterval to Server.AggregateManager.MinimumProcessingInterval and fills AggregateConfiguration from the server defaults. It never consults the provider, even though IHistorianProvider.GetCapabilitiesAsync(nodeId) already returns HistorianNodeCapabilities.Stepped, MinTimeInterval, MaxTimeInterval and DefaultAggregateConfiguration (src/Opc.Ua.Server/Historian/HistorianNodeCapabilities.cs ~127-177). The sample overrides it to clamp to the archive item's sampling interval, align StartTime to the queue size and set Stepped from the item.

Proposal: when a provider resolves for the node, the base implementation should derive the revised filter from GetCapabilitiesAsync (or expose a fluent OnReviseAggregateFilter hook per node / per historian registration).

2. No aggregate-support gate in front of processed reads

HistorianDispatcher.DispatchProcessedReadAsync (~363) forwards every requested aggregate to IHistorianProcessedProvider; there is no check against Server.AggregateManager.IsSupported(aggregateId), and provider reads have no per-operation status channel to answer BadAggregateNotSupported themselves. The sample therefore overrides HistoryReadProcessedAsync (~4703) to filter nodesToProcess first. OnHistoryRead on the builder replaces the whole read for a node, so it cannot act as a pre-filter.

Proposal: the dispatcher should answer BadAggregateNotSupported for aggregates the server does not support before calling the provider, or provider reads should get a status channel per operation.

3. No initial-value priming from history for aggregate-filtered monitored items

For a data-change item with an AggregateFilter whose StartTime lies in the past, the correct initial delivery is the raw history from StartTime to now, not the current value. ReadInitialValue (~6561) reads the attribute, and it runs before OnMonitoredItemCreated (~6537 vs ~6550), so a fluent handler that queues history afterwards delivers values out of time order behind the already-queued current value. The sample overrides ReadInitialValue and calls into its provider's raw-window read.

Proposal: when the node resolves an IHistorianDataProvider and the item carries an AggregateFilter with a past StartTime, the base should pre-queue ReadRawAsync(StartTime, now) instead of the current value; alternatively expose an OnReadInitialValue hook that runs before the default.

4. Historize() forces Historizing = true

HistorianFluentExtensions.Historize(...) calls HistorianBuilder.Historize(..., setHistorizing: true) (HistorianFluentExtensions.cs ~303, HistorianBuilder.cs ~146-161), which sets variable.Historizing = true unconditionally. A server whose archive decides per item whether it is currently archiving (the sample's static "Sample" items report false, the "Dynamic" ones true, and the value changes when the archive configuration is reloaded) cannot use Historize() at all and has to fall back to namespace registration plus hand-set access bits. The sample tests assert both values.

Proposal: add a historizing: parameter (or a Func<bool> / leave the attribute untouched when the provider's IsHistorizingAsync is authoritative) so that Historize() sets the HistoryRead/HistoryWrite access bits and registers the node without overriding the Historizing attribute.

Optional, same area

  • A simulation/poll loop that runs only while a monitored item exists (tracked with the monitored-item lifecycle hooks) would remove the sample's remaining OnMonitoredItemCreated / OnMonitoredItemDeletedAsync timer management.
  • docs/HistoricalAccess.md should state which of these the fluent surface covers once they land; today the sample's overrides are the only documentation of the gaps.

Related: #4397 (on-demand nodes), #4398 (monitored-item lifecycle hooks).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugA bug was identified and should be fixed.

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions