Files
Copilot b3518b53df [Server]: Queue Events from the Server Node using multiple channel consumers (#3823)
# Description

This pull request introduces a scalable consumer task model for the
`MonitoredNode2` class, optimizing the handling of event notifications
for notifier nodes. Any notifier node can opt into (or out of) dynamic
scaling of consumer tasks based on the number of event monitored items,
improving throughput and responsiveness under high event loads. The
Server node (`ObjectIds.Server`) is always auto-opted-in. The
implementation includes safe scaling up and down of consumers, robust
disposal logic, Fluent API integration, and tests to verify correct
delivery of events.

**Scalable consumer task management:**

* Added an `enableMultipleEventConsumers` constructor parameter to
`MonitoredNode2` that enables dynamic scaling of consumer tasks to match
the number of event monitored items. The Server node
(`ObjectIds.Server`) always auto-opts-in regardless of this parameter.
* Added `AllowMultipleEventConsumers(NodeId, bool enable = true)`
protected method on `AsyncCustomNodeManager` with an internal
`MultiConsumerNodeIds` registry, so any node manager can
programmatically opt in or out specific nodes.
* Added `IsMultipleEventConsumerNode(NodeId)` to the `IAsyncNodeManager`
interface, allowing consumers to query multi-consumer opt-in status
through the interface without casting to `AsyncCustomNodeManager`.
* Both `MonitoredNodeMonitoredItemManager` and
`SamplingGroupMonitoredItemManager` use the `IAsyncNodeManager`
interface method when creating `MonitoredNode2` instances.

**Fluent API integration:**

* Added `AllowMultipleEventConsumers(bool enable = true)` method to
`INodeBuilder` and its `NodeBuilder` implementation, allowing any
notifier node to be opted in or out via the fluent builder pipeline
(e.g. `builder.Node("myNotifier").AllowMultipleEventConsumers()`).
* The `NodeManagerBuilder` routes the registration to the owning
`AsyncCustomNodeManager`'s multi-consumer registry, supporting both
opt-in and opt-out.

**Testing and verification:**

* Added unit tests to verify multi-consumer behavior for both Server
node (auto-opt-in) and non-Server nodes (explicit opt-in), as well as
default single-consumer behavior for nodes without opt-in.
* Fixed race condition in permission cache tests
(`OnMonitoredNodeChanged_DefaultPermissionsChanged_CacheInvalidated` and
`InvalidatePermissionCacheForSession` tests) that caused flaky CI
failures. The tests now signal on `QueueValue` (which fires after the
cache is populated) instead of `ValidateRolePermissionsAsync` (which
fires before), ensuring cache invalidation operates on a populated
cache.

## Related Issues

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [ ] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [x] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [x] I have addressed **all** PR feedback received.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-02 07:14:36 +02:00

1054 lines
44 KiB
C#

/* ========================================================================
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Opc.Ua.Server
{
/// <summary>
/// An interface to an object that creates a INodeManager object.
/// </summary>
public interface INodeManagerFactory
{
/// <summary>
/// The INodeManager factory.
/// </summary>
/// <param name="server">The server instance.</param>
/// <param name="configuration">The application configuration.</param>
INodeManager Create(IServerInternal server, ApplicationConfiguration configuration);
/// <summary>
/// The namespace table of the NodeManager.
/// </summary>
ArrayOf<string> NamespacesUris { get; }
}
/// <summary>
/// An interface to an object that manages a set of nodes in the address space.
/// </summary>
public interface INodeManager
{
/// <summary>
/// Returns the NamespaceUris for the Nodes belonging to the NodeManager.
/// </summary>
/// <remarks>
/// <para>By default the MasterNodeManager uses the namespaceIndex to determine who owns an Node.</para>
/// <para>
/// Servers that do not wish to partition their address space this way must provide their own
/// implementation of MasterNodeManager.GetManagerHandle().
/// </para>
/// <para>NodeManagers which depend on a custom partitioning scheme must return a null value.</para>
/// </remarks>
IEnumerable<string> NamespaceUris { get; }
/// <summary>
/// Creates the address space by loading any configuration information an connecting to an underlying system (if applicable).
/// </summary>
/// <returns>A table of references that need to be added to other node managers.</returns>
/// <remarks>
/// A node manager owns a set of nodes. These nodes may be known in advance or they may be stored in an
/// external system are retrived on demand. These nodes may have two way references to nodes that are owned
/// by other node managers. In these cases, the node managers only manage one half of those references. The
/// other half of the reference should be returned to the MasterNodeManager.
/// </remarks>
void CreateAddressSpace(IDictionary<NodeId, IList<IReference>> externalReferences);
/// <summary>
/// Deletes the address by releasing all resources and disconnecting from any underlying system.
/// </summary>
void DeleteAddressSpace();
/// <summary>
/// Returns an opaque handle identifying to the node to the node manager.
/// </summary>
/// <returns>A node handle, null if the node manager does not recognize the node id.</returns>
/// <remarks>
/// The method must not block by querying an underlying system. If the node manager wraps an
/// underlying system then it must check to see if it recognizes the syntax of the node id.
/// The handle in this case may simply be a partially parsed version of the node id.
/// </remarks>
object? GetManagerHandle(NodeId nodeId);
/// <summary>
/// Adds references to the node manager.
/// </summary>
/// <remarks>
/// The node manager checks the dictionary for nodes that it owns and ensures the associated references exist.
/// </remarks>
void AddReferences(IDictionary<NodeId, IList<IReference>> references);
/// <summary>
/// Deletes a reference.
/// </summary>
ServiceResult DeleteReference(
object sourceHandle,
NodeId referenceTypeId,
bool isInverse,
ExpandedNodeId targetId,
bool deleteBidirectional);
/// <summary>
/// Returns the metadata associated with the node.
/// </summary>
/// <remarks>
/// Returns null if the node does not exist.
/// </remarks>
NodeMetadata? GetNodeMetadata(
OperationContext context,
object targetHandle,
BrowseResultMask resultMask);
/// <summary>
/// Returns the set of references that meet the filter criteria.
/// </summary>
/// <param name="context">The context to used when processing the request.</param>
/// <param name="continuationPoint">The continuation point that stores the state of the Browse operation.</param>
/// <param name="references">The list of references that meet the filter criteria.</param>
/// <remarks>
/// NodeManagers will likely have references to other NodeManagers which means they will not be able
/// to apply the NodeClassMask or fill in the attributes for the target Node. In these cases the
/// NodeManager must return a ReferenceDescription with the NodeId and ReferenceTypeId set. The caller will
/// be responsible for filling in the target attributes.
/// The references parameter may already contain references when the method is called. The implementer must
/// include these references when calculating whether a continuation point must be returned.
/// </remarks>
/// <exception cref="ArgumentNullException">Thrown if the context, continuationPoint or references parameters are null.</exception>
/// <exception cref="ServiceResultException">Thrown if an error occurs during processing.</exception>
void Browse(
OperationContext context,
ref ContinuationPoint continuationPoint,
IList<ReferenceDescription> references);
/// <summary>
/// Finds the targets of the relative path from the source node.
/// </summary>
/// <param name="context">The context to used when processing the request.</param>
/// <param name="sourceHandle">The handle for the source node.</param>
/// <param name="relativePath">The relative path to follow.</param>
/// <param name="targetIds">The NodeIds for any target at the end of the relative path.</param>
/// <param name="unresolvedTargetIds">The NodeIds for any local target that is in another NodeManager.</param>
/// <remarks>
/// A null context indicates that the server's internal logic is making the call.
/// The first target in the list must be the target that matches the instance declaration (if applicable).
/// Any local targets that belong to other NodeManagers are returned as unresolvedTargetIds.
/// The caller must check the BrowseName to determine if it matches the relativePath.
/// The implementor must not throw an exception if the source or target nodes do not exist.
/// </remarks>
/// <exception cref="ArgumentNullException">Thrown if the sourceHandle, relativePath or targetIds parameters are null.</exception>
void TranslateBrowsePath(
OperationContext context,
object sourceHandle,
RelativePathElement relativePath,
IList<ExpandedNodeId> targetIds,
IList<NodeId> unresolvedTargetIds);
/// <summary>
/// Reads the attribute values for a set of nodes.
/// </summary>
/// <remarks>
/// <para>
/// The MasterNodeManager pre-processes the nodesToRead and ensures that:
/// - the AttributeId is a known attribute.
/// - the IndexRange, if specified, is valid.
/// - the DataEncoding and the IndexRange are not specified if the AttributeId is not Value.
/// </para>
/// <para>
/// The MasterNodeManager post-processes the values by:
/// - sets values[ii].StatusCode to the value of errors[ii].Code
/// - creates a instance of DataValue if one does not exist and an errors[ii] is bad.
/// - removes timestamps from the DataValue if the client does not want them.
/// </para>
/// <para>
/// The node manager must ignore ReadValueId with the Processed flag set to true.
/// The node manager must set the Processed flag for any ReadValueId that it processes.
/// </para>
/// </remarks>
void Read(
OperationContext context,
double maxAge,
ArrayOf<ReadValueId> nodesToRead,
IList<DataValue> values,
IList<ServiceResult> errors);
/// <summary>
/// Reads the history of a set of items.
/// </summary>
void HistoryRead(
OperationContext context,
HistoryReadDetails details,
TimestampsToReturn timestampsToReturn,
bool releaseContinuationPoints,
ArrayOf<HistoryReadValueId> nodesToRead,
IList<HistoryReadResult> results,
IList<ServiceResult> errors);
/// <summary>
/// Writes a set of values.
/// </summary>
/// <remarks>
/// Each node manager should only process node ids that it recognizes. If it processes a value it
/// must set the Processed flag in the WriteValue structure.
/// </remarks>
void Write(
OperationContext context,
ArrayOf<WriteValue> nodesToWrite,
IList<ServiceResult> errors);
/// <summary>
/// Updates the history for a set of nodes.
/// </summary>
void HistoryUpdate(
OperationContext context,
Type detailsType,
ArrayOf<HistoryUpdateDetails> nodesToUpdate,
IList<HistoryUpdateResult> results,
IList<ServiceResult> errors);
/// <summary>
/// Calls a method defined on an object.
/// </summary>
void Call(
OperationContext context,
ArrayOf<CallMethodRequest> methodsToCall,
IList<CallMethodResult> results,
IList<ServiceResult> errors);
/// <summary>
/// Tells the NodeManager to report events from the specified notifier.
/// </summary>
/// <remarks>
/// This method may be called multiple times for the name monitoredItemId if the
/// context for that MonitoredItem changes (i.e. UserIdentity and/or Locales).
/// </remarks>
ServiceResult SubscribeToEvents(
OperationContext context,
object sourceId,
uint subscriptionId,
IEventMonitoredItem monitoredItem,
bool unsubscribe);
/// <summary>
/// Tells the NodeManager to report events all events from all sources.
/// </summary>
/// <remarks>
/// This method may be called multiple times for the name monitoredItemId if the
/// context for that MonitoredItem changes (i.e. UserIdentity and/or Locales).
/// </remarks>
ServiceResult SubscribeToAllEvents(
OperationContext context,
uint subscriptionId,
IEventMonitoredItem monitoredItem,
bool unsubscribe);
/// <summary>
/// Tells the NodeManager to refresh any conditions.
/// </summary>
ServiceResult ConditionRefresh(
OperationContext context,
IList<IEventMonitoredItem> monitoredItems);
/// <summary>
/// Creates a set of monitored items.
/// </summary>
void CreateMonitoredItems(
OperationContext context,
uint subscriptionId,
double publishingInterval,
TimestampsToReturn timestampsToReturn,
ArrayOf<MonitoredItemCreateRequest> itemsToCreate,
IList<ServiceResult> errors,
IList<MonitoringFilterResult> filterErrors,
IList<IMonitoredItem> monitoredItems,
bool createDurable,
MonitoredItemIdFactory monitoredItemIdFactory);
/// <summary>
/// Restore a set of monitored items after a restart.
/// </summary>
void RestoreMonitoredItems(
IList<IStoredMonitoredItem> itemsToRestore,
IList<IMonitoredItem> monitoredItems,
IUserIdentity savedOwnerIdentity);
/// <summary>
/// Modifies a set of monitored items.
/// </summary>
void ModifyMonitoredItems(
OperationContext context,
TimestampsToReturn timestampsToReturn,
IList<IMonitoredItem> monitoredItems,
ArrayOf<MonitoredItemModifyRequest> itemsToModify,
IList<ServiceResult> errors,
IList<MonitoringFilterResult> filterErrors);
/// <summary>
/// Deletes a set of monitored items.
/// </summary>
void DeleteMonitoredItems(
OperationContext context,
IList<IMonitoredItem> monitoredItems,
IList<bool> processedItems,
IList<ServiceResult> errors);
/// <summary>
/// Transfers a set of monitored items.
/// </summary>
/// <remarks>
/// Queue initial values from monitored items in the node managers.
/// </remarks>
void TransferMonitoredItems(
OperationContext context,
bool sendInitialValues,
IList<IMonitoredItem> monitoredItems,
IList<bool> processedItems,
IList<ServiceResult> errors);
/// <summary>
/// Changes the monitoring mode for a set of monitored items.
/// </summary>
void SetMonitoringMode(
OperationContext context,
MonitoringMode monitoringMode,
IList<IMonitoredItem> monitoredItems,
IList<bool> processedItems,
IList<ServiceResult> errors);
}
/// <summary>
/// An interface to an object that manages a set of nodes in the address space.
/// </summary>
public interface INodeManager2 : INodeManager
{
/// <summary>
/// Called when the session is closed.
/// </summary>
void SessionClosing(OperationContext context, NodeId sessionId, bool deleteSubscriptions);
/// <summary>
/// Returns true if the node is in the view.
/// </summary>
bool IsNodeInView(OperationContext context, NodeId viewId, object nodeHandle);
/// <summary>
/// Returns the metadata needed for validating permissions, associated with the node with
/// the option to optimize services by using a cache.
/// </summary>
/// <remarks>
/// Returns null if the node does not exist.
/// It should return null in case the implementation wishes to handover the task to the parent INodeManager.GetNodeMetadata
/// </remarks>
NodeMetadata? GetPermissionMetadata(
OperationContext context,
object targetHandle,
BrowseResultMask resultMask,
Dictionary<NodeId, Variant[]> uniqueNodesServiceAttributesCache,
bool permissionsOnly);
}
/// <summary>
/// An interface to an object that manages a set of nodes in the address space.
/// </summary>
public interface INodeManager3 : INodeManager2
{
/// <summary>
/// Validates if the specified event monitored item has enough permissions to receive the specified event
/// </summary>
ServiceResult ValidateEventRolePermissions(
IEventMonitoredItem monitoredItem,
IFilterTarget filterTarget);
/// <summary>
/// Validates Role permissions for the specified NodeId
/// </summary>
ServiceResult ValidateRolePermissions(
OperationContext operationContext,
NodeId nodeId,
PermissionType requestedPermission);
/// <summary>
/// Called when a session is activated and the user identity has changed.
/// Implementations should invalidate any cached role permissions for monitored items
/// belonging to the session so that permissions are re-evaluated on the next data change.
/// </summary>
void SessionActivated(OperationContext context, NodeId sessionId);
/// <summary>
/// Resolves the effective <see cref="MethodState"/> for a call request.
/// </summary>
MethodState FindMethodState(
OperationContext context,
CallMethodRequest methodToCall);
}
/// <summary>
/// An asynchronous version of the "Call" method defined on the <see cref="INodeManager3"/> interface.
/// </summary>
public interface ICallAsyncNodeManager
{
/// <summary>
/// Asynchronously calls a method defined on an object.
/// </summary>
ValueTask CallAsync(
OperationContext context,
ArrayOf<CallMethodRequest> methodsToCall,
IList<CallMethodResult> results,
IList<ServiceResult> errors,
CancellationToken cancellationToken = default);
}
/// <summary>
/// An asynchronous version of the "Read" method defined on the <see cref="INodeManager3"/> interface.
/// </summary>
public interface IReadAsyncNodeManager
{
/// <summary>
/// Reads the attribute values for a set of nodes.
/// </summary>
/// <remarks>
/// <para>
/// The MasterNodeManager pre-processes the nodesToRead and ensures that:
/// - the AttributeId is a known attribute.
/// - the IndexRange, if specified, is valid.
/// - the DataEncoding and the IndexRange are not specified if the AttributeId is not Value.
/// </para>
/// <para>
/// The MasterNodeManager post-processes the values by:
/// - sets values[ii].StatusCode to the value of errors[ii].Code
/// - creates a instance of DataValue if one does not exist and an errors[ii] is bad.
/// - removes timestamps from the DataValue if the client does not want them.
/// </para>
/// <para>
/// The node manager must ignore ReadValueId with the Processed flag set to true.
/// The node manager must set the Processed flag for any ReadValueId that it processes.
/// </para>
/// </remarks>
ValueTask ReadAsync(
OperationContext context,
double maxAge,
ArrayOf<ReadValueId> nodesToRead,
IList<DataValue> values,
IList<ServiceResult> errors,
CancellationToken cancellationToken = default);
}
/// <summary>
/// An asynchronous version of the "Write" method defined on the <see cref="INodeManager3"/> interface.
/// </summary>
public interface IWriteAsyncNodeManager
{
/// <summary>
/// Writes a set of values.
/// </summary>
/// <remarks>
/// Each node manager should only process node ids that it recognizes. If it processes a value it
/// must set the Processed flag in the WriteValue structure.
/// </remarks>
ValueTask WriteAsync(
OperationContext context,
ArrayOf<WriteValue> nodesToWrite,
IList<ServiceResult> errors,
CancellationToken cancellationToken = default);
}
/// <summary>
/// An asynchronous version of the "HistoryRead" method defined on the <see cref="INodeManager3"/> interface.
/// </summary>
public interface IHistoryReadAsyncNodeManager
{
/// <summary>
/// Reads the history of a set of items.
/// </summary>
ValueTask HistoryReadAsync(
OperationContext context,
HistoryReadDetails details,
TimestampsToReturn timestampsToReturn,
bool releaseContinuationPoints,
ArrayOf<HistoryReadValueId> nodesToRead,
IList<HistoryReadResult> results,
IList<ServiceResult> errors,
CancellationToken cancellationToken = default);
}
/// <summary>
/// An asynchronous version of the "HistoryUpdate" method defined on the <see cref="INodeManager3"/> interface.
/// </summary>
public interface IHistoryUpdateAsyncNodeManager
{
/// <summary>
/// Updates the history for a set of nodes.
/// </summary>
ValueTask HistoryUpdateAsync(
OperationContext context,
Type detailsType,
ArrayOf<HistoryUpdateDetails> nodesToUpdate,
IList<HistoryUpdateResult> results,
IList<ServiceResult> errors,
CancellationToken cancellationToken = default);
}
/// <summary>
/// An asynchronous version of the "ConditionRefresh" method defined on the <see cref="INodeManager3"/> interface.
/// </summary>
public interface IConditionRefreshAsyncNodeManager
{
/// <summary>
/// Tells the NodeManager to refresh any conditions.
/// </summary>
ValueTask<ServiceResult> ConditionRefreshAsync(
OperationContext context,
IList<IEventMonitoredItem> monitoredItems,
CancellationToken cancellationToken = default);
}
/// <summary>
/// An asynchronous version of the "TranslateBrowsePath" method defined on the <see cref="INodeManager3"/> interface.
/// </summary>
public interface ITranslateBrowsePathAsyncNodeManager
{
/// <summary>
/// Finds the targets of the relative path from the source node.
/// </summary>
/// <param name="context">The context to used when processing the request.</param>
/// <param name="sourceHandle">The handle for the source node.</param>
/// <param name="relativePath">The relative path to follow.</param>
/// <param name="targetIds">The NodeIds for any target at the end of the relative path.</param>
/// <param name="unresolvedTargetIds">The NodeIds for any local target that is in another NodeManager.</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <remarks>
/// A null context indicates that the server's internal logic is making the call.
/// The first target in the list must be the target that matches the instance declaration (if applicable).
/// Any local targets that belong to other NodeManagers are returned as unresolvedTargetIds.
/// The caller must check the BrowseName to determine if it matches the relativePath.
/// The implementor must not throw an exception if the source or target nodes do not exist.
/// </remarks>
/// <exception cref="ArgumentNullException">Thrown if the sourceHandle, relativePath or targetIds parameters are null.</exception>
ValueTask TranslateBrowsePathAsync(
OperationContext context,
object sourceHandle,
RelativePathElement relativePath,
IList<ExpandedNodeId> targetIds,
IList<NodeId> unresolvedTargetIds,
CancellationToken cancellationToken = default);
}
/// <summary>
/// An asynchronous version of the "Browse" method defined on the <see cref="INodeManager3"/> interface.
/// </summary>
public interface IBrowseAsyncNodeManager
{
/// <summary>
/// Returns the set of references that meet the filter criteria.
/// </summary>
/// <param name="context">The context to used when processing the request.</param>
/// <param name="continuationPoint">The continuation point that stores the state of the Browse operation.</param>
/// <param name="references">The list of references that meet the filter criteria.</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <remarks>
/// NodeManagers will likely have references to other NodeManagers which means they will not be able
/// to apply the NodeClassMask or fill in the attributes for the target Node. In these cases the
/// NodeManager must return a ReferenceDescription with the NodeId and ReferenceTypeId set. The caller will
/// be responsible for filling in the target attributes.
/// The references parameter may already contain references when the method is called. The implementer must
/// include these references when calculating whether a continuation point must be returned.
/// </remarks>
/// <returns>The continuation point that stores the state of the Browse operation or null if there are no more references to return.</returns>
/// <exception cref="ArgumentNullException">Thrown if the context, continuationPoint or references parameters are null.</exception>
/// <exception cref="ServiceResultException">Thrown if an error occurs during processing.</exception>
ValueTask<ContinuationPoint?> BrowseAsync(
OperationContext context,
ContinuationPoint continuationPoint,
IList<ReferenceDescription> references,
CancellationToken cancellationToken = default);
}
/// <summary>
/// An asynchronous version of the "SetMonitoringMode" method defined on the <see cref="INodeManager3"/> interface.
/// </summary>
public interface ISetMonitoringModeAsyncNodeManager
{
/// <summary>
/// Changes the monitoring mode for a set of monitored items.
/// </summary>
ValueTask SetMonitoringModeAsync(
OperationContext context,
MonitoringMode monitoringMode,
IList<IMonitoredItem> monitoredItems,
IList<bool> processedItems,
IList<ServiceResult> errors,
CancellationToken cancellationToken = default);
}
/// <summary>
/// An asynchronous version of the "TransferMonitoredItems" method defined on the <see cref="INodeManager3"/> interface.
/// </summary>
public interface ITransferMonitoredItemsAsyncNodeManager
{
/// <summary>
/// Transfers a set of monitored items.
/// </summary>
/// <remarks>
/// Queue initial values from monitored items in the node managers.
/// </remarks>
ValueTask TransferMonitoredItemsAsync(
OperationContext context,
bool sendInitialValues,
IList<IMonitoredItem> monitoredItems,
IList<bool> processedItems,
IList<ServiceResult> errors,
CancellationToken cancellationToken = default);
}
/// <summary>
/// An asynchronous version of the "DeleteMonitoredItems" method defined on the <see cref="INodeManager3"/> interface.
/// </summary>
public interface IDeleteMonitoredItemsAsyncNodeManager
{
/// <summary>
/// Deletes a set of monitored items.
/// </summary>
ValueTask DeleteMonitoredItemsAsync(
OperationContext context,
IList<IMonitoredItem> monitoredItems,
IList<bool> processedItems,
IList<ServiceResult> errors,
CancellationToken cancellationToken = default);
}
/// <summary>
/// An asynchronous version of the "ModifyMonitoredItems" method defined on the <see cref="INodeManager3"/> interface.
/// </summary>
public interface IModifyMonitoredItemsAsyncNodeManager
{
/// <summary>
/// Modifies a set of monitored items.
/// </summary>
ValueTask ModifyMonitoredItemsAsync(
OperationContext context,
TimestampsToReturn timestampsToReturn,
IList<IMonitoredItem> monitoredItems,
ArrayOf<MonitoredItemModifyRequest> itemsToModify,
IList<ServiceResult> errors,
IList<MonitoringFilterResult> filterErrors,
CancellationToken cancellationToken = default);
}
/// <summary>
/// An asynchronous version of the "CreateMonitoredItems" method defined on the <see cref="INodeManager3"/> interface.
/// </summary>
public interface ICreateMonitoredItemsAsyncNodeManager
{
/// <summary>
/// Creates a set of monitored items.
/// </summary>
ValueTask CreateMonitoredItemsAsync(
OperationContext context,
uint subscriptionId,
double publishingInterval,
TimestampsToReturn timestampsToReturn,
ArrayOf<MonitoredItemCreateRequest> itemsToCreate,
IList<ServiceResult> errors,
IList<MonitoringFilterResult> filterErrors,
IList<IMonitoredItem> monitoredItems,
bool createDurable,
MonitoredItemIdFactory monitoredItemIdFactory,
CancellationToken cancellationToken = default);
}
/// <summary>
/// An interface to an object that creates a IAsyncNodeManager object.
/// </summary>
public interface IAsyncNodeManagerFactory
{
/// <summary>
/// The IAsyncNodeManager factory.
/// </summary>
/// <param name="server">The server instance.</param>
/// <param name="configuration">The application configuration.</param>
/// <param name="cancellationToken">The cancellation token</param>
ValueTask<IAsyncNodeManager> CreateAsync(
IServerInternal server,
ApplicationConfiguration configuration,
CancellationToken cancellationToken = default);
/// <summary>
/// The namespace table of the NodeManager.
/// </summary>
ArrayOf<string> NamespacesUris { get; }
}
/// <summary>
/// An asynchronous verison of the <see cref="INodeManager3"/> interface.
/// This interface is in active development and will be extended in future releases.
/// Please use the sub interfaces to implement async support for specific service calls.
/// </summary>
public interface IAsyncNodeManager :
ICallAsyncNodeManager,
IReadAsyncNodeManager,
IWriteAsyncNodeManager,
IHistoryReadAsyncNodeManager,
IHistoryUpdateAsyncNodeManager,
IConditionRefreshAsyncNodeManager,
ITranslateBrowsePathAsyncNodeManager,
IBrowseAsyncNodeManager,
ISetMonitoringModeAsyncNodeManager,
ITransferMonitoredItemsAsyncNodeManager,
IDeleteMonitoredItemsAsyncNodeManager,
IModifyMonitoredItemsAsyncNodeManager,
ICreateMonitoredItemsAsyncNodeManager
{
/// <summary>
/// Resolves the effective <see cref="MethodState"/> for a call request.
/// </summary>
ValueTask<MethodState> FindMethodStateAsync(
OperationContext context,
CallMethodRequest methodToCall,
CancellationToken cancellationToken = default);
/// <summary>
/// Returns the NamespaceUris for the Nodes belonging to the NodeManager.
/// </summary>
/// <remarks>
/// <para>By default the MasterNodeManager uses the namespaceIndex to determine who owns an Node.</para>
/// <para>
/// Servers that do not wish to partition their address space this way must provide their own
/// implementation of MasterNodeManager.GetManagerHandle().
/// </para>
/// <para>NodeManagers which depend on a custom partitioning scheme must return a null value.</para>
/// </remarks>
IEnumerable<string> NamespaceUris { get; }
/// <summary>
/// Shall never be null.
/// Allows access to synchronous methods for compatibility
/// If the NodeManager does not support synchronous calls natively use <see cref="SyncNodeManagerAdapter"/>
/// to wrap the async calls.
/// </summary>
INodeManager SyncNodeManager { get; }
/// <summary>
/// Creates the address space by loading any configuration information an connecting to an underlying system (if applicable).
/// </summary>
/// <returns>A table of references that need to be added to other node managers.</returns>
/// <remarks>
/// A node manager owns a set of nodes. These nodes may be known in advance or they may be stored in an
/// external system are retrived on demand. These nodes may have two way references to nodes that are owned
/// by other node managers. In these cases, the node managers only manage one half of those references. The
/// other half of the reference should be returned to the MasterNodeManager.
/// </remarks>
ValueTask CreateAddressSpaceAsync(
IDictionary<NodeId, IList<IReference>> externalReferences,
CancellationToken cancellationToken = default);
/// <summary>
/// Returns the metadata associated with the node.
/// </summary>
/// <remarks>
/// Returns null if the node does not exist.
/// </remarks>
ValueTask<NodeMetadata> GetNodeMetadataAsync(
OperationContext context,
object targetHandle,
BrowseResultMask resultMask,
CancellationToken cancellationToken = default);
/// <summary>
/// Deletes the address by releasing all resources and disconnecting from any underlying system.
/// </summary>
ValueTask DeleteAddressSpaceAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Returns an opaque handle identifying to the node to the node manager.
/// </summary>
/// <returns>A node handle, null if the node manager does not recognize the node id.</returns>
/// <remarks>
/// The method must not block by querying an underlying system. If the node manager wraps an
/// underlying system then it must check to see if it recognizes the syntax of the node id.
/// The handle in this case may simply be a partially parsed version of the node id.
/// </remarks>
ValueTask<object> GetManagerHandleAsync(NodeId nodeId, CancellationToken cancellationToken = default);
/// <summary>
/// Adds references to the node manager.
/// </summary>
/// <remarks>
/// The node manager checks the dictionary for nodes that it owns and ensures the associated references exist.
/// </remarks>
ValueTask AddReferencesAsync(
IDictionary<NodeId, IList<IReference>> references,
CancellationToken cancellationToken = default);
/// <summary>
/// Deletes a reference.
/// </summary>
ValueTask<ServiceResult> DeleteReferenceAsync(
object sourceHandle,
NodeId referenceTypeId,
bool isInverse,
ExpandedNodeId targetId,
bool deleteBidirectional,
CancellationToken cancellationToken = default);
/// <summary>
/// Called when the session is closed.
/// </summary>
ValueTask SessionClosingAsync(
OperationContext context,
NodeId sessionId,
bool deleteSubscriptions,
CancellationToken cancellationToken = default);
/// <summary>
/// Called when a session is activated and the user identity has changed.
/// Implementations should invalidate any cached role permissions for monitored items
/// belonging to the session so that permissions are re-evaluated on the next data change.
/// </summary>
ValueTask SessionActivatedAsync(
OperationContext context,
NodeId sessionId,
CancellationToken cancellationToken = default);
/// <summary>
/// Returns true if the node is in the view.
/// </summary>
ValueTask<bool> IsNodeInViewAsync(
OperationContext context,
NodeId viewId,
object nodeHandle,
CancellationToken cancellationToken = default);
/// <summary>
/// Returns the metadata needed for validating permissions, associated with the node with
/// the option to optimize services by using a cache.
/// </summary>
/// <remarks>
/// Returns null if the node does not exist.
/// It should return null in case the implementation wishes to handover the task to the parent INodeManager.GetNodeMetadata
/// </remarks>
ValueTask<NodeMetadata?> GetPermissionMetadataAsync(
OperationContext context,
object targetHandle,
BrowseResultMask resultMask,
Dictionary<NodeId, Variant[]> uniqueNodesServiceAttributesCache,
bool permissionsOnly,
CancellationToken cancellationToken = default);
/// <summary>
/// Tells the NodeManager to report events from the specified notifier.
/// </summary>
/// <remarks>
/// This method may be called multiple times for the name monitoredItemId if the
/// context for that MonitoredItem changes (i.e. UserIdentity and/or Locales).
/// </remarks>
ValueTask<ServiceResult> SubscribeToEventsAsync(
OperationContext context,
object sourceId,
uint subscriptionId,
IEventMonitoredItem monitoredItem,
bool unsubscribe,
CancellationToken cancellationToken = default);
/// <summary>
/// Tells the NodeManager to report events all events from all sources.
/// </summary>
/// <remarks>
/// This method may be called multiple times for the name monitoredItemId if the
/// context for that MonitoredItem changes (i.e. UserIdentity and/or Locales).
/// </remarks>
ValueTask<ServiceResult> SubscribeToAllEventsAsync(
OperationContext context,
uint subscriptionId,
IEventMonitoredItem monitoredItem,
bool unsubscribe,
CancellationToken cancellationToken = default);
/// <summary>
/// Restore a set of monitored items after a restart.
/// </summary>
ValueTask RestoreMonitoredItemsAsync(
IList<IStoredMonitoredItem> itemsToRestore,
IList<IMonitoredItem> monitoredItems,
IUserIdentity savedOwnerIdentity,
CancellationToken cancellationToken = default);
/// <summary>
/// Validates if the specified event monitored item has enough permissions to receive the specified event
/// </summary>
ValueTask<ServiceResult> ValidateEventRolePermissionsAsync(
IEventMonitoredItem monitoredItem,
IFilterTarget filterTarget,
CancellationToken cancellationToken = default);
/// <summary>
/// Validates Role permissions for the specified NodeId
/// </summary>
ValueTask<ServiceResult> ValidateRolePermissionsAsync(
OperationContext operationContext,
NodeId nodeId,
PermissionType requestedPermission,
CancellationToken cancellationToken = default);
/// <summary>
/// Returns <c>true</c> if the specified node has opted into
/// multiple event consumer task handling.
/// </summary>
/// <param name="nodeId">The <see cref="NodeId"/> to check.</param>
bool IsMultipleEventConsumerNode(NodeId nodeId);
}
/// <summary>
/// Stores metadata required to process requests related to a node.
/// </summary>
public class NodeMetadata
{
/// <summary>
/// Initializes the object with its handle and NodeId.
/// </summary>
public NodeMetadata(object handle, NodeId nodeId)
{
Handle = handle;
NodeId = nodeId;
}
/// <summary>
/// The handle assigned by the NodeManager that owns the Node.
/// </summary>
public object Handle { get; }
/// <summary>
/// The canonical NodeId for the Node.
/// </summary>
public NodeId NodeId { get; }
/// <summary>
/// The NodeClass for the Node.
/// </summary>
public NodeClass NodeClass { get; set; }
/// <summary>
/// The BrowseName for the Node.
/// </summary>
public QualifiedName BrowseName { get; set; }
/// <summary>
/// The DisplayName for the Node.
/// </summary>
public LocalizedText DisplayName { get; set; }
/// <summary>
/// The type definition for the Node (if one exists).
/// </summary>
public ExpandedNodeId TypeDefinition { get; set; }
/// <summary>
/// The modelling for the Node (if one exists).
/// </summary>
public NodeId ModellingRule { get; set; }
/// <summary>
/// Specifies which attributes are writeable.
/// </summary>
public AttributeWriteMask WriteMask { get; set; }
/// <summary>
/// Whether the Node can be used with event subscriptions or for historial event queries.
/// </summary>
public byte EventNotifier { get; set; }
/// <summary>
/// Whether the Node can be use to read or write current or historical values.
/// </summary>
public byte AccessLevel { get; set; }
/// <summary>
/// Whether the Node is a Method that can be executed.
/// </summary>
public bool Executable { get; set; }
/// <summary>
/// The DataType of the Value attribute for Variable or VariableType nodes.
/// </summary>
public NodeId DataType { get; set; }
/// <summary>
/// The ValueRank for the Value attribute for Variable or VariableType nodes.
/// </summary>
public int ValueRank { get; set; }
/// <summary>
/// The ArrayDimensions for the Value attribute for Variable or VariableType nodes.
/// </summary>
public ArrayOf<uint> ArrayDimensions { get; set; }
/// <summary>
/// Specifies the AccessRestrictions that apply to a Node.
/// </summary>
public AccessRestrictionType AccessRestrictions { get; set; }
/// <summary>
/// The value reflects the DefaultAccessRestrictions Property of the NamespaceMetadata Object for the Namespace
/// to which the Node belongs.
/// </summary>
public AccessRestrictionType DefaultAccessRestrictions { get; set; }
/// <summary>
/// The RolePermissions for the Node.
/// Specifies the Permissions that apply to a Node for all Roles which have access to the Node.
/// </summary>
public ArrayOf<RolePermissionType> RolePermissions { get; set; }
/// <summary>
/// The DefaultRolePermissions of the Node's name-space meta-data
/// The value reflects the DefaultRolePermissions Property from the NamespaceMetadata Object associated with the Node.
/// </summary>
public ArrayOf<RolePermissionType> DefaultRolePermissions { get; set; }
/// <summary>
/// The UserRolePermissions of the Node.
/// Specifies the Permissions that apply to a Node for all Roles granted to current Session.
/// </summary>
public ArrayOf<RolePermissionType> UserRolePermissions { get; set; }
/// <summary>
/// The DefaultUserRolePermissions of the Node.
/// The value reflects the DefaultUserRolePermissions Property from the NamespaceMetadata Object associated with the Node.
/// </summary>
public ArrayOf<RolePermissionType> DefaultUserRolePermissions { get; set; }
/// <summary>
/// Indicates this node is part of a type hierarchy (ObjectType/VariableType
/// or a child thereof). Type hierarchy nodes bypass access restriction and
/// role permission validation.
/// </summary>
public bool IsPartOfTypeHierarchy { get; set; }
}
}