mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-20 00:31:32 +08:00
c4e0721a29
## Summary
### Documentation
- Stage the documentation index as a learning path: a first application,
independent client, server, and PubSub paths, then data and services,
companion models, production, advanced integration, and draft or
experimental capabilities.
- Add Getting started (repository samples or NuGet packages), OPC UA
concepts, Your first information model, and a production readiness
checklist.
- Add Native AOT application publishing, a first PubSub publisher and
subscriber exchange, and the UDP transport options.
- Add or expand contents navigation throughout `docs/`, and keep
contributor documentation separate.
- Correct outdated SDK examples and capability guidance, consolidate
repeated explanations into canonical guides, and repair local links and
heading structure.
- Rewrite What's New in 2.0 as a concise feature overview with short
linked details; keep migration instructions in the Migration Guide.
- Expand the feature catalogue with specification IDs, GDS, LDS, the OPC
10000-21 onboarding registrar, ISA-95, xRegistry, OpenUSD, and
sample-local models; distinguish draft status, explain crypto
pluggability, and bring performance guidance forward.
- Address review feedback on wording, active monitored items during
shadow reload, and the `Samples.md` filename and references.
- Regenerate the `opcua-v20-migration` plugin's bundled copy of the
migration guides so it matches the edited `docs/migrate/2.0.x` guides.
- Merge the latest `master`, preserving upstream resource-isolation
guidance within the task-based documentation index.
- Retain deferred Lingity findings where appropriate; this PR does not
claim that every document meets the Lingity score thresholds.
### Fixes found while verifying the guides
Each fix has regression tests that fail without it.
- **Client:** V2 subscriptions compared their lifetime with the
operation timeout instead of the session timeout, and logged a spurious
warning. `ISubscriptionEngineContext`, which is new in 2.0, gains a
`SessionTimeout` property; implementations must add it.
- **PubSub:** a subscriber now registers the `DataSetMetaData`
configured on its readers (OPC 10000-14 6.2.9.4), and the UADP decoder
decodes DataSetMessages that carry no ConfigurationVersion. Before, a
secured subscriber of the reference sample received no DataSets.
- **Source generator:** a ModelDesign method that declares its arguments
inline, without a method type, produced code that did not compile; it
now uses `MethodState`. The generated code of existing models is
unchanged.
- **MinimalClient sample:** waits for its first data-change notification
before it disconnects.
### Known issues, not addressed here
- UDP multicast: the publisher does not send on the interface that the
subscriber joins, and `MulticastLoopback` defaults to false. The PubSub
guide uses unicast for the exchange on one computer.
- The UDP metadata announcement is sent unsecured on secured writer
groups; a secured subscriber drops it with a warning.
- A server does not start in globalization-invariant mode, because
`ResourceManager` creates `CultureInfo("en-US")`. The Native AOT guide
cautions against the mode.
## Validation
- `git diff --check` and
`./.azurepipelines/validate-migration-plugin.ps1` (15 bundled migration
docs validated, plugin manifests synchronized).
- Local Markdown links, anchors, file-link casing, and contents lists
checked across all 87 documents under `docs/`. The index links every
top-level guide and every target of the previous index.
- Both Getting started routes, Your first information model, the PubSub
exchange, and the Native AOT publish of the Getting started client and
server were run as documented. The NuGet route and the tutorial use the
public `2.0.0-preview.5` packages.
- The changed libraries build for all target frameworks in Release
without warnings.
- Tests on .NET 10: Opc.Ua.SourceGeneration.Tests 199 passed;
Opc.Ua.SourceGeneration.Core.Tests 3,977 passed, 8 skipped;
Opc.Ua.Server.Tests `Fluent` category 621 passed; Opc.Ua.Client.Tests
subscription-engine tests 70 passed; Opc.Ua.PubSub.Tests 1,374 passed, 2
skipped; PubSub fuzz replay 145 passed. The new and affected tests also
pass on .NET Framework 4.8.
- Earlier merge validation: 174 resource-isolation tests passed on .NET
10.
- The full solution suite was not run locally.
Review and validation artifacts are maintained outside the repository.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
257 lines
13 KiB
Markdown
257 lines
13 KiB
Markdown
# Support of the TAP (Task Asynchronous Pattern) for server operations
|
|
|
|
The OPC UA .NET Standard stack has long supported asynchronous server
|
|
operations through the APM (Asynchronous Programming Model). APM uses
|
|
`IAsyncResult` and the Begin/End pattern. .NET Framework 1.0 introduced
|
|
this pattern, which remains supported in .NET 8.0.
|
|
|
|
The stack also supports the TAP (Task Asynchronous Pattern) for server
|
|
operations. TAP uses `Task` and the `async`/`await` keywords introduced
|
|
in .NET Framework 4.0. It is the recommended way to implement asynchronous
|
|
operations in modern .NET applications.
|
|
|
|
Server applications can implement node managers with TAP. This pattern
|
|
improves scalability by using server resources more efficiently.
|
|
|
|
The server library provides these TAP features:
|
|
|
|
- Task-based `RequestQueue` and `TransportListenerCallback`
|
|
- Generated code and `MasterNodeManager` support Task-based operations.
|
|
- The `IAsyncNodeManager` interface supports fully asynchronous node managers.
|
|
- `AsyncNodeManagerAdapter` and `SyncNodeManagerAdapter` let synchronous
|
|
and asynchronous node managers run side by side.
|
|
|
|
## Contents
|
|
|
|
- [Upgrading an existing server](#upgrading-an-existing-server)
|
|
- [Async method calls](#async-method-calls)
|
|
- [AsyncCustomNodeManager](#asynccustomnodemanager)
|
|
- [Registering an AsyncCustomNodeManager](#registering-an-asynccustomnodemanager)
|
|
- [Async browse iteration](#async-browse-iteration)
|
|
- [Locking strategy vs CustomNodeManager2](#locking-strategy-vs-customnodemanager2)
|
|
- [1. Global write semaphore — `m_writeSemaphore`](#1-global-write-semaphore--m_writesemaphore)
|
|
- [2. Monitored-item semaphore — `m_monitoredItemSemaphore`](#2-monitored-item-semaphore--m_monitoreditemsemaphore)
|
|
- [3. Per-node locking for read and attribute access](#3-per-node-locking-for-read-and-attribute-access)
|
|
- [Monitored-item manager selection](#monitored-item-manager-selection)
|
|
- [Fully asynchronous change notifications](#fully-asynchronous-change-notifications)
|
|
- [Creating a custom node manager](#creating-a-custom-node-manager)
|
|
|
|
## Upgrading an existing server
|
|
|
|
- Update `INodeManager.CreateMonitoredItems` to support the new `MonitoredItemIdFactory`.
|
|
- `AsyncCustomNodeManager` is the recommended base class for fully
|
|
asynchronous node managers; see [AsyncCustomNodeManager](#asynccustomnodemanager).
|
|
- Keep using `CustomNodeManager2` and implement the asynchronous interfaces
|
|
your server needs:
|
|
- `IAsyncNodeManager` for full async support
|
|
- `ICallAsyncNodeManager` for async method calls
|
|
- `IReadAsyncNodeManager` for async reading
|
|
- `IWriteAsyncNodeManager` for async writing
|
|
- `IHistoryReadAsyncNodeManager` for async history read
|
|
- `IHistoryUpdateAsyncNodeManager` for async history update
|
|
- `IConditionRefreshAsyncNodeManager` for async condition refresh
|
|
- `ITranslateBrowsePathAsyncNodeManager` for async translate browse path
|
|
- `IBrowseAsyncNodeManager` for async browsing
|
|
- `ISetMonitoringModeAsyncNodeManager` for async monitoring mode changes
|
|
- `ITransferMonitoredItemsAsyncNodeManager` for async monitored item transfer
|
|
- `IDeleteMonitoredItemsAsyncNodeManager` for async monitored item deletion
|
|
- `IModifyMonitoredItemsAsyncNodeManager` for async monitored item modification
|
|
- `ICreateMonitoredItemsAsyncNodeManager` for async monitored item creation
|
|
|
|
> `MasterNodeManager` detects which asynchronous interfaces a node manager
|
|
> implements. It uses an asynchronous implementation when available and
|
|
> otherwise falls back to the synchronous implementation.
|
|
|
|
- The server supports fully asynchronous node managers that implement
|
|
`IAsyncNodeManager`. Register one with
|
|
`StandardServer.RegisterNodeManager(IAsyncNodeManagerFactory)`.
|
|
`IAsyncNodeManager.SyncNodeManager` is required for compatibility; pass
|
|
the node manager to `SyncNodeManagerAdapter` to provide it.
|
|
|
|
## Async method calls
|
|
|
|
`CustomNodeManager2` supports asynchronous method callbacks. Implement
|
|
`IAsyncNodeManager` on your node manager to enable them. Generated method
|
|
state classes expose asynchronous callbacks such as
|
|
`UpdateCertificateMethodState.OnCallAsync`; the server uses these callbacks
|
|
when the node manager implements the interface. For a generic method handler,
|
|
use `MethodState.OnCallMethod2Async`.
|
|
|
|
## AsyncCustomNodeManager
|
|
|
|
`AsyncCustomNodeManager` is the recommended base class for building fully async, TAP-native node managers.
|
|
Unlike the older `CustomNodeManager2`, it implements `IAsyncNodeManager` **directly** rather than
|
|
`INodeManager3`. This has two practical consequences:
|
|
|
|
- All virtual methods are `async ValueTask`-returning from the start, so there is no boilerplate
|
|
wrapping of synchronous code inside `Task.Run` or similar helpers.
|
|
- The `SyncNodeManager` property (required by `IAsyncNodeManager`) is satisfied automatically: the
|
|
constructor calls `this.ToSyncNodeManager()` and stores the resulting `INodeManager3` adapter.
|
|
Callers that still require an `INodeManager3` reference (e.g. legacy subscription code) use that
|
|
adapter; the node manager itself never needs to implement the synchronous interface.
|
|
|
|
### Registering an AsyncCustomNodeManager
|
|
|
|
Use the `IAsyncNodeManagerFactory` overload of `StandardServer.RegisterNodeManager`:
|
|
|
|
```csharp
|
|
server.RegisterNodeManager(context =>
|
|
new MyAsyncNodeManager(server, configuration));
|
|
```
|
|
|
|
### Async browse iteration
|
|
|
|
Every hook a custom node manager overrides is awaitable, including browse
|
|
operations. `AsyncCustomNodeManager.BrowseAsync` and
|
|
`TranslateBrowsePathAsync` iterate an `INodeBrowser` with
|
|
`NextAsync(CancellationToken)` instead of `Next()`. A browser can therefore
|
|
fetch data from an underlying system asynchronously. For example, it can
|
|
retrieve references from another server without occupying a request worker
|
|
with a blocking call. The default `NextAsync` wraps `Next()`, so the browsers
|
|
provided by the stack and existing custom browsers continue to work.
|
|
See [NodeManagers.md](NodeManagers.md#threading-contract-for-nodes-and-browsers)
|
|
for the browser contract.
|
|
|
|
### Locking strategy vs CustomNodeManager2
|
|
|
|
The synchronous `CustomNodeManager2` service paths use coarse-grained manager
|
|
synchronization. `AsyncCustomNodeManager` instead coordinates writes and
|
|
monitored-item management with awaitable semaphores while leaving node-state
|
|
synchronization inside `NodeState`.
|
|
|
|
The following describes the implementation, not locks that application callers
|
|
should acquire. Use the manager's service, lifecycle, and fluent APIs so operation
|
|
admission, cancellation, and disposal remain coordinated.
|
|
|
|
#### 1. Global write semaphore — `m_writeSemaphore`
|
|
|
|
The manager serializes its Write service operations through an awaitable semaphore.
|
|
Read operations do not acquire it. The semaphore does not turn external updates or
|
|
several attribute accesses into a transaction; use the node APIs for state access.
|
|
|
|
#### 2. Monitored-item semaphore — `m_monitoredItemSemaphore`
|
|
|
|
A second `SemaphoreSlim(1, 1)` serializes **monitored-item management**
|
|
operations. It keeps subscription state consistent without blocking reads
|
|
or writes. The semaphore protects:
|
|
|
|
- create, modify, and delete operations
|
|
- monitoring-mode changes
|
|
- event subscriptions and condition refresh
|
|
- monitored-item transfers
|
|
|
|
#### 3. Per-node locking for read and attribute access
|
|
|
|
Reads do not acquire a manager-wide lock. `NodeState` synchronizes attribute and
|
|
collection access internally; the async manager awaits `ReadAttributeAsync` so that
|
|
asynchronous value handlers can complete without blocking a thread.
|
|
|
|
**Do not use `lock (node)` or `lock (handle.Node)`.** Those monitors do not coordinate
|
|
with the node's private locks and cannot protect its state. Use the node's attribute,
|
|
child, and reference APIs instead. Individually synchronized operations do not form
|
|
an atomic multi-attribute transaction.
|
|
|
|
See the [threading contract for nodes and browsers](NodeManagers.md#threading-contract-for-nodes-and-browsers)
|
|
for supported operations, snapshot boundaries, and browser ownership.
|
|
|
|
| Concern | `CustomNodeManager2` | `AsyncCustomNodeManager` |
|
|
|----------------------------------|----------------------------|------------------------------------|
|
|
| Address-space reads | Manager synchronization | Internally synchronized node APIs |
|
|
| Write service operations | Manager synchronization | Awaitable write serialization |
|
|
| Monitored-item management | Manager synchronization | Awaitable monitored-item serialization |
|
|
| Awaiting asynchronous handlers | Use the asynchronous path | Supported by asynchronous operations |
|
|
| Implemented interface | `INodeManager3` | `IAsyncNodeManager` |
|
|
|
|
### Monitored-item manager selection
|
|
|
|
The constructor accepts an optional `useSamplingGroups` flag:
|
|
|
|
```csharp
|
|
// Default: change-triggered (MonitoredNodeMonitoredItemManager)
|
|
public MyNodeManager(IServerInternal server, ApplicationConfiguration config)
|
|
: base(server, config) { }
|
|
|
|
// Opt-in: timer-based sampling (SamplingGroupMonitoredItemManager)
|
|
public MyNodeManager(IServerInternal server, ApplicationConfiguration config)
|
|
: base(server, config, useSamplingGroups: true) { }
|
|
```
|
|
|
|
- **`MonitoredNodeMonitoredItemManager`** (default): node value changes are propagated to
|
|
subscribers immediately by awaiting `NodeState.ClearChangeMasksAsync` after every successful
|
|
write (see [Fully asynchronous change notifications](#fully-asynchronous-change-notifications)).
|
|
No background threads are created per subscription.
|
|
- **`SamplingGroupMonitoredItemManager`**: a background timer thread samples the current node
|
|
value at the negotiated `SamplingInterval`. Write changes are *not* pushed immediately; instead
|
|
the next scheduled sample detects and delivers them. Choose this mode when the data source
|
|
produces values independently of OPC UA write requests (e.g. hardware polling).
|
|
|
|
### Fully asynchronous change notifications
|
|
|
|
`AsyncCustomNodeManager` sends value changes and events to monitored items
|
|
through an asynchronous push path. This path does not block a thread when a
|
|
node uses an asynchronous value-read handler or when the per-node notification
|
|
channel applies back-pressure.
|
|
|
|
`NodeState` exposes asynchronous counterparts of its change-notification API alongside the existing synchronous members:
|
|
|
|
| Synchronous | Asynchronous |
|
|
|-------------|--------------|
|
|
| `ClearChangeMasks(context, includeChildren)` | `ClearChangeMasksAsync(context, includeChildren, ct)` |
|
|
| `OnStateChanged` / `StateChanged` | `OnStateChangedAsync` / `StateChangedAsync` |
|
|
| `ReportEvent(context, e)` | `ReportEventAsync(context, e, ct)` |
|
|
| `OnReportEvent` | `OnReportEventAsync` |
|
|
|
|
These additions do not break existing synchronous callers or sinks.
|
|
`MonitoredNodeMonitoredItemManager` wires the asynchronous sinks. After a
|
|
successful write, `AsyncCustomNodeManager.WriteAsync` flushes changes by
|
|
awaiting `handle.Node.ClearChangeMasksAsync(...)`.
|
|
|
|
The asynchronous sink reads each monitored attribute through
|
|
`NodeState.ReadAttributeAsync`. When registered, an asynchronous
|
|
`OnReadValueAsync` or `OnSimpleReadValueAsync` handler supplies the value. The
|
|
sink then enqueues that snapshot through the bounded per-node channel and
|
|
awaits completion. It reads the value before enqueueing the change, so each
|
|
notification captures the node value at the time of the change. Awaiting both
|
|
operations keeps the producing thread from blocking.
|
|
|
|
The synchronous `ClearChangeMasks` and `ReportEvent` methods also drive the
|
|
asynchronous sinks. They complete inline when the node is synchronously
|
|
readable and the channel has capacity. They block the calling thread only
|
|
while an asynchronous read is in flight or the channel is full. Synchronous
|
|
node managers continue to work without changes, but may block in those two
|
|
cases.
|
|
|
|
Each monitored item's queue controls overflow and discard behavior,
|
|
including FIFO/LIFO `DiscardOldest` and the `Overflow` status bit. The shared
|
|
per-node channel does not drop notifications. As a result, the configured
|
|
queue policy applies to the full stream of changes.
|
|
|
|
### Creating a custom node manager
|
|
|
|
Derive from `AsyncCustomNodeManager` and override only the virtual methods you need:
|
|
|
|
```csharp
|
|
public class MyNodeManager : AsyncCustomNodeManager
|
|
{
|
|
public MyNodeManager(IServerInternal server, ApplicationConfiguration config)
|
|
: base(server, config, "http://my.org/UA/Data/")
|
|
{
|
|
}
|
|
|
|
public override async ValueTask CreateAddressSpaceAsync(
|
|
IDictionary<NodeId, IList<IReference>> externalReferences,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await base.CreateAddressSpaceAsync(externalReferences, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
// build your nodes here
|
|
var myVar = new BaseDataVariableState(null);
|
|
myVar.NodeId = new NodeId("MyVar", NamespaceIndex);
|
|
myVar.Value = 42;
|
|
await AddNodeAsync(SystemContext, default, myVar, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
}
|
|
}
|
|
```
|