mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-21 09:06:37 +08:00
fix/encoder-stack-depth-json-flake
6 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
162fc5ebb3 |
Fix AddNodes scaling and OpenSecureChannel revocation errors found by CTT Monitored Item, Node Management and security runs (#4486)
# Description Findings from running the CTT conformance groups **Monitored Item Services**, **Node Management Services**, **Security General** and **Security User Token** against `ConsoleReferenceServer --ctt` (CTT 1.05.06, scripts 1.05.513). The security groups ran **without `-a`**, against an isolated server PKI built from the CTT's `copyToServer` folders, so the negative certificate tests are meaningful. ## Server fixes ### AddNodes below one parent was quadratic (Node Management Delete Node `Err-002.js`) The CTT adds 15,000 variables below `ns=2;s=CTT` in batches of 5,000 and warned that AddNodes responses took 400–800 ms. For every added node the server searched all children of the parent twice: once in the BrowseName duplicate check in `AsyncCustomNodeManager.AddNodeCoreAsync`, which also copied the children list, and once in the NodeVersion lookup of `FilterChangesByNodeVersion`. - `NodeState.FindChild` now searches children lists with ≥ 64 entries through a BrowseName index kept in a `ConditionalWeakTable`, so nodes with few children get no extra field. - It keeps first-match semantics and is maintained by `AddChild`/`RemoveChild`. - It is dropped on replacement and invalidated when a child is renamed or moves to another parent. - Lists whose children point at another parent (clones) are still searched linearly. - The duplicate check uses `FindChild` instead of copying the list. | 5,000-node AddNodes batch (in-process, Debug) | 1st | 2nd | 3rd | | --- | --- | --- | --- | | before | 769 ms | 2,083 ms | 4,011 ms | | after | 141 ms | 166 ms | 172 ms | In the CTT, AddNodes of 5,000 items now answers in 51–72 ms and the warning is gone. ### OpenSecureChannel leaked revocation errors (Security Certificate Validation `002.js`) Part 4 §6.1.3, Table 106: - For the revocation check (`Bad_CertificateRevoked`, `Bad_CertificateIssuerRevoked`), `Bad_SecurityChecksFailed` **shall** be reported to the client. - For a missing revocation list (`Bad_CertificateRevocationUnknown`, `Bad_CertificateIssuerRevocationUnknown`), it **should** be. `TcpServerChannel` masked only `Bad_CertificateRevoked`, and `StandardServer.OnApplicationCertificateError` (CreateSession) had its own list that missed `Bad_CertificatePolicyCheckFailed`. Both now use one helper, `CertificateErrorReporting` in Opc.Ua.Core, which also inspects the nested errors of the certificate validator (both paths now throw the validation result with `ThrowIfInvalid`). Only validity period, host name, URI and usage errors keep their own code, and audit events still carry the specific result. The CTT warning is gone. ### Sample data - **Monitor Value Change V2 `020.js`** skipped because `Scalar_Static_Arrays_ByteString` had a 1-byte and a 3-byte element. The script checks every element, and the simulation only padded elements 0–2. The seed value now has no element shorter than four bytes, and the simulation pads every element. - **Monitor Value Change V2 `042.js`** got 18 of 19 notifications. The missing item is the Float matrix: its element [1,1] was `-1.03179664E+33`, where adding 1 is below Float precision, so the value really did not change. The server did not drop a notification. The element is now `-1031.79664`. ## CTT results after the fixes | Group | Result | | --- | --- | | Monitored Item Services (208) | Monitor Value Change V2 44/44. Remaining: Monitor Basic `039.js` (C17, missing include), `038.js` warning (C32), and 1–2 sporadic timestamp-tolerance warnings that move between test cases from run to run. | | Node Management Services (15) | Only Add Node `Err-008.js` (Mantis 11266). | | Security General (314) | Certificate Validation: 23 pass, 24 not implemented, 3 not-applicable skips. None CreateSession/ActivateSession: all pass. The other 50 CUs are *Not Implemented* in scripts 1.05.513. | | Security User Token (66) | Anonymous `002.js` (C35) and User Name Password 2 `015.js` (C36) fail because of CTT defects. X509: 18/18 automated cases pass. | ## CTT defects documented (`plans/ctt-issues.md`) - **C32** `038.js` compares a RevisedSamplingInterval of 0 with a project setting instead of the node's MinimumSamplingInterval or the server's MinSupportedSampleRate. - **C33** `020.js` requires every ByteString element to be ≥ 4 bytes, but its message says elements 0–2. - **C34** `RequestedNewNodeId()` always sends `ns=1`, regardless of `RequestedNodeId_Namespace`, so client NodeIds can't be enabled. - **C35** Security User Anonymous picks the `opc.wss` endpoint, which the CTT cannot open (`BadNotSupported`). - **C36** `UaEndpointDescription.FindTokenType` rewrites the cached endpoints' token policies, so `015.js` reports PolicyIds as reused. The server's GetEndpoints output was verified compliant (PolicyIds unique per configuration, Mantis 11258). - Evidence added to **C9** (042.js) and **issue 17** (015.js). `plans/ctt-testing.md` gains how to run the Monitored Item/Node Management groups and a recommended setup for the security groups: isolated PKI from `copyToServer`, no `-a`, PKI restore per part, and waiting for `Server started`. Other findings recorded there: - the Create/DeleteMonitoredItems timing warnings are jitter; - `LoginNameAccessDenied` is not a real user; - `samples\UAReferenceServer.ctt.xml` needs no change (enabling `RequestedNodeId` breaks six Add Node cases because of C34). **Note:** `plans/ctt-testing.md` and the refreshed `plans/ctt-issues.md` are introduced by #4482. The plans files here are based on #4482's versions, and new entries start at C32 because #4481 uses C19–C31. Please merge after #4482 (or rebase the plans files onto it). ## Tests - `NodeStateChildNameIndexTests` (Opc.Ua.Types.Tests): index semantics (duplicates, remove, rename, replace, re-parent, clone, shrink/grow, 2,000 random operations against a linear search). Also a scan guard: 5,000 misses against 50,000 children take 97 ms with the index and 7,083 ms with it disabled. - `AsyncCustomNodeManagerNodeManagementTests.AddNodeAsync_DuplicateBrowseNameUnderParentWithManyChildren…` - `TcpServerChannelCertificateErrorTests` (Opc.Ua.Core.Tests): masked vs reported certificate errors. - Opc.Ua.Server.Tests 5,553 passed, Opc.Ua.Core.Security.Tests 556 passed, Opc.Ua.Core.Tests (Security + Stack) 3,561 passed, InformationModel NodeManagement 26 passed (net10.0). - Opc.Ua.Types.Tests 10,404 passed and 1 failed: `WotSpecFixtureManifestTests.EveryEmbeddedExampleMatchesItsRecordedHash`. That test hashes WoT assets which are `eol=lf` in the index but CRLF in this worktree's checkout; no WoT file is touched by this PR. ## Checklist - [x] I have signed the CLA and read the CONTRIBUTING doc. - [x] I have added tests that prove my fix is effective or that my feature works. - [x] I have added all necessary documentation. - [ ] I ran all tests locally using UA.slnx against .NET Framework and .NET 10 (net10.0 suites listed above only). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: romanett <7413710+romanett@users.noreply.github.com> |
||
|
|
017856f4aa |
Throw when a server base address scheme has no registered transport binding (#4417)
## Description
A base address with an unregistered transport scheme (e.g. `opc.https`
without `AddHttpsTransport()`) was silently skipped in
`StandardServer.InitializeServiceHostsAsync`: no listener was opened,
but the address was still published in `DiscoveryUrls`, so clients
discovered an endpoint that refused every connection. This contradicted
`docs/Transports.md`, which documents that startup should throw a clear
error naming the missing scheme.
### Fix
- `StandardServer.InitializeServiceHostsAsync` now throws
`InvalidOperationException` when `GetListenerFactory(scheme)` returns
`null` for a scheme present in `BaseAddresses`, instead of silently
continuing.
- The error message names the missing scheme and the corresponding
`Add*Transport()` extension to call, mirroring the pattern already used
in `LdsServer.InitializeServiceHostsAsync`.
- Added a `GetTransportName` helper mapping
`opc.tcp`/`https`/`opc.https`/`wss`/`opc.wss` to the
`OpcTcp`/`Https`/`Wss` names used by the DI extensions.
```csharp
ITransportListenerFactory binding = bindingFactory.GetListenerFactory(scheme) ??
throw new InvalidOperationException(
$"No OPC UA transport listener is registered for endpoint scheme '{scheme}'. " +
"Register the matching transport binding, for example by calling " +
$"Add{GetTransportName(scheme)}Transport().");
```
### Tests
-
`StandardServerCoverageTests.InitializeServiceHostsThrowsForUnregisteredScheme`
— asserts the throw and message content for an unregistered scheme.
-
`StandardServerCoverageTests.InitializeServiceHostsUsesRegisteredFactoryAsync`
— asserts unchanged behavior when the factory is registered.
## 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.
- [ ] 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.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
<!-- START COPILOT CODING AGENT SUFFIX -->
- Fixes #4416
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com>
Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
|
||
|
|
4e0b82af5f |
Add IServerContext and move server startup tasks onto it (#4193)
## Stacked on #4188
Base is `marcschier/server-seam-analysis`, not `master`.
`IServerInternal.cs` and
`ServerInternalData.cs` both diverge from master on that branch (chunk
4c removed the two
`object` locks), so branching off master would conflict on both files
*and* design
`IServerContext` against a stale interface. GitHub retargets the base to
`master`
automatically when #4188 merges. **Review #4188 first.**
Closes part of #4184.
## What this does
`IServerInternal` is a service locator: 56 members, 30-odd of which hand
out one of 20+
subsystems. A component that only needs to observe the server and act on
it has to take the
whole thing, and no signature tells a reader which parts a component
actually touches.
This adds `IServerContext` — the ambient view of a running server — and
moves
`IServerStartupTask` onto it.
```csharp
public interface IServerContext
{
ServerState CurrentState { get; }
ServerObjectState ServerObject { get; }
IServiceMessageContext MessageContext { get; }
ServerSystemContext DefaultSystemContext { get; }
ServerSystemContext CreateSystemContext(ISession session);
T? FindPredefinedNode<T>(NodeId nodeId) where T : NodeState;
IEnumerable<T> FindNodeManagers<T>() where T : class;
ValueTask ReportEventAsync(IFilterTarget e, CancellationToken ct = default);
ValueTask CloseSessionAsync(OperationContext context, NodeId sessionId,
bool deleteSubscriptions, CancellationToken ct = default);
ValueTask DeleteSubscriptionAsync(uint subscriptionId, CancellationToken ct = default);
void UpdateServerDiagnostics(Action<ServerDiagnosticsSummaryDataType> update);
}
```
Nothing on it returns a subsystem. A component that needs one takes it
as a constructor
dependency, which states the requirement instead of hiding it behind an
ambient handle.
`IServerInternal : IServerContext`, so every existing handle already
satisfies the new seam
and consumers migrate one at a time.
## The member set was measured, not guessed
Every member of `IServerInternal` was counted by read site across `src`,
`samples` and
`tests`, split by area. `Ext` is non-test production consumers, which is
what decides
whether something belongs on an ambient interface.
| Member | InServer | OtherSrc | Sample | Test | **Ext** | On
`ISystemContext`? |
|---|---:|---:|---:|---:|---:|---|
| `NamespaceUris` | 63 | 50 | 34 | 131 | **84** | yes |
| `Telemetry` | 58 | 23 | 35 | 5 | **58** | yes |
| `MessageContext` | 2 | 8 | 7 | 11 | **15** | no |
| `TypeTree` | 42 | 3 | 11 | 10 | **14** | yes, as `TypeTable` |
| `NodeManager` | 66 | 7 | 6 | 74 | **13** | no |
| `DefaultSystemContext` | 21 | 4 | 1 | 7 | **5** | no |
| `IdentityRegistry` | 1 | 4 | 1 | 1 | **5** | no |
| `SubscriptionManager` | 23 | 0 | 0 | 17 | **0** | no |
| `SessionManager` | 21 | 0 | 0 | 15 | **0** | no |
| `RequestManager` | 10 | 0 | 0 | 23 | **0** | no |
| `AggregateManager` | 12 | 0 | 0 | 2 | **0** | no |
| `ModellingRulesManager`, `ConformanceUnitsManager`,
`DiagnosticsEnabled` | 0 | 0 | 0 | 0 | **0** | no |
Two things fall out. The highest-traffic members are already carried by
`ISystemContext`,
which `ServerSystemContext` copies off the server at construction — so
most of those 278
`NamespaceUris` reads are callers reaching through the server for
something they already
hold. And the subsystems with zero external demand are exactly the ones
that should be
injected rather than published.
The check that matters: the eight `IServerStartupTask` implementations
in
`Opc.Ua.Redundancy.Server` are the heaviest real consumer of the ambient
handle, and
between them they touch **eight distinct members**. That is the shape
the interface is cut
to.
## Commits
### `e4a912eb` Remove seven unused members from `IServerInternal`
The deletion test, applied before anything is designed against the
interface.
| Removed | Evidence |
|---|---|
| `CloseSession(OperationContext, NodeId, bool)` | zero call sites;
sync-over-async; already obsolete |
| `ReadServerDiagnostics<TResult>` | zero production sites — **added one
commit earlier in #4188 for symmetry and never called** |
| `ModellingRulesManager` | property never read; `ServerInternalData`
owns the lifetime |
| `ConformanceUnitsManager` | same |
| `DiagnosticsEnabled` | façade over
`IDiagnosticsNodeManager.DiagnosticsEnabled`, which is what callers use
|
| `Status` | one site, itself inside an obsolete method |
| `ServerDiagnostics` | only reachable through the diagnostics methods
that replaced it |
`EndpointAddresses` was kept — `Opc.Ua.Di.Tests` uses it to resolve an
endpoint URL. Thin,
but real.
The only build fallout was three lines in one test assembly.
`Opc.Ua.Redundancy.Server`,
`Opc.Ua.Redundancy.Kubernetes`, `Opc.Ua.Gds.Server.Common`,
`Opc.Ua.WotCon.Server`,
`Opc.Ua.PubSub.Server`, `Opc.Ua.Di.Server`, `ConsoleReferenceServer` and
`Quickstarts.Servers` all built untouched. That is the evidence the
members were dead.
### `ffcc80b9` Add `IServerContext`
Additive. `ServerInternalData` implements it; no caller changes.
### `e8c4e15f` Hand startup tasks `IServerContext`
Nine implementations, `OpcUaServerHostedService`, and the test call
sites.
### `1b4de97a` Drop the `IServerStartupTask` migration-guide section
Review feedback. `IServerStartupTask` does not exist in `master378`, so
it is a 2.0-only
concept and nobody migrating from 1.5.378 can own an implementation of
it. The **Removed
members on `IServerInternal`** section stays - those members *are* in
master378 - and is now
self-contained.
### `2ab7d689` Merge `marcschier/server-seam-analysis`
Picks up the base branch including its merge of master. Textually clean,
no conflicts.
### `6a650f12` Migrate `RobotIntentHostStartupTask` to `IServerContext`
The merge was textually clean but **semantically broken**: master's
Robot Intent feature
(#4165) added a tenth `IServerStartupTask` implementation written
against the old
`IServerInternal` signature, so it no longer satisfied the interface.
Its body walked
`server.NodeManager.AsyncNodeManagers` and type-tested each entry for
`RobotIntentNodeManager` - exactly the pattern `FindNodeManagers<T>()`
replaces - so it
migrated the same way as the other nine. `FindNodeManagers` also
searches the synchronous
registry and de-dupes by reference, so a manager registered on both
faces still starts its
intent controller hosts exactly once.
## Three findings from migrating real consumers
**`MessageContext` is not derivable.** The design had callers use
`DefaultSystemContext.AsMessageContext()`, on the reasoning that a
message context can be
built from a system context. It can't: that conversion carries the
namespace tables and the
factory but **not** `MaxStringLength`, `MaxArrayLength` or
`MaxByteStringLength`. Four
replication paths in `Opc.Ua.Redundancy.Server` would have silently
fallen back to default
decoding limits — a component quietly accepting more than the server is
configured to. It
is a real member, and the interface documents why.
**`new`-shadowed members split the mock surface.** `IServerInternal`
first redeclared
`DefaultSystemContext` with a narrower type and `CurrentState` with a
setter. That
compiles, but a shadowed member is a *second interface slot*: production
code calling
through `IServerContext` read a slot that every existing
`Mock<IServerInternal>` leaves
unstubbed. Seventeen tests went from passing to `NullReferenceException`
**while still
compiling**. The narrower typing bought no encapsulation anyway — the
object really is a
`ServerSystemContext` and any caller can cast — so both shadows are
gone. `CurrentState` is
read-only on the ambient interface and the one site that assigns it uses
the concrete type,
as `GetStatus` already does.
**`IServiceProvider` is deliberately *not* on `OnServerStartedAsync`.**
The plan sketched
it. All nine implementations already receive their dependencies through
their constructors
from the builder extensions, so a container on the callback would have
replaced one service
locator with another.
## `FindNodeManagers<T>`
Two tasks walked `server.NodeManager.NodeManagers` and type-tested each
entry for
`ILocalAddressSpaceSource`; two more reached for a specific manager to
cast it to a
capability. Asking for the capability says what the caller wants; asking
for the registry
says how to find it.
```csharp
// before
foreach (INodeManager nodeManager in server.NodeManager.NodeManagers)
{
if (nodeManager is not ILocalAddressSpaceSource source) { continue; }
...
}
// after
foreach (ILocalAddressSpaceSource source in server.FindNodeManagers<ILocalAddressSpaceSource>())
{
...
}
```
It searches both the asynchronous and synchronous registries:
`ToSyncNodeManager()` returns
`this` when a manager already implements `INodeManager`, so the two
lists overlap and a
capability may sit on either face.
## Tests
Mock harnesses are **wired, not stubbed** — they return the node
managers and contexts the
tests already build, so the tests keep asserting against real behaviour
rather than against
a mock that returns whatever is convenient. Four new tests cover
`CreateSystemContext`
(identity/locale propagation, null guard), the explicit
`DefaultSystemContext` identity,
and `FindPredefinedNode` before the address space is bound.
| Suite | net10.0 | net48 |
|---|---|---|
| `Opc.Ua.Redundancy.Server.Tests` | 573 passed | 573 passed |
| `Opc.Ua.Redundancy.Kubernetes.Tests` | 97 passed | n/a (net8/net10
only) |
| `Opc.Ua.Server.Tests` | 4028 passed, 1 failed | 225 passed, 1 failed
(filtered) |
The single failure is
`ConfigureApplicationBuildsSharedClientAndServerConfigurationAsync`,
a local certificate-store problem. Verified it reproduces unchanged at
HEAD with these
changes stashed — it is not caused by this PR.
All twelve dependent assemblies and samples build clean on both TFMs,
zero warnings.
### Re-verified after the base merge (`6a650f12`)
`Opc.Ua.Server`, `Redundancy.Server`, `Redundancy.Kubernetes`,
`Robotics.Server`,
`WotCon.Server`, `Di.Server`, `PubSub.Server`, `Gds.Server.Common` and
`XRegistry.Server` all build clean on net10.0. `Opc.Ua.Robotics.Tests`
491 passed,
`Opc.Ua.Redundancy.Server.Tests` 573 passed,
`Opc.Ua.Redundancy.Kubernetes.Tests` 97 passed.
### CI status: 119 passed, 3 failed - none caused by this PR
| Failing job | Why it is not this PR |
|---|---|
| `SourceGeneration_Core_Tests_windows` (net48) |
`GenerateAndCompileDesignFileAsync(..., CSharp11, ...)` fails with
`CS9058: Feature 'Lock object' is not available in C# 11.0` because
generated `Opc.Ua.NodeStates.g.cs` emits `System.Threading.Lock`.
**Identical failure on base #4188 (build 16737)**, which contains none
of these commits. This PR touches no source-generation file. |
| `SourceGeneration_Core_Tests_windows` (net10.0) | Same root cause;
also failing on base #4188. |
| `PubSub_Eth_Tests_windows` (net48) | Agent DNS outage:
`NuGetToolInstaller` died with `ERR:getaddrinfo ENOTFOUND
dist.nuget.org`. The job aborted before restore/build, so **the test
step never executed**. Infra flake; needs a re-run, not a code change. |
Both `SourceGeneration.Core` failures belong to the base branch and
should be fixed there
rather than papered over here.
## Correction to the recorded plan
Chunk 7 (the frozen bind phase) was recorded as the largest mechanical
footprint remaining
and the one most likely to stall. It isn't: **each of the 12 `Set*`
mutators has exactly one
call site**, in the `StandardServer` wiring, plus one sample. It is a
~13 call-site change.
The 268-file figure that produced that assessment counted files
*referencing*
`IServerInternal`, which chunk 7 never has to touch, because the
interface survives and only
the mutators move off it.
## Docs
`docs/MigrationGuide.md` gains one section: the removed
`IServerInternal` members with their
replacements, plus the note that `MessageContext`,
`DefaultSystemContext`, `CurrentState`,
`ServerObject`, `ReportEventAsync`, `CloseSessionAsync`,
`DeleteSubscriptionAsync` and
`UpdateServerDiagnostics` moved *down* to `IServerContext` and stay
reachable through
`IServerInternal` unchanged, so no call site has to move.
An earlier revision also documented the `IServerStartupTask` signature
change there. That
was dropped after review (
|
||
|
|
e73e71184d |
[WotCon] Add OPC UA WoT Connectivity 1.1 with the Types, source generation, Server and xRegistry work it builds on (#4128)
## Summary Adds OPC UA WoT Connectivity 1.1 to the stack, together with the Types, source generation, Server and xRegistry work it is built on. This branch is the merge point for the fourteen-PR stack that replaced integration PR #4093; every constituent PR was reviewed and approved separately and is listed below. 414 files changed, +108,936 / -3,199. Roughly 46k added lines of product code, 57k of tests, plus samples, tools and documentation. ## Types - Complete `ExtensionObject` raw-body decoding for binary, XML and JSON bodies by resolving the concrete type through the message context's encodeable factory, and simplify the `Variant` structure helpers by delegating to it (#4128). - Add `IAtomicFileReplace` as an optional `IFileSystem` capability so existing external implementations keep working, with atomic publish for `LocalFileSystem` via `File.Replace`/`File.Move` and for `VirtualFileSystem` by re-keying the in-memory entry (#4128). - Add lossless conversion between WoT documents and NodeSet2 (`Opc.Ua.Wot.WotNodeSetConverter`). A byte-exact `uav:nodeSet` envelope is preserved when requested, the structured `uav:nodes` projection is used when the readable vocabulary is incomplete, and NodeSet2 is otherwise synthesized from readable WoT terms. Unmapped JSON members survive a round trip as pointer-addressed residue (#4131). ## Source generation - Improve the NodeSet to ModelDesign conversion and the generated node state (#4132). - Generate OPC UA models directly from WoT Thing Description files (#4134). ## Server - Add live NodeManager shadow and immediate reload. A reload materializes a new generation beside the active one and switches atomically; the superseded generation is retired gracefully so existing MonitoredItems keep being served until they drain, or immediately when the caller asks for it. Includes the request admission and drain machinery that lets an orderly shutdown wait for admitted requests instead of tearing down underneath them (#4147). - Make subscription transfer between sessions transactional, so a failed transfer leaves neither session holding a partially moved subscription (#4150). ## xRegistry - Allow clients to use explicit registry roots rather than assuming a well-known location (#4130). ## WoT Connectivity - Add the WoT Connectivity 1.1 information model: a registry-first revision layered on the abstract xRegistry base model, incorporating the complete published OPC 10100-1 v1.02 surface as deprecated nodes in the same namespace so existing 1.02 clients keep working (#4139). - Add the protocol binding abstractions and planners that compile WoT forms into executable plans (#4140). - Add the HTTP, Modbus, OPC UA and MQTT binding executors (#4144). - Add the registry and its stores, including document versioning, validation, dependency resolution and refresh (#4146). - Add the registry client (#4142). - Materialize WoT Thing Descriptions into the server address space, deriving types from Thing Models and instances from Thing Descriptions (#4154). - Restore the WoT asset registry bridge so the deprecated 1.02 asset surface is backed by the registry (#4171). - Add the WoT Connectivity samples and complete the documentation (#4156), and restore the remaining coverage and docs (#4172). ## New projects `src/Opc.Ua.WotCon.Bindings`, `src/Opc.Ua.WotCon.Bindings.Mqtt`, `tests/Opc.Ua.WotCon.Bindings.Tests`, `tests/Opc.Ua.WotCon.Samples.Tests`, and the `samples/WotCon` AggregationServer, AggregationClient and FlatTagServer samples. ## Constituent pull requests Merged into this branch top-down, each independently approved: | PR | Title | | --- | --- | | #4128 | [Types] Complete ExtensionObject decoding and add an atomic file replace capability | | #4131 | [Types] Add lossless conversion between WoT documents and NodeSet2 | | #4132 | [SourceGeneration] Improve NodeSet to ModelDesign conversion and generated node state | | #4134 | [SourceGeneration] Generate OPC UA models from WoT Thing Description files | | #4147 | [Server] Add live NodeManager shadow and immediate reload | | #4150 | [Server] Make subscription transfer between sessions transactional | | #4130 | [XRegistry] Allow clients to use explicit registry roots | | #4139 | [WotCon] Add the WoT Connectivity 1.1 information model | | #4140 | [WotCon] Add the WoT protocol binding abstractions and planners | | #4144 | [WotCon] Add the HTTP, Modbus, OPC UA and MQTT binding executors | | #4146 | [WotCon] Add the WoT Connectivity registry and its stores | | #4142 | [WotCon] Add the WoT Connectivity registry client | | #4154 | [WotCon] Materialize WoT Thing Descriptions into the server address space | | #4156 | [WotCon] Add the WoT Connectivity samples and complete the documentation | | #4171 | Restore WoT asset registry bridge | | #4172 | Restore WoT coverage and docs | Supersedes #4093. ## Follow-up The WoT Connectivity and WoT Binding drafts have moved to 1.1-draft2 since this work was authored. Aligning with that revision - the removed group vocabulary, the new projection/View construct, the remaining model vocabulary terms and the two new portable-identity validation rules - is tracked separately and will follow in its own pull request. ## Validation Built for every target framework with zero warnings and zero errors, and validated at the tip of the stack with `Opc.Ua.WotCon.Tests` at 999 passed / 0 failed and `Opc.Ua.Server.Tests` at 4024 passed / 0 failed. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8 |
||
|
|
275c3800b5 |
[Server] Add live NodeManager lifecycle support (#4094)
# Description Adds supported live add, reload, and remove lifecycle operations for NodeManager instances on a running server. The change introduces a DI-backed `INodeManagerLifecycle` service with direct `StandardServer` access, generation-aware registration handles, copy-on-write routing, transactional prepare/commit/rollback, runtime NodeSet convenience APIs, append-only namespace updates, model and semantic change notifications, additive-compatible complex type priming, and complete teardown behavior. Lifecycle publication is coordinated with requests, sessions, subscriptions, monitored items, continuation points, and shutdown so retired managers cannot remain reachable or be disposed while still in use. Compatible monitored items transfer to replacement generations without a transient bad status. Removed or incompatible Nodes publish `BadNodeIdUnknown` as required by OPC UA Part 4 section 5.8.4.1, retain their MonitoredItem identity, and automatically recover when a compatible Node with the same NodeId returns. Registration is documented end to end in the new [docs/NodeManagerRegistration.md](docs/NodeManagerRegistration.md), which covers compile-time, startup, and runtime registration together with the guarantees for MonitoredItems, Browse continuation points, namespace indexes, DataTypes, and change notifications. ## Review feedback addressed Interface surface reduced, as requested: - `INodeManagerMonitoredItemTracker` folded into `ISubscriptionMonitoredItemLifecycle`. - `INodeManagerMutationCoordinator` and the asynchronous MonitoredItem recovery entry point folded into `IDynamicNodeManagerHost`. - `ISessionClosingRegistry` folded into `IServerInternal`, then replaced by `ISession.IsClosing`. - `ISubscriptionDeletionRegistry` folded into `ISubscriptionManager`, then removed entirely. - `INodeManagerContinuationPointTracker` folded into `ISession`. - The synchronous recovery entry point kept on its own `ISyncNodeManagerMonitoredItemRecovery`, because `AddPredefinedNode` cannot await; it still routes through the asynchronous `INodeManagerMonitoredItemLifecycle` that `AsyncNodeManagerAdapter` exposes. - `IMonitoredItemLifecycle` renamed to `IDetachableMonitoredItem`. Reach into the Subscription area reduced to the minimum the feature needs: - The lifecycle feature itself leaves `SubscriptionManager` almost untouched: two guards that reject a request from a Session that is closing, on `CreateSubscription` and on `TransferSubscriptions`, because it would create state that is about to be torn down. The deletion claim registry, the session closing gates, the closed-session bookkeeping and the deletion restore paths introduced by earlier revisions are gone. Transferring a subscription away from a session that is closing stays allowed, since that is the recovery path the service exists for; the race that exposed is fixed by serialising source-queue closure with the transfer rather than by rejecting it. - Queueing is disabled again for a MonitoredItem with `QueueSize <= 1`, as before this PR. Without a queue the Client is served the last sampled value, so the missing-Node notification simply becomes that value; losing values is expected for a MonitoredItem without queuing. - What remains of the notification is one `DataValue` plus one bool in `DataChangeQueueHandler`, mirroring the existing overflow pair, which stops it being the value discarded when a configured queue is full. - `MonitoredItem` keeps its original publish path, its original queue creation and restore paths, its original triggering behaviour, and no per-epoch bookkeeping. - `IStoredMonitoredItem` carries `IsDeleted` and `IsDetached`, so a restored item reports the state it was stored with rather than one inferred from the last value's StatusCode. Other feedback: - Documented `HostedNodeManagerLifecycle`, `IDynamicNodeManagerHost`, `NodeManagerRoutingTable`, and the `RequestManager` validation and request scopes. - Renamed `ValidateMonitoredItemAsync` to `CanAttachMonitoredItemAsync` and `RestoreMonitoredItemAsync` to `RecoverMonitoredItemAsync`, so the lifecycle API no longer collides with the durable Subscription `RestoreMonitoredItem` path used at server startup. - Removed the `CustomNodeManager2` dependency from the synchronous MonitoredItem recovery path. - `DataChangeQueueHandler` is back to the two constructors 1.5.378 shipped, each taking an optional `TimeProvider`. - Session closing is a one-way latch, and the Session is removed even when teardown throws. - Documented what a custom NodeManager has to implement to take part in live lifecycle operations, and which NodeManager implements `INodeManagerReloadParticipant` today. ## Fixes found during review - The guard that stops a NodeManager lifecycle operation being started from inside a service handler never fired. It was backed by an `AsyncLocal` written inside `async ValidateRequestAsync`, which is invisible to the caller that awaited it, so a lifecycle call from a NodeManager callback waited for its own request and held the lifecycle semaphore forever. The mark is now applied by the service dispatcher, and the drain is bounded so no path can wedge the lifecycle permanently. - `ValidateRequestAsync` is no longer virtual. An override that rejected a request after calling `base` left it registered forever; subclasses now use `OnRequestValidatedAsync`, whose failures complete the request. - A MonitoredItem deleted and disposed while a reload was handing it to the replacement NodeManager could be attached anyway. The item now reserves itself for the hand-over and the transition takes it back if it was disposed meanwhile. - The Overflow bit is recorded and applied when the value is published, as the surrounding design does, rather than written into the value at enqueue time. - MonitoredItem cleanup for a Subscription being deleted no longer passes null entries to `DeleteMonitoredItemsAsync`. - Browse continuation points of a retired NodeManager are persisted and disposed outside the Session lock. ## Open review discussions Three threads are open, replied to and awaiting reviewer decisions: separating service-call dispatch from NodeManager handling in `MasterNodeManager` (tracked as #4098), the request-scoping shape in `StandardServer`, and how much of the async MonitoredItem service implementation should stay in `Subscription`. Issue #4102 records a specification ambiguity found while implementing the missing-Node notification: Part 4 §5.8.4.1 makes it mandatory, but the protected over-capacity queue entry §5.13.1.5 defines applies to `EventQueueOverflowEventType` only, so the specification does not say how it survives a full queue. ## Validation - Full `Opc.Ua.Server.Tests` on net10.0: 3,768 passed, 5 skipped. - Lifecycle, MonitoredItem, Subscription and continuation point suites: 185 passed on net10.0 and 75 on net48. - Server builds complete with zero warnings or errors. - Two failures are pre-existing and unrelated: `DurableDataValueQueueVerifyReferenceBatchingAsync` (flaky `BatchPersistor` file contention, passes in isolation on net10.0 and net48) and `ConfigureApplicationBuildsSharedClientAndServerConfigurationAsync` (fails on a clean tree with these changes stashed). ## Related Issues - Fixes #3993 - Follow-up tracked in #4098 |
||
|
|
7cec2d614d | Move repository files to the consolidated layout |