mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 06:31:35 +08:00
master
2809 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
55bd944609 |
WoT: Types-side TD/TM-NodeSet conversion with consolidated corpus and minimal consumer compat (#4758)
# Description Adds the pure Types-side WoT Thing Description / Thing Model to NodeSet conversion: resolution, context, declarations, types, values, localization, URI/format handling, bounded hashing, plus supporting native XML codec, NodeSet alias/value helpers, RelativePathFormatter and a JsonElement equality polyfill. - Carrier-witness fix: a missing native event is accepted only when the owning preserved type declares it through a `GeneratesEvent` reference (directly or via its type/supertype chain) AND a bounded, validated selected EventType carrier identity equals the normalized authored native target. Native-present events get a full compare; an ambiguous native key and a missing map stay strict; a readable document is never archive authority. - Support-only dependencies: additive per-instance, non-serialized `WotDocument.IsSupportOnly(kind, name)` (ArgumentNullException guard). The resolver marks support-only after closure; explicit selection wins; the View builder excludes only true support-only members, so unit/actsOn dependencies stay in the JSON. Nested controls cover inner value-only projection under an outer value-only selection (unit kept, support-only) versus an outer explicit SelectAll (explicit, not support-only). - Documentation/XML comments and member-order only in commit 5c368d3a; no logic change. - Timeout repair on current head |
||
|
|
768b39e1be |
Server: surface failures to restore durable subscriptions (#4795)
# Description SubscriptionManager.RestoreSubscriptionsAsync logged and swallowed every restore failure: a subscription store that threw, a store that reported an unsuccessful restore, and a stored subscription that could not be recreated. The server then started without those subscriptions, the store was told the restore had completed (so it cleaned up what it held), and nothing but a log line showed that subscriptions had been lost. A failed restore now fails the restore, and with it the startup: - a store that throws fails with BadInternalError and the store's exception as the inner exception; - a store that reports Success = false fails with BadInternalError; - stored subscriptions that cannot be recreated are collected and reported as an AggregateException, before the store is told that the restore completed, so the store keeps what it holds. SubscriptionManager.ContinueOnRestoreFailure (default false) restores the previous behaviour for applications that prefer to start without the subscriptions. Cancellation is no longer swallowed unless that option is set. The Quickstarts durable subscription store reported Success = false when it had nothing stored; it now reports a successful restore of no subscriptions, and keeps reporting a failure when its file cannot be read. ## Related Issues None. ## Checklist - [x] 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. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] 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 <223556219+Copilot@users.noreply.github.com> Co-authored-by: Marc Schier <marcschier@users.noreply.github.com> |
||
|
|
7fb986bd0d |
Add recycling operator console and sample container images (#4810)
# Description Add a local browser-based operator console for the synthetic recycling line, using direct, encrypted OPC UA connections and the existing typed coordination interface. - Extract a sample-local, non-packable `Recycling.Client` library shared by the server and console. Preserve the model XML bytes, namespace/type identifiers, resource identity, wire format and journal schema; the server retains its .NET 8/9/10 targets. - Add a .NET 10 ASP.NET Core workstation host with bounded SSE state/trend delivery, native companion inspection, paged material/genealogy reads, and role-limited coordinator commands. Exact command envelopes are retained for explicit retries; uncertain outcomes are not automatically reissued. - Add the operator interface using local HTML/SVG, CSS and JavaScript modules: ten process groups, substep inspection, faults/constraints, persistent quality holds, trends, material tracing and simulator administration. Show raw quality/provenance and handle stale data, run changes, browser expiry and large identifiers explicitly. No browser-generated plant telemetry or frontend framework/npm build is introduced. - Keep application/user credentials in external certificate stores. Use strict X.509 trust, signed/encrypted UA sessions, local browser bootstrap sessions, same-origin/CSRF checks, and a conservative configured control ceiling. This is a local synthetic sample, not a physical emergency-stop or shared remote-user system. - Register `recyclingserver` and `recyclingclient` in the existing Images CI matrix, using source/prebuilt Dockerfile stages and the existing portable-IL/multiarchitecture packaging flow. Add an optional loopback-published Compose console service. No new schedules or NuGet publication entries are added. - Add managed, real-UA, Chromium, NativeAOT and image-contract qualification, with Microsoft.Playwright as a test-only centrally managed dependency, and update usage/deployment documentation. ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [ ] 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. - [ ] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] 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. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> |
||
|
|
44a8cb277d |
plans/ctt-issues.md: record C60 (CTT publish thread stall in the A&C Limit/Level CUs) (#4820)
The CTT regression run on master ( |
||
|
|
849840be5d |
perf: publish/write/event load scenarios for the server load harness (#4817)
Builds on #4815 (merged); rebased onto master. Two commits, touching only `perf/`. Extends `perf/ServerLoadHarness` and `perf/TraceAnalyzer` to profile publish-, write- and event-heavy server load that mirrors the explicit Sessions `LoadTest`. Built for the publish/write load investigation: [findings report](https://claude.ai/artifact/BWuEEPcvvbTAbjY44mJFsa). ## New options | Option | Scenario | Meaning | |---|---|---| | `--scenario events` | | Event subscriptions on the Server object; a server-side `EventGenerator` (in `EventWorkload.cs`) reports events, driven by the server child's new `events <rate>` / `events stop` stdin commands. | | `--event-rate` | `events` | Events/s the server reports; 0 = as fast as one thread can. | | `--writers N` | `sub` | Dedicated writer sessions in addition to `--sessions` (0: the first subscriber session writes, as before). | | `--write-interval 0` | `sub` | Closed-loop writers instead of a timer. | | `--write-batch` | `sub` | Values per Write; writers rotate through the nodes in slices. | | `--node-type all` | `sub` | The LoadTest's Int16…UInt64 mass node set (up to 600 nodes). | | `--queue-size` | `sub`, `events` | Monitored item queue size (default 1 / 10000). | | `--publish-requests` | all classic | Minimum outstanding Publish requests per session. | Also: - `Workload.Secondary` / `OnServerReport` hooks and a `per-op2:` result line: server cost per written value (`sub`) or per generated event (`events`). - TraceAnalyzer `threads` mode: busy share of each thread with its main roles (finds serialized stages). - TraceAnalyzer `cpu <trace> <top> <focus>` additionally lists the focus frame's direct callees. - README rows and paragraphs for all of the above. Conflicts with #4815's rewrite (native workers, `suspend`/`samples` modes) were resolved so #4815's code and text stay and these additions are layered on top. ## Verification Builds: `CustomTestTarget=net10.0 dotnet build perf/ServerLoadHarness -c Release` and `dotnet build perf/TraceAnalyzer -c Release`: 0 warnings, 0 errors. Smoke tests (`--duration 5 --warmup 3`, `DOTNET_gcServer=1 SERVER_DOTNET_gcServer=1`), each run only once foreign CPU was below 1.5 cores, with foreign CPU measured during the run (avg 0.5–0.6 cores): | Run | Result | |---|---| | `events --sessions 5 --subs 3 --event-rate 2000` | generated/s=2009, events/s=30413, received/expected=100.9 %; `per-op2` 588.9 µs CPU / 31.5 KB per generated event | | `sub --sessions 5 --subs 3 --nodes 600 --node-type all --writers 4 --write-interval 0 --write-batch 100` | 131k values/s written, 77k notif/s, writeErrors=0; received/expected=3.9 % (queue size 1 with closed-loop writers: values are overwritten before publishing, as intended); `per-op2` 50.5 µs / 1680 B per written value | | `sub --sessions 2 --subs 1 --nodes 100 --queue-size 2 --publish-requests 4` | received/expected=98.0 %, writeErrors=0 | | same as the second run with `--trace` | 23 MB trace; `TraceAnalyzer threads` lists 58 threads with busy shares and roles; `cpu … 10 MonitoredItem.Publish` prints callers, first Opc.Ua frame and the new "direct callees of" section | #4815's own scenarios still parse: `counters` passes; `native-read` (with `--server-affinity`, `--thread-states`) and `native-counters` start the contract server and fail only at launching the (absent) native worker executable, as expected without the workers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Marc Schier <marcschier@users.noreply.github.com> |
||
|
|
7e25e1a3d7 |
Extract remaining shared companion, rendering and packaging changes (#4806)
# Description The shared AI deployment contracts and instance registration; DI software-upload outcomes, digest handling and simulator; bounded Positioning/Pumps reads; PubSub raw-data/matrix and security-provider handling; Vision read-quality contracts; xRegistry resource writes and instance projection; OpenUSD authoring, embedded viewport and retained-frame capture APIs; standalone sample CLI controls; and the generic Debug/RID package-policy and fork/Dependabot coverage-comment fixes. The Linux native prerequisite step is retained for **Vision tests only**. ## Related Issues - Related tracking issue: [#4461](https://github.com/OPCFoundation/UA-.NETStandard/issues/4461), without automatic closure. - Source work: [#4462](https://github.com/OPCFoundation/UA-.NETStandard/pull/4462), which remains intact for the coordinator's later integration. - Immediate stacked predecessor: [#4804](https://github.com/OPCFoundation/UA-.NETStandard/pull/4804). ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [ ] 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. - [ ] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] 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. |
||
|
|
4e77ba3f84 |
Redundancy: snapshot and restore for RaftSharedKeyValueStore (#4794)
# Description RaftSharedKeyValueStore applies every committed command but offers no way to capture its state. An application that wants to bound the Raft log cannot snapshot the store before compacting its consensus log, and after a restart it cannot restore the state without replaying the whole log. Add CreateSnapshotAsync, which commits a barrier through the consensus and encodes the state in a versioned format, and RestoreSnapshot, which replaces the state before the store starts applying the log. A malformed snapshot raises FormatException; restoring after start raises InvalidOperationException. Tests: RaftSharedKeyValueStoreSnapshotTests (round trip, log applied after restore, restore after start rejected, corrupt snapshot rejected). ## Related Issues None. ## Checklist - [x] 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. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] 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. - [ ] I have addressed **all** PR feedback received. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Marc Schier <marcschier@users.noreply.github.com> |
||
|
|
2de64d7d93 |
Source generator: keep NodeSet2 references to nodes of referenced models (#4796)
# Description A reference in a NodeSet2 input whose target is a node of another model was dropped without a diagnostic when that model is supplied by a referenced assembly rather than by an AdditionalFiles input. The dependency metadata of a referenced assembly carries types only, so the importer could not resolve an instance such as DI's DeviceSet and skipped every reference to it: a device organized by DeviceSet (inverse Organizes) lost that reference, and so did any non-hierarchical reference to such a node. When the target of a reference is in another, non-zero namespace and no loaded design defines it, the importer now creates a stand-in that carries only the NodeId the generated AddReference call needs. The stand-in is not generated. References whose reference type is unknown are still skipped. Tests generate a DI-style NodeSet2 whose device is organized by DeviceSet against DI supplied as a referenced assembly and as an AdditionalFile, and compile the result against the stack. ## Related Issues None. ## Checklist - [x] 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. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] 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. - [ ] I have addressed **all** PR feedback received. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Marc Schier <marcschier@users.noreply.github.com> |
||
|
|
b1c0cd20fd |
Fix classic browse-path subscription restoration (#4814)
# Description Persisted classic subscriptions retain their server/client handles but previously lost relative-path resolution metadata. Transfer could succeed, yet a later `ApplyChangesAsync` attempted to resolve an already-created item and failed. The SDK now owns this restoration lifecycle without exposing a public resolution-state setter. - Preserve the originally monitored target in internal snapshot state and restore both `ResolvedNodeId` and `Status.NodeId`. Transfer/reactivation use handles without re-browsing; fresh creation resolves paths again. - Version classic subscription/configuration files and read existing unversioned 2.0 files using frozen legacy wire layouts. Legacy missing targets remain unknown rather than being guessed. - Exclude existing items from creation-time resolution, reserve creation before asynchronous translation, and reject stale translation results. Failed deletion retains identity and pending retry state. - Support forward-only snapshot input using position tracking and bounded, per-ExtensionObject buffering in the binary decoder, including short reads. No whole-file buffering or reflection-based serialization is introduced. - Add unit/integration regressions and document the supported lifecycle and migration implications. **Compatibility:** Newly written files require the updated SDK. Existing 2.0 files remain readable. Assigning a different `StartNodeId` or `RelativePath` to a created/in-flight item now immediately throws `BadInvalidState`; unchanged assignments remain allowed. No V2 subscription-engine changes or 1.5 XML import are included. ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [ ] 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. - [ ] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] 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. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. |
||
|
|
8e96408b2a |
Fuzz oracles: confirm net48 allocation findings by re-running the target (#4818)
## Problem `FuzzCrashAssets(AflfuzzBinaryDecoder)` failed intermittently on net48 (run 38897795750, test-windows-net48 1/16, on #4815, which changes only `perf/`): > 10562496 bytes allocated for 42 input bytes, budget 8391296 bytes On .NET Framework, `FuzzOracles` measures allocations with `AppDomain.MonitoringTotalAllocatedMemorySize`. That counter covers the whole AppDomain, so it also counts other threads in the test host. Measured on net48 Release for `crash-0bdcbfafc13981ae6f665ad52cabc1cbe21b0238`: | Scenario | Bytes | |---|---| | First decode in the process (one-time XML setup) | 1,083,232 | | Later decodes, same thread or a fresh thread | 16,440 | | Same decode with one concurrent thread allocating | 10.7 to 23.8 MB (over budget) | The decoder does not have a per-thread cost, and #4812 did not regress it. The CI TRX shows that tests ran one at a time. The failure came about 2 s into the run, right after a burst of about 100 fast test cases, which fits vstest serializing results on a background thread. ## Fix `FuzzOracles.RunTarget` re-runs a target when the allocation finding comes from a counter that is not per thread (`AllocationConfirmationRuns = 2`). It reports the finding only if every run confirms it. A real over-budget decode allocates the same amount every time, while a burst on another thread rarely lines up with all three runs. This is the same approach the time oracle already uses (`TimeConfirmationRuns`). Runtimes with a per-thread counter (net8+) behave as before. `FuzzMethods` playback goes through `RunTarget` too, so it also gets the confirmation runs. ## Tests - New tests in `FuzzOraclesTests`: - A finding that every run confirms is still reported, after 3 runs on net48 and 1 run on net8+. - A finding inflated by another thread is ignored (net48 only). This test fails when the confirmation runs are set to 0. - net48 Release: Opc.Ua.Fuzzing.Tests 156/156, Opc.Ua.Encoders.Fuzz.Tests 8130/8130. - net10.0: FuzzOraclesTests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8b56937fdf |
perf: drive the server with external native benchmark workers; TraceAnalyzer GC modes (#4815)
## What this adds Harness-only changes under `perf/` (not part of `UA.slnx`, `net10.0`). No product code changes. ### External native worker scenarios (`ServerLoadHarness`) Two new scenarios run the native open62541 workers of the [o6-automation/opcua-benchmarks](https://github.com/o6-automation/opcua-benchmarks) project against a server, so the .NET server and other OPC UA stacks are measured with the same client as that project's published comparison: | Scenario | What it measures | |---|---| | `native-read` | Fixed-load scalar Read throughput (`throughput/open62541/client.c --worker async`): `--clients` worker processes, each with `--inflight` single-value Reads of ns=1;i=1001..1100 in flight, starting together. Reports aggregate req/s, and server CPU and allocated bytes per request. | | `native-counters` | Subscription capacity (`subscription/native/client.c`): one subscription with `--nodes` UInt64 counters at `--sampling` 10 ms / `--pub` 1000 ms, queue size 1. Passes when every item gets 99 % of the samples its sampling interval calls for. | The result block has the same lines as the other scenarios, and the CPU time of the native workers goes in `client-per-op:`. The server is either the harness child in **contract mode** (`server --contract 1`) or any external executable (`--server-exe`, `--server-args` with `{port}`, `{items}`, `{sampling}` and `{pki}` placeholders), for example the benchmark's open62541 server or one of its .NET workers. By default the harness child is configured like the benchmark's .NET worker for UA-.NETStandard 2.0.1: | Setting | Default | Override | |---|---|---| | Node managers | `AsyncCustomNodeManager` | `--async-node-manager 0` (`CustomNodeManager2`) | | Counter increment | under a lock owned by the counters | `--counter-lock 0` (interlocked) | | Garbage collector | Server GC | `SERVER_DOTNET_gcServer=0` | | `MaxQueuedRequestCount` | clients × in flight + 3 (minimum 200) | `--queue` | Other options: `--native-client`, `--clients`, `--inflight`, `--iterations`, `--warmup-calls`, `--server-affinity` (processor mask; combine it with `SERVER_DOTNET_PROCESSOR_COUNT`, because the mask is applied after the runtime has sized itself), `--client-affinity`, `--server-priority`, `--thread-states 1` (samples Running/Ready server threads and appends a `sched:` line), `--security encrypt` (Basic256Sha256 SignAndEncrypt with generated DER identities) and `--verbose`. Also, the `MinRequestThreadCount` of the server child is now the smaller of 50 and `--threads`, so `--threads` below 50 takes effect. **Worker protocol.** The workers define a few names that are kept as string literals in named constants: the environment variables `O6_BENCHMARK_SECURITY_POLICY`, `O6_BENCHMARK_CERTIFICATE` and `O6_BENCHMARK_PRIVATE_KEY`, the `O6_BENCHMARK_READY` stdout token, the identity application URI / subject prefixes, and the server URI `urn:o6:benchmark:server`. The workers need that server URI because the contract puts its nodes in the namespace of the server URI and the workers address them as ns=1. Identifiers, file names, scenario and option names are neutral. ### TraceAnalyzer - `cpu` with a focus frame also reports the first `Opc.Ua` frame above it, past framework wrappers such as `Monitor.Enter` or `SemaphoreSlim`. - **GC-poll attribution:** the thread-time sampler suspends running threads at their next GC poll, so many on-CPU samples have `Thread.PollGCWorker` as their leaf. `cpu` used to drop them as waits (in one trace only 3.5k of 20k on-CPU samples were kept); it now attributes them to the caller. - `gc` mode (for `--trace-profile gc`): collections by generation, type and reason; the suspension time and the whole pause, each with count, total and percentiles. - `samples` mode: all samples by type (managed/external) and by leaf and first `Opc.Ua` frame, including the ones `cpu` leaves out. - `suspend <trace> [thresholdMs]` mode: every SuspendEE longer than the threshold, with the events recorded inside it by thread (an `AllocationTick` there means a thread that had not reached a safe point yet); event counts including the heap count DATAS chose and GC handle churn by kind. ## Building the workers The benchmark repository is AGPL-3.0, so nothing of it is vendored here. Clone it and build the workers separately. On Windows they build with `zig cc` and CMake against the open62541 commit the benchmark pins (OpenSSL backend, `UA_ENABLE_DEBUG_SANITIZER=OFF`, Release). The subscription workers also need `poll()` on stdin replaced by `PeekNamedPipe`, plus unbuffered control output. `perf/README.md` describes all of this. ## Commits 1. `perf: drive the server with external native benchmark workers` 2. `perf: configure the contract server like the benchmark's 2.0.1 .NET worker` 3. `perf: TraceAnalyzer gc and samples modes, samples taken at GC polls` 4. `perf: scheduling and GC-suspension diagnostics for the native worker runs` Each commit builds with 0 warnings (`CustomTestTarget=net10.0 dotnet build perf/ServerLoadHarness -c Release`, `dotnet build perf/TraceAnalyzer -c Release`), and `dotnet format whitespace` makes no changes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c3701e5804 |
CI CTT: call the private repository through Actions REST endpoints only (#4816)
The first run after configuring the GitHub App ([run](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38895888727)) minted its token correctly but failed on the dispatch: > unable to determine default branch for OPCFoundation/UA-.NETStandard-CTT: GraphQL: Resource not accessible by integration (repository.defaultBranchRef) `gh workflow run` (and other `gh` commands) make GraphQL calls that need Contents access, but the App only has Actions read/write. Dispatch, run lookup, polling and the summary download now use `curl` on the Actions REST API, so the App needs no more than Actions read/write. ## Test plan - [x] `bash -n` on the step script - [x] Run-list filter, run status and `ctt-public-summary` artifact download checked with `curl` against the private repository (earlier run `repeat-1`) - [ ] After merge: Actions → CTT regression → Run workflow on master 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
716858aa7d |
Source generator: option to emit exactly the nodes of a NodeSet2 (#4797)
# Description For a NodeSet2 input the generator built the predefined nodes from the merged instance hierarchy. A subtype therefore got a copy of every Mandatory InstanceDeclaration of its supertypes as a new child node, with a NodeId the NodeSet2 never assigned (the supertype's numeric identifier in the subtype's namespace, or a string identifier derived from the path when the supertype comes from a referenced assembly). A DeviceType subtype got eight such children from DI's DeviceType, so the address space a server loads from the generated code differs from the NodeSet2 it was generated from. ModelSourceGeneratorEmitNodeSetNodesOnly (GeneratorOptions .EmitNodeSetNodesOnly), off by default, makes the predefined nodes of a NodeSet2 input exactly the nodes the NodeSet2 declares: a child the generator derives from a supertype or type definition is not added to a predefined node. The typed state classes are unchanged and still reach inherited members through the state classes of their supertypes, and the CreateInstanceOf* factories still create every Mandatory child of the type. ModelDesign inputs are not affected. IModelDesign.IsSourceNodeSet tells the generator whether the target model was read from a NodeSet2. A test in Opc.Ua.Di.Tests generates a DI-style NodeSet2 with the option on, exports the generated predefined nodes as a NodeSet2 and compares it with the input, node for node and reference for reference. ## Related Issues None. ## Checklist - [x] 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. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] 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. - [ ] I have addressed **all** PR feedback received. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Marc Schier <marcschier@users.noreply.github.com> |
||
|
|
87012a147f |
Machinery: add bounded inspection and result transfers (#4804)
# Description - Add opt-in bounded browsing and strict reads with original source, status, timestamp, and structured-value evidence. Preserve the existing tolerant constructors and convenience projections, and wire options through session extensions, factories, and hosting registration. - Add selected-result streaming downloads with byte/request limits and independent cleanup, using `TemporaryFileTransferClient.CopyToAsync` from the client/server predecessor. Retain the detailed transfer regressions in the Machinery test layer. - Expose genuinely null unpublished result slots, carry the two Machinery sample updates, and include the complete Machinery guide and package README changes. ## Related Issues - Related tracking issue: #4461; this extraction does not close it. - Frozen source PR: #4462; this extraction does not close or modify it. - Immediate predecessor: #4803 (`marcschier-extract-amb-changes`). The earlier dependencies are client/server #4802 and core/types #4801. ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [ ] 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. - [ ] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] 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. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. |
||
|
|
8772f37275 |
Fix recycling scenario defaults and MCP interoperability (#4808)
# Description Fix five failures reproduced while exercising the recycling sample through a locally built OPC UA MCP server: - Support Certificate authentication through named, host-approved X.509 identity selectors and the existing certificate/identity providers. Tool arguments cannot supply private keys or arbitrary certificate-store paths. - Register custom enumeration dependencies from the validated, bounded type snapshot before loading requested structure roots. - Preserve structured fields and opaque encoded bodies in generic read/call results using the selected session's UA codecs. - Add bounded, explicitly typed UA JSON method arguments while retaining the existing string argument behavior and C# overloads. - Preserve omitted defaults in sparse recycling scenario JSON; explicit invalid values and unknown members still fail validation. All six shipped scenario files are covered by regressions. Update the MCP and recycling documentation. This PR contains one fix commit on a feature branch in **OPCFoundation/UA-.NETStandard**, based directly on upstream `master` at `95850b7e5`. No local registration, certificates, scenario journals or live-test artifacts are included. ### Validation **Final commit `d199feba0`: 216 affected MCP tests and 16 recycling CLI/scenario tests passed on .NET 10**, with no failures, fixture setup/teardown errors or build/analyzer warnings. The repository's TRX verdict helper accepted both reports. **Draft: the full-solution gate is not green.** The .NET 10 solution run encountered an `Opc.Ua.OneFuzz.Validator.Tests` fixture-publication failure outside the files changed by this PR: `MSB3030`, missing `Opc.Ua.OneFuzz.TestDependency.runtimeconfig.json`. It affected 210 tests; the run was stopped rather than expanding this PR into unrelated OneFuzz work. The full .NET Framework 4.8 solution suite was not run. Before the final rebase, the complete managed recycling suite passed **117**, and the CLI/scenario tests passed **16 each on .NET 8 and .NET 9**, with no failures or fixture teardown errors. The five explicit NativeAOT smoke cases were not selected. Actual live qualification used one coordinator and all ten independent process servers with strict certificate trust. Healthy, combined-fault and zero-utility runs each produced 24 complete packages with exact component conservation. Also exercised native companion discovery, subscriptions, authorization, pause/step/enable/disable, durable request replay, quality holds, reset and coordinator crash recovery. NativeAOT executables, container deployment and parity against a complete external producer contract were not qualified. <details> <summary>Exact validation commands and results</summary> Commands ran from the repository root with an isolated .NET SDK 10.0.401. `$root` denotes the private validation-artifact directory; no machine-specific path or key material is included here. Affected MCP matrix, final commit — **passed: 216; failed: 0; skipped: 0**: ```powershell dotnet test tests\Opc.Ua.Tools.Tests\Opc.Ua.Tools.Tests.csproj -c Release -f net10.0 -p:CustomTestTarget=net10.0 -p:CustomTargetFrameworks=net10.0 -p:UseSharedCompilation=false -m:1 -nr:false --no-restore --nologo -v:quiet --filter 'FullyQualifiedName~SchemaRemoteTests|FullyQualifiedName~OpcUaJsonHelperTests|FullyQualifiedName~MethodServiceToolsTests|FullyQualifiedName~ConvenienceToolsTests|FullyQualifiedName~AttributeServiceToolsTests|FullyQualifiedName~ViewServiceToolsTests|FullyQualifiedName~OpcUaSessionManagerTests|FullyQualifiedName~ConnectionToolsTests|FullyQualifiedName~McpHostBuilderTests|FullyQualifiedName~ConfigurationToolsTests' --logger 'trx;LogFileName=mcp-pr.trx' --results-directory "$root\pr-validation\focused" ``` .NET 10 CLI/scenario regressions, final commit — **passed: 16; failed: 0; skipped: 0**: ```powershell dotnet test tests\Opc.Ua.Recycling.Samples.Tests\Opc.Ua.Recycling.Samples.Tests.csproj -c Release -f net10.0 -p:CustomTestTarget=net10.0 -p:CustomTargetFrameworks=net10.0 -p:UseSharedCompilation=false -m:1 -nr:false --no-restore --nologo -v:quiet --filter 'FullyQualifiedName~RecyclingCommandLineTests' --logger 'trx;LogFileName=recycling-cli-pr.trx' --results-directory "$root\pr-validation\focused" ``` Complete managed recycling suite, before rebase — **passed: 117; failed: 0; skipped: 0** (five explicit NativeAOT cases not selected): ```powershell dotnet test tests\Opc.Ua.Recycling.Samples.Tests\Opc.Ua.Recycling.Samples.Tests.csproj -c Release -f net10.0 -p:CustomTestTarget=net10.0 -p:CustomTargetFrameworks=net10.0 -p:UseSharedCompilation=false -m:1 -nr:false --no-restore --nologo -v:quiet --logger 'trx;LogFileName=recycling-final.trx' --results-directory (Join-Path $root 'results') ``` .NET 8 CLI/scenario regressions, before rebase — **passed: 16; failed: 0; skipped: 0**: ```powershell dotnet test tests\Opc.Ua.Recycling.Samples.Tests\Opc.Ua.Recycling.Samples.Tests.csproj -c Release -f net8.0 -p:CustomTestTarget=net8.0 -p:CustomTargetFrameworks=net8.0 -p:UseSharedCompilation=false -m:1 -nr:false --nologo -v:quiet --filter 'FullyQualifiedName~RecyclingCommandLineTests' --logger 'trx;LogFileName=recycling-cli-net8.trx' --results-directory "$root\results" ``` .NET 9 CLI/scenario regressions, before rebase — **passed: 16; failed: 0; skipped: 0**: ```powershell dotnet test tests\Opc.Ua.Recycling.Samples.Tests\Opc.Ua.Recycling.Samples.Tests.csproj -c Release -f net9.0 -p:CustomTestTarget=net9.0 -p:CustomTargetFrameworks=net9.0 -p:UseSharedCompilation=false -m:1 -nr:false --nologo -v:quiet --filter 'FullyQualifiedName~RecyclingCommandLineTests' --logger 'trx;LogFileName=recycling-cli-net9.trx' --results-directory "$root\results" ``` Full .NET 10 solution — **not passed**: stopped after the OneFuzz fixture failure above; incomplete solution run, not a clean validation result: ```powershell dotnet test UA.slnx -c Release -p:CustomTestTarget=net10.0 -p:CustomTargetFrameworks=net10.0 -p:UseSharedCompilation=false -m:1 -nr:false --nologo -v:quiet --logger trx --results-directory "$root\pr-validation\net10" ``` </details> ## Related Issues Follow-up to [#4772](https://github.com/OPCFoundation/UA-.NETStandard/pull/4772), which introduced the recycling sample. No separate tracking issue is currently linked. These fixes preserve the existing architecture and address failures reproduced during live qualification. ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [ ] 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. - [ ] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] 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. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. Regression tests were added and passed as recorded above; aggregate coverage was not collected, so the combined coverage checklist item remains unchecked. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com> |
||
|
|
68f062eecc |
Server request path: one response header, single-node Read without collections, codec streams reused per thread (S1-S6, -38 % allocation per Read) (#4812)
## Description
Six small changes to the per-request path from the o6 benchmark gap
report (S1–S6), chosen from the allocation profile of the o6 read
benchmark (`AsyncCustomNodeManager`, Server GC, 10 clients × 100
single-value Reads in flight). Each change is its own commit. The
receive loop, its threading and its dispatch are not touched; there are
no wire, public API or generator changes.
### Results
Paired A/B runs, builds alternated per round, 5 rounds unless noted,
medians (min..max). Baseline: master
|
||
|
|
b3c1e60f46 |
AMB: add bounded strict reads and configurable sample hosting (#4803)
# Description Extract the AMB layer from #4462 onto the client/server predecessor #4802, then address the four review findings in `6428686ae45724510bd6bd00619f4992f3d36e46`. The initial extraction (`95b0caa1ac30d1fdacaf7a780f19c5f97e6fd02d`) contained exactly 19 paths: 12 AMB client files, four AMB test files, two sample entry points and `docs/AMB.md`. Its code/tests matched the frozen source `5b03e9afa492cdb36049ee52ae844f630f89d8db...390c4a7a51b71da40caec2d22ef74518bbe670ce`; only the two agreed UaLens-only documentation passages were omitted. The review follow-up intentionally changes that extracted implementation and adds the related factory/registration tests and package documentation. No UaLens, Machinery, Pumps, Positioning, native-rendering, build or CI changes are introduced by this layer. The AMB client provides opt-in bounded traversal, strict response/type validation and original `AssetReadValue` evidence while preserving tolerant defaults. The samples expose validated host, port, PKI root, certificate auto-accept and bounded-runtime options, with unchanged standalone defaults. The review corrections are: - Accept header-validated empty BrowseNext release responses required by Part 4 §5.9.3.2, as well as the stack's single-result response. Invalid cleanup results still preserve the original failure. - Keep event observation tolerant of denied/limited maintenance-class discovery without caching incomplete classes. Subsequent strict reads retry discovery and still report errors. - Accept typed-null ExtensionObjects only in the nullable structure-field checks, preserving original evidence without weakening scalar validation. - Pass optional immutable limits through `AddAssetManagementClient`, the registered factory/delegate and directly constructed factories. Preserve existing overloads and tolerant behavior when options are omitted; reject invalid limits before opening a session. ### Review regression evidence | Requirement | Evidence | | --- | --- | | Accept empty cleanup responses without masking cancellation or limits | `OneBeyondReferenceLimitReleasesAcquiredContinuationAsync` and `CancellationWhileAwaitingNextReleasesTheSubmittedContinuationAsync`, each with empty and single-result responses; malformed responses remain covered | | Preserve observation fallback and do not cache incomplete classes | `ObservationFallsBackWithoutCachingIncompleteClassesAsync`: both observation APIs, denied browsing and exhausted budget, correct filter/event evidence, and a subsequent strict rediscovery | | Preserve typed-null structure evidence without weakening scalar checks | `ContextAndEntryReadsKeepSourceAndNullStructureEvidenceAsync`, `MaintenanceTypedNullStructuresKeepOriginalEvidenceAsync`, `ScalarHealthRejectsTypedNullStructuresAsync`, plus wrong-non-null-structure rejection tests | | Apply configured hosted limits while retaining tolerant defaults | `HostedClientOptionsReachClientsAsync`: registered factory, registered delegate and direct factory, singleton and builder registration, actual one-reference limit and read evidence; `InvalidHostedLimitsAreRejectedBeforeOpeningASession` | ### Validation All commands below ran in this layer's worktree with the existing pinned .NET SDK 10.0.401. No SDK installation, global.json change, registry/path-policy change, test suppression or analyzer suppression was made. The short owned NUnit directory `D:\a61r` and its generated certificates/state were removed afterward. **Latest full AMB suite: Passed: 285, Failed: 0, Skipped: 0 on each framework.** Both builds emitted no compiler/analyzer diagnostics. TRX files confirm the regression cases executed on both TFMs. ```powershell & 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe' test 'tests\Opc.Ua.AMB.Tests\Opc.Ua.AMB.Tests.csproj' --no-restore --configuration Release --framework net10.0 -p:CustomTestTarget=net10.0 --nologo --verbosity minimal --logger 'trx;LogFileName=amb-feedback-net10.trx' --results-directory 'C:\Users\mschier\.copilot\session-state\61d7ed73-c89e-406a-a049-1d8ba9681308\files\TestResults\feedback-net10' -- 'NUnit.WorkDirectory=D:\a61r' & 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe' test 'tests\Opc.Ua.AMB.Tests\Opc.Ua.AMB.Tests.csproj' --configuration Release --framework net48 -p:CustomTestTarget=net48 --nologo --verbosity minimal --logger 'trx;LogFileName=amb-feedback-net48.trx' --results-directory 'C:\Users\mschier\.copilot\session-state\61d7ed73-c89e-406a-a049-1d8ba9681308\files\TestResults\feedback-net48' -- 'NUnit.WorkDirectory=D:\a61r' ``` **Focused regression run: Passed: 127, Failed: 0, Skipped: 0.** Before the production fixes, the focused repro executed 112 cases with 16 failures reproducing all four reported defects. Additional registration/validation cases were included in the final run. ```powershell & 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe' test 'tests\Opc.Ua.AMB.Tests\Opc.Ua.AMB.Tests.csproj' --no-restore --configuration Release --framework net10.0 -p:CustomTestTarget=net10.0 --nologo --verbosity minimal --filter 'FullyQualifiedName~AmbBoundedClientTests|FullyQualifiedName~AmbClientTests|FullyQualifiedName~HostedClientOptionsReachClientsAsync' --logger 'trx;LogFileName=amb-feedback-focused.trx' --results-directory 'C:\Users\mschier\.copilot\session-state\61d7ed73-c89e-406a-a049-1d8ba9681308\files\TestResults\feedback-focused' -- 'NUnit.WorkDirectory=D:\a61r' ``` **Whitespace and newly introduced style findings: passed.** `git diff --check` and the following commands exited 0: ```powershell & 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe' format whitespace --folder --verify-no-changes --verbosity quiet --include 'src\Opc.Ua.AMB.Client\AmbClient.Bounds.cs' 'src\Opc.Ua.AMB.Client\AmbClient.Health.cs' 'src\Opc.Ua.AMB.Client\AmbClient.Structure.cs' 'src\Opc.Ua.AMB.Client\AmbClient.Values.cs' 'src\Opc.Ua.AMB.Client\AmbClientFactory.cs' 'src\Opc.Ua.AMB.Client\Hosting\OpcUaAmbClientBuilderExtensions.cs' 'tests\Opc.Ua.AMB.Tests\AmbBoundedClientTests.cs' 'tests\Opc.Ua.AMB.Tests\AmbBoundedClientTests.Traversal.cs' 'tests\Opc.Ua.AMB.Tests\AmbBoundedClientTests.Values.cs' 'tests\Opc.Ua.AMB.Tests\AmbClientServerE2eTests.cs' 'tests\Opc.Ua.AMB.Tests\AmbClientTests.cs' $env:CustomTestTarget = 'net10.0' $env:Configuration = 'Release' & 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe' format style 'tests\Opc.Ua.AMB.Tests\Opc.Ua.AMB.Tests.csproj' --no-restore --verify-no-changes --severity info --diagnostics IDE0039 --verbosity quiet --include 'tests\Opc.Ua.AMB.Tests\AmbClientServerE2eTests.cs' 'tests\Opc.Ua.AMB.Tests\AmbClientTests.cs' ``` The broader formatter check was **not clean**. It reported pre-existing informational suggestions and design-time `NUnit1003` diagnostics for generated browse-name constants, including pre-existing test cases. Those diagnostics do not occur in the actual builds, which compile and execute those cases successfully. Two new local-function suggestions were corrected and rechecked above; no unrelated baseline cleanup or suppression was included. ```powershell $env:CustomTestTarget = 'net10.0' & 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe' format 'tests\Opc.Ua.AMB.Tests\Opc.Ua.AMB.Tests.csproj' --no-restore --verify-no-changes --severity info --verbosity quiet --include 'src\Opc.Ua.AMB.Client\AmbClient.Bounds.cs' 'src\Opc.Ua.AMB.Client\AmbClient.Health.cs' 'src\Opc.Ua.AMB.Client\AmbClient.Structure.cs' 'src\Opc.Ua.AMB.Client\AmbClient.Values.cs' 'src\Opc.Ua.AMB.Client\AmbClientFactory.cs' 'src\Opc.Ua.AMB.Client\Hosting\OpcUaAmbClientBuilderExtensions.cs' 'tests\Opc.Ua.AMB.Tests\AmbBoundedClientTests.cs' 'tests\Opc.Ua.AMB.Tests\AmbBoundedClientTests.Traversal.cs' 'tests\Opc.Ua.AMB.Tests\AmbBoundedClientTests.Values.cs' 'tests\Opc.Ua.AMB.Tests\AmbClientServerE2eTests.cs' 'tests\Opc.Ua.AMB.Tests\AmbClientTests.cs' ``` <details> <summary>Initial extraction validation and environment history</summary> The original extraction passed all 263 AMB tests on both TFMs. Its initial net48 run had one unchanged maintenance-test server-start timeout; shortening only NUnit.WorkDirectory resolved it, consistent with certificate-store path lengths. This follow-up used a short directory from the start and all 285 cases passed on both frameworks. A no-restore test invocation during the follow-up initially executed no tests; it was not counted as passing evidence. A no-restore build identified `NETSDK1005` (missing net10.0 target assets after switching TFMs). The normal test invocation restored the target assets. Initial regression-test compilation issues were corrected before capturing the 16 failing behavioral cases. Both samples were built during extraction on both TFMs; each command below passed with 0 warnings and 0 errors. They were not rebuilt during this review follow-up, which does not modify the samples. ```powershell & 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe' build 'samples\AMB\AmbClient\AmbClient.csproj' --configuration Release --framework net10.0 -p:CustomTestTarget=net10.0 --nologo --verbosity quiet & 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe' build 'samples\AMB\AmbServer\AmbServer.csproj' --configuration Release --framework net10.0 -p:CustomTestTarget=net10.0 --nologo --verbosity quiet & 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe' build 'samples\AMB\AmbClient\AmbClient.csproj' --configuration Release --framework net48 -p:CustomTestTarget=net48 --nologo --verbosity quiet & 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe' build 'samples\AMB\AmbServer\AmbServer.csproj' --configuration Release --framework net48 -p:CustomTestTarget=net48 --nologo --verbosity quiet ``` </details> This remains a draft: the complete `UA.slnx` net48/net10.0 matrix, coverage, CI/CodeQL and CLA status are not established by this focused validation. No coverage increase or complete-solution pass is claimed. The feedback commit is based on the original AMB extraction; integration of later lower-layer updates is coordinated separately, without rewriting this branch or native stack 4807. ## Related Issues - References #4461, the existing tracking issue; this PR does not close it. - Extracted from #4462 at frozen head `390c4a7a51b71da40caec2d22ef74518bbe670ce`; the source PR is not closed by this PR. - Immediate predecessor: #4802 (`marcschier-extract-client-and-server`), above core/types #4801. The original extraction parent was `86ff10d3994f132f7343a929abdae209bc2e69b7`. This is an isolated extraction and review correction of the existing tracked implementation, not a claim of new architectural sign-off. ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [ ] 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. - [ ] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] 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. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [x] I have addressed **all** PR feedback received. |
||
|
|
1f6700c30b |
Fail closed when reconnect cannot preserve endpoint security (#4809)
# Description It was reported that endpoint refresh during reconnect (`UpdateBeforeConnect` / reconnect endpoint update) could silently change a connection's security configuration (e.g. from `SignAndEncrypt` to a different mode/policy) without the application knowing. Investigation showed the hard downgrade paths had already been fixed, but the legacy `SessionReconnectHandler` still had a composite-score fallback that could select a *different* security mode/policy when the original combination was no longer offered by the (unauthenticated) discovery reply. A stronger mode could offset a weaker or deprecated policy in that score. Managed failover/alternate-network endpoints also had no explicit mode/policy guard. Changes: - **`SessionReconnectHandler`**: removed `SelectFallbackEndpoint` and the alternate-policy fallback. If the exact mode/policy is unavailable, reconnect fails (logged as an error) instead of changing security. - **`ManagedSession`**: added a mode/policy mismatch check; mismatched alternate-network endpoints, reactivation targets and failover/recreation targets are rejected with `BadSecurityModeRejected` / `BadSecurityPolicyRejected` before a channel is opened. - Endpoint refresh for **certificate rotation is retained**; new server certificates are still subject to normal trust validation. - **Docs** (`docs/Sessions.md`): documents exact mode/policy preservation during recovery and that `UpdateBeforeConnect` does not authorize security negotiation. Changing policy requires explicit reconfiguration and a new connection. - **Tests**: replaced fallback-selection tests with a fail-closed legacy reconnect test; added managed alternate-endpoint policy mismatch and failover mode mismatch tests. Validation: - `dotnet test tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj -f net10.0 --filter "FullyQualifiedName~SessionReconnectHandlerTests|FullyQualifiedName~ManagedSessionTests"` — passed (48/48) - `dotnet test tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj -f net10.0 --filter "FullyQualifiedName~ConfiguredEndpointTests"` — passed (46/46) - Full UA.slnx suite and net48 tests: not run locally. ## Related Issues - Reported security concern: reconnect endpoint refresh could change the session's security configuration without notifying the application. ## 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. - [x] 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. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
1b7db89f26 |
PKCS#11 store: write and delete certificate objects on the token (#4792)
# Description Pkcs11CertificateStore.AddAsync returned success without writing anything, so a certificate added to a token backed store (a trust list update, a renewed application certificate) was silently lost. DeleteAsync always threw NotSupportedException. AddAsync now writes the certificate as an X.509 certificate object with CKA_VALUE, CKA_SUBJECT, CKA_ISSUER, CKA_SERIAL_NUMBER, CKA_LABEL and a CKA_ID that links it to the key it belongs to: the id the store's object filter selects, else the id of a certificate on the token with the same public key, else the id of the key whose modulus (RSA) or EC point matches, else the SHA-1 hash of the public key. A renewed certificate for a key generated on the token is therefore paired with that key by LoadPrivateKeyAsync. A certificate the token already holds is not written again. A private key offered with the certificate is still never written to the token; it is counted in RejectedPrivateKeyWrites as before. DeleteAsync destroys the certificate objects with the thumbprint and returns whether there were any. Key objects are never destroyed. The PKCS#11 test double now supports C_CreateObject and C_DestroyObject. Tests cover both operations against it and, in the token backed fixture, against SoftHSM2. The two tests that pinned the old behaviour (a dropped write, a delete that throws) are removed. ## Related Issues None. ## Checklist - [x] 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. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] 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. - [ ] I have addressed **all** PR feedback received. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Marc Schier <marcschier@users.noreply.github.com> |
||
|
|
e78d36ce4b |
Client/server transfer, WSS, and hosting fixes (#4802)
# Description Layer 2 of the upstream non-UaLens extraction, with core/types prerequisite #4801 (`marcschier-extract-core-and-types`). The initial extraction commit `86ff10d3994f132f7343a929abdae209bc2e69b7` started from `8881e096fb5e4eaa36f3c13f8caa57733373bd19` and copied exactly 13 client/server paths from #4462 at frozen head `390c4a7a51b71da40caec2d22ef74518bbe670ce`, using its original base `5b03e9afa492cdb36049ee52ae844f630f89d8db`. Those initial blobs were source-identical; the core/types layer was not duplicated or rewritten. - Add bounded temporary-file copying with byte/request limits, session ownership checks, and independent bounded cleanup. - Refresh revised monitored-item parameters after snapshot transfer and notify the target handler after successful handle synchronization. - Apply WSS opening deadlines while preserving caller cancellation, and cover the wire envelope and request timeout behavior. - Make hosted server presets honor role configuration without replacing an explicitly registered role manager; include the corresponding tests and documentation. **Review follow-up:** commit `a5c8fe0884ad3346106529deba71d93c5df20a9a` intentionally corrects three defects in the extracted bounded-copy implementation. It captures one `SessionRequestContext` for managed transfers, calls the standard `FileType.Close` declaration without metadata discovery, and retains valid generation handles before rejecting non-Good status or malformed metadata. Read/Close remain strictly validated, and transfer/cleanup failures remain independently visible. The follow-up also adds 21 direct-client regression cases and updates `docs/FileSystemClient.md`, bringing this PR to 15 changed paths. The current bounded-copy implementation is therefore intentionally no longer identical to the frozen source. No UaLens projects/UI, AMB or Machinery implementations, samples, CI/native renderer changes, release changes, or generic infrastructure files are included. The follow-up does not merge/rebase a lower or upper stack layer. **Current review-fix validation:** normal analyzers and warnings-as-errors remained enabled, using the existing SDK **10.0.401** without changing `global.json`, installing an SDK, or adding suppressions. The final file-system and session-context selection passed **241 tests on net10.0 and 241 tests on net48**, with **0 failed and 0 skipped** on each. Both selections include all 21 new regressions. Before the production fix, the first 20-case regression suite had 13 failures and 7 passes, reproducing the lost cleanup, cancellation, session-recreation and Close-discovery defects. An additional deterministic recovery-gate race case was then added and passed on both frameworks. Informational-severity formatting/style/analyzer checks passed for the changed C# files. | Review requirement | Direct regression evidence | | --- | --- | | Pin the managed generation through copying and cleanup | `CopyRejectsRecreatedManagedGenerationAsync`, `CopyRejectsRecoveryBetweenGenerationAndReadDispatchAsync` | | Preserve a generation response that returns after cancellation | `ManagedCopyClosesHandleReturnedAfterCancellationAsync` with immediate and delayed responses through a real managed transport lease | | Do not spend the Close deadline discovering metadata | `CopyUsesDeclarationCloseWithoutDiscoveryAsync` for ordinary and managed sessions | | Retain valid handles before rejecting generation status/metadata | `CopyClosesHandleBeforeRejectingGenerationStatusAsync`, `CopyClosesHandleFromMalformedGenerationResponseAsync` | | Preserve error semantics and strict Read/Close validation | `CopyRejectsGenerationWithoutInventingHandleAsync`, `CopyPreservesGenerationAndCleanupFailuresAsync`, `CopyKeepsReadAndCloseStatusValidationAsync` | <details> <summary>Exact review-fix validation commands and results</summary> Commands ran from this layer's repository root. The following variables expand the actual executable, filters and results paths. Console logs and TRX reports were retained in the session artifacts. ```powershell $dotnet = 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe' $env:DOTNET_ROOT = 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk' $env:DOTNET_ROOT_X64 = $env:DOTNET_ROOT $env:DOTNET_CLI_TELEMETRY_OPTOUT = '1' $client = 'tests\Opc.Ua.Client.Tests\Opc.Ua.Client.Tests.csproj' $filter = 'FullyQualifiedName~Opc.Ua.Client.Tests.FileSystem|FullyQualifiedName~Opc.Ua.Client.Tests.SessionContexts' $r10 = 'C:\Users\mschier\.copilot\session-state\9b51b89f-b5ed-4ed7-a097-cca67efe7b0e\files\feedback-net10' $r48 = 'C:\Users\mschier\.copilot\session-state\9b51b89f-b5ed-4ed7-a097-cca67efe7b0e\files\feedback-net48' # Before modifying production code, after the new tests compiled: & $dotnet test $client --no-restore -c Release -f net10.0 -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~TemporaryFileTransferClientBoundedReadTests' --logger 'trx;LogFileName=bounded-copy-feedback-red.trx' --results-directory $r10 --verbosity quiet # Expected regression failure: Passed: 7, Failed: 13, Skipped: 0. # After the production fix, before adding the explicit queued-recovery case: & $dotnet test $client --no-restore -c Release -f net10.0 -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~TemporaryFileTransferClientBoundedReadTests' --logger 'trx;LogFileName=bounded-copy-feedback-green.trx' --results-directory $r10 --verbosity quiet # Passed: 20, Failed: 0, Skipped: 0. # Final complete affected selections, including all 21 new cases: & $dotnet test $client --no-restore -c Release -f net48 -p:CustomTestTarget=net48 --filter $filter --logger 'trx;LogFileName=file-system-context-feedback-final.trx' --results-directory $r48 --verbosity quiet # Passed: 241, Failed: 0, Skipped: 0. & $dotnet test $client -c Release -f net10.0 -p:CustomTestTarget=net10.0 --filter $filter --logger 'trx;LogFileName=file-system-context-feedback-final.trx' --results-directory $r10 --verbosity quiet # Passed: 241, Failed: 0, Skipped: 0. $sourceFile = 'D:\git\copilot-worktrees\UA-wot-d3-final\marcschier-improved-invention\src\Opc.Ua.Client\FileSystem\TemporaryFileTransferClient.BoundedRead.cs' $testFile = 'D:\git\copilot-worktrees\UA-wot-d3-final\marcschier-improved-invention\tests\Opc.Ua.Client.Tests\FileSystem\TemporaryFileTransferClientBoundedReadTests.cs' $env:CustomTestTarget = 'net48' & $dotnet format 'src\Opc.Ua.Client\Opc.Ua.Client.csproj' --include $sourceFile --no-restore --verify-no-changes --severity info --verbosity quiet # Passed, exit 0. $env:CustomTestTarget = 'net10.0' & $dotnet format $client --include $testFile --no-restore --verify-no-changes --severity info --verbosity quiet # Passed, exit 0. git diff --check # Passed. CRLF, final newlines and the 120-column C# limit were also checked. ``` Initial no-restore test attempts after switching frameworks exited without executing tests and were not counted as passes. Diagnostic `dotnet build ... --no-restore` runs reported `NETSDK1005` for the missing framework assets; normal restore was then allowed. Test-authoring compile errors were corrected before recording the red run, without suppressions. Intermediate broader selections passed 240/240 on each framework before the queued-recovery case increased the final count to 241. </details> **Initial-extraction validation (commit `86ff10d3994f132f7343a929abdae209bc2e69b7`):** these results are historical, not a claim that the broader initial selections were rerun after the review fix. The server test assembly was built as a dependency of the client project before its `--no-build` runs. | Initial targeted scope | net10.0 | net48 | | --- | --- | --- | | Client subscriptions and file system | Passed: 546, Failed: 0, Skipped: 0 | Passed: 542, Failed: 0, Skipped: 0 | | Hosted roles, identity, composition, fluent APIs and builder coverage | Passed: 164, Failed: 0, Skipped: 0 | Passed: 164, Failed: 0, Skipped: 0 | | WebAPI WSS transport | Passed: 37, Failed: 0, Skipped: 0 | Not supported: the repository excludes this .NET 8+ test project on net48 | | V2 transfer and snapshot integration | Passed: 6, Failed: 0, Skipped: 0 | Passed: 6, Failed: 0, Skipped: 0 | The first broader net48 hosting run had **9 startup timeouts and certificate leaks** (155 passed). A single startup case reproduced the failure. With only a shorter `NUnit.WorkDirectory`, the isolated case and then all 164 hosting cases passed, with certificate leak tracking enabled and no teardown leaks. This is consistent with the fixture's documented .NET Framework PKI path-length constraint; no source or diagnostic suppression was changed. Both failed runs and successful reruns are recorded below. **Deferred coverage:** the broader domain-level `MachineryResultTransferTests*.cs` fixtures remain in Machinery layer #4804. They were not imported or run here. This review follow-up adds direct-client coverage for the three defects instead. The Machinery owner has been informed that the old expectations for failed Close discovery and zero cleanup after a returned non-Good generation response must be reconciled when the lower fixes are propagated. **Not run:** complete `UA.slnx` suites on net48/net10.0, a coverage measurement, CI/CodeQL completion, native/fuzz qualifications, or package publishing. This remains a draft pending full-stack integration and CI evidence. <details> <summary>Initial-extraction validation command arguments and outcomes</summary> All commands ran from this layer's repository root. Variables below expand the executable, project paths, filters and results directories used by the actual commands; test output was also retained in session-local log files. ```powershell $dotnet = 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe' $env:DOTNET_ROOT = 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk' $env:DOTNET_ROOT_X64 = $env:DOTNET_ROOT $env:DOTNET_CLI_TELEMETRY_OPTOUT = '1' $r10 = 'C:\Users\mschier\.copilot\session-state\9b51b89f-b5ed-4ed7-a097-cca67efe7b0e\files\validation-net10' $r48 = 'C:\Users\mschier\.copilot\session-state\9b51b89f-b5ed-4ed7-a097-cca67efe7b0e\files\validation-net48' $client = 'tests\Opc.Ua.Client.Tests\Opc.Ua.Client.Tests.csproj' $server = 'tests\Opc.Ua.Server.Tests\Opc.Ua.Server.Tests.csproj' $webapi = 'tests\Opc.Ua.Bindings.Https.WebApi.Tests\Opc.Ua.Bindings.Https.WebApi.Tests.csproj' $subscriptions = 'tests\Opc.Ua.Subscriptions.Tests\Opc.Ua.Subscriptions.Tests.csproj' $clientFilter = 'FullyQualifiedName~Opc.Ua.Client.Subscriptions|FullyQualifiedName~Opc.Ua.Client.Tests.FileSystem' $serverFilter = 'FullyQualifiedName~HostedRolePresetTests|FullyQualifiedName~IdentityHostingTests|FullyQualifiedName~ServerFluentApiHostingTests|FullyQualifiedName~ServerCompositionHostingTests|FullyQualifiedName~OpcUaServerBuilderExtensionsCoverageTests' $subscriptionFilter = 'FullyQualifiedName~Opc.Ua.Subscriptions.Tests.TransferSubscriptionTests|FullyQualifiedName~Opc.Ua.Subscriptions.Tests.SubscriptionSnapshotTests' & $dotnet --version # Passed: 10.0.401. & $dotnet test $client --no-restore -c Release -f net10.0 -p:CustomTestTarget=net10.0 --filter $clientFilter --logger 'trx;LogFileName=client-net10.trx' --results-directory $r10 --verbosity quiet # Exit 0 but no tests executed: missing restore metadata. NOT counted as validation. & $dotnet build $client --no-restore -c Release -f net10.0 -p:CustomTestTarget=net10.0 --verbosity quiet # Failed: NETSDK1004, missing obj\project.assets.json. Normal restore was then allowed in test commands. & $dotnet test $client -c Release -f net10.0 -p:CustomTestTarget=net10.0 --filter $clientFilter --logger 'trx;LogFileName=client-net10.trx' --results-directory $r10 --verbosity quiet # Passed: 546, Failed: 0, Skipped: 0. & $dotnet test $server --no-build --no-restore -c Release -f net10.0 -p:CustomTestTarget=net10.0 --filter $serverFilter --logger 'trx;LogFileName=server-net10.trx' --results-directory $r10 --verbosity quiet # Passed: 164, Failed: 0, Skipped: 0. & $dotnet test $webapi -c Release -f net10.0 -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~WebApiWssTransportChannelTests' --logger 'trx;LogFileName=webapi-net10.trx' --results-directory $r10 --verbosity quiet # Passed: 37, Failed: 0, Skipped: 0. & $dotnet test $client -c Release -f net48 -p:CustomTestTarget=net48 --filter $clientFilter --logger 'trx;LogFileName=client-net48.trx' --results-directory $r48 --verbosity quiet # Passed: 542, Failed: 0, Skipped: 0. & $dotnet test $server --no-build --no-restore -c Release -f net48 -p:CustomTestTarget=net48 --filter $serverFilter --logger 'trx;LogFileName=server-net48.trx' --results-directory $r48 --verbosity quiet # Failed: Passed: 155, Failed: 9, Skipped: 0; startup timeouts and 9 undisposed certificates at teardown. & $dotnet test $subscriptions -c Release -f net48 -p:CustomTestTarget=net48 --filter $subscriptionFilter --logger 'trx;LogFileName=subscriptions-net48.trx' --results-directory $r48 --verbosity quiet # Passed: 6, Failed: 0, Skipped: 0. & $dotnet test $subscriptions -c Release -f net10.0 -p:CustomTestTarget=net10.0 --filter $subscriptionFilter --logger 'trx;LogFileName=subscriptions-net10.trx' --results-directory $r10 --verbosity quiet # Passed: 6, Failed: 0, Skipped: 0. & $dotnet msbuild $webapi -p:CustomTestTarget=net48 -p:TargetFramework=net48 -getProperty:TargetFramework,RestrictForLegacyTfm,_RestrictedToLegacyTfm,IsTestProject,EnableDefaultCompileItems # Passed: net48 / true / true / false / false. Confirms the net48 project is an empty non-test shell. # Each following diagnostic invocation had certificate allocation tracking enabled. $env:OPCUA_CERTIFICATE_LEAK_TRACKING = '1' & $dotnet test $server --no-build --no-restore -c Release -f net48 -p:CustomTestTarget=net48 --filter 'FullyQualifiedName~HostedRolePresetTests' --logger 'trx;LogFileName=hosted-role-net48.trx' --results-directory $r48 --verbosity quiet # Passed: 10, Failed: 0, Skipped: 0. & $dotnet test $server --no-build --no-restore -c Release -f net48 -p:CustomTestTarget=net48 --filter 'FullyQualifiedName~ServerFluentApiHostingTests.AddServerWithCustomServerStartsCustomServerAsync' --logger 'trx;LogFileName=hosting-startup-net48.trx' --results-directory $r48 --verbosity quiet # Failed: Passed: 0, Failed: 1, Skipped: 0; 30-second startup timeout and one certificate leak. # A fresh, dedicated short working directory was created for the following two runs. & $dotnet test $server --no-build --no-restore -c Release -f net48 -p:CustomTestTarget=net48 --filter 'FullyQualifiedName~ServerFluentApiHostingTests.AddServerWithCustomServerStartsCustomServerAsync' --logger 'trx;LogFileName=hosting-startup-net48-shortpath.trx' --results-directory $r48 --verbosity quiet -- 'NUnit.WorkDirectory=D:\Temp\opc-l2-6caf4556' # Passed: 1, Failed: 0, Skipped: 0; no certificate leak. & $dotnet test $server --no-build --no-restore -c Release -f net48 -p:CustomTestTarget=net48 --filter $serverFilter --logger 'trx;LogFileName=server-net48-shortpath.trx' --results-directory $r48 --verbosity quiet -- 'NUnit.WorkDirectory=D:\Temp\opc-l2-6caf4556' # Passed: 164, Failed: 0, Skipped: 0; no certificate leak or assembly-level teardown error. # The dedicated short working directory and its generated PKI contents were removed afterward. ``` TRX inspection confirmed all initially added client (3), WSS (4), and hosted-role (10) regression cases executed on supported frameworks. The initial 13-path set and source blobs were verified before the extraction commit. A separate mistyped SDK availability probe (`uals-528-deep-repro` instead of `ualens-528-deep-repro`) failed before launching any process; all actual builds/tests used the correct verified SDK above. </details> ## Related Issues Related to #4461; extracted from source PR #4462. Neither is closed by this PR. Prerequisite: #4801, the core/types layer. _If this is a relatively large or complex change, a design must have been discussed in the related tracking issue and signed off (which becomes the Architectural Decision Record (ADR))._ ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [ ] 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. - [ ] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] 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. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [x] I have addressed **all** PR feedback received. |
||
|
|
0374e19121 |
Add MachineTool, WMTP, GMS and CranesHoists companions (#4765)
# Description _Describe the changes here to communicate to the maintainers why they should accept this pull request. By default - this will become the Commit message after merging and thus define history._ Add provider-backed MachineTool, bundled WMTP, GMS and CranesHoists companions using the existing Machinery/MachineVision fluent, DI and typed-client approach. **The PR is open/non-draft by user choice. Current head `ecc4a7d6ca3e8004cf28ab54da605da302d026bf` is mergeable and has 108 successful checks, six intentionally skipped checks, zero failures and zero pending checks. All five primary workflows completed successfully at that exact head.** | Family | Released specification/model | Packaging | | --- | --- | --- | | MachineTool | OPC 40501-1, 1.02.0 | Model, Server, Client and MCP; normal root versions | | WMTP | OPC 40505, 1.0.0 | Bundled in MachineTool; distinct CLR/wire namespace and independent WMTP-only hosting | | GMS | OPC 40210 reference 1.0.1, normative model 1.0.0 | Model, Server, Client and MCP; normal root versions | | CranesHoists | OPC 40020-1, 1.00 | Model, Server, Client and MCP; all four plus Debug variants forced preview | There are **12 new packable projects**. No separate WMTP package/profile/guide, root version bump, tag, release or package publication is included. Mandatory instance structure is the default; selected optional features require explicit providers. The implementation reuses Machinery results/jobs/files/ProcessValues and Robotics topology, including retained MachineTool Production, defined GMS jobs/parts, modern ISA95/Machinery jobs, shared-owner hosting and isolated instances alongside MachineVision. Runtime and integration fixes preserve raw quality/timestamps, coherent acquisition, namespace ownership, accepted-configuration state, borrowed-resource lifetime, session/node/file cleanup, terminal-value publication, event propagation and endpoint-aware capability publication. Firmware/licence transfer means provider acceptance, not installation/activation. Cancellation does not silently abort another caller's operation. The later upstream MCP expansion is integrated without losing either family set or its default-off privileged/live execution policy. ### Conformance and scope Canonical model inputs are pinned to UA-Nodeset revision `96a08945425d0295edb3607d2a38f6841ef44046`. **GMS upstream exception:** [Annex A.1](https://reference.opcfoundation.org/specs/OPC-40210/annex-a) makes the released NodeSet normative, but §12.3.2.7 requires undefined `GMS_ProductionStatisticsType`. Inherited statistics and defined jobs/parts are implemented without inventing a standard type/NodeId; the complete GMS Production facet is withheld. The requirement ledger records **102 positively evidenced CU rows, 96 directly negative-tested rows and six explicit negative-evidence limitations**. All 648 test-class/name/TRX links were independently resolved to passing cases. The six limitations concern Stacklight, retained legacy PowerOnDuration, Machinery PowerOnDuration, WorkingUnit, Obligation and MaintenanceMode. They are not claims that every invalid-input or hardware-control behavior was tested. The undefined GMS requirement is outside the 102 defined CUs. No external certification claim is made. Retained baseline complete-assembly line coverage, with generated code retained and no source exclusions: | Family | Model | Server | Client | | --- | ---: | ---: | ---: | | MachineTool/WMTP | 83.80% | 91.98% | 84.74% | | GMS | 95.51% | 90.63% | 88.38% | | CranesHoists | 92.76% | 94.45% | 92.24% | These nine measurements predate the later focused review fixes and merges; they are not relabelled as new `ecc4a7d6c` percentages. The current head's separate CI code-coverage check succeeded. No new exclusions or threshold relaxations were introduced. ## Related Issues - Fixes [#4764](https://github.com/OPCFoundation/UA-.NETStandard/issues/4764), the implementation/design and acceptance tracker. ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [ ] 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. - [x] 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. - [x] I have addressed **all** PR feedback received. |
||
|
|
5bd3e116e1 |
Server request path: no large struct copies, slim per-request system context, no string table per request (R2, R5) (#4805)
## Description Two changes to the per-request server path from the o6 benchmark roadmap (R2, R5), found in the CPU and allocation profiles of the o6 read benchmark (`AsyncCustomNodeManager`, Server GC, 10 clients × 100 Reads in flight) on master |
||
|
|
12a76ade55 |
PubSub server: organize PublishedDataSets under PublishSubscribe.PublishedDataSets (#4793)
# Description PubSubNodeManager looked up the parent of the PublishedDataSet and DataSetFolder objects it materializes by NodeId i=14478. That is DataSetFolderType's <DataSetFolderName> placeholder, an InstanceDeclaration of the type, not the PublishSubscribe.PublishedDataSets object (i=17371). Every PublishedDataSet therefore became a child of the type definition and the standard PublishedDataSets folder stayed empty. Use ObjectIds.PublishSubscribe_PublishedDataSets as the parent. The test harnesses now expose the real folder and the placeholder, and a new test asserts that a PublishedDataSet ends up under the folder and that the placeholder gains no children. ## Related Issues None. ## Checklist - [x] 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. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] 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. - [ ] I have addressed **all** PR feedback received. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Marc Schier <marcschier@users.noreply.github.com> |
||
|
|
95850b7e55 |
TCP listener: complete requests without an async state machine per request (R7) (#4788)
Roadmap item R7 of the o6 benchmark gap analysis (master `5b03e9afa`
profile). `TcpTransportListener.OnRequestReceivedAsync` was an `async
void` method. Every request allocated an `AsyncStateMachineBox` when it
awaited the request processing, which almost always completes later on a
queue worker. In the profile the box was about 4 % of the server's
on-CPU samples (`GetStateMachineBox`) and 4 % of the allocated bytes.
The listener now completes a request without an async state machine. The
handling of every other outcome is unchanged:
- failures;
- sends that wait for the channel gate;
- closed channels;
- the retry on the session's new channel.
No wire, configuration or public API change.
## Results
Paired A/B against master `af601ebca` (= `5b03e9afa` + #4778). The
builds alternate per round, with the order rotated each round, on the
same machine. The table shows medians of 5 rounds. The machine was
shared with other work; allocated bytes per request are deterministic.
- **o6read** uses the native open62541 client workers of the
o6-automation benchmark, in the o6 2.0.1 configuration:
`AsyncCustomNodeManager`, Server GC, queue = clients × outstanding + 3.
- **"4 cores"** pins the server to 4 cores (`--server-affinity 0xF`,
`DOTNET_PROCESSOR_COUNT=4`).
- **The other rows** use the harness's .NET client, 20 sessions unless
noted.
| Scenario | req/s | Server CPU µs/req | Alloc B/req |
|---|---|---|---|
| o6read 10×100, 4 cores | 130,300 → 130,200 | 30.3 → 29.8 (−2 %) |
4,706 → 4,554 (**−152 B**) |
| o6read 10×100 encrypted, 4 cores | 116,300 → 118,500 (+2 %) | 33.4 →
32.8 (−2 %) | 4,747 → 4,606 (**−141 B**) |
| o6read 10×100, 24 cores | 344,500 → 340,200 (−1 %) | 53.6 → 54.1 (+1
%) | 4,710 → 4,509 (**−4 %**) |
| o6read 10×1000, 24 cores | 310,800 → 306,800 (−1 %) | 55.6 → 57.7 (+4
%, noise) | 4,719 → 4,530 (**−4 %**) |
| read --nodes 1, 20 sessions | 189,500 → 200,800 (+6 %) | 49.7 → 49.0
(−1 %) | 4,624 → 4,413 (**−5 %**) |
| read --nodes 1, 200 sessions | 174,500 → 183,200 (+5 %) | 50.2 → 46.7
(−7 %) | 4,510 → 4,322 (**−4 %**) |
| browse | 36,490 → 36,220 | 179 → 174 (−3 %) | 39,004 → 38,796 |
| sub (5 × 100 items, 100 ms) | 94,020 → 93,530 notif/s | 8.2 → 7.5
(rounds 4.5–11.2) | 145 → 143 |
**Effect:**
- **Allocation.** Every request through the TCP listener allocates
140–210 B less (−3 to −5 %).
- **CPU.** CPU per request falls by about 2 % on 4 cores. On 24 cores it
is within the spread of the rounds, where waiting dominates.
### Profile
cpu and alloc traces, 24 cores, 10×100, quiet runs at 433–466k req/s.
The traces were taken with this PR and #4787 (R3) together; the frames
below belong to this PR only.
| Frame | master | this PR |
|---|---|---|
| `GetStateMachineBox` of `OnRequestReceivedAsync`, exclusive | 4.0 % of
on-CPU samples | 0.03 % |
| `OnRequestReceivedAsync` state machine box | 4.1 % of allocated bytes
| 0 |
| `PendingRequest` (pool rent and return) | – | not visible; the time
under `PendingRequest.OnProcessed` (1.9 %) is the channel's send-order
lock |
## Changes
- **`OnRequestReceived`** starts the processing synchronously. A request
still processing is awaited by a pooled `PendingRequest`. It keeps the
request's state in fields and continues with a cached delegate. The
response value task source is pooled already, so a request allocates
nothing for this in steady state. The continuation flows the execution
context, as the `await` did.
- **Synchronous completion.** When the processing succeeded and the
channel queued the response without waiting, the request completes
synchronously. The request and response are returned to their pools and
the pending-request count is released.
- **`CompleteRequestAsync`** is an `async void` method with the previous
body, and handles every other outcome:
- a processing failure, thrown synchronously or later (logged, answered
with a service fault);
- a send that waits for the channel gate;
- a send on a closed channel (quiet log) and its retry on the session's
new channel.
- **Kestrel TCP and WebSocket** use a different handler (no service
fault, no pending-request scope, no request reuse). They are left
unchanged.
## Testing
- **`TcpTransportListenerRequestCompletionTests` (new)**, with a
loopback listener and a UA-TCP client:
- 1200 concurrent requests on 4 channels each get their own response.
The requests complete synchronously, through a task, or later through a
value task source on another thread, so the pooled continuations are
reused;
- a processing failure, thrown synchronously, returned as a faulted
task, or set later, is logged as an error once and answered with a
service fault, and the channel keeps working;
- a callback value task source that throws when its state is read, the
continuation is registered or the result is read fails only that
request: logged, answered with a service fault, channel keeps working
(these time out without the fix in
|
||
|
|
19a97da52e |
Server: confirm unchanged isolation classifications at dispatch without the full lookup (#4787)
Roadmap item R3 of the o6 benchmark gap analysis (master `5b03e9afa` profile). Each request was revalidated twice by the resource-isolation provider: at dequeue, and again before execution. Each revalidation repeated: - the owner table lookup (a `ConditionalWeakTable`); - the session binding lookup (token and session dictionaries, the session's expiry clock); - the classifier; - the owner key checks. Now the classification also returns the evidence it was derived from. The queue confirms that evidence at dispatch and runs the full revalidation only when it no longer holds. No wire, configuration or public API change. Every rejection status code, and the order of the checks relative to cancellation and session close, is unchanged: the confirmation only ever short-circuits to Good, and the full revalidation decides every other outcome. ## Results Paired A/B against master `af601ebca` (= `5b03e9afa` + #4778). The builds alternate per round, with the order rotated each round, on the same machine. The table shows medians of 5 rounds, with the throughput range of the rounds in brackets. The machine was shared with other work; allocated bytes per request are deterministic. - **o6read** uses the native open62541 client workers of the o6-automation benchmark, in the o6 2.0.1 configuration: `AsyncCustomNodeManager`, Server GC, queue = clients × outstanding + 3. - **"4 cores"** pins the server to 4 cores (`--server-affinity 0xF`, `DOTNET_PROCESSOR_COUNT=4`). - **The other rows** use the harness's .NET client, 20 sessions unless noted. | Scenario | req/s | Server CPU µs/req | Alloc B/req | |---|---|---|---| | o6read 10×100, 4 cores | 130,300 → 135,400 (+4 %) [129–135k → 132–140k] | 30.3 → 28.6 (**−6 %**) | 4,706 → 4,709 | | o6read 10×100 encrypted, 4 cores | 116,300 → 122,500 (**+5 %**) [113–119k → 118–126k] | 33.4 → 31.5 (**−6 %**) | 4,747 → 4,756 | | o6read 10×100, 24 cores | 344,500 → 347,400 (+1 %) [328–350k → 331–362k] | 53.6 → 52.2 (−3 %) | 4,710 → 4,716 | | o6read 10×1000, 24 cores | 310,800 → 307,300 (−1 %) [297–332k → 307–323k] | 55.6 → 57.5 (+4 %, noise) | 4,719 → 4,730 | | read --nodes 1, 20 sessions | 189,500 → 198,900 (+5 %) | 49.7 → 48.2 (−3 %) | 4,624 → 4,623 | | read --nodes 1, 200 sessions | 174,500 → 179,100 (+3 %) | 50.2 → 44.7 (**−11 %**) | 4,510 → 4,521 | | browse | 36,490 → 35,750 (−2 %) | 179 → 175 (−3 %) | 39,004 → 39,016 | | sub (5 × 100 items, 100 ms) | 94,020 → 93,180 notif/s | 8.2 → 9.2 (rounds 4.5–12.7) | 145 → 145 | **Where it helps:** CPU per request falls by 6 % on 4 cores, plain and encrypted, and by 3–11 % in the read scenarios with the .NET client. **Where it does not:** on 24 cores, CPU per request is within the spread of the rounds. There the cost is dominated by workers of one channel waiting for each other (the request hop and the send-order lock), which this PR does not change. ### Profile cpu trace, 24 cores, 10×100, quiet runs at 433–439k req/s. The trace was taken with this PR and #4788 (R7) together; the frames below belong to this PR only. | Frame | master | this PR | |---|---|---| | `FairRequestQueue.GetRevalidationStatus`, inclusive | 15.4 % | 14.0 % | | of which the provider and session lookups | 15.4 % | 3.9 % (`IsAdmissionCurrent`, mostly the session's expiry clock) | | `SessionManager.TryGetSessionContext`, inclusive | 14.6 % | 0.2 % (admission only) | The lookups are gone, but the dequeue-time check still accounts for about 10 % of the samples as its own time. That is the first touch of the request (entry, request object, channel context) on the worker thread, right after the receive thread wrote it on another core. On master the same cache misses showed up inside `TryGetSessionContext`, which is why the profile share (16.5 %) overstated what this change could save. Removing the request hop (separate work) is what removes those misses. ## Changes ### Evidence with the classification - **Internal interface.** `DefaultServerResourceIsolationProvider` implements a new internal interface, `IResourceIsolationAdmissionEvidence`. Its `Classify` also returns the evidence: the issued owner identity, which holds the channel snapshot and the session binding. - **Kept on the entry.** `FairRequestQueue` keeps the evidence, an internal `ResourceIsolationAdmissionEvidence` record that only the issuing provider interprets, on the queue entry. - **Confirmation.** Before the full revalidation, the queue asks the provider whether the evidence still holds. This applies to both checks, at dequeue and before execution. The provider: - compares the channel snapshot with the request's channel context, as before, so mutated channel evidence is still detected; - asks the binding's liveness check. Only a confirmed Good is short-circuited. - **No evidence, so every revalidation runs in full:** - a classifier is configured, because it may map the same inputs differently at any time; - the request has a token but no binding, because an activation may bind it while the request waits; - the binding comes from another binding provider, which has no liveness check. ### Binding liveness (`SessionManager`) A `SessionBindingContext` issued by `SessionManager` carries an internal liveness check. It runs the checks of `TryGetSessionContext` that can change while the binding and the request's channel context stay the same, without the token and session dictionary lookups: - **The dispatch binding is still this binding.** A removal, a new activation, an identity change, a transfer and an activation in commit all replace it. A commit always creates a new instance. - **The session's channel certificate was not replaced.** `SetRestoredSessionTransferSecurityState` can replace it without a new binding, so the certificate has a version. - **The manager is not shutting down.** Shutdown empties the token index without replacing the bindings. - **The session itself** is activated, not closing or claimed, not expired (the clock-based `HasExpired`, including expiry before the monitor closes the session), and still valid for the channel. These checks run in the same order as in `TryGetSessionContext`. A replaced binding releases its session, so owners that still reference the binding do not keep a closed session alive. ## Testing - **`RuntimeResourceIsolationTests` (new):** - the evidence confirms only an unchanged binding and channel, and a mutated certificate ends it without consulting the session; - it is withheld with a classifier, for a binding without liveness, and for a token without a binding; - a disposed provider throws; - the check allocates nothing. - **`SessionBindingTests` (new):** - the liveness check agrees with a fresh `TryGetSessionContext` through re-activation, transfer and close, an identity change, expiry before the monitor closes the session, an activation in commit, a session that starts closing, and shutdown; - replacing the transfer-security certificate with `SetRestoredSessionTransferSecurityState` keeps the binding but ends the liveness check (fails without the certificate version); - a race of 1000 transfers and a close against 4 readers never confirms a replaced binding, or any binding after the close. - **`FairRequestQueueTests` (new):** - current evidence replaces both full revalidations; - stale evidence at dequeue, or before execution, falls back to the full revalidation and its status (`BadSessionIdInvalid`, `BadServerTooBusy`), and the request does not run. - **Local, net10.0**, with both PRs applied, all 0 failed: - Opc.Ua.Core.Tests (Stack.Server, Stack.Tcp, Stack.Transport): 2753 passed; - Opc.Ua.Server.Tests: 8750 passed; - Opc.Ua.Sessions.Tests: 857 passed (379 skipped as on master); - Opc.Ua.Redundancy.Server.Tests: 884 passed; Opc.Ua.Redundancy.Client.Tests: 134 passed; - Opc.Ua.Stress.Tests Contract + Integration: 45 passed. net48 builds. `dotnet format whitespace --verify-no-changes` passes on the changed files. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d764161a53 |
Extract core/types fixes for schemas, replay, and source generation (#4801)
# Description Extract the reusable core/types layer from #4462 without including UaLens or the other planned extraction layers. - Preserve null versus empty in `ByteString.Copy`, and provide older-framework path, random-byte, and SHA-256 convenience APIs. - Encode abstract numeric structure fields (`Number`, `Integer`, and `UInteger`) as Variants in JSON, XSD, and BSD schemas, with regression coverage and schema documentation. - Add opt-in complete-file validation for finalized PCAP replay while retaining the existing tolerant reader default. - Keep placeholder references on generated type templates, not runtime instances, while preserving ordinary external references. **Initial extraction:** commit `8881e096fb5e4eaa36f3c13f8caa57733373bd19` preserves the assigned changes from source `390c4a7a51b71da40caec2d22ef74518bbe670ce`, based on pinned `master` commit `5b03e9afa492cdb36049ee52ae844f630f89d8db`: 20 exact full-file extractions and only the generic replay paragraph in `docs/Diagnostics.md`. The UaLens capture-window paragraph remains with the source PR. No UaLens projects, UI, native-rendering paths, dependencies, or workflows are included. **Review follow-up:** `814c0f2dc6056ea5eb898611565b3e96cfa65104` fixes direct-placeholder references that bypassed filtering through `TargetPath` and emitted an undefined `forInstance` guard in instance-only factories. Filtering now follows target resolution. Compilation/reference-shape regressions cover ObjectType/VariableType, both placeholder rules, and direct-child/external targets; `docs/NodeManagers.md` documents the behavior. The review thread was resolved after publication and verification. **CI follow-up:** `93ec3928bd5a9cb2ea86110085e79131db17d443` addresses both causes found in [CI run 38700770996](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38700770996): - `MODELGEN012` treated an identical payload-less transitive dependency declaration as a competing model producer after the SDK Programs model appeared in newer `master`. Matching closure rows no longer emit that diagnostic. Producer ordering is unchanged; actual producers, differing declarations, and malformed payload handling retain their existing diagnostics. New regression cases exercise both reference orders, and `docs/ModelDependencies.md` records the distinction. - The queue-fairness test sampled OS scheduling and failed on macOS. This reuses the exact one-file correction from `84d2c34d3902bfe700a87f6c30708b435500878f` in #4765: hold the existing heavy-owner publication hook, prove concurrent work-conserving progress, then assert exact recovered 1:3 service, all 960 unique requests, and complete queue/token/owner/lease cleanup. No production scheduler or fairness bound was changed. The PR now changes 26 paths. Its pinned extraction ancestry is preserved: no rebase, force push, or merge of newer `master` was performed. This remains the bottom of native stack 4807, ordered 4801 -> 4802 -> 4803 -> 4804 -> 4806. ### Current CI-repair validation Windows, existing .NET SDK **10.0.401**, **Release**, with analyzers enabled. CI tested merge `9a2d252793da12aeb99ed379dfc9b03e89c912b7` (master `d40843c23a5fe8116ffe380cf937d88759f5218b` plus PR head `814c0f2dc6056ea5eb898611565b3e96cfa65104`), which includes newer tests absent from the pinned extraction branch. That exact merge was reproduced in a disposable detached worktree, then the proposed patch was applied. Git patch-id verified that the tested patch and committed branch patch were identical (`b7ed3ba2b61d1862825dd33d93c50c64d3a759ea`). Before the fix, all four failing client-accessor cases reproduced `MODELGEN012`, and both new matching-reexport cases failed. After the fix: | Context / scope | Framework | Passed | Failed | Skipped | | --- | --- | ---: | ---: | ---: | | Exact CI merge + patch: client-accessor and dependency fixtures | net10.0 | 32 | 0 | 0 | | Same fixtures with CI coverage instrumentation | net10.0 | 32 | 0 | 0 | | Exact CI merge + patch: entire SourceGeneration.Tests project | net10.0 | 295 | 0 | 0 | | Exact CI merge + patch: entire SourceGeneration.Tests project | net48 | 295 | 0 | 0 | | Preserved PR branch: dependency and placeholder fixtures | net10.0 | 30 | 0 | 0 | | Preserved PR branch: dependency and placeholder fixtures | net48 | 30 | 0 | 0 | | Preserved PR branch: entire FairRequestQueueTests fixture, with coverage | net10.0 | 93 | 0 | 0 | | Preserved PR branch: entire FairRequestQueueTests fixture | net48 | 93 | 0 | 0 | | Exact CI merge + patch: entire FairRequestQueueTests fixture, with coverage | net10.0 | 93 | 0 | 0 | All nine commands exited **0**. The repository TRX verdict helpers verified **993 passing test executions, 0 failures, and 0 fixture setup/teardown failures** across these overlapping runs. No build/analyzer warnings were reported. Normal restore resolved missing assets/target entries in the new checkout and when switching TFMs; no dependency declaration or SDK pin was changed. Exact successful test argument sets follow. Variables abbreviate the executable, project paths, and results directory only; output was also captured to separate local logs. ```powershell $env:DOTNET_ROOT = 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk' $env:PATH = "$env:DOTNET_ROOT;$env:PATH" $dotnet = "$env:DOTNET_ROOT\dotnet.exe" $generator = 'tests\Opc.Ua.SourceGeneration.Tests\Opc.Ua.SourceGeneration.Tests.csproj' $coreTests = 'tests\Opc.Ua.Core.Tests\Opc.Ua.Core.Tests.csproj' $results = 'C:\Users\mschier\.copilot\session-state\8f497e89-7fc2-4f48-880b-4803d97c1c12\files\ci-validation' # In the disposable exact-CI-merge checkout, with the identical proposed patch applied: Set-Location 'D:\Temp\ua-ci4801-8f497e89' # Passed: 32, Failed: 0, Skipped: 0 & $dotnet test $generator --no-restore --configuration Release --framework net10.0 -p:CustomTestTarget=net10.0 -p:RunAnalyzers=true -p:RunAnalyzersDuringBuild=true --filter 'FullyQualifiedName~ClientAccessorGeneratorTests|FullyQualifiedName~ModelDependencyScannerTests' --logger 'trx;LogFileName=ci-metadata-green-net10.trx' --results-directory $results --verbosity minimal # Passed: 295, Failed: 0, Skipped: 0; reuses the analyzer-enabled build above & $dotnet test $generator --no-build --no-restore --configuration Release --framework net10.0 -p:CustomTestTarget=net10.0 --logger 'trx;LogFileName=ci-sourcegeneration-merge-net10.trx' --results-directory $results --verbosity quiet # Passed: 295, Failed: 0, Skipped: 0 & $dotnet test $generator --configuration Release --framework net48 -p:CustomTestTarget=net48 -p:RunAnalyzers=true -p:RunAnalyzersDuringBuild=true --logger 'trx;LogFileName=ci-sourcegeneration-merge-net48.trx' --results-directory $results --verbosity minimal # Passed: 93, Failed: 0, Skipped: 0 & $dotnet test $coreTests --configuration Release --framework net10.0 -p:CustomTestTarget=net10.0 -p:RunAnalyzers=true -p:RunAnalyzersDuringBuild=true -p:CollectCoverage=true --collect 'XPlat Code Coverage' --settings 'tests\coverlet.runsettings.xml' --filter 'FullyQualifiedName~FairRequestQueueTests' --logger 'trx;LogFileName=ci-fairqueue-merge-net10.trx' --results-directory $results --verbosity minimal # Passed: 32, Failed: 0, Skipped: 0 & $dotnet test $generator --configuration Release --framework net10.0 -p:CustomTestTarget=net10.0 -p:RunAnalyzers=true -p:RunAnalyzersDuringBuild=true -p:CollectCoverage=true --collect 'XPlat Code Coverage' --settings 'tests\coverlet.runsettings.xml' --filter 'FullyQualifiedName~ClientAccessorGeneratorTests|FullyQualifiedName~ModelDependencyScannerTests' --logger 'trx;LogFileName=ci-metadata-coverage-net10.trx' --results-directory $results --verbosity minimal # In the preserved PR branch worktree: Set-Location 'D:\git\copilot-worktrees\UA-wot-d3-final\marcschier-congenial-umbrella' # Passed: 30, Failed: 0, Skipped: 0 & $dotnet test $generator --configuration Release --framework net10.0 -p:CustomTestTarget=net10.0 -p:RunAnalyzers=true -p:RunAnalyzersDuringBuild=true --filter 'FullyQualifiedName~ModelDependencyScannerTests|FullyQualifiedName~PlaceholderReferenceGeneratorTests' --logger 'trx;LogFileName=ci-sourcegeneration-branch-net10.trx' --results-directory $results --verbosity minimal # Passed: 93, Failed: 0, Skipped: 0 & $dotnet test $coreTests --no-restore --configuration Release --framework net10.0 -p:CustomTestTarget=net10.0 -p:RunAnalyzers=true -p:RunAnalyzersDuringBuild=true -p:CollectCoverage=true --collect 'XPlat Code Coverage' --settings 'tests\coverlet.runsettings.xml' --filter 'FullyQualifiedName~FairRequestQueueTests' --logger 'trx;LogFileName=ci-fairqueue-branch-net10.trx' --results-directory $results --verbosity minimal # Passed: 93, Failed: 0, Skipped: 0 & $dotnet test $coreTests --configuration Release --framework net48 -p:CustomTestTarget=net48 -p:RunAnalyzers=true -p:RunAnalyzersDuringBuild=true --filter 'FullyQualifiedName~FairRequestQueueTests' --logger 'trx;LogFileName=ci-fairqueue-branch-net48.trx' --results-directory $results --verbosity minimal # Passed: 30, Failed: 0, Skipped: 0 & $dotnet test $generator --configuration Release --framework net48 -p:CustomTestTarget=net48 -p:RunAnalyzers=true -p:RunAnalyzersDuringBuild=true --filter 'FullyQualifiedName~ModelDependencyScannerTests|FullyQualifiedName~PlaceholderReferenceGeneratorTests' --logger 'trx;LogFileName=ci-sourcegeneration-branch-net48.trx' --results-directory $results --verbosity minimal ``` `git diff --cached --check` passed. The queue-test blob was verified byte-for-byte against the existing published correction. The upstream branch was verified as `93ec3928bd5a9cb2ea86110085e79131db17d443` after push. **Remote CI is pending, not claimed green:** [new CI run 38712127138](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38712127138). Cross-platform confirmation, CodeQL, coverage gates, and the complete `UA.slnx` suites on this new head are not established by these local checks. The source PR's checks are not claimed as evidence for this branch. ### Earlier validation The original extraction had 1,407 passing scoped test executions across net10.0/net48. The placeholder-review follow-up had six passing focused cases and 269 passing tests in the full source-generator project on each framework, with analyzer-enabled builds. Those are historical results for their respective commits, not fresh full-suite evidence for the CI-repair head. Earlier command details remain in this PR's edit history. ## Related Issues - Related to #4461 (existing tracking issue; not auto-closing). - Extracted from #4462 (source PR; remains open and is not auto-closing). - Reuses the bounded queue-test correction from #4765; does not incorporate that PR's other changes. _If this is a relatively large or complex change, a design must have been discussed in the related tracking issue and signed off (which becomes the Architectural Decision Record (ADR))._ This began as an exact extraction of existing behavior. The subsequent commits address the reported placeholder-generation defect and observed CI failures; they are not a new design or a claim of design sign-off. ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [ ] 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. - [ ] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] 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. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [x] I have addressed **all** PR feedback received. |
||
|
|
d40843c23a |
Client: make the UaFileInfo constructor public (#4790)
# Description FileSystemClient hands out UaFileInfo objects only for files it resolves by path below its root directory. An application that already knows the NodeId of a FileType object, for example from a reference or a method result, could not get a UaFileInfo for it because the constructor was internal, and had to fall back to the raw FileTypeClient proxy. The constructor is now public and documented, and it rejects a null segment list. A test creates a UaFileInfo for a known NodeId and reads its metadata through it. ## Related Issues None. ## Checklist - [x] 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. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] 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. - [ ] I have addressed **all** PR feedback received. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Marc Schier <marcschier@users.noreply.github.com> |
||
|
|
15d0ce7bd5 |
Implement OPC UA Part 10 Programs (#4789)
# Description Implement OPC UA Part 10 Programs across `Opc.Ua.Core`/`Core.Types`, `Opc.Ua.Server`, and `Opc.Ua.Client`, reusing the existing state-machine, source-generation, node-management, auditing, and streaming-subscription infrastructure. - Add asynchronous provider-backed execution, standard controls, internal/autonomous transitions, generation checks, and nested-state activation/resumption. - Add checked creation/adoption, vendor Create adapters, deletion, recycling and instance limits, auto-delete, DI, and direct-construction paths. - Maintain ProgramDiagnostic2 and retained final results; publish captured intermediate results and concrete transition/audit events with access controls. - Add generated SDK models/proxies, typed client discovery and reads, event streams, and subscribe-before-call readiness handling. - Add unit, connected client/server, generated Annex A-style model, and NativeAOT coverage. Document usage and migration in `docs/Programs.md` and related guides. ### Compatibility and design decisions The linked Part 10 document is v1.05.00; namespace-zero definitions remain aligned with 1.05.07. Documentation distinguishes normative requirements from conflicting specification text and SDK conventions. - Correct state numbers to Halted=11, Ready=12, Running=13, Suspended=14. Generated subtypes retain inherited namespace-zero state/transition identifiers. - Managed Reset is Halted-to-Ready only. Preserve the existing low-level Reset-from-Suspended behavior for compatibility. - Preserve UInt32 limit types; use `UInt32.MaxValue` as a documented SDK unlimited convention. RecycleCount overflow reports BadOutOfRange without stopping unlimited execution. - In-memory default with injectable providers. Durable recovery, distributed execution/failover, and a production Annex A download engine are excluded. ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [ ] 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. - [ ] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] 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. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. Tests and focused coverage evidence are above; the combined tests/coverage checkbox remains open because a whole-repository coverage increase has not been established. CLA status, CI/CodeQL results, tracking-issue approval, and PR feedback are not asserted. |
||
|
|
9746c62b49 |
Server: make TrustList.ValidateNewTrustListAsync public (#4799)
# Description TrustList validates an uploaded TrustList before CloseAndUpdate commits it: every certificate is validated against the new TrustList (the uploaded certificates and CRLs, or the current stores for lists that were not specified), CRLs must belong to a CA of the result, and IssuerCertificates must be CA certificates. The check was private, so an application that applies a TrustList by other means, for example a GDS push it receives through its own channel, could not apply the same check first and had to duplicate it. The method is now public and documented; its behaviour is unchanged. A test calls it for an accepted TrustList, for one whose issuer is missing and for one that lists a leaf certificate as an issuer. ## Related Issues None. ## Checklist - [x] 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. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] 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. - [ ] I have addressed **all** PR feedback received. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Marc Schier <marcschier@users.noreply.github.com> |
||
|
|
724757122b |
Add recycling simulator and plastics/rubber companion packages (#4772)
# Description Add a generic, company-neutral recycling simulation sample and reusable plastics/rubber companion-model packages. One durable causal coordinator and ten separately launched OPC UA process views provide deterministic material balances, finite buffers, genealogy, utilities, quality holds/disposition, authenticated controls and crash recovery. Process views consume committed state instead of running independent simulations. Real companion-model equipment instances use DI/Machinery/AMB discovery, Machine Vision, Pumps/Scales, granulate drying, extrusion, filtration and pelletizing. Vendor/process extensions remain sample-local. Connectors and WoT conversion are out of scope. **External source-contract parity and complete deployment qualification are not claimed complete.** The sample and managed tests target **.NET 8, 9 and 10**. The consolidated [recycling guide](samples/Recycling/README.md) explains the actual composition, simulation, observation paths and exposed data; the application-directory README points to that guide. Typed client member extensions are emitted at build time through the default-false `ModelSourceGeneratorClientAccessorsOnly` opt-in. Extrusion uses it for all twelve parts instead of checked-in generated C# and a Python generator. The shared client runtime retains typed values, quality, timestamps and streaming behavior. ### Package layout Six packages contain fourteen models in two model-producing assemblies: | Specifications | Model | Typed client | Server runtime | | --- | --- | --- | --- | | OPC 40083 and OPC 40082-6 | `Opc.Ua.PlasticsRubber` | `Opc.Ua.PlasticsRubber.Client` | `Opc.Ua.PlasticsRubber.Server` | | OPC 40084 parts 1–12 | `Opc.Ua.Extrusion` | `Opc.Ua.Extrusion.Client` | `Opc.Ua.Extrusion.Server` | NuGet IDs use the `OPCFoundation.NetStandard.` prefix. All six packages are registered in `.azurepipelines/expected-packages.txt` and the generated preview-pack solution. Granulate-drying code lives in `GranulateDrying` folders in the PlasticsRubber projects; extrusion uses per-part folders. There are no standalone granulate-drying or per-part extrusion packages. Published model identities and CLR namespaces remain distinct and unchanged. The common 1.03 interface is independently authored on the explicitly MIT-licensed 1.02 implementation basis; the official 1.03 XML is not redistributed. The other imported models retain their individual MIT notices. All 42 pinned model, identifier and basis files retain their manifest-backed bytes, including their staged Git blobs. No formal companion certification is claimed; the disputed Filter profile is not advertised. ## Related Issues No tracking issue is currently linked. ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [ ] 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. - [ ] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] 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. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [x] I have addressed **all** PR feedback received. Current-head CI and coverage remain pending; the full local `UA.slnx` test matrix is not claimed complete. |
||
|
|
ac29b51048 |
Remove experimental markers from type interfaces (#4785)
# Description Remove `UA_NETStandard_1` from `IType`, `IBuiltInType`, `IEnumeratedType`, and `IEncodeableType`. Add a regression test to guard their public metadata. - **Follow-up candidates:** `Variant.FromEnumeration`, `AmbientMessageContext`, `Utils.ParseExtension`/`UpdateExtension`, and `LoggerUtils` retain `UA_NETStandard_1` and are not changed here. - **Retained:** `OPCUA_EXPERIMENTAL_KC_BRIDGE` for the intentionally experimental KeyCredential bridge. ## 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. - [ ] 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 #4784 --------- 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> |
||
|
|
af601ebca8 |
Complete model-change reporting for committed node deletions (#4778)
# Description Finalize deletion model-change reporting from observed address-space outcomes, including when later cleanup fails or the caller cancels the request. A shared internal snapshot captures registered subtree nodes, pre-removal NodeVersion eligibility, and relevant reference endpoints. Completion reports only removed nodes and references actually removed from surviving local nodes, then rethrows the original deletion failure. If reporting also fails, both errors remain visible. This preserves notifications for versioned descendants below unversioned roots and applies strict deletion eligibility to the service-facing overload. It avoids blindly reporting the requested subtree from a finally block. For endpoints owned by another node manager in the same server, reporting requires target-reference cleanup to be both requested and successful because the remaining foreign references cannot be inspected locally. A successful service deletion with `DeleteTargetReferences = false` does not imply foreign reference removal. Reporting covers both directions of hierarchical and non-hierarchical references; absolute targets are excluded, matching the cleanup path. Master cleanup now propagates a bad per-reference `DeleteReferenceAsync` result as a `ServiceResultException`. A normal return with a bad status is therefore not mistaken for successful foreign cleanup. Committed local changes are still reported before the cleanup failure is rethrown; unconfirmed foreign reference removals are not reported. The branch includes the merge of current master after #4776 and #4777 merged. This PR preserves their per-node monitored-item restoration, independent cleanup budgets, and NodeVersion eligibility behavior. It does not introduce further monitored-item completion changes; the remaining diff is outcome-based model-change completion, its regression tests, and documentation. It contains no release-version changes. ## Related Issues _Reference all GitHub issues this PR addresses. If there is no issue yet, open one and link it here._ _If this is a relatively large or complex change, a design must have been discussed in the related tracking issue and signed off (which becomes the Architectural Decision Record (ADR))._ - Fixes #4769 - Related: #4768, #4770 (addressed by merged PRs #4776 and #4777). - Design and failure reproduction are recorded in #4769. ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [x] 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. - [x] 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. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. ## Validation ### Current head: `524e46fbe` The server test project and its dependencies built without build/analyzer warnings or errors. The following command covers the deletion/model-change regressions, the complete monitored-item lifecycle fixture, and the master node-management dispatcher fixture: ```powershell & 'C:\Program Files\dotnet\dotnet.exe' exec 'C:\Program Files\dotnet\sdk\10.0.402\dotnet.dll' test tests\Opc.Ua.Server.Tests\Opc.Ua.Server.Tests.csproj --no-restore -f net48 -p:CustomTestTarget=net48 -p:NuGetAudit=false -m:2 --filter "FullyQualifiedName~AsyncCustomNodeManagerTests&Name~Delet|FullyQualifiedName~AsyncCustomNodeManagerNodeManagementTests&Name~DeleteNode|FullyQualifiedName~MonitoredItemManagerLifecycleTests|FullyQualifiedName~MasterNodeManagerNodeManagementTests" --logger "trx;LogFileName=foreign-cleanup-status-net48.trx" --results-directory 'C:\Users\crpogace\.copilot\session-state\f4f431fc-413e-40d4-88a7-83f6ade0c9a7\files\foreign-cleanup-status-results' ``` Result: **296 passed, 0 failed**; 25 fixture combinations are inapplicable to the legacy adapter and were not executed. Regression coverage includes late cleanup failure/cancellation, pre-mutation cancellation, partial subtree removal, service eligibility, versioned descendants, preservation of simultaneous deletion/reporting failures, foreign-reference direction/type combinations, requested versus omitted cleanup, failed cleanup, and absolute-target exclusion. New regressions exercise the real master cleanup dispatcher with `Good`, `BadNotSupported`, and `BadUnexpectedError` results across async internal/service deletion and legacy runtime deletion. Before the fix, 10 bad-status cases failed; the 5 successful-cleanup controls passed. **Current-head net10.0 tests were not executed locally:** the machine now has SDK 10.0.402 rather than the pinned 10.0.401. Restore requires `Microsoft.NET.ILLink.Tasks` 10.0.13, which is absent from the local cache, and the NuGet endpoint fails its TLS handshake. This is an environment/restore blocker, not a test failure. The explicit SDK invocation above does not change the repository SDK pin. `NuGetAudit=false` was invocation-only because the audit endpoint was unavailable; repository audit settings are unchanged. The earlier pre-merge revision `0c53e143b` passed 96 focused deletion tests on each of net10.0 and net48, with 22 inapplicable fixture combinations per framework. Those historical results do not establish net10.0 validation of the current merged head. Full net48/net10.0 solution validation has not been completed locally. The ordinary .NET 10 VSTest invocation cannot run the unchanged AOT projects' Microsoft.Testing.Platform suites; this is a mixed-runner invocation limitation, not evidence of an AOT compilation defect. Their supported validation path is native publish-and-run, as used in CI. A separate local Historian native AOT run passed 2/2 tests earlier in this work, but does not establish complete AOT or current-head validation. CI was still running when this description was updated; the full-suite and CI checklist items remain unchecked. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
5b03e9afa4 |
Channel send path: seal responses in the writer, lock-free gate, fewer locks and allocations per request (#4782)
Carved out of #4771 (roadmap item A1 of the o6 benchmark gap analysis): everything except running requests to completion on the receive loop. #4771 keeps run to completion, stacked on this PR, for a later release. Requests still go through the fair request queue and its workers, as on master. What changes is the channel around them: - the response is encoded without the channel gate; - the channel's single writer numbers, secures and sends; - requests take fewer locks and allocations on the channel. No wire, configuration or public API change. ## Results Paired A/B against master `6cfb8cf19` (with #4767). The builds alternate per round on the same machine, and the table shows medians of 5 rounds with the throughput range of the rounds in brackets. The machine was shared with other work, so the CPU and throughput ranges are wide; allocated bytes per request are deterministic. - **o6read** uses the native open62541 client workers of the o6-automation benchmark, in the o6 2.0.1 configuration: `AsyncCustomNodeManager`, Server GC, queue = clients × outstanding + 3. - **"4 cores"** pins the server to 4 cores. - **The other rows** use the harness's .NET client: 20 sessions unless noted, workstation GC, ReferenceServer. - **Contention** counts runtime lock-contention events in the measured window. | Scenario | req/s | Server CPU µs/req | Alloc B/req | Contention | |---|---|---|---|---| | o6read 10×100, 24 cores | 254,600 → 264,700 (+4 %) [228–292k → 255–270k] | 67.4 → 69.5 | 5,874 → 4,709 (**−20 %**) | 6,766 → 1,811 | | o6read 10×1000, 24 cores | 248,900 → 241,300 (−3 %) [231–258k → 233–260k] | 69.0 → 71.3 | 5,866 → 4,718 (**−20 %**) | 6,080 → 1,456 | | o6read 10×100 encrypted, 24 cores | 203,900 → 232,000 (**+14 %**) [111–226k → 204–264k] | 71.6 → 70.2 | 5,985 → 4,752 (**−21 %**) | 5,166 → 1,288 | | o6read 10×100 `--o6-async 0` (`CustomNodeManager2`), 24 cores | 235,600 → 243,900 (+4 %) | 40.1 → 38.8 | 6,702 → 5,886 (−12 %) | 148k → 124k | | o6read 1 client × 1000 (one channel) | 64,300 → 92,600 (**+44 %**) [52–69k → 76–100k] | 79.8 → 74.5 (−7 %) | 6,003 → 5,063 (−16 %) | 224 → 128 | | o6read 10×100, 4 cores | 156,600 → 140,400 (−10 %, noise: rounds 139–192k → 121–184k) | 25.0 → 28.3 | 5,435 → 4,688 (−14 %) | 97 → 184 | | o6read 10×1000, 4 cores | 107,100 → 125,200 (**+17 %**) [103–116k → 120–136k] | 36.5 → 31.7 (**−13 %**) | 9,966 → 4,767 (**−52 %**) | 312 → 160 | | o6read 10×100 encrypted, 4 cores | 103,300 → 126,300 (**+22 %**) [102–105k → 115–143k] | 37.9 → 31.3 (**−17 %**) | 5,498 → 4,736 (−14 %) | 46 → 29 | | o6read 10×100 `--o6-async 0`, 4 cores | 186,600 → 187,700 (+1 %) | 21.1 → 21.0 | 6,587 → 5,915 (−10 %) | 17.7k → 4.2k | | read --nodes 1, 20 sessions | 208,300 → 205,900 (−1 %) | 49.1 → 48.3 | 4,905 → 4,621 (−6 %) | p50/p99 0.072/1.18 → 0.072/1.26 ms | | read --nodes 1, 200 sessions | 216,400 → 208,600 (−4 %) | 42.0 → 43.2 | 4,790 → 4,518 (−6 %) | p50/p99 0.606/3.56 → 0.615/4.05 ms | | read --nodes 100 | 31,950 → 32,470 (+2 %) | 278 → 253 (−9 %) | 73,702 → 73,436 | p50/p99 0.290/2.51 → 0.283/2.48 ms | | browse | 38,380 → 40,460 (+5 %) | 162 → 161 | 39,305 → 38,981 | p50 0.349 → 0.330 ms | | sub (5 × 100 items, 100 ms) | 88,190 → 89,010 notif/s | 11.1 → 10.7 | 159 → 153 | p50/p99 20.7/64.3 → 20.8/66.0 ms | **Where it helps:** - Allocation per request falls in every scenario, by 20 % for o6read. - Lock contention falls by 70–80 % for o6read. - Throughput rises where the channel gate serialised the work: - one deep pipeline on one channel: +44 %; - encrypted: +14 % on 24 cores, +22 % on 4 cores; - 1000 in flight on 4 cores: +17 %. **Where it does not:** CPU per request at 24 cores is unchanged. The requests still go through the fair queue's workers, and the hand-off to a worker and back is what #4771 removes. The 4-core 10×100 row spread as much within each side as between the sides. ## Changes ### Sending - **Seal in the writer.** The producer encodes the response without the channel gate and queues it unsealed. The channel's single writer takes the queued responses in order under a per-channel send-order lock: it assigns sequence numbers, selects the current token, signs and encrypts, and sends the batch. Messages that number themselves (OpenSecureChannel, asymmetric faults, the client's requests) hold the same lock until they are queued. Token changes that can dispose a token take it as well. - **Reconnect.** The old transport is detached before the new token is activated. Unsealed responses still queued for it are then dropped without taking sequence numbers. The new transport is attached and the OpenSecureChannel response numbered under one hold of the send-order lock, so the response is the first message on the new transport. The channel opens again only after that. - **Failure past the sequence numbers.** A message whose security fails after its chunks took sequence numbers stops all numbering on that transport. The writer drops the messages producers numbered for it after the gap, and reports a transport error once it has left the send-order lock. That faults the channel (server) or reconnects (client). - **Numbered writes without the lock.** The writer takes writes that their producers numbered without the send-order lock, because they are queued in number order. A client channel queues only such writes, so its writer no longer waits for the lock that its request threads take. - **Writer on the pool.** The writer always starts on the thread pool. Running it on the producer's thread sent batches of one message. - **Buffers.** The encoded message is a struct in the write-queue entry. A small single-chunk message moves to a right-sized buffer only when writes are already queued, so deep pipelines no longer exhaust the bounded buffer pool (−52 % allocation at 10×1000 on 4 cores). ### Locks and allocations per request - **`ChannelGate`** takes the gate with one compare-and-exchange when it is free, and queues waiters under a lock only when it is not. `SemaphoreSlim` took its monitor on every wait and release, which the receive loop does for every message. The contract is unchanged: not re-entrant, FIFO handoff, cancellable waits, synchronous completion when free. The transport's send lock is a `ChannelGate` too. - **Namespace and server tables.** Every encoder and decoder reads `ServiceMessageContext.NamespaceUris`, `ServerUris` and `MappingVersion`. They are now read lock-free from one immutable snapshot that setters swap atomically. - **Allocation-free tracking.** Tracking a pending request (`PendingRequestScope`) and handing a decoded request out of the gate allocate nothing. - **Single-chunk decode.** A request in one chunk is decoded from that chunk's buffer. - **Fair-queue entry.** An entry coordinates its cancellation registration with an interlocked state (`RegistrationState`) instead of a lock object. ### Client fix `Session.CloseAsync` disposed the session synchronously, and `Dispose` waits for the async dispose of the session and its subscription engine. Closing 1000 sessions at once blocked one pool thread per session while that dispose needed pool threads, and the close took minutes in `ServerManySessionsLoadTestAsync(1000)`. `CloseAsync` now awaits `DisposeAsync`, and dispose awaits an engine that implements `IAsyncDisposable`, as `DefaultSubscriptionEngine` now does. ## Behaviour notes - **Wire order and tokens.** Sequence numbers stay strictly increasing in wire order, and the chunks of one message stay contiguous. Token renewal applies at a message boundary. After a reconnect, the OpenSecureChannel response is the first message on the new transport. - **Back-pressure** (#4707) is unchanged. The receive loop stops reading above 100 queued writes. - **Client.** `Session.CloseAsync` no longer blocks a thread on the dispose. ## Not in this PR The striped server-stop tokens of #4771 are dropped. Since #4767 a server request no longer registers with the stop token, because the deadline monitor polls it. ## Testing - `UaSCBinaryChannelSendOrderTests` covers: - wire order and random interleaving; - token renewal (decrypt and verify) for five policies; - buffers returned once; - a seal failure after numbering stops the channel, and a message numbered after the gap is not sent; - the replaced transport; - back-pressure; - a response that cannot be encoded is answered with a fault. - `TcpReconnectKeyAgreementTests.AResponseQueuedForTheDroppedTransportIsNotSecuredWithTheNewTokenAsync` (new) holds the old transport's sends during a reconnect. It fails on the previous order. The seal-failure test fails without the gap check. - Also run: `ChannelGateTests`, `TcpByteTransportTests`, `ServiceMessageContextMappingTests`. - Local, net10.0: - Opc.Ua.Core.Tests (Stack.Server, Transport, Tcp, ChannelGate): 2739 passed, 0 failed; - Opc.Ua.Configuration.Tests: 241 passed, 0 failed. net48 builds. `dotnet format whitespace --verify-no-changes` passes on the changed files. - CI: the `CI` workflow on this head is running; the commits were tested together with run to completion in #4771 (its last full CI run passed). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2f72239a72 |
Quickstarts alarms: report one event per Enable instead of two (#4780)
Every Enable call on a reference-server alarm produced **two** condition
events (different EventIds, same Time); Disable produced one.
## Cause
`ConditionTypeHolder.OnEnableDisableAlarm` is registered as the
condition's `OnEnableDisable` hook, which
`ConditionState.ProcessBeforeEnableDisable` calls *before*
`OnEnableCalled`/`OnDisableCalled` change the `EnabledState`. The hook
switched the state (`SetEnableState`) and called `ReportEvent()` itself;
the core then ran `UpdateStateAfterEnable` and `ReportStateChange` for
the same change, because every concrete holder sets
`AutoReportStateChanges` (`AcknowledgeableConditionTypeHolder`). On
Disable the hook's `ReportEvent` is skipped (the condition is already
disabled), so only Enable doubled.
Part 9 §5.5.2: a change of `EnabledState` causes a ConditionType Event
Notification — one per change. The duplicate also made the CTT's *A and
C Enable Test_002* flaky: it took the second notification as the answer
to its next call (worked around in the CTT scripts).
## Fix
The hook keeps setting the `Message` ("Enabling" / "Disabling alarm …",
carried by the core's event) and leaves the state change and its event
to the core. For a condition without `AutoReportStateChanges` it still
changes the state and reports, as before.
## Tests
`AlarmsAndConditionsEnableTests.DisableEnableViaTypeAndInstanceMethodNoEventsDuringDisabledAsync`
now asserts exactly one condition event in the Enable call window —
fails on master (`Expected: 1 But was: 2`), passes with the fix. All A&C
history tests (152, 3 runs) and client alarm tests (55) pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
d90ff9fd8f |
Server: release the HTTPS listener's pinned TLS certificate when it fails to open (#4775)
## Problem A full local `Opc.Ua.Server.Tests` run (net10.0) passed all 8606 tests, but the assembly-level certificate leak check failed the teardown: ``` Certificate leak detected: 1 instance(s) created but not disposed UNREACHABLE UNDISPOSED CERTIFICATES: at Certificate.AddRef() at HttpsTransportListener.PrepareTlsCertificate() at HttpsTransportListener.StartAsync / OpenAsync at ServerBase.CreateServiceHostEndpointAsync ... at ServerStartupTests.StartServerAsync(String uriScheme) (https case) ``` It did not reproduce when `ServerStartupTests` ran on its own. ## Root cause `HttpsTransportListener.StartAsync` pins the TLS certificate (`PrepareTlsCertificate` takes an `AddRef` handle) **before** Kestrel binds the port. If the port is already taken, Kestrel's `IOException` escapes `OpenAsync`. `ServerBase.CreateServiceHostEndpointAsync` logs and rethrows it **without disposing the listener**. The listener is not in `TransportListeners` yet, so nothing ever releases it. `ServerFixture` then retries on another port and the test passes, but the pinned certificate is lost. Ports collide when parallel suites race for the port `GetNextFreeIPPort` hands out, which is why the leak only showed up under load. ## Fix - **`ServerBase.CreateServiceHostEndpointAsync`** disposes a listener that fails to open. Only listeners in `TransportListeners` are disposed when the server stops, so a failed one has no other owner. The `listener` parameter doc now states that the server takes ownership. - **`HttpsTransportListener.StartAsync`** stops the listener again when the start fails. That releases the pinned certificate and any host it built, so any caller that drops a listener after a failed open leaks nothing. If the cleanup also fails, both errors are reported in an `AggregateException`, the same pattern as `ServerBase.CompleteServerStartAsync`. Either change alone fixes the reported leak. Both are kept: the first fixes the ownership gap for every transport, the second makes the HTTPS listener clean up after itself whoever the caller is. ## Tests - `ServerStartupTests.StartServerOnTakenPortRetriesOnAnotherPortAsync(https | opc.https)`: holds the port with a dual-mode `TcpListener`, so the first start fails and the fixture retries. **Before the fix**, the assembly leak check reported 2 leaked handles (one per scheme) with the exact creation stack above. **After the fix**, it is clean. - opc.tcp is not covered: on Windows the UA TCP listener binds even while another socket holds the wildcard port, so its start does not fail. - `ServerStartupTests.CreateServiceHostEndpointDisposesListenerThatFailedToOpenAsync`: mock listener whose `OpenAsync` throws; checks that `DisposeAsync` is called once and the original exception is rethrown. - `HttpsTransportListenerTests.OpenOnTakenPortReleasesPinnedTlsCertificateAsync`: real listener and certificate on a taken port; checks that the pinned certificate fields are cleared after the failed open. Each unit test fails with only its own fix reverted. With either fix alone, the end-to-end test leaves the leak check clean. ## Local verification (net10.0) - `Opc.Ua.Server.Tests` `ServerStartupTests`: 8/8 passed, leak check clean. - `Opc.Ua.Core.Tests` `HttpsTransportListenerTests`, `WssAdmissionTests`, `SharedKestrelHost*`: 74/74 passed. This includes the "partially opened listener" tests, which rely on a failed `OpenAsync`. - The full `Opc.Ua.Server.Tests` run was stopped partway through, with no failures up to that point. net48 was not built locally; CI covers the full suites and the other target frameworks. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b36cacbeac |
ManagedSession: skip redundant outer reconnect after channel-manager recovery (fixes BoundClientCannotRetryAcrossFaultedManagedEntryReplacementAsync flake) (#4781)
## Problem `SessionBindingLiveTests.BoundClientCannotRetryAcrossFaultedManagedEntryReplacementAsync` (added in #4752) failed once on CI (`test-windows-net10.0 (2/15)` of #4775, run 38508438526): ``` Assert.That(lease.State, Is.EqualTo(ChannelState.Ready)) Expected: Ready But was: TransportReconnecting ``` ## Root cause Only the `ManagedSession` fixture variant (`[TestFixture(true)]`) fails; the plain-`Session` run in the same job passed. From the failing run's log: - 28.476: the budgeted reconnect is exhausted and the channel goes `Faulted`. `ManagedSession.OnManagedChannelStateChanged` calls `TriggerReconnect`, so the outer `ConnectionStateMachine` takes over by design: `Reconnect attempt 0, delay 1025 ms`. - 28.976 – 29.502: the test's manual `ReconnectAsync` swaps in a fresh entry and runs its reconnect cycle, which reactivates the session. `Reconnect completed ... Outcome=success` is logged and the state is `Ready`. - 29.502: the outer backoff expires (`ManagedSession: Reconnecting.`). `HandleReconnectAsync` runs a full transport reconnect on the channel that was just recovered, and the state goes `Ready -> TransportReconnecting` just before the test reads `lease.State`. `ClientChannelManager.ReconnectAsync` behaves correctly: it returns only after its own swap and cycle have completed. The defect is the redundant outer reconnect. It disconnects a freshly reactivated channel and costs an extra transport connection (which could also break the test's "no retry" connection-count assertion). ## Fix **`ManagedSession`** - Count channel-driven session reactivations: `Ready` transitions out of `TransportConnectedSessionReactivating` on this session's lease. A participant that failed fatally is removed from the entry before `Ready`, so it is never counted. - When a channel fault (`Faulted`/`Closed`) or a keep-alive failure requests the outer reconnect, record the current count as the trigger mark. - `HandleReconnectAsync` (under the recovery writer lock) skips the reconnect and reports success if a reactivation happened after the mark, the channel is still `Ready` and no channel recovery is in progress. It logs `ManagedSession: Reconnect skipped; the channel manager already reactivated the session.` (`ManagedSession + 30`). - Explicit `ManagedSession.ReconnectAsync` requests are counted separately (requested vs. served by a completed reconnect that started after them). While one is outstanding, the outer reconnect is never skipped, whatever automatic triggers happen meanwhile. A trigger the state machine rejects clears the mark, and overwriting the mark with a later trigger only makes skipping less likely. **Tests** - New `ManagedSessionSkipsOuterReconnectAfterChannelManagerReactivationAsync` (managed variant only). A gated `IReconnectPolicy` holds the outer reconnect in its backoff while the test faults the lease, recovers it through the manager and takes a connection snapshot; it then releases the gate, waits for `Connected`, and asserts no extra transport connection and a working session. No wall-clock dependency. Without the skip it fails with one extra connection. - New `ExplicitManagedReconnectRunsAfterChannelManagerReactivationAsync`: an explicit `ReconnectAsync` joins the gated outer reconnect, a later fault re-triggers it, and the manager recovers the channel. The explicit reconnect must still open a new connection. It failed (`Expected: greater than 9, But was: 9`) before explicit requests were tracked separately. - `BoundClientCannotRetryAcrossFaultedManagedEntryReplacementAsync`: in the managed variant, wait for the outer recovery to start before the manual reconnect, and await `StateMachine.WaitForConnectedAsync` before asserting `Ready` and taking the connection-count snapshot. - `WithSessionAsync` takes an optional `IReconnectPolicy`; new helpers `FaultLeaseAsync` and `GatedReconnectPolicy`. ## Verification - Injected `await Task.Delay(200)` after the reconnect cycle in `ClientChannelManager.ReconnectLeaseAsync` (not committed). The old test then fails every time with the exact CI assertion, and the updated test passes. - `CustomTestTarget=net10.0 dotnet test -c Release -f net10.0 --filter FullyQualifiedName~SessionBindingLiveTests`: 5 sequential runs, 94/94 passed each time. These runs had the test change only; the `ManagedSession` skip was not yet in place. - With the skip: the new test and the original test pass; with the skip disabled, the new test fails as described above. - `--filter "FullyQualifiedName~ManagedSession|FullyQualifiedName~SessionBindingLive|FullyQualifiedName~Reconnect"` on `Opc.Ua.Client.Tests` (net10.0, Release) with the final code: 514 passed, 2 skipped (the managed-only tests in the plain-`Session` fixture). Full `Opc.Ua.Client.Tests` / `Opc.Ua.Sessions.Tests` runs and the net48 build are left to CI. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
27c17fb410 |
Preserve NodeVersion eligibility when deleting runtime nodes (#4777)
# Description Preserve a runtime node's NodeVersion eligibility before removing it from the address space. Previously, deletion removed the local node before model-change filtering, which then treated it as an unresolved external node and emitted a spurious NodeDeleted entry. This fixes unversioned runtime/session-diagnostics deletion in both AsyncCustomNodeManager and the legacy CustomNodeManager. Strict mode suppresses unversioned deletion entries; versioned nodes and the explicit compatibility mode keep their events. The regression matrix exercises all four eligibility combinations across both async configurations and the legacy adapter. The documentation now cites OPC UA Part 3 section 9.32.2, which requires NodeVersion/model-change correlation. This PR is intentionally limited to the original eligibility bug. Per-node monitoring completion and outcome-based model-change completion are separate changes tracked in #4768 and #4769, including broader subtree/service-deletion handling. ## Related Issues _Reference all GitHub issues this PR addresses. If there is no issue yet, open one and link it here._ _If this is a relatively large or complex change, a design must have been discussed in the related tracking issue and signed off (which becomes the Architectural Decision Record (ADR))._ - Fixes #4770 - Related: #4768, #4769 ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [x] 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. - [x] 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. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. ## Validation The server test project and its dependencies built successfully on net10.0 and net48 with no new build/analyzer warnings. Focused reruns: ```powershell dotnet test tests\Opc.Ua.Server.Tests\Opc.Ua.Server.Tests.csproj --no-build --no-restore -f net10.0 -p:CustomTestTarget=net10.0 --filter "Name~DeleteNodePreservesModelChangeEligibilityAsync" ``` Result: **12 passed, 0 failed**. ```powershell dotnet test tests\Opc.Ua.Server.Tests\Opc.Ua.Server.Tests.csproj --no-build --no-restore -f net48 -p:CustomTestTarget=net48 --filter "Name~DeleteNodePreservesModelChangeEligibilityAsync" ``` Result: **12 passed, 0 failed**. Full-solution .NET 10 validation was attempted, but the unchanged AOT test projects reject the VSTest target with Microsoft.Testing.Platform 2.4.1 under SDK 10.0.401. The run was stopped after this blocker was confirmed. It is not reported as a passing suite. Exact isolated reproduction of the blocker: ```powershell dotnet test tests\Opc.Ua.Aot.Tests\Opc.Ua.Aot.Tests.csproj --no-build --no-restore -p:CustomTestTarget=net10.0 -p:NuGetAudit=false ``` Result: **failed** with "Testing with VSTest target is no longer supported by Microsoft.Testing.Platform on .NET 10 SDK and later." The full net48 solution was not run. Restore/build commands used `NuGetAudit=false` because NuGet's audit endpoint was unavailable; repository audit settings are unchanged. Published as a draft by request until the full validation/CI gates are satisfied. No release-version files are included; this branch starts from master. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
f1e51c19f1 |
Restore monitoring for survivors of partial node deletion (#4776)
# Description Complete failed recursive deletion according to each monitored node's actual state, rather than inferring the entire subtree's outcome from the root. Monitored items for removed nodes remain detached and source-deleted. Items for surviving descendants are restored with their existing identity and ownership. Restoration attempts continue after individual failures, and compensation failures are surfaced together with the original deletion failure. The change covers internal and service-facing async deletion and the synchronous compatibility path. Regression tests monitor both the removed root and surviving child across both monitored-item implementations. ## Related Issues _Reference all GitHub issues this PR addresses. If there is no issue yet, open one and link it here._ _If this is a relatively large or complex change, a design must have been discussed in the related tracking issue and signed off (which becomes the Architectural Decision Record (ADR))._ - Fixes #4768 - Related: #4769, #4770 - Design and failure reproduction are recorded in #4768; this draft awaits review/sign-off. ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [x] 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. - [x] 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. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. ## Validation The server test project and its dependencies built successfully on net10.0 and net48 with no new build/analyzer warnings. The entire monitored-item lifecycle fixture, including six new partial-subtree cases and four new compensation-failure cases, passed: ```powershell dotnet test tests\Opc.Ua.Server.Tests\Opc.Ua.Server.Tests.csproj --no-build --no-restore -f net10.0 -p:CustomTestTarget=net10.0 --filter "FullyQualifiedName~MonitoredItemManagerLifecycleTests" ``` Result: **63 passed, 0 failed**. ```powershell dotnet test tests\Opc.Ua.Server.Tests\Opc.Ua.Server.Tests.csproj --no-build --no-restore -f net48 -p:CustomTestTarget=net48 --filter "FullyQualifiedName~MonitoredItemManagerLifecycleTests" ``` Result: **63 passed, 0 failed**. The full-solution .NET 10 VSTest invocation is blocked by the unchanged AOT test projects' Microsoft.Testing.Platform configuration. Full net48/net10 solution validation and CI remain outstanding. The blocker was reproduced in the original-fix worktree with: ```powershell dotnet test tests\Opc.Ua.Aot.Tests\Opc.Ua.Aot.Tests.csproj --no-build --no-restore -p:CustomTestTarget=net10.0 -p:NuGetAudit=false ``` Result: **failed** because Microsoft.Testing.Platform 2.4.1 rejects VSTest under SDK 10.0.401. Restore/build commands used `NuGetAudit=false` because the audit endpoint was unavailable; repository audit settings are unchanged. This is an independent master-based draft. It does not include the separate NodeVersion eligibility fix or the model-change completion redesign. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
89c1275d9d |
Fix managed browse closed-channel and cancellation handling (#4774)
# Description Fix managed browse failure reporting when the session's channel has closed or the caller has already cancelled the operation. - Use the existing checked `TransportChannel` accessor in `ClientBase.MessageContext`, so a closed channel reports `ServiceResultException(BadSecureChannelClosed)` instead of `NullReferenceException` during `Browser` construction. - Check cancellation at entry to `SessionClientExtensions.ManagedBrowseAsync` and the array-based `Browser.BrowseAsync`, before session access or request submission. - Document failure semantics and add unit plus real client/server regression tests for closed channels, pre-cancelled empty/non-empty inputs, Browse/BrowseNext request failures and cancellation, and mixed per-node results. - Preserve existing propagation of request exceptions and the per-node result/error contract. No dependency, analyzer configuration, or public API signature changes. The real-session before-fix run reproduced three failures (closed channel, and pre-cancelled closed channel with empty/non-empty input). All corresponding regressions pass after the fix. ## Related Issues Fix the closed-channel and pre-cancelled managed-browse behavior described in the linked bug report. _If this is a relatively large or complex change, a design must have been discussed in the related tracking issue and signed off (which becomes the Architectural Decision Record (ADR))._ - Fixes #4773 ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [x] 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. - [x] 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. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. ## Validation Windows, .NET SDK 10.0.401. **134 targeted test cases passed**, including existing browser and continuation-point regressions: | Suite | net10.0 | net48 | | --- | ---: | ---: | | BrowserTests + BrowserAuditRegressionTests | 27 passed | 27 passed | | ClientBaseTests | 18 passed | 18 passed | | ContinuationPointInBatchTest managed-browse cases | 22 passed | 22 passed | | Total | **67 passed** | **67 passed** | Tests were added and prove the fix, but coverage percentages were not collected; the combined tests/coverage checklist item is therefore left unchecked. CLA status has not been independently verified. ### Exact test commands and results Commands ran from the repository root. Test output was redirected to local logs. ```powershell # PASS: 27 tests. dotnet test Tests\Opc.Ua.Client.Tests\Opc.Ua.Client.Tests.csproj --no-restore -f net10.0 -p:CustomTestTarget=net10.0 -p:NuGetAudit=false '-p:WarningsNotAsErrors=CA2000%3BCA2007' --filter "FullyQualifiedName~Opc.Ua.Client.Tests.BrowserTests|FullyQualifiedName~BrowserAuditRegressionTests" --logger "console;verbosity=normal" # PASS: 18 tests. dotnet test Tests\Opc.Ua.Core.Tests\Opc.Ua.Core.Tests.csproj --no-restore -f net10.0 -p:CustomTestTarget=net10.0 -p:NuGetAudit=false '-p:WarningsNotAsErrors=CA2000%3BCA2007' --filter "FullyQualifiedName~ClientBaseTests" --logger "console;verbosity=normal" # PASS: 22 tests, using the freshly rebuilt net10.0 test binaries. dotnet test Tests\Opc.Ua.Sessions.Tests\Opc.Ua.Sessions.Tests.csproj --no-build --no-restore -f net10.0 --filter "FullyQualifiedName~ContinuationPointInBatchTest&Name~ManagedBrowse" --logger "console;verbosity=normal" # PASS: 27 tests. dotnet test Tests\Opc.Ua.Client.Tests\Opc.Ua.Client.Tests.csproj -f net48 -p:CustomTestTarget=net48 -p:NuGetAudit=false '-p:WarningsNotAsErrors=CA2000%3BCA2007' --filter "FullyQualifiedName~Opc.Ua.Client.Tests.BrowserTests|FullyQualifiedName~BrowserAuditRegressionTests" --logger "console;verbosity=normal" # PASS: 18 tests. dotnet test Tests\Opc.Ua.Core.Tests\Opc.Ua.Core.Tests.csproj -f net48 -p:CustomTestTarget=net48 -p:NuGetAudit=false '-p:WarningsNotAsErrors=CA2000%3BCA2007' --filter "FullyQualifiedName~ClientBaseTests" --logger "console;verbosity=normal" # PASS: 22 tests. dotnet test Tests\Opc.Ua.Sessions.Tests\Opc.Ua.Sessions.Tests.csproj -f net48 -p:CustomTestTarget=net48 -p:NuGetAudit=false '-p:WarningsNotAsErrors=CA2000%3BCA2007' --filter "FullyQualifiedName~ContinuationPointInBatchTest&Name~ManagedBrowse" --logger "console;verbosity=normal" ``` ### Production build and patch checks ```powershell # PASS: production Client/Core dependency build, 0 warnings and 0 errors. # Normal warnings-as-errors policy; no analyzer downgrades. dotnet build src\Opc.Ua.Client\Opc.Ua.Client.csproj --no-restore -f net48 -p:CustomTestTarget=net48 -p:NuGetAudit=false --verbosity minimal # PASS: no whitespace errors. git diff --check ``` All six changed files retain CRLF line endings. No build/analyzer diagnostics were reported in the changed files. ### Validation limitations - Initial unmodified-policy test builds failed on unavailable NuGet audit data (`NU1900`) and unrelated existing `CA2000`/`CA2007` diagnostics in test code. - The test commands use the documented offline `NuGetAudit=false` option and command-only analyzer downgrades. TODO: remove those command-only downgrades when the existing certificate/transport test diagnostics are fixed. Repository build/analyzer settings are unchanged. - An initial full `ContinuationPointInBatchTest` fixture invocation rebuilt the net10.0 binaries but was stopped during expensive existing node-cache traversals. It was replaced by the passing managed-browse subset above. - The complete `UA.slnx` suites were **not run**. Full-solution validation, coverage measurement, and CI/CodeQL results remain outstanding. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
6cfb8cf196 |
Server: remove per-request context copies and lock hot spots from the request path (−16 % allocation per Read) (#4767)
## Description Five small changes to the per-request server path, found in the CPU and allocation profiles of the o6 read benchmark (`AsyncCustomNodeManager`, Server GC, 10 clients × 100 Reads in flight) on master |
||
|
|
be50f02700 |
Add additional companion spec MCP tool profiles (#4731)
# Description Expose existing stack APIs through **337 additional domain MCP tools**: 114 industrial-companion tools, 35 WoT/xRegistry/document tools, and 188 higher-level stack-workflow tools. The fully enabled catalogue contains **492 tools**; profile selection is not an execution grant. The packages now follow the reviewed structure: **`Opc.Ua.Mcp.Workflows`** contains roles/users, files, alarms and history; **`Opc.Ua.Mcp.WotCon`** contains management/documents and independently registered live WoT execution. GDS and schema tools remain separate packages. The executable composes **18 libraries**. | Workflow | Added domain tools | Profiles | | --- | ---: | --- | | GDS directory, certificates, reference-only credentials/tokens and Part 21 onboarding | 46 | `gds`, `certificates`, `credentials`, `onboarding` | | Roles/users, regular and temporary files, alarms/conditions and historical access | 74 | `roles`, `users`, `files`, `alarms`, `history` | | Bounded runtime types, XSD/BSD/UA JSON, PubSub and REST OpenAPI artifacts | 6 | `schemas` | | Named PubSub transports, remote configuration, declarative adapters/bridges and SKS | 52 | `pubsub-runtime`, `pubsub-configuration`, `pubsub-bridge`, `pubsub-security` | | Live HTTP(S), MQTT, Modbus TCP and OPC UA WoT forms | 10 | `wot-runtime` | - Reuse typed clients, generated proxies, SDK transports, schema providers and injected services rather than introducing another session manager or protocol engine. - Require host enablement plus exact caller/session/secret/network grants. Protected generic Call/Write/node-management/history/file paths enforce the same policy. - Bind classifications, dispatches, file handles and historical cursors to actual UA generations. Recovery cannot retarget an old handle or authorized mutation. Preserve acknowledged results after late cancellation; do not imply atomicity or retry business commands. - Use opaque secret-store references, explicit writable-store registration, issuer/resource/caller binding and reserved capacity before issuance. Report remote success/local persistence failure as partial completion; redact provider-controlled diagnostic text. - Bound new named runtimes to eight per host, two per caller, five-minute idle expiry and a thirty-minute absolute lease by default. Pending/closing resources consume quota; failed cleanup remains visible. Credential invalidation and session recovery close dependent runtimes. - Keep document planning separate from execution, artifacts root-confined and real JSON inputs typed. Preserve old profile ordinals while widening the set to 64 bits. CI grades fully enabled privileged catalogues rather than connection-only views. **Limitations:** live Kafka remains explicitly unavailable because the backend cannot enforce policy on broker-discovered connections; configuration/schema/capability inspection stays available. The latest public Dekaf v1.23.5 was also checked and its DNS injection remains internal. No private-API workaround or bootstrap-only security claim was added. Full legacy Core registration retains its documented JSON-reflection limitation: native tests isolate its four connection descriptors and validate the expanded production registrations, not whole-host NativeAOT support. ## Related Issues - Fixes #4730. ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [ ] 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. - [x] 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. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [x] I have addressed **all** PR feedback received. |
||
|
|
8c52cba0b2 |
CTT reference server: faster alarm cycle in --ctt mode, MinSubscriptionLifetime 1000 ms (#4766)
Shortens CTT runs against the reference server without changing what the CTT verifies. ## Changes - **Alarm simulation step in `--ctt` mode: 1000 → 500 ms.** `AlarmNodeManagerFactory.SimulationInterval` (default stays 1000 ms) is passed to `AlarmNodeManager`; `ConsoleReferenceServer --ctt` sets it via `Quickstarts.Servers.Utils.ConfigureCttModeNodeManagers()` to `Utils.CttAlarmSimulationInterval` (500 ms) before the node managers are created. A full alarm cycle (40 steps through all limits) takes 20 s instead of 40 s. Every alarm still passes through all states and limits; the alarm NodeIds carry the interval (`…ExclusiveLevelAlarmType.500.Alarm` in CTT mode). - **`Ctt.ReferenceServer.Config.xml`: `MinSubscriptionLifetime` 10000 → 1000 ms.** CTT scripts request short subscription lifetimes on purpose and then wait for them to expire (Subscription Basic 063/016, Durable 012, Minimum 02 004, Publish Min 05 Err-001, Diagnostics 024); the 10 s floor inflated those waits. Part 4 sets no minimum; the server still enforces lifetime ≥ 3 × keep-alive. ## Measured (patched CTT 1.05.513 scripts) | Part | Before | After | | --- | ---: | ---: | | A&C (p18), alarm step only + CTT Alarm Cycle Time 20 | 932 s | 576 s | | Subscriptions (p16), MinSubscriptionLifetime only | 679 s | 579 s | Combined full run with the matching CTT-side changes (ReadyToRun server build, publish interval 150 ms, SecureChannel min lifetime 1000 ms, A&C Refresh script waits): **72 → 54 min**, 0 regressions against the CTT baseline (the only error is the known CTT-binary defect GDS 019). The CTT CI repo derives its `Alarm Cycle Time` from `Utils.CttAlarmSimulationInterval`, so runs against branches without this change keep the old 30 s setting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
40288fa7d8 |
Tests: weighted FairRequestQueue test asserts the median of three windows (#4763)
# Description `FairRequestQueueTests.WeightedServiceHoldsUnderConcurrentWorkersAsync` failed once on GitHub Actions macOS net10.0 (#4753, run 38349863236): light=124 heavy=276, ratio 2.23 against an expected 2.5-3.5. It passed on every other CI run and 30/30 locally. **Root cause.** `TrySelect` dequeues an owner's turn token from the lock-free ready queue. On the last token of a turn, `TakeTurnToken` CASes the turn and calls `PushTurn` for the next one. A worker preempted between those two steps leaves the owner with no queued token while it is stalled. The work-conserving dispatch serves the other owner in the meantime, and that share is never made up. Delay injection confirms this: sleeping 1 ms on every 7th turn publish collapses 1:3 to roughly 1:1 (light=199 heavy=201). Publishing a turn ahead of time would only cover stalls shorter than one round (microseconds), while real preemption lasts milliseconds. Making up the lost share (deficit or virtual time) would mean redesigning the scheduler. So this PR documents the bounded skew and makes the test robust instead. **Changes** - `FairRequestQueue` remarks: the weighting holds over time, not in every window, and a preempted worker can cost an owner its share for the length of the stall. - `WeightedServiceHoldsUnderConcurrentWorkersAsync` now measures 3 independent windows through `MeasureWeightedServiceAsync` and asserts that the **median** heavy/light ratio is in 2.5-3.5. The failure message lists all three windows. - Each window uses perOwner = 480 and measured = 560, with queue limits of `2 * perOwner`. That stays under the TestProvider's 1000-request cap and keeps both owners backlogged. - Each window still checks that both owners stay backlogged, then drains the queue and checks that everything is back to zero. The median tolerates one skewed window and still fails when the weighting is lost systematically. With stalls injected into one window only, the median is 3.0 and the test passes (e.g. `light=238 heavy=322; light=140 heavy=420; light=140 heavy=420`). With stalls injected into every window, it still fails. **Validation:** the `FairRequestQueueTests` fixture passes 93/93 on net10.0 Release. The weighted test passed 30/30 runs pinned to 2 cores (`start /affinity 3`). `dotnet format whitespace --verify-no-changes` is clean, and the build has 0 warnings. ## Related Issues - Flaky test seen on #4753 after #4750 (FairRequestQueue without a queue-wide lock) ## Checklist - [x] 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. - [x] 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. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3e433eb6c4 |
XRegistry: registration lifecycle, identifier allocation, registrar roles and atomic Resource replace (#4756)
# Description Extracts the generic xRegistry registration lifecycle from the WoT work into a standalone, master-targeted change: - Canonical xRegistry identifier allocation (`XRegistryIdentifier`) with case-folded bounded collision handling and exact Version allocation/delete semantics. - Registrar roles / capabilities in the xRegistry NodeSet2 model (only the roles/capabilities facts). - Default-Version views and Resource Meta handling. - FileType read/write modes, session ownership/discard, cursor handling, and short reads until actual EOF on the client. - Atomic whole-document `ReplaceAsync` on the in-memory and file-system resource stores. - Generic projection engine/provisioning and projection contracts, label ownership/no-op behavior, Resource-vs-Version event targets. - Documentation in `docs/XRegistry.md` and XML docs for owned members. - Tests: Identifier, RegistrarRoles, Attribute, Projection, RegistrationEvent, FileMode, ResourceStore, Client, VersionAssignment, concurrent read. Intentionally excluded (handled in separate PRs): trusted Federation / Registry origin model and provider (G9); WoT lease/dispatcher, prepared manifest, 0.7 model/version bump (G12). Merged current `master` ( |
||
|
|
c2b95e2085 |
XRegistry: trusted logical-Resource federation identity (#4755)
# Description Adds trusted **logical-Resource** federation identity to XRegistry - A federation proxy binds an origin registry (`RegistryOriginDataType`), application, portable `ResourceNodeId` and Xid, never an endpoint or content-digest identity. - `XRegistryFederationTarget`, `IXRegistryFederationProvider`, `XRegistryClient.Federation` (verify/follow/relocate; uses master's `ISessionBindingProvider.CreateBindingAsync`). - `XRegistryFederationNodeManager`, `XRegistryServerOptions`/DI: required target/provider verification and proxy admission. - Model hunks only: `RegistryOriginDataType` 63026 (+encodings 63591/63592), `OriginRegistry` 63593 + `ResourceType` HasProperty, `ExternalReference`/`ResourceUrl` description rewrites. - Tests (`XRegistryFederationNodeManagerTests`), package READMEs, `docs/XRegistry.md` Federation section (links `SessionBindings.md`). Merged current master ( |
||
|
|
0e40e12296 |
CI: CTT regression runs, weekly and on PRs labelled 'ctt' (#4760)
## Summary Adds the **CTT regression** workflow. It runs the OPC UA Compliance Test Tool against the reference server: - every Sunday on master; - manually; - on pull requests a maintainer labels `ctt`. The CTT is member-only software, so the run itself happens in the private repository `OPCFoundation/UA-.NETStandard-CTT`. This workflow does four things: 1. Starts the private run with `workflow_dispatch`, passing the PR head ref and the commit that was labelled. A push after labelling is not run; label again. 2. Waits for the run (about 1.5 hours) and downloads only its public summary: result counts per part and the test cases that changed against the baseline. CTT scripts and logs are never downloaded. 3. Posts the result as the `CTT` commit status, as a PR comment (updated in place) and in the job summary, then removes the label. 4. Feeds a README badge for the weekly master run. The results are a regression signal for this code base, not a certification. The summary says so. ## Security - **Secret access:** `pull_request_target` is used only to get the access secret. The job never checks out or runs PR code; PR data is passed through `env`. - **PR code runs next to the CTT** on the private runner, so it could read the CTT files. Only label a PR after reviewing it. - **Access to the private repository:** a GitHub App installed on that repository only, or a fine-grained token, with Actions read/write. A run takes longer than an App installation token lives (one hour), so the job mints a new installation token for each call. ## Setup needed after merge - [ ] Create a GitHub App, or use an existing one. Permissions: Actions read/write and Metadata read. Install it on `OPCFoundation/UA-.NETStandard-CTT` only. - [ ] Set variable `CTT_APP_ID` and secret `CTT_APP_PRIVATE_KEY` in this repository. Alternatively, set secret `CTT_TOKEN` with a fine-grained token that has the same permissions on the same repository. - [ ] Create the `ctt` label. ## Test plan - [x] Private workflow on GitHub-hosted `windows-latest`. Silent CTT install, server build, generated test PKI and all 23 parts ran in about 83 minutes and matched the local baseline. Remaining differences: Pass→Warning flips from response-timestamp delays, which led to raising Time Tolerance to 250 ms, and one ResendData timing case. - [x] Embedded bash scripts pass `bash -n`; App JWT signing checked against a throwaway RSA key. - [ ] End to end from this repository after merge, once the secret is set: run it manually, then label a PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3a31473cf4 |
Server: no server-wide or per-session locks in per-request session validation (#4753)
# Description
Every request passes through `SessionManager.TryGetSessionContext` at
admission and again at dispatch
(`FairRequestQueue.GetRevalidationStatus` →
`DefaultServerResourceIsolationProvider` →
`ISessionBindingProvider.TryGetSessionContext`), and then through
`Session.ValidateRequest`. Together they took:
1. the server-wide `m_bindingsLock`, to confirm the candidate binding
was still the committed one;
2. the per-session diagnostics lock in `Session.HasExpired`. It is
inlined, so CPU traces charge its spin to `TryGetSessionContext`;
3. the session lock in `Session.IsSecureChannelValid`;
4. the session lock around all of `Session.ValidateRequest`, plus the
diagnostics lock twice (contact refresh, diagnostic counters);
5. the server-wide `SessionManager` event lock, to raise
`SessionDiagnosticsChanged` for every request even when nothing
subscribes.
With the FairRequestQueue lock also removed (a local change without the
queue-wide lock, not yet a PR), these were the next waiting points:
14–16.5 % of on-CPU samples spun in `Lock.TryEnterSlow` under
`TryGetSessionContext`, and once that was fixed another 7–10 % under
`Session.ValidateRequest`.
## Changes
**SessionManager: confirm the binding without the bindings lock**
- `SessionActivationState` publishes a `DispatchBinding` (the committed
binding while no activation is committing, otherwise `null`) with
`Volatile.Write`. The `BindingContext` and `IsCommitting` setters
republish it, and every writer of those already runs under
`m_bindingsLock`.
- The lookup reads it once for the candidate and once to confirm. A
commit always creates a new `SessionBindingContext`, so an unchanged
reference means nothing replaced it. A token is mapped to a session
object at most once and is never re-added after removal, so finding the
token still mapped to the same session afterwards means both held at the
instant of the confirming read, which is the guarantee the lock gave.
- IsCommitting, transfer, close/claim, shutdown and the
certificate/security checks are unchanged.
**Session: read the channel id without the session lock**
- `SecureChannelId` is published with a volatile write in `Activate`,
and `IsSecureChannelValid` reads it without the session lock.
**Session: request validation and diagnostic counters without locks**
- Request counts are interlocked increments of per-session slots (total
and error per `RequestType`, plus unauthorized). They are applied to
`SessionDiagnostics` under the diagnostics lock whenever it is read
(`ReadDiagnostics`, `UpdateDiagnostics`, the diagnostics-node callback,
`ClientLastContactTime`), so readers see the same totals.
- The contact refresh is a compare-and-swap on the tick count, and
requests within the same millisecond do not write. An expiry observed by
`HasExpired` or by a refresh is recorded in the contact word
(`kExpiredFlag`), so a request that read the clock before the monitor
saw the timeout cannot refresh it afterwards. This keeps the documented
"a request cannot revive a session the monitor saw expire" guarantee
without the lock. The intermediate commit that made `HasExpired`
lock-free on its own had opened that window; this commit closes it.
- `ValidateRequest` takes no session lock: the channel id and
`Activated` are published with volatile writes.
- `RaiseSessionEvent` returns before taking the event lock when a
`DiagnosticsChanged` event has no subscriber.
Behaviour differences reviewers should know about:
- `ClientLastContactTime` is millisecond-granular, and two concurrent
refreshes can publish their wall-clock times in reverse order, which
leaves it up to about 1 ms older than the latest contact.
- After an expiry has been observed, an `Activate` no longer refreshes
the contact time. `SessionManager` already rejects activation of an
expired session before that point.
## Tests
- `LookupDuringActivationCommitReturnsNoBindingAsync`: a lookup inside
the commit window (after `Session.Activate`, before the binding commit)
returns no binding. A test session re-implements `ISession.Activate` to
run the hook. Before this test, publishing the binding regardless of
`IsCommitting` passed all 690 session tests.
- `LookupRacingTransfersAndCloseReturnsOnlyCommittedBindingsAsync`: 4
readers look up while 1000 transfers alternate between two channels,
followed by a close. Each result must be a committed binding of the
queried channel, no older than the last completed activation and never
decreasing, and nothing may come back after the close.
- `RequestRacingObservedExpiryDoesNotReviveTheSessionAsync`: a clock
hook lets the monitor observe the expiry between a request's clock read
and its refresh, and the request must fail with `Bad_SessionClosed`. It
fails if `HasExpired` does not record the expiry.
- `ConcurrentRequestsAreCountedExactlyAsync`: 8 threads × 2100
validations (Read/Browse, plus rejected Writes). The totals, per-service
counts, error counts and unauthorized count must be exact, and a
concurrent reader never sees the total go backwards.
- Local runs (net10.0): `Opc.Ua.Server.Tests` 8487 passed / 0 failed;
`Opc.Ua.Sessions.Tests` 851 passed / 0 failed / 379 skipped. net48
builds.
## Measurements
`perf/ServerLoadHarness`, Server GC, paired alternating A/B, 7 rounds.
Baseline is master
|
||
|
|
a14b23e7dd |
Server: cheaper symmetric crypto path, FairRequestQueue without a queue-wide lock (#4750)
# Server: cheaper symmetric crypto path, FairRequestQueue without a queue-wide lock Roadmap items E1 and 5b of the o6 benchmark gap analysis, one commit each. - **E1:** Windows one-shot `Aes.EncryptCbc`/`DecryptCbc` import the key (`BCryptImportKey`) on every message, although #4720 caches one `Aes` per channel token. The AES-GCM and ChaCha20-Poly1305 policies also created a new keyed instance per message. - **5b:** with `AsyncCustomNodeManager`, which is what the o6 benchmark's 2.0.1 worker uses, the `FairRequestQueue` gate became the largest waiting point. `TryEnqueue`, `TrySelect` and `TryDequeue` spun on one `Lock` for every request. No wire, configuration or public API changes. ## Results These are paired A/B runs against `origin/master`, five rounds per scenario. The two harness builds alternate per round, starting side swapped every round, on the same shared machine (Ryzen AI 9 HX 370, 12 cores / 24 threads, Windows 11). The tables show medians, with ranges in brackets where they matter. The server is configured like the o6 2.0.1 worker: `AsyncCustomNodeManager`, Server GC, queue sized clients × outstanding + 3. o6read uses the benchmark's native open62541 client workers; `read` uses the harness's .NET client. On this machine throughput moves between rounds by up to 2×, so CPU and bytes per request are the reliable signals. ### E1: symmetric crypto (measured on |
||
|
|
63adef0d10 |
Fix the Sessions LoadTest fixture and the server bottlenecks it exposed (#4749)
The `[Explicit]` Sessions `LoadTest` fixture has not started since #4541 (also on the 2.0.0 tag): it set `MaxChannelCount = MaxSessionCount = 10100`, and resource isolation refuses to start unless the channel cap covers the sessions plus the bootstrap and reconnect floors plus one. Every LoadTest case failed in `OneTimeSetUp`. Running the fixture again, and profiling it and the `perf/ServerLoadHarness`, found several bottlenecks that shipped in 2.0. ## Changes **Test** - `MaxChannelCount = MaxSessionCount + 3`, the same margin as the framework defaults (100 / 103). - Notifications are counted with per-node `Interlocked` counters. The shared `ConcurrentDictionary<NodeId, int>`, updated by 750 subscriptions, took 13 % of the process CPU in bucket-lock contention. - After the writer stops, and after the last event, the tests wait until the received count stops growing instead of a fixed delay, and log the drain time. **Server / client** - **Detached monitored item recovery** (#4094): every node a NodeManager added (`AddPredefinedNodeAsync`, recursively for children) scanned every monitored item of every subscription for detached items. `CreateSubscription` adds ~30 diagnostics nodes, so with 450k monitored items subscription creation was quadratic: 69 % of CPU, and `CreateSubscription` timed out. `MonitoredItem` now keeps an exact process-wide count of live detached or deleted items, and `MasterNodeManager` skips recovery while it is zero. All flag changes go through one helper under the item lock; a disposed item does not count, because it cannot be attached again. - **Client channel gate** (#4232): `UaSCUaBinaryClientChannel.SendRequestAsync` entered the gate with a blocking `Gate.Enter()` inside an async method, and `SemaphoreSlim.Wait` spun for 19 % of CPU with many requests in flight on one channel. It now awaits `Gate.EnterAsync(ct)`, like the server side. - **ResourceManager** - Status code and symbolic id translation looked up their maps under the manager lock and translated while holding it. Sessions that request diagnostics do this for every notification: 83 % of lock contention. The maps are copy-on-write and read without the lock. - Translation tables were `SortedDictionary<string, string>` with the default, culture-sensitive comparer: up to 1 µs per Browse display name, 22 % of Browse server CPU. They now use an ordinal `Dictionary`. - **Namespace default permissions**: every permission check read `DefaultAccessRestrictions` / `DefaultRolePermissions` / `DefaultUserRolePermissions` of the node's namespace through two server-wide locks and deep-copied the role permission array, per node. That was 99 % of lock contention for Read/Write of 100 values. The values are now read once per namespace per service request and remembered on the `OperationContext`. Contexts that are not a service request (monitored item and session contexts, which can live long) remember nothing, so a changed default applies to them at once. ## Results `perf/ServerLoadHarness`, paired A/B against `master` (3 alternating rounds, medians, 20 sessions, 24 cores). The harness numbers predate the recovery, client gate and status code translation commits, which those scenarios do not exercise: | Scenario | Throughput | Server CPU / op | Server alloc / op | Lock contention | |---|---|---|---|---| | Read, 100 values | +19 % | −28 % | −18 % | −96 % | | Read, 100 values, SignAndEncrypt | +27 % | −26 % | −18 % | −98 % | | Write, 100 values | noise ¹ | −19 % | −17 % | −88 % | | Browse (Server object) | +3 % | −16 % | −10 % | −2 % ² | | Read, 1 value | +12 % | −4 % | ±0 | ±0 | ¹ Throughput varied run to run on the shared machine; per-op CPU and allocation are the reliable signal. ² Browse contention is the Server node's browse lock in `NodeState.CreateBrowser` (all sessions browse the same node); not changed here. `LoadTest` (opc.tcp, Release, in-process client and server): | Case | master | this PR | |---|---|---| | `ServerReadLoadTestAsync` | setup failure | passes (50M value reads, 0 errors) | | `ServerManySessionsLoadTestAsync(500)` | setup failure | passes | | `ServerSubscribeLoadTest_Classic` | setup failure; on 2026-09-26 (before #4541): `CreateSubscription` `BadRequestTimeout` | 76–85 % received, **fails** (needs 99 %) | | `ServerSubscribeLoadTest_ManagedSessionV2` | setup failure; 2026-09-26: 1,885 notifications received | 35–45 M notifications, 77–85 % received, **fails** | | `ServerEventSubscribeLoadTestAsync` | setup failure; 2026-09-26: 34.6 % | passes: 100 % delivered, drained ~40 s after the last event | The subscribe cases now lose notifications rather than deliver them late: delivery stops ~2 s after the last write. Every monitored item has queue size 1 and the writer is not paced by delivery, so whatever exceeds the publish rate (~585k notifications/s here, SignAndEncrypt with the client in the same process) is overwritten. The event test lost nothing either: it waited a fixed 2 s after the last event and counted the backlog as lost. It now waits until delivery stops. Its throughput is bound by garbage collection: the event consumer of the Server node spends ~90 % of its wall time suspended for workstation GCs of the shared test process. With `DOTNET_gcServer=1` it generates 702 instead of ~210 events/s and drains in 7 s instead of ~40 s, and the subscribe cases reach 87 % (Classic) and 93 % (ManagedSessionV2) at 78–106 M notifications. ## Testing - `Opc.Ua.Server.Tests` 8468 passed / 5 skipped, `Opc.Ua.Client.Tests` 2788 passed, `Opc.Ua.Sessions.Tests` 851 passed / 379 skipped, `Opc.Ua.Core.Tests` Tcp/Channel 797 passed (net10.0). - New `OperationContextDefaultPermissionsCacheTests`. - `Opc.Ua.Server` builds for net48; `dotnet format whitespace` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7c965fd636 |
Capture metadata images and generation-bound session clients (#4752)
# Description - Add isolated NodeState reference images, TypeTable graph snapshots, EncodeableFactory registration forks/views, StringTable mutation versions/private copies, and ServiceMessageContext mapping-holder generation tracking. Reference images are lazy for empty nodes; selected-image clones own independent storage and publication ownership. - Add the complete generic NodeBrowser continuation-dependency capability. This does not implement server browse-history retention, NodeManager read images, or server batch/readiness admission. - Add optional client binding capture for SessionClient, native Session and ManagedSession, with managed-lease/native-channel generation composition. Bindings capture authenticated-session identity, mapping holders/versions and owned endpoint/policy/configuration observations. Replacement, reconnect, mapping ABA and disposal permanently invalidate stale bindings; disposing a binding does not dispose its owner. - Preserve cancellation and master pooled-response ownership: a rejected pooled response is released exactly once; an accepted response remains caller-owned. Keep native transport submission under its replacement gate and completion outside the gate, without routing ordinary unbound requests through binding machinery. - Rename the existing server lookup interface and all actual consumers with minimal hunks; add focused fixtures, generated FileTypeClient usage guidance, and package/transport documentation. **Breaking preview type identity:** the existing 2.0-preview server lookup `Opc.Ua.ISessionBindingProvider` becomes `Opc.Ua.IServerSessionBindingProvider`. Server lookup consumers must update their referenced type and rebuild. This is neither source-compatible nor binary-additive for that preview type. The new client capability is optional and adds no mandatory members to existing ISession/ISessionClient/ITransportChannel contracts. This preview-to-preview rename is documented in SessionBindings/Transports, not in the 1.5-to-2.0 MigrationGuide. **Ownership boundary:** no RequestLifetime changes, certificate-manager/channel-entry adoption, BaseVariableState changes, Method-result feature extraction, NodeManager batch/readiness or browse-history retention, session restoration, registry federation, WoT conversion, or WoT runtime/planners/events. DependencyInjection/ResourceIsolation/index changes are lookup/link closure only. SessionManager's PR hunk changes only its implemented lookup-interface name; newer master public diagnostic masking is not replaced. ## Related Issues _Reference all GitHub issues this PR addresses. If there is no issue yet, open one and link it here._ _If this is a relatively large or complex change, a design must have been discussed in the related tracking issue and signed off (which becomes the Architectural Decision Record (ADR))._ - Part of #4511: user-approved combined groups 5+6 Wave A extraction. This PR does not close the umbrella or publish Wave B. ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [ ] 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. - [ ] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] 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. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [x] I have addressed **all** PR feedback received. ### Review qualification Review fixes are published at **`6b0eb1d5c32a439300fbcaff974b9af8b909161b`**, changing eight already-owned files. All five received review threads were replied to and resolved after verification. Factory selector installation and target-selection-plus-commit share the root publication gate, with consistent root→image release/capture ordering. TypeTable selector installation and final mutation routing share its existing gate. Native bound channel validity uses volatile observations; the two owned XML defects are corrected. | Requirement | Evidence | |---|---| | Selector installation must not hide TypeTable mutations | `SelectorInstalledDuringNodePreparationReceivesMutation`: old Add routing failed on both TFMs; corrected routing passes on both within **148/148** factory/TypeTable controls | | Selector installation must not race factory commit routing | Static common-root-gate/order proof plus baseline-GREEN `BuilderCapturedBeforeOwnershipUsesSelectedFactory`, `ViewOwnerReleasePreservesRegistrationsAndAllowsReuse`, `ViewOwnerReleaseCanRetryAfterPendingPublication`, `PublicationProtectsPendingAndRetiredFactoryImages` | | Lock-free validity must observe owner transitions | Static volatile-read reasoning plus native `BoundRequestCannotDispatchWhileNativeReplacementIsPendingAsync`, `BoundResponseRejectsInvalidationAfterNativeDispatchAsync`, `BoundClientCannotRetryAcrossFaultedManagedEntryReplacementAsync`: **12/12 each TFM**; Core binding/pooled ownership controls **17/17 each TFM** | | Correct owned XML documentation | “an unmanaged” and one multiline `ReferenceUpdate.Next` summary; scoped whitespace/INFO and build evidence below | Factory controls are **not** claimed as a deterministic factory RED, and public transport controls are **not** claimed as a deterministic weak-memory RED. No private-lock reflection, artificial production seam, or source-name assertion was added. The TypeTable RED uses the real `ILocalNode.NodeClass` callback during node preparation, and both expected RED receipts/TRX were retained. All accepted review runs use the unchanged admitted SDK wrapper, normal project references, sole mutex, one node/no shared compiler, finite resource guards and canonical short-C build outputs. Source hashes were frozen per command and matched the committed bytes afterward. SDK ownership was released **before** commit/push/review replies. No automatic merge was requested. Scoped whitespace passed across the four affected projects. Review INFO classification is **0 owned findings / 13 untouched baseline findings excluded** (whole-file formatter exits 1,1,0,0); no baseline RCS1140/RCS0055 expansion or suppression. The final normal net10 public Client/Core/Types trim/AOT-analyzer closure passed **0 warnings / 0 errors**. This is not a NativeAOT publish/run. <details> <summary>Accepted review command payloads and results</summary> These exact argument payloads were executed through the unchanged guarded SDK wrapper. `$proof` expands to the protected results-directory below. Successful explicit builds of the same source preceded the `--no-build` runs. ```powershell $proof = 'C:\Users\mschier\.copilot\session-state\a3b74048-c52f-4c87-8773-7a46c9ab0087\files\qualification-review-4752' # Exit 0: 148 passed, 0 failed dotnet test tests\Opc.Ua.Types.Tests\Opc.Ua.Types.Tests.csproj -f net48 -p:CustomTestTarget=net48 -p:UseArtifactsOutput=true -p:ArtifactsPath=C:\cp56-a3b7-v2 -nr:false --no-restore --verbosity minimal --filter "FullyQualifiedName~EncodeableFactorySnapshotTests|FullyQualifiedName~TypeTableTests" --logger "trx;LogFileName=review-types-green-net48.trx" --results-directory $proof # Exit 0: 148 passed, 0 failed dotnet test tests\Opc.Ua.Types.Tests\Opc.Ua.Types.Tests.csproj -f net10.0 --no-restore --no-build --filter "FullyQualifiedName~EncodeableFactorySnapshotTests|FullyQualifiedName~TypeTableTests" --logger "trx;LogFileName=review-types-green-net10.trx" --results-directory $proof -p:CustomTestTarget=net10.0 -p:UseArtifactsOutput=true -p:ArtifactsPath=C:\cp56-a3b7-v2 -nr:false --verbosity minimal # The following two per-TFM payloads each exited 0: # Core: 17/17 net48 and 17/17 net10.0; Client: 12/12 net48 and 12/12 net10.0 foreach ($tfm in @('net48', 'net10.0')) { dotnet test tests\Opc.Ua.Core.Tests\Opc.Ua.Core.Tests.csproj -f $tfm --no-restore --no-build --filter "FullyQualifiedName~SessionBindingResponseTests|FullyQualifiedName~SessionBindingContextTests" --logger "trx;LogFileName=review-actual-core-green-$tfm.trx" --results-directory $proof -p:CustomTestTarget=$tfm -p:UseArtifactsOutput=true -p:ArtifactsPath=C:\cp56-a3b7-v2 -nr:false --verbosity minimal dotnet test tests\Opc.Ua.Client.Tests\Opc.Ua.Client.Tests.csproj -f $tfm --no-restore --no-build --filter "FullyQualifiedName~BoundRequestCannotDispatchWhileNativeReplacementIsPendingAsync|FullyQualifiedName~BoundResponseRejectsInvalidationAfterNativeDispatchAsync|FullyQualifiedName~BoundClientCannotRetryAcrossFaultedManagedEntryReplacementAsync" --logger "trx;LogFileName=review-actual-client-green-$tfm.trx" --results-directory $proof -p:CustomTestTarget=$tfm -p:UseArtifactsOutput=true -p:ArtifactsPath=C:\cp56-a3b7-v2 -nr:false --verbosity minimal } # Exit 0: 0 warnings, 0 errors dotnet build src\Opc.Ua.Client\Opc.Ua.Client.csproj -f net10.0 -p:CustomTestTarget=net10.0 -p:UseArtifactsOutput=true -p:ArtifactsPath=C:\cp56-a3b7-v2 -nr:false --no-restore --verbosity minimal ``` The six expanded GREEN commands, exact results/TRX hashes and two expected RED commands are retained in `qualification-review-4752\qualification-summary.json`, SHA256 `C5E71A4D8D37BAC8803F6DF38B6BCE2B70F3D0AFD5DEC8493FAE9DB440DCFB5A`. Committed/qualified byte identity is in `publication-identity.json`, SHA256 `CD9515FE96703F8B241F2B7B9B71F73EF0FF2626C2C8BCCE40307971A8F8E809`. Per-command raw exits, guards, hashes and reports are retained outside disposable build roots. For each record in the protected `format-inputs.json` (Types/Core/Client libraries and Types.Tests), the recorded Project/Include values expand these exact payloads: ```powershell # Workspace properties recorded with the formatter receipts: $env:CustomTestTarget = 'net10.0' $env:UseArtifactsOutput = 'true' $env:ArtifactsPath = 'C:\cp56-a3b7-v2' # All four exit 0 dotnet format whitespace $Project --no-restore --verify-no-changes --verbosity minimal --include $Include # Exits 1,1,0,0 due only to untouched baseline findings; owned=0, baseline=13 dotnet format $Project --no-restore --severity info --verify-no-changes --verbosity minimal --report "$proof\formats\review-info-$i" --include $Include ``` Initial fresh matrix evidence remains separately identified and unchanged: Types274/Core119/Client151/server137 net10; Types274/Core119/Client151/server135 net48, all passing. Qualification base remains `0dff94bd378e79b8cf1a47e1aaf340f9b4b40048`; the initial published head was `9fee4583`. All 16 initial exact command arrays/results/TRX hashes remain in protected `qualification-v2\qualification-summary.json`, SHA256 `88E10240C20049B59A3401BE007C6528A85DF2F3DCFAF9D11D908CEF2946DA4E`. Failures/incidents remain retained rather than relabeled as success: original disappeared-root historical counts are invalidated; the large-TRX controller DOM-parser failure is preserved separately from accepted streaming receipts; the review argument-array defect/default-D-output controls are preserved but excluded from canonical review qualification. Correctly separated argument rechecks produced the accepted short-C Core/Client receipts. The sole SDK wrapper was not changed. Full UA.slnx, measured coverage increase, CLA verification, NativeAOT publication and current-head/master CI/CodeQL remain unclaimed. Actual merge—not resolved review threads—unlocks Wave B. </details> |