mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-28 22:55:49 +08:00
feature/endpoint-message-registry
2897
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6a9cad2c9f |
Wait for complete transfer initial values before stopping publication
The transfer fixture stopped after the first dynamic target notification while immediately asserting at least MonitoredItemCount notifications. CI observed an initial partial batch of one value rather than all ten. Match readiness to the existing final count assertion for SendInitialValues; preserve the ordinary changed-value wait, assertion thresholds and timeout budgets. |
||
|
|
994b4b02c9 |
Wait for backup reverse connection before redundancy discovery tests
CI could finish a dialed initial connection before the backup's first ReverseHello. Initial redundancy discovery snapshots available member connections, so the fixture could wait forever for an endpoint absent from that snapshot. Wait for the intended backup connection before returning the fixture; keep the no-backup path, timeout budgets and production behavior unchanged. |
||
|
|
563b3b32c7 | Merge branch 'master' into feature/endpoint-message-registry | ||
|
|
9e48f3d04d |
Client: apply TransportQuotas in ManagedSessionBuilder URL shortcuts (#4906)
# Description `ManagedSessionBuilder.UseEndpoint(string url, ...)`, `UseWebApiEndpoint(...)` and `UseWssOpenApiEndpoint(...)` created their endpoint with `new ConfiguredEndpoint(null, description, null)`. The `null` endpoint configuration falls back to `EndpointConfiguration.Create()` defaults (`MaxStringLength = 65535`, `MaxArrayLength = 65535`, `MaxMessageSize = 2 MiB`, ...). The transport channels build their message context from the endpoint configuration, so the builder's `ApplicationConfiguration.TransportQuotas` were silently ignored and `session.MessageContext` kept the defaults. The three shortcuts now build the endpoint with `EndpointConfiguration.Create(configuration)`, the same as `AddDiscoveryAndConnect` since #4810. Reported in #4905. That report was about `AddDiscoveryAndConnect`, which is already fixed on master. While investigating it I found the same pattern in these builder shortcuts. ## Tests - `ManagedSessionBuilderExtendedTests.UrlShortcutUsesApplicationTransportQuotas`: unit test for all three shortcuts, checks the built endpoint configuration. - `ManagedSessionBuilderTransportQuotaTests.UseEndpointUrlSessionUsesApplicationTransportQuotasAsync`: connects to an in-process reference server through `UseEndpoint(url)` and checks `session.MessageContext`. All four new test cases fail without the fix (they get the 65535 / 2 MiB defaults). Local runs on net10.0: - Full `Opc.Ua.Client.Tests`: 3327 passed, 2 skipped, 1 failed. The failure was `ManagedSessionServerRedundancyTests.ConnectWithProbingSelectsMemberWithHigherServiceLevelAsync`, which doesn't use the changed shortcuts. That fixture passed 3/3 when rerun on its own. - `Opc.Ua.Sessions.Tests` `HttpsWebApiIntegrationTests` and `WssOpenApiIntegrationTests`, which use `UseWebApiEndpoint` / `UseWssOpenApiEndpoint`: 19/19 passed. `Opc.Ua.Client.Tests` also compiles for net48. ## Related Issues - Related to #4905 ## 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> |
||
|
|
3c5aa421d3 |
Client: server redundancy over Reverse Connect member connections (#4885)
# Description ## Depends on This pull request depends on #4883 (application-supplied reverse connection sources) and includes its commits. #4884 (ServiceLevel-driven failover, member probing and read statuses) is already merged into master. Current master and the concurrent dependency merges are preserved without rewriting branch history. Review the complete PR diff, not only the last commit. Master's ManagedSession logging event IDs 32-35 are preserved; unused reverse-connection cleanup uses event ID 36. ## Problem With Reverse Connect, each member of a non-transparent redundant Server set opens its own connections to the Client. The existing redundancy path assumed dialed connections for endpoint resolution, ServiceLevel probes and recovery, preventing a Client from using the members' incoming connections consistently. ## Change - `IReverseConnectionAvailability : IReverseConnectionSource` exposes `GetAvailableEndpointUrls(string serverUri)`. `ReverseConnectManager` advertises unaccepted held connections and excludes a connection as soon as it is accepted, including inside the accepting callback. - Optional `ServerRedundancyOptions.MemberConnections` selects the reverse-connect member resolver. Endpoint matching preserves the ServerUri, security mode, security policy and URL scheme checks. - Member probing, failover and subsequent recovery use the member source, including for a custom resolver or explicit target whose endpoint does not set `ReverseConnect.Enabled`. - The initial Server retains its authenticated endpoint and original source, manager or outbound transport. A-to-B-to-A failover uses that original path consistently for resolution, availability, probing and recovery. - Switching to another member waits for at least `PeerDiscoveryTimeout`, including after the previous reconnect budget expires. The reverse-source dependency's cancellation and exhausted-budget handling are preserved. - Without `MemberConnections`, existing dialed member behavior remains unchanged. ## Tests The real reference-server fixture covers source, manager and outbound initial connections, each with marked and unmarked member endpoints. All six combinations exercise member failover, transport closure and member recovery, then handler-selected failback to the initial Server with probing enabled. They assert readable ServiceLevel, fresh member connection requests and no initial-Server waits through the member source. The manager lifecycle regression asserts that a held connection is available before acceptance and unavailable inside the accepting callback. Existing resolver, handler, reactive failover, unavailable-member and reverse-source budget/cancellation tests remain enabled. ## Repair Evidence, 2026-10-23 Local Windows developer checks with .NET SDK 10.0.402, Release/net10.0, before the later concurrent master integration: - Initial-source/member routing: 6 passed. - Combined dependency budgets, routing and accepted availability: 15 passed. - Affected client redundancy, resolver, handler and manager lifecycle suites: 198 passed. - Every listed run completed with 0 build warnings and 0 build errors. - The dependency's WoT retained-input fixture repair passed the original case and both injected-conflict cases (3 tests), plus 15 neighboring retained-input and retry-boundary cases. It checks exactly one correct input per bounded publication attempt and retains the exact-one cold-recovery assertion. No production WoT behavior is changed by that repair. The newly integrated baseline is being checked separately; the PR comments and GitHub checks report its results. These prior results do not claim completion of current-head full CI. ## Earlier Evidence The original PR description recorded these results for historical head `dc471de5d`, not the current head: client builds on net10.0/net9.0/net8.0/net48 with no warnings or errors; affected classes 107/107 on net10.0 and net48; Sessions 80/80 on net10.0; the full Client project 3311 executed and passed out of 3315 total; changed-file whitespace/style/analyzer checks clean. Those historical checks have not been relabeled as current-head evidence. ## Operational Notes ServiceLevel probing consumes member connections. Members must continue opening connections while they are probed and recovered. A previously resolved member remains selectable while its probes obtain connections; a member whose probe cannot obtain a connection and reports none available becomes unavailable. The initial Server's original transport is not subject to the separate member-source availability filter. ## Checklist - [x] I have signed the CLA and read CONTRIBUTING. - [x] Regression tests cover the repaired behavior. - [x] Reverse Connect documentation describes initial-source failback, unmarked endpoints and accepted-connection availability. - [ ] Current-head full solution tests on .NET Framework and .NET 10 have completed successfully. - [ ] All required current-head CI checks and CodeQL checks have passed. - [ ] All review threads have been addressed and reviewed. --------- Co-authored-by: Marc Schier <marcschier@users.noreply.github.com> |
||
|
|
53e9e25717 |
Add explicit published WoT Binding 1.00 consumer interoperability (#4892)
# Description Implement the approved published OPC 10101 v1.00 consumer- Baseline: [OPC 10101 v1.00, published 2026-01-08](https://reference.opcfoundation.org/specs/OPC-10101/v1.00). Reviewed plaintext SHA-256: `4ec1170c1c33c63907e7810e6ab0537d62b55735bc2d1191ec7f57bbf9926d93`. Later draft vocabulary is not normative 1.00 evidence. - Explicit `WotBindingProcessingMode.PublishedOneZero` in planner/extractor and MCP options. Current remains the default; no heuristic selection from vocabulary/revision. Original TDs need no invented revision annotation. - Published numeric namespaces use original effective document context, not the connected NamespaceArray or unrelated later planning context. - Actions require action-level array-valued `uav:componentOf` (§7.1.3). Native Core ownership/endpoint checks select one valid Object receiver; a valid explicit form receiver can disambiguate. No retired scalar form fallback. - Native `InputArguments`/`OutputArguments` establish object-schema argument ordering, DataTypes and ranks without later `argumentLayout`/`fieldOrder` or JSON-order guesses. Native integer widths constrain actual values too. - Shared Method/receiver resolution pins the same pair for MCP authorization and dispatch. Direct/captured channels retain preflight/session checks. - Planning uses the live source-routed resolver document alongside local declarations from NodeSet import; serialized `resolvedFrom` markers and local containment cannot manufacture source ownership authority. Read/write/observe/default operations, property mapping annotations, exact channel/auth/allOf constraints and external token-provider applicability have version-qualified evidence. Documentation distinguishes converter identifier options from runtime processing selection. The [25-requirement traceability ledger](https://github.com/OPCFoundation/UA-.NETStandard/blob/a24ed5319f099980e9e67d536c4268de24d3ae49/tests/Opc.Ua.Types.Tests/Wot/Assets/wot-binding-traceability.json) includes concrete tests, assertion-level evidence and qualifications. Locator completeness is not implementation completeness. ## Related Issues - Approved interoperability follow-up to merged #4877; differential excludes its safe receiver/provenance implementation. - #4869 remains outside scope and is not closed. ## 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. |
||
|
|
689358e2e3 |
WebApi tests: verify opcua+openapi WebSocket upgrade credential sources (#4893)
# Description Test-only change (no production code changes). Adds regression coverage for WebSocket upgrade authentication introduced in #4699, using real Kestrel/TLS listeners in `RealHttpsListenerIntegrationTests.Authentication.cs`: - A `WebSocketUpgradeRecorder` startup contributor captures, **before** authentication runs, whether the upgrade request carried an `Authorization` header or a client certificate. This proves the credential source, since the server internally copies a subprotocol token into the `Authorization` header for the JwtBearer handler. - `opcua+openapi+<accesstoken>` authenticates **without** any `Authorization` header or client certificate (HTTP 101, subprotocol selected, upstream identity propagated). This is the browser-compatible path. - Plain `opcua+openapi` upgrades without credentials are rejected with 401 when Basic or Bearer REST auth is configured, and succeed (101) when a valid header is supplied. - With mTLS, a missing client certificate is rejected and a valid certificate is accepted without any header. - New test `TokenOpenApiWebSocketUpgradeWithoutAuthorizationHeaderRejectsUnvalidatedTokenAsync`: bogus or forged bearer tokens, and subprotocol tokens when the Bearer scheme is not registered (Basic and mTLS configurations), are rejected with 401 and never dispatched. - The expired-token test now asserts the credential source for both the header and subprotocol variants. ## Related Issues - Follow-up to #4699 / #4674 ## Validation - `dotnet test tests\Opc.Ua.Bindings.Https.WebApi.Tests -f net10.0`: **520 passed**, 0 failed - `dotnet test tests\Opc.Ua.Bindings.Https.WebApi.Tests -f net8.0 --filter FullyQualifiedName~RealHttpsListenerIntegrationTests`: **126 passed**, 0 failed - Not run: the full UA.slnx suite. net48 does not apply because this binding targets net8.0+ only. ## 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. |
||
|
|
c65156f325 | Merge branch 'master' into feature/endpoint-message-registry | ||
|
|
f3980f9e72 |
Client: managed session recovery over application-supplied reverse connections (#4883)
# Description
## Problem
With Reverse Connect (OPC 10000-6, 7.1.3) the server opens the
transport connection and sends `ReverseHello`; the client never dials.
To
re-establish a lost session (OPC 10000-4, 6.7: a new SecureChannel, then
`ActivateSession` for the existing session, then republish or
`TransferSubscriptions`) the client therefore needs a fresh
server-initiated
connection for every recovery attempt.
A `ManagedSession` can obtain such connections only from a
`ReverseConnectManager`:
- The `ITransportWaitingConnection` passed to
`ManagedSession.CreateAsync` is
single-use and consumed by the initial connect. Without a manager every
later
recovery fails with `BadSecureChannelClosed` ("A fresh reverse
connection is
required.") until the reconnect policy gives up.
- Recovery calls `ReverseConnectManager.WaitForConnectionAsync`
directly. The
manager owns its listeners and has no extension point, so connections an
application accepts itself cannot be fed into it.
Applications that accept reverse connections themselves cannot use the
managed
recovery today. Typical reasons are admission decisions before the OPC
UA
handshake (source network, the server URI and endpoint URL from
`ReverseHello`), a bounded number of held connections per server, or
connections accepted by another process and handed over. Such an
application
has to drive `ManagedSession.ReconnectAsync(ITransportWaitingConnection,
...)`
itself and install a reconnect policy that refuses every attempt of the
managed
loop, which duplicates the backoff and budget of the managed loop and
races its
keep-alive based failure detection.
## API
```csharp
namespace Opc.Ua.Client
{
public interface IReverseConnectionSource
{
ValueTask<ITransportWaitingConnection> WaitForConnectionAsync(
Uri endpointUrl, string? serverUri, CancellationToken ct);
}
public class ReverseConnectManager : IDisposable, IAsyncDisposable, IReverseConnectionSource
{
// explicit implementation; delegates to WaitForConnectionAsync(Uri, string?, CancellationToken)
}
public sealed record ManagedSessionOptions
{
public IReverseConnectionSource? ReverseConnectionSource { get; init; }
}
public sealed class ManagedSessionBuilder
{
public ManagedSessionBuilder UseReverseConnect(IReverseConnectionSource source, Uri serverUri);
}
}
```
## Behaviour
1. Initial connect: an explicit initial `connection` argument is still
used
first. Otherwise the session awaits the source (after the connect gate
admitted the attempt, like the manager path). When the endpoint is
refreshed
before connecting, discovery runs over one connection from the source
and the
session over the next one, as with a manager.
2. Recovery: every place that awaits the manager during recovery
(reconnect,
in-place recreate, network-redundancy alternate, endpoint refresh,
explicit
`ReconnectAsync` without a connection, mirrored-session reactivation)
awaits
the source instead. A recovery attempt that runs under the reconnect
budget
waits at most for the remaining `MaxTotalReconnectTime`; when the budget
runs
out the source's token is cancelled and the attempt fails with
`BadTimeout`,
so the state machine leaves `Reconnecting` as it does after a failed
dial. A
source that throws ends that attempt like a failed dial; the next
attempt
follows the policy's delay.
3. Precedence: `ManagedSession.CreateAsync(options, ...,
reverseConnectManager)`
with `options.ReverseConnectionSource` set throws `ArgumentException`.
The two
`UseReverseConnect` builder overloads replace each other.
4. Ownership: the session closes a connection it received after the wait
was
cancelled (closing the session, disposal, budget exhausted), so a source
does
not have to track abandoned connections. A source that returns `null`
fails
the attempt with `BadUnexpectedError` instead of falling back to an
outbound
connection.
5. Sessions without the option behave as before; the manager paths are
unchanged (the manager's initial connect still goes through the session
factory's manager overload).
## Tests
`tests/Opc.Ua.Client.Tests/Session/ManagedSessionReverseConnectionSourceTests.cs`
(unit, no server):
- `CreateAsyncRejectsASourceCombinedWithAManagerAsync`
- `BuilderUseReverseConnectWithSourceSetsTheSourceAndServerUri`
- `BuilderUseReverseConnectWithAManagerReplacesTheSourceAsync`
- `BuilderUseReverseConnectRejectsNullArguments`
- `ReverseConnectManagerWaitsThroughTheSourceInterfaceAsync`
- `CancelledConnectCancelsTheSourceAndClosesALateConnectionAsync`
`tests/Opc.Ua.Sessions.Tests/ManagedSessionReverseConnectionSourceTests.cs`
(in-process reference server that keeps dialing the client's reverse
connect
listener; the source wraps that listener's `ReverseConnectManager` and
records
every call):
- `InitialConnectAwaitsTheSourceOnceAsync`
- `RecoveryReactivatesTheSessionOverAConnectionFromTheSourceAsync`:
closes the
transport under the SecureChannel; the session reactivates the same
`SessionId` over a new connection from the source, one source call per
attempt, and the subscription continues with higher sequence numbers
(not
recreated).
- `EachRecoveryAttemptAwaitsTheSourceOnceFollowingThePolicyAsync`: the
source
fails the first two recovery calls; exactly four calls, spaced by the
policy's delay.
- `ASourceThatNeverYieldsEndsRecoveryWhenTheBudgetIsExhaustedAsync`: one
wait
is cancelled when the 3 s budget is used up and the session reaches
`Disconnected`.
- `DisposingWhileRecoveryWaitsCancelsTheSourceAsync`
- `ReverseConnectManagerRecoveryIsUnchangedAsync`: regression for the
manager
path.
## Compatibility
Additive only: a new interface, a new init-only option, a new builder
overload
and an explicit interface implementation on `ReverseConnectManager`. No
existing signature changes. `IManagedSessionFactory` is unchanged;
callers of
`ConnectAsync(endpoint, configure)` can call `UseReverseConnect(source,
...)` in
`configure`.
## Related Issues
- No tracking issue; the problem and the design are described above.
## 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. (Run
locally: the affected test classes on net48 and net10.0 and the whole
Opc.Ua.Client.Tests project on net10.0; not the whole UA.slnx.)
- [ ] 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: Marc Schier <marcschier@users.noreply.github.com>
|
||
|
|
5d6c10a6e9 |
Align WoT projections with Binding and Connectivity specs (#4877)
# Description Align WoT Binding, Connectivity, and required Projections support with the merged official specification baseline. Projection-local affordances use ordinary TD/TM conversion and Binding planning. The canonical View retains its identity and organizes converter-resolved local/imported Nodes without cloning imported Nodes. Generated local identities are resource-scoped; authored identities are preserved. Security/schema closure retains projection ownership. Counts include local Binding-owned Nodes plus View-owned Nodes across dry-run, commit, unchanged refresh and restart. Runtime configuration forwards the converter-resolved active-root namespace for multi-model closures. Recovery rejects absent/inconsistent committed canonical View evidence rather than resetting publication. Local+imported recovery preserves committed membership/digest when desired inputs have changed. Fixtures use short isolated storage roots for net48/MAX_PATH. Security names use exact UTF-8 percent encoding; dependency-support names retain base64url. OPC UA forms with omitted content type select the registered octet-stream codec. Historical fixture bytes remain unchanged with explicit current derived expectations. UA Binary serialization remains unsupported/unclaimed: the raw-byte/text codec is not UA Binary. Dependency observation disclosure checks management policy, source group/exact-Version and every captured local target origin/group/exact-Version. Existing native role/channel-restriction validation is reused, including namespace defaults and the virtual role hook. Denied/unavailable targets reject the entire value with generic access failure, no partial graph or target-identity diagnostics. A pruned target makes remote disclosure fail closed without erasing the stored committed graph. Both committed snapshots and completed attempts are tested. Explicit-node tests do not establish separate namespace-default evidence. The current capture path generates local registry pins, not arbitrary federated/provider targets; remote authorization is outside this implemented capability and is not claimed. ## Related Issues - Related to #4869 only historically. Performance work is explicitly out of current scope; this PR does not fix or close it. ## 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. - [ ] I have addressed **all** PR feedback received. |
||
|
|
533937830b |
Restore UaLens Linux rendering (#4891)
# Description Restore the Linux rendering checks temporarily excluded in #4822 by updating all eight OpenUSD pins to the public **0.16.6-alpha** release. The upstream fix keeps shared GPU surfaces where supported. When Avalonia/Mesa cannot import external images or semaphores, it presents completed Vulkan frames through host-memory readback instead. The renderer, scene, and acceptance criteria remain unchanged. - Update the complete OpenUSD package family and its verified-version assertion together. - Restore both live embedded-viewport workflow cases, the native workflow smoke case, and the native viewport artifact case used by both distribution jobs. - Remove the temporary-exclusion notices and document the host-memory presentation path and physical-GPU qualification limits. All 21 resolved OpenUSD packages were independently verified on nuget.org at source commit `0ebd61af4651457d903ed30f40965e8dcb060255`, including the Metal and native RID graph. No private feed or replaced DLLs are used. [Upstream release](https://github.com/marcschier/openusd-dotnet/releases/tag/v0.16.6-alpha). ### Validation The eight-file patch was carried unchanged onto `master` at `97cc145811bc2d9cef11e685392696435b10df36`. The following Windows/.NET 10 checks were rerun on that base: | Scope | Result | | --- | --- | | Live viewport and diagnostic workflows | Passed: 14, Failed: 0, Skipped: 0 | | OpenUSD package-version policy | Passed: 1, Failed: 0, Skipped: 0 | | Viewport and rendered Vision adapters | Passed: 103, Failed: 0, Skipped: 0 | | Retained-camera provider and blank-frame guards | Passed: 45, Failed: 0, Skipped: 0 | <details> <summary>Windows commands and results</summary> These options and reporting paths were used for each invocation: ```powershell $options = @( '-c', 'Release', '-f', 'net10.0', '-p:CustomTestTarget=net10.0', '-p:UseArtifactsOutput=true', '-p:ArtifactsPath=D:\git\UA-.NETStandard_master\TestResults\ua-stack-feedback20261016', '-m:2' ) $results = 'C:\Users\mschier\.copilot\session-state\abd33a2e-7139-44e1-9912-6e4451102b4e\files\openusd0166-pr-results' # Passed: 14, Failed: 0, Skipped: 0 dotnet test tests\Opc.Ua.Lens.Workflow.Tests\Opc.Ua.Lens.Workflow.Tests.csproj @options --filter 'FullyQualifiedName~OpenUsdViewportWindowTests|FullyQualifiedName~DiagnosticCommandTests' --logger 'trx;LogFileName=workflow.trx' --results-directory $results # Passed: 1, Failed: 0, Skipped: 0 dotnet test tests\Opc.Ua.Tools.Tests\Opc.Ua.Tools.Tests.csproj @options --filter 'FullyQualifiedName~OpenUsdPackageFamilyUsesOneVerifiedPrerelease' --logger 'trx;LogFileName=policy.trx' --results-directory $results # Passed: 103, Failed: 0, Skipped: 0 dotnet test tests\Opc.Ua.Lens.Tests\Opc.Ua.Lens.Tests.csproj @options --filter 'FullyQualifiedName~OpenUsdViewport|FullyQualifiedName~RenderedVisionCaptureTests' --logger 'trx;LogFileName=adapters.trx' --results-directory $results # Passed: 45, Failed: 0, Skipped: 0 dotnet test tests\Opc.Ua.Vision.Tests\Opc.Ua.Vision.Tests.csproj @options --filter 'FullyQualifiedName~RetainedSceneCameraCaptureProviderTests|FullyQualifiedName~BlankFrameGuardTests' --logger 'trx;LogFileName=capture.trx' --results-directory $results ``` The selected builds completed without new compiler/analyzer warnings. `git diff --check` and the following formatting check passed: ```powershell dotnet format whitespace --folder --include tests\Opc.Ua.Tools.Tests\PackageVersionPolicyTests.cs tests\Opc.Ua.Lens.Artifacts.Tests\PublishedArtifactTests.cs tests\Opc.Ua.Lens.Workflow.Tests\Companions\OpenUsdViewportWindowTests.cs tests\Opc.Ua.Lens.Workflow.Tests\Desktop\DiagnosticCommandTests.cs --verify-no-changes --verbosity quiet ``` </details> The same patch was qualified before the base update on Ubuntu 24.04.5, Mesa 25.2.8, and Xvfb, with no GPU device mounted: | Scope | Result | | --- | --- | | Original live telemetry, picking, retained-camera capture, edit, resize, and drain workflows | Passed: 14, Failed: 0, Skipped: 0 | | Published Linux NativeAOT startup and native viewport | Passed: 2, Failed: 0, Skipped: 0 | | Freshly packed and installed Linux managed tool startup and native viewport | Passed: 2, Failed: 0, Skipped: 0 | Both artifact forms produced nonblank 256x192 Vulkan captures with different decoded-pixel hashes after the scene edit, exit code zero, and both required cleanup/smoke markers. No assertions, deadlines, or workflow selectors were weakened. <details> <summary>Linux commands and results before the base update</summary> The source was mounted read-only at `/repo`; isolated build outputs were under `/work`, and retained results were under `/evidence`. These commands ran in PowerShell inside the Ubuntu container: ```powershell $options = @('-c', 'Release', '-f', 'net10.0', '-p:CustomTestTarget=net10.0', '-p:UseArtifactsOutput=true', '-p:ArtifactsPath=/work/build', '-m:2') $env:LD_LIBRARY_PATH = '/work/build/bin/Opc.Ua.Lens.Workflow.Tests/release_net10.0:/work/build/bin/Opc.Ua.Lens.Workflow.Tests/release_net10.0/runtimes/linux-x64/native' # Passed: 14, Failed: 0, Skipped: 0 xvfb-run --auto-servernum '--server-args=-screen 0 1600x1000x24 -dpi 96' dotnet test /repo/tests/Opc.Ua.Lens.Workflow.Tests/Opc.Ua.Lens.Workflow.Tests.csproj @options --filter 'FullyQualifiedName~OpenUsdViewportWindowTests|FullyQualifiedName~DiagnosticCommandTests' --logger 'trx;LogFileName=workflow-linux.trx' --results-directory /evidence/results -- NUnit.NumberOfTestWorkers=0 RunConfiguration.TreatNoTestsAsError=true # Passed: NativeAOT publish, with trim/AOT diagnostics enabled dotnet publish /repo/tools/Opc.Ua.Lens/Opc.Ua.Lens.csproj -c Release -f net10.0 -p:CustomTestTarget=net10.0 -p:UseArtifactsOutput=true -p:ArtifactsPath=/work/aotbuild -r linux-x64 --self-contained true -p:PackAsTool=false -p:PublishAotEnabled=true -p:SuppressTrimAnalysisWarnings=false -p:TrimmerSingleWarn=false -m:2 -o /work/native -bl:/evidence/native.binlog $env:UALENS_ARTIFACT_EXECUTABLE = '/work/native/UaLens' $env:UALENS_ARTIFACT_LOG_DIRECTORY = '/evidence/native-linux' $env:UALENS_ARTIFACT_X11_VULKAN = 'true' $env:UALENS_ARTIFACT_ALLOW_SOFTWARE_GL = 'false' # Passed: 2, Failed: 0, Skipped: 0 xvfb-run --auto-servernum '--server-args=-screen 0 1600x1000x24 -dpi 96' dotnet test /repo/tests/Opc.Ua.Lens.Artifacts.Tests/Opc.Ua.Lens.Artifacts.Tests.csproj @options --filter 'FullyQualifiedName~PublishedArtifactTests' --logger 'trx;LogFileName=native-linux.trx' --results-directory /evidence/results -- NUnit.NumberOfTestWorkers=0 RunConfiguration.TreatNoTestsAsError=true # Passed: restore, pack the selector and all three RID payloads, and isolated install $pack = @('-p:CustomTestTarget=net10.0', '-p:UseArtifactsOutput=true', '-p:ArtifactsPath=/work/managedbuild', '-p:PublishAotEnabled=false', '-p:PublishAot=false', '-p:PublishTrimmed=false', '-p:PackAsTool=true') dotnet restore /repo/tools/Opc.Ua.Lens/Opc.Ua.Lens.csproj -p:Configuration=Release @pack dotnet pack /repo/tools/Opc.Ua.Lens/Opc.Ua.Lens.csproj -c Release --no-restore -p:TargetFramework=net10.0 @pack -m:2 -o /work/managed-packages -bl:/evidence/managed.binlog dotnet tool install OPCFoundation.NetStandard.Opc.Ua.Lens --version 2.0.2-preview.100.g30f255be63 --tool-path /work/installed/tool --configfile /work/installed/NuGet.config --no-cache $env:UALENS_ARTIFACT_EXECUTABLE = '/work/installed/tool/ualens' $env:UALENS_ARTIFACT_LOG_DIRECTORY = '/evidence/installed-linux' # Passed: 2, Failed: 0, Skipped: 0 xvfb-run --auto-servernum '--server-args=-screen 0 1600x1000x24 -dpi 96' dotnet test /repo/tests/Opc.Ua.Lens.Artifacts.Tests/Opc.Ua.Lens.Artifacts.Tests.csproj @options --filter 'FullyQualifiedName~PublishedArtifactTests' --logger 'trx;LogFileName=installed-linux.trx' --results-directory /evidence/results -- NUnit.NumberOfTestWorkers=0 RunConfiguration.TreatNoTestsAsError=true ``` The install configuration contained only the freshly packed `/work/managed-packages` source; installation used an isolated tool cache. The Linux SDK was 10.0.401, allowed by the repository's minimum-SDK policy. </details> **Not run:** full `UA.slnx` suites on .NET Framework and .NET 10, physical-GPU/display qualification, or downstream macOS/Windows NativeAOT artifacts for this follow-up. Linux artifact results above predate the move onto current `master`; the PR's CI must qualify the final head. Coverage was not recollected, and no solution-wide or current-head cross-platform CI pass is claimed. ## Related Issues - Fixes #4890. - Follow-up to #4822. No new application architecture or public UA API is introduced; the compatibility repair is in the released OpenUSD dependency. ## 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. - [ ] 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. The existing rendering tests are restored, not replaced with weaker cases. The remaining checklist items are not asserted by the focused local evidence above. |
||
|
|
5699678b48 |
Merge master and preserve registry CI coverage
Keep the native registry, publishing-mode and package-inventory checks while adopting upstream disjoint CI shards. Preserve awaited NUnit 5 assertions with ConfigureAwait and upstream using order. |
||
|
|
97cc145811 |
Client redundancy: ServiceLevel-driven failover, member ServiceLevel probing and read statuses (#4884)
# Description
## Problem
OPC 10000-4, clause 6.6.2 "Server redundancy", lets a Client of a
non-transparent redundant Server set (Cold, Warm, Hot, HotAndMirrored)
connect to the Server with the highest `ServiceLevel` and fail over when
the Server in use is lost or its `ServiceLevel` leaves the Healthy
subrange (6.6.2.4.2 "ServiceLevel": 200-255 Healthy, 2-199 Degraded, 1
NoData, 0 Maintenance; 6.6.2.4.5 "Client failover behaviour"). The
managed session on `master` covers part of this:
- `ManagedSession` fails over only in the `Failover` state of
`ConnectionStateMachine`, which is entered after the reconnect policy
gives up. `DefaultServerRedundancyHandler.ShouldFailover` /
`ServerRedundancyHandlerExtensions.ShouldFailover` implement the
subrange rules, but nothing in the client calls them: a connected
session stays on a Server that reports Degraded or Maintenance until its
connection breaks. There is no public member that starts a failover
(`HandleFailoverAsync` is private).
- Members known only from `ServerUriArray` get `ServiceLevel` NoData
with `ServiceLevelKnown = false` (internal). Their `ServiceLevel` is
never read, so neither the initial connect nor `SelectFailoverTarget`
can prefer the member with the highest `ServiceLevel`.
- `ReadRedundancyInfoAsync` maps a bad status of `RedundancySupport`,
`ServerUriArray`, `RedundantServerArray` or `CurrentServerId` to an
empty or default value, and peer endpoint resolution failures are only
logged. An application cannot report which redundancy Node the Server
refused or why a member has no endpoint.
An application that must fail over on `ServiceLevel` therefore polls the
`ServiceLevel` itself, opens its own sessions to read each member, and,
lacking a failover trigger, closes the managed session and opens a new
one - losing the transfer or recreate of its subscriptions that the
managed failover performs.
## API (all additions; defaults keep the current behaviour)
```csharp
public partial class ManagedSession
{
public Task FailoverAsync(ConfiguredEndpoint? target = null, CancellationToken ct = default);
public ServerRedundancyInfo? RedundancyInfo { get; } // implements IManagedSession.RedundancyInfo
}
public interface IManagedSession : ISession
{
ServerRedundancyInfo? RedundancyInfo { get; } // new interface member
}
public sealed record ServerRedundancyOptions
{
public bool ProbeMemberServiceLevel { get; init; } // default false
public TimeSpan? ServiceLevelMonitoringInterval { get; init; } // default null (off); must be positive when set
}
public sealed class RedundantServer
{
public bool ServiceLevelKnown { get; init; } = true; // was internal
public ServiceResult? ResolutionError { get; init; } // new
}
public sealed class ServerRedundancyInfo
{
public IReadOnlyDictionary<NodeId, StatusCode> ReadStatuses { get; init; } // new, empty by default
}
```
## Behaviour
1. **Read statuses and resolution errors.**
`DefaultServerRedundancyHandler` records the status of every Node it
reads from the Server in use (`RedundancySupport`, `ServiceLevel`,
`RedundantServerArray`, `ServerUriArray`, `EstimatedReturnTime`, and
`CurrentServerId` for Transparent redundancy) in
`ServerRedundancyInfo.ReadStatuses`. A peer whose endpoint resolution
fails carries the exact status in `RedundantServer.ResolutionError` (the
discovery `ServiceResult`, `BadTimeout` when the discovery bound
expires, `BadNotFound` when no endpoint matches the security and
transport in use). `IManagedSession.RedundancyInfo` exposes the last
snapshot.
2. **Member probing** (`ProbeMemberServiceLevel`). After a member's
endpoint is resolved, `ManagedSession` reads `Server.ServiceLevel` and
`Server.ServerStatus.State` of every other member over a short session
with the identity of the session in use, bounded by
`PeerDiscoveryTimeout`, and replaces the reported values. A member that
cannot be probed keeps the values reported for it and carries the
failure in `ResolutionError` (for example `BadNotReadable`). With
probing, the initial connect switches to the member with the highest
`ServiceLevel` when it is higher than that of the Server it connected to
(6.6.2.4.5).
3. **`FailoverAsync`.** Fails over a connected session to `target`, or
to the target the redundancy handler selects from a refreshed snapshot,
through the existing in-place recreate (subscriptions are transferred or
recreated; with token reuse enabled a HotAndMirrored target reactivates
the session). The connection state machine gets an internal
`RequestFailover` that enters `Failover` with the requested operation;
`ConnectionStateChanged` reports `Failover` then `Connected`. Throws
`BadNotSupported` without a redundancy handler, `BadInvalidState` when
not connected, `BadNothingToDo` when the handler selects no target, and
the failover error otherwise. A failed requested failover transitions to
`Reconnecting` (not `Disconnected` plus the recovery timer), so the
reconnect policy restores the session at once. Cancelling `ct` stops
waiting; a failover in progress completes.
4. **ServiceLevel monitoring** (`ServiceLevelMonitoringInterval`). While
connected to a non-transparent set, `ManagedSession` reads the
`ServiceLevel` of the Server in use at the interval. While it is Healthy
nothing else happens (a Healthy Server is not left, 6.6.2.4.3).
Otherwise the snapshot is refreshed (with probing when enabled),
`ShouldFailover` decides, and a warranted failover runs `FailoverAsync`
to the handler's target. The loop ends with the session.
The switch logic of the automatic failover (`HandleFailoverAsync`) is
factored into a helper shared with the initial selection and
`FailoverAsync`; its behaviour is unchanged.
## Tests
- `ManagedSessionServerRedundancyTests` (new, in-process Warm set of two
`ReferenceServer` fixtures with distinct ApplicationUris,
`Server.ServerRedundancy` promoted to `NonTransparentRedundancyType`
with each other in `ServerUriArray`, `ServiceLevel` set through
`IServerServiceLevelControl`, endpoints resolved by a test
`IRedundantServerEndpointResolver`):
- without probing the session stays on the configured Server; the peer
has `ServiceLevelKnown = false` (defaults unchanged);
- with probing the initial connect selects the member with the higher
`ServiceLevel` (250 over 210);
- a member that refuses the `ServiceLevel` read keeps an unknown
`ServiceLevel` and reports `BadNotReadable` in `ResolutionError`;
- a refused `RedundantServerArray` reports `BadNotReadable` in
`ReadStatuses`;
- lowering the `ServiceLevel` of the Server in use to 150 while the peer
reports 230 fails over within the monitoring interval,
`ConnectionStateChanged` reports only `Failover` and `Connected`, the
same subscription object keeps delivering data changes from the new
Server (sequence numbers restart there, as the subscription is recreated
on another Server);
- `FailoverAsync` to an explicit target and to the handler-selected
target;
- `FailoverAsync` without a redundancy handler throws `BadNotSupported`.
- `ServerRedundancyHandlerTests`: resolution error of a refusing and of
an unresolved peer, read status of each Node, validation of the
monitoring interval.
- `ConnectionStateMachineTests`: a requested failover runs its operation
in `Failover` and bypasses the failover delegate; a failed requested
failover reconnects without `Disconnected`; `RequestFailover` is refused
when not connected.
## Compatibility
- Source/binary additions only, except that `IManagedSession` gains
`RedundancyInfo`; `ManagedSession` is the only implementation in the
repository, external implementers of `IManagedSession` must add the
property.
- `RedundantServer.ServiceLevelKnown` changes from internal to public
(same default `true`).
- With the default options nothing changes at runtime: no probing, no
monitoring, the automatic failover path is unchanged. The only
default-visible differences are the new `ReadStatuses`/`ResolutionError`
data.
- Probing opens one short additional session per member;
reverse-connect-only members cannot be probed and report the probe
failure in `ResolutionError`.
## Related Issues
- No tracking issue; the problem and the design are described above.
## 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.
- [ ] 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. (Run
locally: the affected test classes on net48 and net10.0 and the whole
Opc.Ua.Client.Tests project on net10.0; not the whole UA.slnx.)
- [ ] 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: Marc Schier <marcschier@users.noreply.github.com>
|
||
|
|
be9ccf912c |
Client: evaluate the durable subscription lifetime on every physical create (#4882)
# Description
## Problem
`ISubscription.SetAsDurableAsync(TimeSpan lifetime, CancellationToken
ct)` marks a subscription durable with the Server method
`SetSubscriptionDurable` (OPC 10000-5, "SetSubscriptionDurable Method";
OPC 10000-4, "Durable Subscriptions"). `SetSubscriptionDurable` must be
called before the subscription has monitored items; a conforming server
rejects it afterwards with `Bad_InvalidState`.
`LogicalSubscription` stores the `TimeSpan` and installs an
`OnAfterCreateAsync` hook on each partition. The partition state machine
runs the hook after every `CreateSubscription` and before the first
`CreateMonitoredItems`, so durability survives every later physical
create:
- recreate after a service call reports `BadSubscriptionIdInvalid`
(`Subscription.ApplyStateChangesAsync` → `ResetToRecreateAsync`),
- recreate after `TransferSubscriptions` fails on session recreate
(`SubscriptionManager.RecreateSubscriptionsAsync` → `RecreateAsync`) or
on load (`RestoreTransferAsync` → `ResetToRecreateAsync`),
- recreate after a status-change timeout or an unsolicited
`Good_SubscriptionTransferred`,
- a new secondary partition (`TryApplyDurableToNewPartition`).
The hook always re-applies the lifetime fixed by the first call. An
application whose durable lifetime must not exceed a bound that shrinks
over time (for example an authorization or retention window that ends at
a known instant, after which the server must no longer queue
notifications) cannot keep that bound after a recreate, and it has no
way to supply a new value at the right moment:
- calling `SetAsDurableAsync` again after the recreate is rejected once
monitored items exist;
- `ISubscriptionNotificationHandler.OnSubscriptionStateChangedAsync` is
dispatched fire-and-forget, so a call made from it can arrive after the
first `CreateMonitoredItems`;
- `SubscriptionOptions` has no durable setting and `OnAfterCreateAsync`
is internal.
## API
```csharp
namespace Opc.Ua.Client.Subscriptions
{
public interface ISubscription
{
// existing
ValueTask<TimeSpan> SetAsDurableAsync(TimeSpan lifetime, CancellationToken ct = default);
// new
ValueTask<TimeSpan> SetAsDurableAsync(Func<TimeSpan?> lifetime, CancellationToken ct = default);
}
}
```
## Behaviour
- The provider is evaluated once for the call itself; the value is sent
to every partition that is already created (unchanged: the call returns
the minimum revised lifetime of those partitions, or the requested value
when none is created yet).
- The provider is stored and evaluated again by the existing
after-create hook before every later physical create, between
`CreateSubscription` and the first `CreateMonitoredItems` of that
create.
- A `null` result requests no durability for that create: no
`SetSubscriptionDurable` is sent and the recreated subscription is a
regular one. A `null` result for the call itself returns `TimeSpan.Zero`
without contacting the server.
- An exception thrown by the provider during a later create is handled
exactly like a rejected `SetSubscriptionDurable` call today: the state
machine logs a warning, reports `SubscriptionState.Modified`, and the
subscription continues (its monitored items are created). An exception
during the call itself propagates to the caller.
- `SetAsDurableAsync(TimeSpan)` now forwards to `SetAsDurableAsync(() =>
lifetime)`; its behaviour is unchanged.
- `docs/Subscriptions.md` describes the provider and corrects the
statement that the hook is cleared after invocation (on master it stays
installed).
Not included: a notification reporting the outcome (revised lifetime or
status) of each re-applied `SetSubscriptionDurable`. Today a failed
re-apply is only logged and surfaced as `SubscriptionState.Modified`;
this could be a follow-up if maintainers want it.
## Tests
New tests in
`tests/Opc.Ua.Client.Tests/Subscription/SubscriptionTests.DurableLifetime.cs`
drive a real partition (`Subscription`) behind
`SubscriptionManager`/`LogicalSubscription` against mocked services and
record `CreateSubscription`, `SetSubscriptionDurable` (requested hours),
`CreateMonitoredItems`, `TransferSubscriptions` and
`DeleteSubscriptions` in call order. The provider returns 10 h for the
initial call and 7 h afterwards:
-
`DurableLifetimeProviderIsEvaluatedOnRecreateAfterSubscriptionIdInvalidAsync`:
`ModifySubscription` returns `BadSubscriptionIdInvalid`; the recreate
sends 7 h between `CreateSubscription` and the first
`CreateMonitoredItems`.
-
`DurableLifetimeProviderIsEvaluatedOnRecreateAfterFailedTransferAsync`:
`TransferSubscriptions` on session recreate returns
`BadSubscriptionIdInvalid`; delete, create, 7 h, items.
- `DurableLifetimeProviderIsEvaluatedForNewPartitionAsync`:
`MaxMonitoredItemsPerPartition = 1`; the second item mints a partition
that sends 7 h before its items.
-
`DurableLifetimeProviderReturningNullSendsNoSetSubscriptionDurableAsync`:
provider returns `null` on recreate; no `SetSubscriptionDurable`, items
are created.
-
`DurableLifetimeProviderThatThrowsLeavesRecreatedSubscriptionRunningAsync`:
provider throws on recreate; no `SetSubscriptionDurable`, items are
created, subscription stays created.
- `DurableFixedLifetimeIsReappliedOnRecreateAsync`: regression of the
`TimeSpan` overload (10 h again on recreate).
Unit tests in `LogicalSubscriptionTests`:
`SetAsDurableWithProviderEvaluatesItOnceForAllCreatedPartitionsAsync`,
`SetAsDurableWithProviderReturningNullSendsNothingAsync`,
`SetAsDurableWithNullProviderThrowsAsync`.
Negative control: with the hook evaluating the provider once at
installation (the previous fixed-value behaviour), the
recreate-after-`BadSubscriptionIdInvalid`, failed-transfer, null and
throwing-provider tests fail (4 of 10).
Local results (Windows, .NET SDK 10.0.402, Release):
| Run | Result |
| --- | --- |
| `Opc.Ua.Client` build, net48/net8.0/net9.0/net10.0 | 0 warnings, 0
errors |
| `SubscriptionTests` + `LogicalSubscriptionTests`, net10.0 | 105/105
passed |
| `SubscriptionTests` + `LogicalSubscriptionTests`, net48 | 105/105
passed |
| `Opc.Ua.Client.Tests`, namespace `Opc.Ua.Client.Subscriptions`,
net10.0 | 353/353 passed |
| `Opc.Ua.Subscriptions.Tests` `SubscriptionManagerCoverageTests` +
`StreamingSubscriptionCoverageTests`, net10.0 | 72/72 passed |
| `Opc.Ua.Subscriptions.Durable.Tests` `SubscriptionDurableTests` +
`DurableSubscriptionTest*` (in-process reference server with durable
subscriptions enabled), net10.0 | 21/21 passed |
## Related observation (not changed here)
A secondary partition minted by `SubscriptionManager.Add`'s partition
factory starts its state machine in the `Subscription` constructor,
before the factory calls `TryApplyDurableToNewPartition`. When
`CreateSubscription` completes before the factory returns (observed with
a synchronously completing mock), `RunAfterCreateHookAsync` finds no
hook and the new partition is created without `SetSubscriptionDurable`,
for the `TimeSpan` overload as well. With a real round trip the hook is
installed first;
`DurableLifetimeProviderIsEvaluatedForNewPartitionAsync` therefore holds
the secondary's `CreateSubscription` response until the factory has
returned. Installing the hook before the partition's first state-manager
pass would remove the window.
## Compatibility
- Source and binary compatible for callers of `ISubscription`.
- `ISubscription` gains a member. Custom implementations of the public
interface (outside the stack; the stack's own implementations are
internal) must add the overload; a default interface implementation is
not possible because the library also targets .NET Framework 4.8. The
three test doubles in the repository were updated.
- No wire, configuration or server change.
## Related Issues
- Independent of #4881; with #4881 the lifetime provider is also applied
to a new partition that is created before the partition factory returns.
## 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. (Run
locally: the affected test classes on net48 and net10.0 and the whole
Opc.Ua.Client.Tests project on net10.0; not the whole UA.slnx.)
- [ ] 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: Marc Schier <marcschier@users.noreply.github.com>
|
||
|
|
00479792ae |
Client: install the durable hook before a new partition creates itself (#4881)
# Description ## Defect `LogicalSubscription.SetAsDurableAsync(TimeSpan)` records the durable lifetime and installs an `OnAfterCreateAsync` hook on every partition, so that `SetSubscriptionDurable` (OPC 10000-5) is called between `CreateSubscription` and the first `CreateMonitoredItems` of each physical create, as OPC 10000-4 requires for durable subscriptions. A secondary partition minted on demand (when the logical subscription exceeds `MaxMonitoredItemsPerPartition`) starts its state machine inside its constructor. The partition factory in `SubscriptionManager` installed the hook only after the constructor returned (`TryApplyDurableToNewPartition`). When the server answered `CreateSubscription` before the factory returned, the create pass read no hook, the partition was created without `SetSubscriptionDurable`, and its monitored items were created on a non-durable subscription. The hook only runs after a create, so it never ran for that partition; a later `SetSubscriptionDurable` is refused by a conforming server once items exist (`Bad_InvalidState`). The outcome depended on timing. ## Reproduction `SubscriptionTests.DurableIsSetOnNewPartitionCreatedBeforeFactoryReturnsAsync`: a mocked server answers every call immediately, `MaxMonitoredItemsPerPartition = 1`, the logical subscription is made durable for 10 h, and the partition factory returns only after the new partition reports `Created`. Result on `master` ( |
||
|
|
47fbf1673b |
PlasticsRubber datasets: keep transfer reclamation alive when a tick's gate wait times out (#4879)
## Proposed changes Fixes a timing-dependent failure in `ProductionDatasetBinding` (added by #4834). It showed up on CI for #4874, which does not touch this code: macOS net10.0, run 39725790220, job 119649381350, `ProductionDatasetProtocolTests.ServerDeadlineCancelsProviderAndExplicitClientCancellationDoesNotAllocateAsync`: ``` System.AggregateException : Dataset binding cleanup failed. (The operation was canceled.) at ProductionDatasetBinding.DisposeCoreAsync() ... line 571 at ProductionDatasetServerFixture.DisposeAsync() ... ``` ### Race `MaintainTransfersAsync` admits each 1 s reclamation tick through `RunRegisteredAsync`. Its gate wait is linked to `OperationTimeout`. If another operation holds the gate past that deadline, the tick's `m_gate.WaitAsync` throws `OperationCanceledException` while `m_stopping` is **not** cancelled. The catch filter `!reclaiming && m_stopping.IsCancellationRequested` therefore doesn't match, so the maintenance task ends Canceled: - periodic reclamation of abandoned transfers stops silently for the rest of the binding's life (`m_maintenance.IsFaulted` stays false), and - `DisposeCoreAsync` awaits `m_maintenance` and reports the stale OCE as a cleanup failure. The failing test sets `OperationTimeout = 150 ms` and blocks the commit provider until that deadline, so the bug shows only when a tick lands inside that window. It is rare locally and more likely on a slow runner. The cancellation does **not** come from disposal interrupting a started reclaim: `ExpireCoreAsync` and `RetireAsync` observe no token. The stack trace of the repro confirms that the OCE comes from the gate wait. ### Fix An admission wait that expires before the reclaim starts (`!reclaiming`) now skips that tick and retries at the next interval. When `m_stopping` is cancelled, the loop still ends. A reclaim that has already started still reports its failures, and disposal still retires every remaining slot. ### Regression test `ReclamationTickTimingOutBehindBusyGateKeepsReclaimingAndDisposesCleanlyAsync`: `OperationTimeout = 100 ms`, and a commit provider holds the gate for 2.5 s (across at least two ticks). The test then opens an abandoned transfer, advances the test clock past `TransferTimeout`, and waits for **periodic** reclamation to remove the file node. Finally it disposes the binding. - Before the fix: both assertions fail. The file node is never reclaimed, and disposal throws the same `AggregateException` as CI, with the inner OCE from `SemaphoreSlim.WaitAsync` in `MaintainTransfersAsync`. - After the fix: passes. The full `Opc.Ua.PlasticsRubber.Datasets.Tests` project passes on net10.0 (52/52). net48 builds with 0 warnings. ## Types of changes - [x] Bugfix (non-breaking change which fixes an issue) 🤖 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> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> |
||
|
|
fb4eece24d |
Fix OpenUSD sample discovery and migrate artifacts to xRegistry (#4880)
# 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._
Fix OpenUSD sample discovery and integrate artifact delivery with the
xRegistry companion model.
- Load xRegistry before OpenUSD in the sample node managers, mount
stages with `Organizes`, and align root-layer identifiers with served
`.usda` files.
- Bootstrap immutable logical Resources and exact Versions in
`Artifacts`. Stage `Assets` folders provide non-owning `Organizes`
views, and explicit container identifiers allow sharing.
- Update the binding model to draft 0.9.0 while preserving existing
NodeIds and the `HasAddIn` correction.
- Disambiguate default container identifiers by stage instance, publish
the required read-only capabilities map, and preserve standard and
xRegistry BrowseName namespaces.
- Add registry, file-delivery, sharing, collision, qualified-browse, and
live-discovery regressions. Update the OpenUSD documentation.
Review fixes are in
|
||
|
|
bc97bf7206 |
Interop: foreign peers log on with the trusted X509 user certificate (#4878)
# Description The nightly [Foreign stack interop](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/39717131386) run failed on all 13 jobs with one check: `X509UserToken` → `BadIdentityTokenRejected`. #4834 made the reference server stop auto-accepting X509 user certificates (application autoaccept is not user enrollment). It also changed `LegacyClientInteropTests` to trust a generated user certificate and pass it to the peer as `--x509-user-cert` (PFX). Only the 1.5 peer reads that option. The foreign peers (gopcua, milo, asyncua, node-opcua, open62541) still created their own self-signed certificate, which the server now rejects as untrusted. The foreign workflow is skipped on PRs without the `interop` label, so #4834's CI did not catch it. ## Changes - `LegacyClientInteropTests` also writes the trusted user certificate as DER plus an unencrypted PKCS#8 PEM key and passes `--x509-user-cert-der` / `--x509-user-key-pem` to the feature-check run. - gopcua, milo, asyncua, node-opcua and open62541 use those files in `X509UserToken`. Without the options they still create a self-signed certificate. - The peers README documents the options. No server behaviour changes. async-opcua is unchanged: its `X509UserToken` check is already an expected difference (empty user token signature) and its job passed. ## Verification Run locally on Windows, net10.0 Release: - gopcua: whole `LegacyClientInteropTests` class, 81/81. - milo, asyncua, node-opcua and open62541: the 18 feature checks each pass, including `X509UserToken`. - The default 1.5 peer still passes all 18 feature checks. - The fixture builds for net48. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
67c827c354 |
Wait for the server to be running before GranulateDrying tests connect (#4876)
## Problem In CI run [39616233672](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/39616233672/job/119282247661) (test-windows-net48 7/18, on docs-only PR #4868), `GranulateDryingClientServerTests(False)` failed all 94 tests in `OneTimeSetUp`. `GetEndpointsAsync` returned `BadServerHalted`. ## Root cause `GranulateDryingTestHost` signalled readiness from the `ConfigureGranulateDrying` callback. That callback runs during `DiNodeManager.CreateAddressSpaceAsync`, inside `StandardServer.StartApplicationAsync`, so the server has not finished starting yet. But the TCP listener is already accepting at that point: `TcpServiceHost` opens it in `ServerBase.InitializeServiceHostsAsync`, before `StartApplicationAsync`. Until `StandardServer` clears `ServerError` (its constructor sets `BadServerHalted`), `EndpointBase.GetServerForContext` answers every request with `BadServerHalted`. In the CI run, this was the first server started in the test process on a cold net48 runner, and it took about 16 s to reach the callback. The client then connected about 0.4 s later, while the rest of startup (session and subscription managers, node-manager readiness) was still running. The TRX log shows the secure channel opening at 14:41:59.43 and `GetEndpoints` failing about 0.1 s later. **Repro:** adding `await Task.Delay(3000)` after `ready.TrySetResult` in the callback fails all 188 `GranulateDryingClientServerTests` locally (net48 Release) with the same `BadServerHalted` stack. With this fix and the same delay, all 188 pass. ## Fix The host now registers an `IServerStartupTask` and waits for it. `OpcUaServerHostedService` runs startup tasks only after `application.StartAsync` has returned, which is when the server is Running and `ServerError` is cleared. The host still waits on `ExecuteTask` as well, so a failed start throws instead of hanging. This is the same pattern `RepositoryCompanionLiveTests` already uses. ## Tests - net10.0: `Opc.Ua.PlasticsRubber.Runtime.Tests` 597/597 passed. - net48 Release: `GranulateDryingClientServerTests` 188/188 passed, both with and without the injected delay. On a full project run, 592/597 passed. The 5 failures are `DiagnosticsBindingTests` and `MaterialListBindingTests`, which don't use this host. They fail only locally, because the PFX path in the deep worktree is longer than the net48 MAX_PATH limit (`DirectoryNotFoundException`). They passed in the CI run. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4fd76c1ec6 |
Fix PublishWorkerCount race, quiet harness stop errors, track certificate leaks in CI (#4875)
Three reliability fixes found during the client GC investigation. ## 1. PublishWorkerCount lagged a running publish worker (flaky test) `SubscriptionTests.IntentionalDeletionDropsPublishDemandWithoutUnregisteringSubscriptionAsync(True)` failed once on Linux CI (run 39261903989): `PublishWorkerCount` was 0 although a publish was already in flight. **Cause.** The publish controller started new workers with `Task.Run` and only then updated `PublishWorkerCount`. A worker could issue its first publish before the controller got that far. **Proof.** A 200 ms delay between starting the workers and updating the count reproduces the CI assertion in both test cases. **Fix.** The controller now sets the count before it starts the workers. **Verification:** - The test passes with the delay still in place. - 30 of 30 loop runs passed. - Client.Tests (net10.0): 3282 passed. One unrelated test failed under machine load: `SessionBindingLiveTests.BoundRequestCannotDispatchWhileNativeReplacementIsPendingAsync`, a channel reconnect. It passes 5/5 on its own. - The net48 build of Opc.Ua.Client is clean. ## 2. ServerLoadHarness: "error: BadRequestInterrupted" at the end of a run The harness printed `error: [80840000] (BadRequestInterrupted)` lines that looked like warm-up failures. They are the requests still in flight when the workload is stopped. `ChannelAsyncOperation` reports a cancelled token as BadRequestInterrupted, not as `OperationCanceledException`. Every one of these errors had the stop token cancelled (instrumented run with read, browse and write workloads). This is not a product bug. The read/write loops now exit quietly in that case, and read, write and sub runs no longer print the errors. ## 3. CI: record certificate allocation stacks Opc.Ua.Sessions.Tests sometimes fails its assembly teardown with "Certificate leak detected: 1 instance(s)": - 4 jobs between 2026-10-12 and 2026-10-19, out of about 278 macOS net10.0 shard runs. - Never seen on Linux or Windows. - The runs already included the fix for the failed HTTPS listener open (#4775). It did not reproduce locally: one full tracked run and six tracked runs of the WSS/HTTPS/reverse-connect fixtures were all clean. The per-fixture attribution showed no unbalanced fixture, which suggests work that outlives its fixture. The CI test step now sets `OPCUA_CERTIFICATE_LEAK_TRACKING=1`, so the next occurrence reports the allocation stack and the owning fixture. Tracked handles are removed from the tracker on dispose, and the WSS/HTTPS fixtures ran no slower with tracking on. The leak itself is **not** fixed by this PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
df6adec1af |
Refresh server benchmarks for 2.0.2: GC modes, processor placement, delivery (#4868)
Closes #4842. Measures the items that `docs/Benchmarks.md` listed under *Open measurements* and makes every published table reproducible from checked-in files. All runs: `master` |
||
|
|
48871a7e87 |
Client: completed requests no longer keep their responses alive (#4870)
## Problem A client request that lives across a GC is promoted. Publish requests always are: they wait at the server for the publishing interval. When the response arrives, two **dead** old objects still reference it: 1. `UaSCUaBinaryChannel.WriteOperation.MessageBody`: `UaSCUaBinaryClientChannel.SendRequestAsync` read it but never cleared it. 2. The box of the generated `SessionClient.*Async` method. It is `async ValueTask<T>` with the default builder, so a call that completes asynchronously leaves a `Task<T>` box holding the result and the hoisted `response` locals. The GC treats old objects as live. Every gen0 and gen1 collection therefore keeps the whole response alive through the card table until a full collection. With 100k monitored items, every `PublishResponse` (100 `MonitoredItemNotification`s) ended up promoted to gen2. Evidence (ClrMD scan of a live client dump, 25 sessions × 40 subscriptions × 100 items): 1,073 KB of gen0 was kept only by dead old objects, against 136 KB kept by live ones. The dead holders were exactly the two above, holding the same responses. Gen2 held 37 MB of dead notifications. ## Change - `SendRequestAsync` clears `operation.MessageBody` once it has read the response. - `ClientApiTemplates`: generated `SessionClient` async methods use `PoolingAsyncValueTaskMethodBuilder` on .NET 6+. A completed box resets its result and state machine. This is the same pattern #4849 applied to the channel and `ManagedSession`. Contract note: the generated `SessionClient` calls now return pooled value tasks, so each must be awaited once, like any value task. All sync-over-async call sites in the repo use `.AsTask()` first. After the fix, young data kept only by dead old objects is 0 KB (was 1,073 KB). ## Measurements `perf/ServerLoadHarness`, client `client-gc:` line. Master vs this branch, 3 alternating rounds, runs gated on foreign CPU load (shared machine). Ranges are over the rounds. | scenario | build | pause share | GC p50 ms | survived/gen0 | survived/gen1 | gen2 MB | B/op | |---|---|---|---|---|---|---|---| | sub 100k items (25×40×100, pub 500 ms) | master | 4.9–5.4 % | 7.9–8.2 | 2.28–2.57 MB | 1.4–1.7 MB | 121–123 | 235–238 | | | this PR | 4.0–4.5 % | 6.1–6.9 | 0.64–0.71 MB | 0.19 MB | 86 | 233–240 | | sub 50k items (25×20×100) | master | 1.7–2.2 % | 5.5–6.5 | 2.1–2.2 MB | 0.8–1.1 MB | 56–62 | 233–238 | | | this PR | 1.7–1.8 % | 5.2–5.5 | 0.39–0.41 MB | 0.1–0.2 MB | 44–49 | 232 | | read 1 node, 200 sessions | master | 8.9–9.2 % | 2.2–2.4 | 200 KB | | 20 | 1456 | | | this PR | 7.3–7.7 % | 2.2–2.7 | 113–163 KB | | 20 | 1272 | | read 1 node, 20 sessions | master | 3.3–3.6 % | 0.9–1.2 | 50–53 KB | | 4.4 | 1456 | | | this PR | 2.8–2.9 % | 0.85–1.16 | 32–46 KB | | 4.4 | 1272 | The first fix round at 100k ran while another process starved the server (46k notif/s); it is left out of the fix row. - At 50k items, gen1 collections drop from 15 to 1 per 10 s. - GC phases at 100k items (TraceAnalyzer `gc`): card marking 3.3 → 2.55 ms (2.8 → 0.72 MB marked), plan+relocate+compact 6.05 → 3.27 ms. - Allocation: −184 B per request (−12.6 %), because the `Task` box per call is gone. ## Tests net10.0: Opc.Ua.Client.Tests 3283 passed / 0 failed, Opc.Ua.Core.Tests 6535/0, Opc.Ua.SourceGeneration.Core.Tests 4412/0, Opc.Ua.Sessions.Tests 857/0. The net48 Release build of Opc.Ua.Client (with Core) is clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6fe5399510 |
Add UaLens desktop engineering workspace (#4822)
# Description Add **UaLens**, an Avalonia desktop engineering workspace for OPC UA exploration, monitoring, typed editing, administration and guided companion-specification tasks. This is a **new draft PR for `pisrapp` against `master`**, replacing closed, unmerged #4462 without reopening it. The shared prerequisites (#4801, #4802, #4803, #4804 and #4806) have all merged; their implementation is no longer part of this PR's delta. ### Scope - Multi-document workspace with connection, security and identity controls; browsing, subscriptions, events, alarms, history, writes and method calls. - Offline NodeSet2 exploration, typed scalar/array/matrix editing, certificate/GDS and account/role tools. - UaLens companion workflows, repository sample launching and the Aspire AppHost. - PubSub commissioning and capture/replay integration, plus the UaLens-facing OpenUSD viewport and rendered Vision workflows. - UaLens projects, test assemblies, UI assets, guides, solution/dependency entries, desktop test wiring and delivery workflows. Head **`6f46c51fcfef275059bbe77ef39c64c19ebc1053`** is one UaLens-only commit directly above master **`44a8cb277df450162c29313e2f4c862e7c683752`**. The rebase required no conflicts. All **967 UaLens file blobs**, the full-index binary patch, commit-message text and author/co-author attribution are unchanged from the previously published layer. There is **no implementation delta under `src/`**. The original history is retained in recovery refs, including `refs/pr-stack/backups/master20261017/ualens`. No feature was disabled, and no export/path safeguards, native acceptance criteria, test assertions or CI gates were weakened. The four previously recorded blank-line-at-EOF findings in the UaLens dialog files remain unchanged; they are not reported as newly fixed. ### Validation Fresh validation ran on the **exact PR head `6f46c51f`**, using the existing pinned SDK **10.0.401**, normal repository analyzers and physical/non-linked build outputs: ```powershell $d = 'D:\git\UA-.NETStandard_master\TestResults\ualens-528-deep-repro\sdk\dotnet.exe' $b = 'D:\git\UA-.NETStandard_master\TestResults\ua-stack-feedback20261016' $r = 'C:\Users\mschier\.copilot\session-state\abd33a2e-7139-44e1-9912-6e4451102b4e\files\ualens-new-draft20261017-net10' $env:DOTNET_ROOT = Split-Path -Parent $d $env:DOTNET_ROOT_X64 = $env:DOTNET_ROOT $env:DOTNET_CLI_TELEMETRY_OPTOUT = '1' $env:PATH = $env:DOTNET_ROOT + [IO.Path]::PathSeparator + $env:PATH & $d test 'tests\Opc.Ua.Lens.Tests\Opc.Ua.Lens.Tests.csproj' --no-restore --artifacts-path $b -c Release -f net10.0 -p:CustomTestTarget=net10.0 --filter 'TestCategory!=LongRunning&TestCategory!=Stress' --logger 'trx;LogFileName=ualens-new-draft.trx' --results-directory $r --blame-hang --blame-hang-timeout 3m --blame-hang-dump-type none --verbosity minimal # PASS, exit0: Passed: 4524, Failed: 0, Skipped: 0. ``` The saved TRX contains **4524 Passed and 44 explicit/prerequisite NotExecuted records** (4568 total). The repository verdict helper confirms zero failed tests, fixture setup/teardown failures or run-level errors, without toleration. No compiler/analyzer diagnostics were emitted. Logs, TRX and `ualens-new-draft20261017-validation.json` retain this exact-head evidence. **Not run for this new draft:** the full `UA.slnx` net48/net10 matrix, fresh coverage, other desktop TFMs, application-desktop/native workflow suites, NativeAOT, GPU/per-RID/installed-runtime or package-release qualification. UaLens targets .NET8+; no net48 UaLens execution is claimed. Remote CI and CodeQL are not yet claimed green. Historical native diagnostics and successful managed tests are not substitutes for rendered-frame or release acceptance. The previous PR and retained session artifacts preserve the full extraction, review, failed-attempt and diagnostic history. Historical intermittent CI failures were not silently reclassified as root-cause fixes; no old diagnostic grants or producer/release operations were reused for this PR. ## 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))._ - References #4461, the existing closed UaLens design/qualification tracking issue; no new design approval is implied. - Replaces closed, unmerged #4462 for the remaining UaLens work. - Builds on merged prerequisites #4801, #4802, #4803, #4804 and #4806; this PR does not close them. ## 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. The test/coverage item remains unchecked because fresh coverage was not collected. This draft does not claim CLA confirmation, full-matrix clearance, native qualification or completed review. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Alexander Köpke <alexander.koepke@microsoft.com> Co-authored-by: Copilot <Copilot@users.noreply.github.com> |
||
|
|
4d00467f7e |
Add missing component to PumpType instances (#4867)
This pull request introduces improvements to how components and their references are managed and exposed in both the Machine Vision and Recycling sample domains. The main focus is on ensuring that components use the correct `HasComponent` reference type, and that pumps expose mandatory two-state status labels with appropriate properties and descriptions. Additional tests are included to verify these behaviors. **Component Reference Improvements:** * Updated the `WithComponents` method in the Vision documentation and implementation to use `HasComponent` references instead of `Organizes`, ensuring that components are referenced in compliance with OPC UA modeling best practices. [[1]](diffhunk://#diff-c885bd41583e603a034705800d1cd03213b4f3c8b1e1611b31121652e6548c5aL340-R341) [[2]](diffhunk://#diff-89611f4e76aef466943f80ed4079b1275d4ee2016610ad2407f7c8932f2d07e0L252-R252) * Modified the Machine Vision asset builder and related tests to verify that components are correctly referenced with `HasComponent`, and that shared components are properly managed without reparenting or replacing. [[1]](diffhunk://#diff-c265154f7bd0bbcbaa003aa050270e193e47ba941b41f9158a49f799efbcdf14L506-R517) [[2]](diffhunk://#diff-c265154f7bd0bbcbaa003aa050270e193e47ba941b41f9158a49f799efbcdf14R528-R535) **Pump Status Label Enhancements:** * Refactored pump status variable initialization in the Recycling sample to ensure that the mandatory two-state properties (`FalseState` and `TrueState`) are set with the labels "Stopped" and "Running", and that a descriptive text is provided. * Updated the Recycling sample documentation to clarify that each pump's status exposes the mandatory `FalseState` and `TrueState` properties as "Stopped" and "Running", reflecting the synthetic pump state. **Testing:** * Added a comprehensive test to verify that every circulation and drain pump exposes readable mandatory two-state status labels (`FalseState` and `TrueState`) with the correct values and reference types.# 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._ ## 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 #github-issue-number, ... ## 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. - [ ] 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. |
||
|
|
f6b27025a6 |
Classify sensitive log parameters with Microsoft.Extensions.Compliance data classification (#4864)
## Proposed changes Fixes #4857. The stack's log messages wrote network addresses (endpoint, discovery and reverse connect URLs, host names, IP addresses, ports), application and user identities, file paths and exception texts as plain parameters. Applications had no reliable way to keep them out of general-purpose logs. This PR replaces the stack's own redaction mechanism with the standard .NET [data classification and redaction](https://learn.microsoft.com/dotnet/core/extensions/data-redaction) packages. Each sensitive `[LoggerMessage]` parameter is classified. The application decides per classification what reaches its logs: erase it, replace it with an HMAC token so entries stay correlatable, or pass it through. ### Taxonomy `Opc.Ua.Redaction.OpcUaDataClassifications` defines the `OpcUa` taxonomy and one attribute per classification: | Classification | Used for | Parameters | |---|---|---| | `[NetworkAddress]` | URLs, host and domain names, IP/MAC addresses, end points, ports | 120 | | `[ApplicationIdentity]` | certificates, subject, issuer, thumbprint, ApplicationUri | 103 | | `[FilePath]` | file, directory and certificate store paths | 67 | | `[SensitiveText]` | exception or remote-supplied text passed as a parameter | 55 | | `[UserIdentity]` | user names, token subjects | 7 | Correlation ids (`ChannelId`, `SessionId`, ...), status codes and NodeIds stay unclassified. ### Pipeline and defaults - Opc.Ua.Core now references `Microsoft.Extensions.Telemetry(.Abstractions)` 10.11.0, so every project above Core uses the `Microsoft.Gen.Logging` generator. Opc.Ua.Client already did, through Http.Resilience. The packages ship net462 and netstandard2.0 builds, so net48 is supported. - **A plain `ILoggerFactory` logs `(null)` in place of classified values.** Classified values are only written by the redaction-aware logger. - The new `ILoggingBuilder.EnableOpcUaRedaction()` turns that logger on. It is `EnableRedaction()` with the discriminator off; otherwise every unredacted value gets `:ParamName` appended. `AddOpcUa().AddLogging(...)` and `DefaultTelemetry.Create(...)` call it, as do the samples, so they log the same information as before until redactors are registered. - `services.AddOpcUa()` alone does not change the host's logging. Generic-host apps call `builder.Logging.EnableOpcUaRedaction()`. This is documented as a behavior-breaking change in `docs/migrate/2.0.x/telemetry.md`. ### Obsolete API `Redact`, `RedactionWrapper<T>`, `IRedactionStrategy`, `RedactionStrategies` and `SimpleRedactionStrategy` are `[Obsolete]`. They still work, but the stack no longer uses them. ### Not classified - The `Exception` passed to a log call. Applications can drop exception messages with `LoggerEnrichmentOptions.IncludeExceptionMessage`. - Exception and `ServiceResult` message texts. - `ServiceResult`/`StatusCode` parameters and logged data values. - The few log calls whose level is decided at run time. - `Opc.Ua.Vision.OpenUsd`, which references only Opc.Ua.Types and cannot see the attributes. ### Generator behavior differences These are visible to log consumers and to tests: - `IFormattable` values such as `NodeId` are stored in the structured state as invariant strings, not as objects. - The generated log state is a thread-local object that is cleared after the call. - Classified values are pooled redactor objects, valid only during the `Log` call. Tests that inspected log state after the fact now capture it during the call. The shared `RecordingLoggerProvider` snapshots classified values. ## Related Issues - Fixes #4857 ## Types of changes - [ ] Bugfix (non-breaking change which fixes an issue) - [x] Enhancement (non-breaking change which adds functionality) - [ ] Test enhancement (non-breaking change to increase test coverage) - [x] Breaking change (fix or feature that would cause existing functionality to not work as expected; plain logger factories drop classified values) - [x] Documentation Update (if none of the other choices apply) ## Checklist - [x] I have read the CONTRIBUTING doc. - [x] I have signed the CLA. - [x] I ran tests locally with my changes, all passed. - [x] I fixed all failing tests in the CI pipelines. - [x] I fixed all introduced issues with CodeQL and LGTM. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added necessary documentation (if appropriate). - [ ] Any dependent changes have been merged and published in downstream modules. ## Further comments New tests: - `LogClassificationCoverageTests` (Tools.Tests) scans all 104 stack assemblies and fails if an address, identity or path parameter of a `[LoggerMessage]` method is unclassified. - `DataClassificationLoggingTests` (Core.Tests) pins erasing and HMAC redaction, pass-through, the plain-factory drop, and both stack defaults. Local results on net10.0: | Suite | Passed | Failed | |---|---|---| | Core.Tests | 6,489 | 0 | | Client.Tests | 3,231 | 0 | | Tools.Tests | 7,210 | 0 | | PubSub.Tests | 1,536 | 0 | | Core.Diagnostics.Tests | 571 | 0 | | Configuration.Tests | 241 | 0 | - **Server.Tests:** the full run predates the final test fixes and had 12 failures. All 12 were in the fixtures fixed afterwards, which now pass: RoleStateBinding (45/45) and the FileDirectory and compatibility-logging fixtures (74/74 in total). The full Core.Tests and Server.Tests runs were not repeated after those fixes. - **Builds:** `UA.slnx` builds with 0 errors for net10.0 and net48, and the whitespace check passes. - **NativeAOT:** not confirmed locally, because the machine has no C++ linker. A trimmed publish of `Opc.Ua.Aot.Tests` reported no trim or AOT warnings from the stack, the new packages or the generated logging code; its only errors come from the TUnit test framework (`FSharp.Core` not found). The `aot-test` CI job has to confirm the native build. 🤖 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> |
||
|
|
5899354441 |
Clarify documentation, migration guidance, and SDK selection (#4823)
# Description Remove stock phrasing and correct misleading or inconsistent technical documentation. The initial two passes covered 367 Markdown files, followed by an interactive review of 169 editorial flags. The U5 interpretation originally deferred in that review has since received the upstream Mantis answer recorded by #4843; remaining server/spec follow-ups stay explicit in the CTT log. - Correct package responsibilities, migration steps, API names, ownership, protocol descriptions and links. Keep bundled migration guides synchronized. Preserve historical conclusions as historical records with separate re-check notes. - Provide at least two rendered TOC levels throughout `docs/`. Convert appropriate architecture figures to Mermaid without changing code examples, transcripts or historical quotations. - Address the October 19 feedback with concrete setup/migration instructions, corrected certificate paths and PubSub names, shorter repeated caveats, consistent XML comments, and a full review of the Concepts guide. Retain #4839's API corrections. - Integrate #4841's runtime/performance guidance and review incoming/recent prose. Preserve dated measurements rather than claiming new benchmark runs; clarify allocation ratios, startup-only processor settings, JIT warm-up and the benchmark commit's scope. Missing raw measurement artifacts and repeat ranges remain tracked by #4842. - Merge master through `e8c5d800a` in `32c6394b7`. Resolve the CTT log conflict using the updated Mantis states and OPC answers, retaining historical framing and re-checks for C28/C32/C43. - Address the latest naming/index feedback in `adb5bf693`: rename the guides to `docs/WotCon.md` and `docs/WotNodeSetConversion.md`; move the provider and DataType-declaration contracts into the conversion guide and remove `docs/WoT.md`. Rename `samples/DI` to `samples/Di` and `samples/Isa95` to `samples/ISA95`. Add content-index READMEs to Di, FluentApi, ISA95, Lds and PubSub; update solution entries, project references, Docker/CI paths, test fixture paths and documentation links with exact casing. **WoT guide scope.** This branch retains the detailed conversion guide that #4758 had replaced with an anchor stub. Conversion limits, verified document sets, preservation rules and provider ownership now live together in `WotNodeSetConversion.md`; binding/runtime guidance remains in `WotBindings.md` and `WotCon.md`. The design-rationale/test-ledger prose was shortened, the oversized argument-table cell moved into its section, and existing anchors preserved. **SDK policy is a separate, explicit user-directed change.** `global.json` sets a stable minimum of `10.0.400`, `rollForward: latestMajor`, and `allowPrerelease: false`. Newer major SDKs are permitted; CI's explicit SDK pins are unchanged. README, GettingStarted, DeveloperGuide and Recycling instructions reflect the possible analyzer differences. The image-contract regression fix accepts compatible stable SDK images instead of requiring exact equality with the floor. The latest four rename/index threads are resolved after publication. Three older threads remain open: universal obsolete-removal timing, unconditional nonce-strength wording, and a blanket Vision NativeAOT claim. Existing implementation/evidence constraints are explained in their inline replies. The two-level TOCs remain as requested. ## Validation - `git diff --check` and staged diff checks: passed. The untouched upstream AutoID NodeSet retains its provenance bytes, including two existing license-header trailing spaces. - `.\.azurepipelines\validate-migration-plugin.ps1`: passed; 15 bundled migration documents and manifests synchronized. - Documentation/path validation: **101** documents retain two rendered TOC levels (**2,228** entries), with idempotent formatting. **52** new relative links and their anchors passed exact-case checks. No tracked references to the old guide/sample paths remain. Both relocated provider-contract sections are preserved in the destination. - `dotnet sln UA.slnx list`: passed; shows the renamed Di/ISA95 projects and the existing FluentApi/Lds/PubSub projects. Updated solution and pump ProjectReference paths also passed exact-case existence checks. - Latest focused commands, run from the repository root: ```powershell # Passed: 61 channel send-order and reconnect tests. dotnet test tests\Opc.Ua.Core.Tests\Opc.Ua.Core.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~UaSCBinaryChannelSendOrderTests|FullyQualifiedName~TcpReconnectKeyAgreementTests' --verbosity minimal # Passed: 5 pump datasheet tests; builds the renamed pump sample. dotnet test tests\Opc.Ua.Di.Tests\Opc.Ua.Di.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~PumpDatasheetConformanceTests' --verbosity minimal # Passed: 4 repository NodeSet path/round-trip tests, including the moved pump models. dotnet test tests\Opc.Ua.Types.Tests\Opc.Ua.Types.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~RepositorySourceSurvivesSerializedLinkedPartitionsAsync' --verbosity minimal # Passed: build, zero warnings/errors. dotnet build samples\ISA95\MinimalIsa95Server\MinimalIsa95Server.csproj -p:CustomTestTarget=net10.0 -f net10.0 --verbosity minimal # Passed: dependent sample-test project build, zero warnings/errors. dotnet build tests\Opc.Ua.WotCon.Samples.Tests\Opc.Ua.WotCon.Samples.Tests.csproj -p:CustomTestTarget=net10.0 -f net10.0 --verbosity minimal ``` Earlier focused integration checks passed 2,620 tests across AutoID, event queues/scheduling, source generation, MCP/package profiles and WoT. Both reviewed Mermaid diagrams rendered locally without network access. These checks are not a full solution run. The full solution has **not** been rerun for this review. Earlier `dotnet test UA.slnx -p:CustomTestTarget=net10.0 --verbosity quiet` did not pass: three AOT projects rejected VSTest and 20 Vision tests failed during graphics initialization with `OutOfMemoryException`. Remaining long-running tests were stopped at the user's request. A focused Vision rerun passed 558 tests and reproduced those 20 failures. The earlier `dotnet test UA.slnx -p:CustomTestTarget=net48 --verbosity quiet` was blocked by the former SDK pin and was not retried after the policy change. The user approved proceeding with those limitations documented; current remote CI is not claimed green. ## Related Issues - Follow-up #4842 tracks the requested 2.0.2+ benchmark/tuning and diagnostic-tool documentation refresh. #4841 supplies documentation and earlier results, not all newly requested measurements. - #4758 explains the conversion guide's earlier consolidation; the current provider contracts are now consolidated into `WotNodeSetConversion.md`. - #4839 supplies the reviewed API corrections; #4843 supplies the updated CTT issue record. - The original prose cleanup and SDK policy were user-requested without a source issue. No new runtime architecture is introduced by these documentation edits. ## 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. --------- Co-authored-by: Roman Ettlinger <romanett98@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0bd7aaaad6 |
AutoID: keep every reader reachable from DeviceSet, and let a reader be a component of a machine (#4853)
# Description Follow-up to the AutoID companion (#4825). Two changes to where the AutoID server places its readers: 1. **`IAutoIdReaderBuilder.WithParent` is made conformant with OPC 10000-100 4.9.** Until now it placed a reader below another node *instead of* `DeviceSet`, and the docs suggested "a component of a machine" as the use case. A machine in the Machinery `Machines` folder is not reachable from `DeviceSet`, so such a reader could not be located from `DeviceSet`, which DI 4.9 requires for every `ComponentType` instance (every AutoID device type derives from DI `DeviceType`). Now the reader is still placed below the parent (`HasComponent`, unchanged), and: - when `DeviceSet` already reaches the parent through hierarchical references (for example another reader, or any device it organizes), nothing else is added: the reader is a component of a composed device and DI 4.9 wants "only the root instance" referenced from `DeviceSet`; - otherwise `DeviceSet` also organizes the reader (`Organizes`, both directions), so it stays locatable. 2. **New opt-in `IAutoIdReaderBuilder.AsComponentOf(BaseObjectState machine)`**, available only when AutoID is composed with Machinery (`AddAutoIdToMachinery`, or a DI owner that loads the Machinery model). It makes the reader an identifiable component of the machine per OPC 40001-1 11: - the reader is placed below the machine's `Components` add-in (`HasComponent`, as `MachineComponentsType` declares for `<Component>`); - it gets the `2:Identification` add-in (`HasAddIn`) typed `MachineryComponentIdentificationType`, which holds no copies: it references the reader's own DI nameplate properties with `HasProperty` (`Manufacturer`, `SerialNumber`, and those of `Model`, `ManufacturerUri`, `ProductCode`, `HardwareRevision`, `SoftwareRevision`, `DeviceRevision`, `DeviceClass`, `ProductInstanceUri` that are set, plus `AssetId`/`ComponentName` when published; `DeviceManual` and `RevisionCounter` are left out as 40001-1 10.2 recommends); - the reader stays a device: the same `DeviceSet` rule as for `WithParent` applies with `Components` as the parent, so in the usual case (a machine in `Machines` that `DeviceSet` does not reach) `DeviceSet` organizes it directly. It is not added to `Machines`. - Checked before anything is registered, each failing with `Bad_ConfigurationError` and a message naming the way out: the server has no Machinery model (names `AddAutoIdToMachinery`), the machine is not a node of the AutoID owner, the machine has no `Components` add-in (names `WithComponents`), or the reader nameplate lacks `Manufacturer` and/or `SerialNumber` (mandatory in `MachineryItemIdentificationType`). `WithParent` and `AsComponentOf` exclude each other (`InvalidOperationException`). Default behaviour is unchanged: a reader built without `WithParent`/`AsComponentOf` is organized by `DeviceSet` and not added to `Machines`. Rollback on failure covers the new references: a failed build removes the `DeviceSet` reference, the component and its add-in, so the name can be used again. ## Specification basis | Statement | Source | | --- | --- | | "The DeviceSet Object is the starting point to locate Devices. It shall either directly or indirectly reference all instances of a subtype of ComponentType with a Hierarchical Reference. For complex Devices that are composed of various components that are also Devices, only the root instance shall be referenced from the DeviceSet Object. The components of such complex Devices shall be locatable by following Hierarchical References from the root instance." | OPC 10000-100 (DI) 1.05.0, 4.9 | | AutoID device types are "Subtype of DeviceType defined in OPC 10000-100". OPC 30010 does not mention `DeviceSet`, Machinery or `Machines`. | OPC 30010 (AutoID) 1.01, 6.1 Table 7 | | "Each Machine providing this building block provides the Components Object, which directly references all identifiable components of a Machine." / "Note: That does not preclude that a Machine is organizing its components in various other hierarchies as well." | OPC 40001-1 (Machinery) 1.04.2, 11.1 | | "The MachineComponentsType provides HasComponent References to all identifiable components of a Machine"; Table 27: `0:HasComponent Object <Component> 0:BaseObjectType OP`; Table 28: `<Component> 0:HasAddIn Object 2:Identification MachineryItemIdentificationType M`; "As all identifiable components shall contain the Identification Object". | OPC 40001-1 1.04.2, 11.2 | | `MachineryItemIdentificationType`: `2:Manufacturer` M, `2:SerialNumber` M, all other members O. | OPC 40001-1 1.04.2, 8.3 Table 16 | | `MachineryComponentIdentificationType` is the component subtype of `MachineryItemIdentificationType` (adds `2:DeviceRevision`); "It is not recommended to use the optional Properties 2:RevisionCounter and 2:DeviceManual"; a missing component `2:ProductInstanceUri` "shall be omitted". | OPC 40001-1 1.04.2, 10.2 | | "it is recommended to keep this approach and let several References lead to the same Node, rather than duplicating the Node" / Robotics example: identification Properties "are only referenced from the Identification functional group". | OPC 40001-1 1.04.2, 7.3 and Annex B.2 (informative) | | "It is expected that domain-specific companion specifications using this specification will define what assets of the domain in what usage are considered to be Machines or components of Machines." | OPC 40001-1 1.04.2, 11.1 (Note) | | Organizes "can be used to span multiple hierarchies"; more than one hierarchical path to a Node is allowed. | OPC 10000-3 1.05, 7.11, 7.5 | **SDK decisions** (not specified by OPC 30010, which predates Machinery): - A reader is never added to `Machines`: no specification classifies an AutoID device as a machine (40001-1 11.1 leaves that to the domain specifications, AutoID is silent). - A reader becomes a component of a machine only when the application asks for it (`AsComponentOf`). - `WithParent` keeps its parent and adds the `DeviceSet` reference rather than rejecting parents outside `DeviceSet` (see below). - The component `Identification` references the reader's nameplate instead of copying it. - AutoID does not add Machinery conformance units; the Machinery owner advertises them for what its builders created. ### Why `WithParent` adds a reference instead of rejecting DI 4.9 asks for reachability, not for a particular parent, and OPC 10000-3 allows several hierarchical paths to one node. Adding `Organizes` from `DeviceSet` satisfies the "shall" while keeping the parent the application asked for, and it is the same reference type `DeviceSet` already uses for every reader (and the DI node manager for every device). Rejecting such parents with `Bad_ConfigurationError` would break existing callers that hang a reader below a machine or folder. When `DeviceSet` already reaches the parent, no reference is added, so the "only the root instance" rule of 4.9 holds for composed devices. Reachability is decided once when the reader is built, by following the forward hierarchical references of the owner's nodes. ### Why `AsComponentOf` keeps the reader referenced from `DeviceSet` Machinery does not require machines to be in `DeviceSet` (the stock builders put them in `Machines` only), so a reader that is only below `Components` would again be unreachable from `DeviceSet`. The same reachability rule as for `WithParent` applies: if the machine is itself reachable from `DeviceSet` (for example a machine that is also a DI device organized by `DeviceSet`), the reader is reached through it and gets no direct reference, otherwise `DeviceSet` organizes it. ### Shape of `AsComponentOf` It takes the machine's `BaseObjectState`, like `WithParent(NodeState)` and `IMachineryBuildContext.BindMachine(TState, ...)`; with the stock builders that is `IMachineHandle<TState>.State`. It requires the machine's `Components` add-in to exist (`IMachineBuilder.WithComponents`, or a companion-declared one bound with `WithComponents(components, ...)`) instead of creating one, so the Machinery builders stay the only place that creates Machinery building blocks (`MachineryBuildingBlocks` organization, facets). The reader's NodeState parent is the `Components` object, so the DI node manager attaches it with `HasComponent` and the existing registration and rollback code applies unchanged. ## Behaviour change for existing callers - `WithParent(parent)` with a parent that `DeviceSet` does not reach (for example a machine in `Machines`, or an application folder): the reader is now *also* browsable from `DeviceSet` (one extra `Organizes` reference, both directions), and an `AutoIdClient.EnumerateReadersAsync()` (which browses `DeviceSet`) now finds it. Its parent, NodeId and `HasComponent` reference are unchanged. - `WithParent(parent)` with a parent `DeviceSet` reaches (for example another reader): unchanged. - Default placement: unchanged. - `IAutoIdReaderBuilder` gains a member. The interface is implemented only inside the package (instances come from `AutoIdRuntime.Reader`), and the package is new in 2.0. ## Changes - `src/Opc.Ua.AutoID.Server/AutoIdReaderPlacement.cs` (new, internal): `DeviceSet` lookup, the reachability check, the `Organizes` reference and its removal, `Components` resolution and validation, the nameplate check, and the `Identification` add-in. - `src/Opc.Ua.AutoID.Server/AutoIdRuntime.cs`: the builder's nameplate delegate runs once (validated before anything is registered, then copied into the DI builder); `AsComponentOf` resolves `Components` as the parent before the device is created; after registration the add-in is added and the `DeviceSet` reference kept; rollback removes the reference. - `src/Opc.Ua.AutoID.Server/Builders/IAutoIdReaderBuilder.cs`, `AutoIdReaderBuilder.cs`: `AsComponentOf`, mutual exclusion with `WithParent`, XML docs stating the exact behaviour. - `docs/AutoID.md`: new "Placement" subsection under "Hosting readers" (what DI 4.9 and Machinery 11 require, what is an SDK decision, a table per builder call, an example), the composition table rows for readers and `Machines`, the test fixture table. `src/Opc.Ua.AutoID.Server/NugetREADME.md`: a short paragraph. - Tests (below). ## Tests - `AutoIdReaderLifecycleTests` (stand-alone owner): `ReadersCanHangBelowAnExplicitParentAsync` now also asserts that `DeviceSet` does not organize a reader below another reader; `AReaderBelowAParentOutsideTheDeviceSetIsStillOrganizedByItAsync`; `AFailedCreationBelowAnOutsideParentLeavesNoDeviceSetReferenceAsync` (rollback); `AStandaloneReaderCannotBeAComponentOfAMachineAsync` (`Bad_ConfigurationError` naming `AddAutoIdToMachinery`); argument checks for `AsComponentOf(null)` and both call orders of `WithParent`/`AsComponentOf`. - `AutoIdCompositionTests` (Machinery owner, in process): `AReaderBecomesAComponentOfAMachineAndStaysADeviceAsync` (parent is `Components`, `HasComponent`, `DeviceSet` organizes it, `Machines` does not, `Identification` with `HasAddIn` and the component type, `HasProperty` to the reader's own `Manufacturer`/`SerialNumber`/`Model` nodes, no `DeviceManual`, owned subtree); `AComponentNeedsAMachineWithComponentsAndRegistersNothingOtherwiseAsync`; `AComponentNeedsTheMandatoryIdentificationAsync`; `AComponentNeedsAMachineOfTheOwnerAsync`; `AFailedComponentCreationIsRolledBackAsync`. - `AutoIdPlacementEndToEndTests` (new, hosted Machinery + AutoID over a real session): a default reader, a reader below that reader, a reader `AsComponentOf` a machine and a reader `WithParent` the machine. Browsing `DeviceSet` finds the three roots and not the nested reader, the outer reader reaches the nested one; `MachineryClient.DiscoverMachinesAsync` returns only the machine and `Machines` contains no reader; `MachineryClient.EnumerateComponentsAsync` lists only the component reader, `ReadIdentificationAsync` reads `Manufacturer`, `SerialNumber`, `ProductCode` through the add-in, which is `HasAddIn`-referenced and typed `MachineryComponentIdentificationType` and whose `HasProperty` targets are the reader's own nameplate nodes; `AutoIdClient.EnumerateReadersAsync` finds the three roots. - The existing default assertions (`TheReadersShareTheDeviceSetWithThePumpAndStayOutOfTheMachinesFolderAsync`, `TheMachineryAndPumpsClientsStillFindTheirInstancesAsync`, `ComposedReadersLiveBelowTheSharedDeviceSetAsync`) are unchanged. - Fail-without: with the runtime wiring reverted to master (builder surface kept so the tests compile), 10 of the new tests fail: all `AsComponentOf` cases, both `WithParent` cases outside `DeviceSet` and the three session tests that look for the component or the extra `DeviceSet` reference. Removing only the rollback of the `DeviceSet` reference makes both rollback tests fail. `NoReaderBecomesAMachineAsync` and the existing default tests pass with and without the change, as intended. Local results (macOS arm64, SDK 10.0.402, head `238ed9b69` on master `9bfc780e1`; the same three commits are now based on master `ac24123dd` as `c6d70909b`, no AutoID file changed in between): `Opc.Ua.AutoID.Tests` net10.0 318/318 passed; `Opc.Ua.AutoID.Server` all TFMs and the test project with `CustomTestTarget=net48` build with 0 warnings, 0 errors (net48 tests run on Windows CI; there is no mono host here). `dotnet format whitespace/style/analyzers --verify-no-changes` on the changed files: no new findings beyond the conventions of the surrounding test files. ## Commits 1. `fix(autoid): keep every reader reachable from DeviceSet` 2. `feat(autoid): make a reader a component of a machine` 3. `docs(autoid): document where readers are placed` The branch is based on OPCFoundation/master `ac24123dd` (#4852, the last master commit with a green build). Fork CI was not repeated on this head: earlier fork runs of this branch failed only in test projects outside this PR (the master build was broken by the NUnit 5 migration #4840 and fixed in #4852; master is currently broken again in `Opc.Ua.Core.Tests` by #4849). The upstream checks of this PR test it against the current master. ## Related Issues - Follow-up to #4825 (tracking issue #4824). - #4823 (open) edits prose in other paragraphs of `docs/AutoID.md` (table of contents, the DI-owner paragraph, providers). This PR adds a subsection and changes two rows of the composition table and one row of the test table; no hunk overlaps. The table of contents is deliberately not touched here because #4823 adds the subsection entries for "Hosting readers"; the "Placement" entry can be added there or after both are merged. ## 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. (Local build of the changed projects: 0 warnings, 0 errors.) - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. (Locally the AutoID test project on net10.0; net48 is built but runs only on Windows CI.) - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. (Pending the upstream checks; see the CI note above.) - [x] I have addressed **all** PR feedback received. --------- Co-authored-by: Marc Schier <marcschier@users.noreply.github.com> |
||
|
|
a51f08ad6f |
AutoID client: read a device's status without browsing (#4854)
# Description A follow-up to #4825. It adds `AutoIdClient.RefreshDeviceAsync(AutoIdDeviceSnapshot, CancellationToken)`, which reads the variables of a reader device that change at runtime again through the NodeIds a snapshot already carries. It makes one Read and no Browse. ## Why `ReadDeviceAsync` browses the children of the device on every call. OPC 30010 makes most device variables and methods optional, so the browse is how the client finds out what this device publishes. `AutoIdReadOptions.StatusOnly` only narrows the Read. This is the point the Copilot review raised on #4825 ([discussion](https://github.com/OPCFoundation/UA-.NETStandard/pull/4825#discussion_r4308032841)). |
||
|
|
2836da7ec8 |
Fix sessionless ResultTransfer ownership bypass and other issues. (#4834)
# Description Correct ownership, endpoint validation, policy propagation, subscription delivery, file handling and generated-model behavior across the stack, tools and samples. Add focused regression tests and update the corresponding guides and sample documentation. ### Previously included corrections - Require an active, matching session to generate or access transient ResultTransfer file handles. Cover both sessionless generation and reads against a handle owned by an active session, using live session owners in the Machinery tests. - Keep physical filesystem operations within their configured mount and apply the same confinement checks throughout file operations. - Validate LDS semaphore paths consistently during server registration and registration-store maintenance. ### Individual issues addressed | # | Component | Issue and correction | |---|---|---| | 1 | AI jobs and transfers | Jobs and transfers did not consistently retain their creator's identity and session. Apply ownership checks to descendants, file handles, subscriptions and execution, and recheck deployment permissions before dispatch. | | 2 | Password-management client | Password-bearing calls could use signing-only channels or follow a replacement connection. Capture an encrypted transport generation and keep dispatch bound to it; preserve ordinary non-password edits. | | 3 | GDS and certificate-push clients | Connections omitted hostname verification. Validate the original configured DNS name or IP address before session activation. | | 4 | Certificate push | Private-key uploads could use signing-only calls. Require an encrypted, generation-bound dispatch while preserving certificate-only updates and restoring the caller's identity afterward. | | 5 | MQTT client | The .NET Framework custom-trust callback could discard TLS hostname, certificate-availability and usage errors. Retain those checks while supporting configured issuer trust. | | 6 | DTLS handshake | Certificate trust was not tied to the configured peer address. Match the certificate's DNS or IP identity to the intended endpoint. | | 7 | Replicated address spaces | Serialized nodes lost per-node and descendant permission metadata. Preserve role permissions, user-role permissions and access restrictions, including updates to existing replica nodes. | | 8 | MCP PubSub imports | Imports without groups skipped checks on the effective broker destination. Check the actual endpoint before resolving credentials or activating the connection. | | 9 | Reference-server accounts | A disabled username account could authenticate again with its previous roles. Reject disabled accounts during fresh authentication. | | 10 | Console reference client | The sample disabled hostname checks when connecting to an otherwise trusted peer. Retain the original requested host and validate it before activation. | | 11 | Capture replay and export | Replay and raw export could accept non-capture files or include key material. Require complete PCAP/PCAPNG containers and exclude key sidecars and decryption-secret blocks. | | 12 | PubSub adapter fallback | The fallback client configuration automatically accepted peer certificates. Apply normal trust and hostname validation before activation or reads. | | 13 | Kafka transport | Explicit TLS enablement was checked but did not reach both final Confluent configurations. Use the same effective protocol for validation, producers and consumers. | | 14 | PubSub actions | Configuration rebuilds could widen a connection-local action handler's scope. Preserve its original connection association and reject mismatched dispatch. | | 15 | PubSub readers | Reader-level message-protection settings were accepted but ignored. Apply the effective group and reader settings before accepting and publishing received values. | | 16 | PubSub key groups | Different `SecurityGroupId` values could share one connection-wide key context. Reject mixed groups on a connection and require separate connections for distinct groups. | | 17 | MCP session registrations | Session-name replacement or reuse could retain another registration's credential bindings. Bind access to the registration, caller and authenticated endpoint, and preserve that identity when borrowing sessions. | | 18 | Viewer HTTP host | Requests reached the viewer's OPC UA session without consistent Host, Origin and port checks. Validate the request destination and browser origin before using the session, while retaining supported local MCP clients. | | 19 | Vision file operations | File operations did not consistently apply protected-file checks, and remote handles lacked caller and connection context. Apply the shared checks and use host-side opaque handles bound to caller, target and generation. | | 20 | DTLS record integrity | The integrity-only MAC used a truncated sequence number. Authenticate the full reconstructed sequence number and epoch so records remain distinct across rollover. | | 21 | AMB client sample | Connection setup omitted hostname validation. Preserve the originally requested host and verify the peer identity before activation. | | 22 | Robot sample writes | The R2 `SpeedOverride` callback could target R1 state. Bind each callback to its own robot and apply that robot's write policy. | | 23 | AI result properties | Optional result children inherited remotely writable defaults. Preserve their declaration restrictions and keep server-produced results read-only. | | 24 | WSS peer-trust updates | Existing channels did not consistently respond to peer-trust changes. Revalidate complete chains for forward and reverse WSS channels and close only those that no longer validate. | | 25 | GDS revocation checks | Caller-provided certificates could cause issuer retrieval from embedded URLs. Resolve validation from configured local trust, issuers and CRLs without fetching caller-provided locations. | | 26 | GMS observations | Published correction observations retained writable model defaults. Reject remote writes while preserving publisher-side updates. | | 27 | Production-dataset monitoring | Subscription admission and delivery could use the reporter's context instead of each subscriber's permissions. Evaluate the subscriber at admission and on every delivery, including cancellation and disposal paths. | | 28 | WoT MQTT destinations | Hostless MQTT forms could imply a localhost destination before loopback checks. Reject them before credential lookup or connection attempts and validate the effective endpoint. | | 29 | XRegistry opaque reads | Content-ID reads and notifications did not consistently honor encrypted-read requirements or the currently accepted transport. Recheck the bound endpoint and reject transport downgrades before rebinding, including when protected values are already queued. | | 30 | PubSub capture dissection | Key loading did not consistently check the configured capture-root boundary. Check each actual key path immediately before opening it. | | 31 | PubSub publication queues | Imported WriterGroup queues were not fully checked against publish-topic grants. Check the destination actually used by each group. | | 32 | MCP PubSub mutations | A caller-supplied mode could select subscribe grants for an operation that publishes. Derive the required direction and parent context from the actual mutation. | | 33 | Generated write masks | Model import and generation could widen an explicit `UserWriteMask`. Preserve it through import, copy, merge, inheritance and dependency metadata. | | 34 | Generated method flags | An explicit `UserExecutable=false` could be lost. Preserve its specified state and resulting method behavior through the same model paths. | | 35 | Generated role identifiers | Unresolved role identifiers could cause permission entries to be omitted. Preserve namespace-qualified role IDs and report a diagnostic when a policy cannot be represented. | | 36 | Aggregation-server defaults | The `localhost` default did not reliably constrain listener binding. Use numeric loopback for the default endpoint and command-line fallback. | | 37 | Certificate-validation cache | Cached results could skip current leaf or issuer validity checks. Recheck validity windows on cache hits and keep hostname validation independent of cached chain trust. | | 38 | Software-update folders | Revision names and marker contents containing dot segments could select paths outside the software root. Normalize and check every derived path against that root. | | 39 | USDA arrays | Array elements were not encoded for their string or asset context. Escape them as data and preserve valid composition and round-trip behavior. | | 40 | Adapter hot reload | A changed dataset definition with the same name could keep old fields, subscriptions or cached values active. Compare complete immutable definitions and retire old state before replacement. | | 41 | Adapter index ranges | Subscription publication could ignore `IndexRange` and return the complete value. Preserve the requested projection and its status and timestamps; reject unsupported range providers. | | 42 | PubSub metadata and targets | Metadata from one connection could change another connection's target mapping. Scope metadata to its connection and provenance, and resolve targets using actual typed field IDs rather than field names or unintended positional fallback. | | 43 | Scales product locks | Product locks were not applied across every remote mutation path. Apply them consistently to value writes, filling calculations and related recipe/controller operations while retaining permitted updates. | | 44 | Event permission caching | Cached event decisions could outlive identity, role or node-policy changes. Revalidate the current subscriber, event type and source permissions instead of reusing a stale decision. | | 45 | WoT Modbus destinations | Hostless Modbus forms could imply localhost before loopback checks. Reject them before connecting and validate the effective endpoint. | | 46 | MCP local PKI tools | Peer-trust mutations did not consistently require the caller's `CertificateAdministration` grant. Check the capability before trust and untrust operations. | | 47 | Robot-control HTTP bridge | The bridge lacked consistent Host, Origin and port validation and caller-specific session grants. Apply those checks before borrowing the OPC UA session and retain supported local clients. | | 48 | Subscription transfers | Transferred subscriptions could retain cached notifications after the destination's permissions changed. Require authorization continuity between source and destination identities and roles, invalidate stale retransmission data when continuity fails, and preserve retransmission behavior for compatible transfers. | ### Compatibility notes - Replication uses the versioned NSRP v1 policy envelope. Upgrade peers together and re-encode or reseed legacy topology from the authoritative model; missing permission metadata is not inferred from old records. - Integrity-only DTLS peers must be upgraded together for the full sequence/epoch MAC. AEAD behavior is unchanged. - Distinct PubSub key groups require separate connections. - Custom sessions that cannot capture the actual encrypted transport, and subscription providers that cannot honor a requested range, are rejected rather than silently changing the requested behavior. - Subscription transfers retain retransmission data only when the destination authorization remains compatible with the source snapshot; otherwise the old sequence numbers and cached data are invalidated. ## Related Issues No separate tracking issue is linked. The individual corrections are listed above. ## 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. - [ ] 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. |
||
|
|
994d7b34da |
Fix CI test duration regressions (#4861)
# Description Partition the long-running sample and WoT test suites into bounded, non-overlapping CI shards, and give all workloads the standard 45-minute test-execution budget. Account for setup/build overhead separately in the job timeout so builds do not consume test timeouts. Remove the former sample, integration, WoT, and server-specific timeout exceptions while retaining complete test scheduling. Focused matrix tests pass (25/25); full and PR matrix generation succeeds at 238 and 98 jobs, respectively. PowerShell syntax parsing and `git diff --check` pass. The full solution test suite was not run locally. ## Related Issues - Fixes #4858 ## 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. - [ ] 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. |
||
|
|
4c86b11fa5 | Merge branch 'master' into feature/endpoint-message-registry | ||
|
|
6d2c7c0c67 |
WoT: retire a generation's event bindings before releasing their sources (#4860)
Fixes the intermittent failure of `WotEventModesLiveTests.NativeRetiringGenerationUsesCapturedActionsUntilDrainAsync` (introduced with #4757), which currently fails on master and on unrelated PRs, e.g. the master push run [39410642994](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/39410642994) and #4836's run [39410362062](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/39410362062/job/118588752785): ``` Expected: BadEventIdUnknown [0x809A0000] But was: BadSecurityChecksFailed [0x80130000] ``` ### Cause `WotProjectionBindingRuntime.DisposeAsync` disposed the event sources and channel slots (which disposes the captured `WotEventSource` binding) before it cleared its `WotProjectedEventBinding`s. During a shadow reload, the route registry resolves a Condition action's EventId across generations, so a call arriving in that window still resolved on the retiring binding and invoked through the disposed source: `WotCapturedConditionAction.InvokeAsync` → `Source.Validate()` → `BadSecurityChecksFailed`. ### Fix - The runtime clears its event bindings right after cancelling its lifetime, so no further action resolves on them, and waits until the action calls already resolved on them have ended before disposing the sources and slots. - `TryResolveOwnAction` / `ResolveAction` hand out a call handle that the Method handler holds until the invocation ends (`WotProjectedEventBinding.WhenActionCallsCompletedAsync`). All touched types are internal; no public API changes. ### Tests - New `WotRetainedConditionRefreshTests.RetiredBindingWaitsForAResolvedActionCallAndResolvesNoOtherAsync` (both identity modes): a retired binding keeps a resolved call until it is disposed and resolves no other call (`BadEventIdUnknown`). - `NativeRetiringGenerationUsesCapturedActionsUntilDrainAsync` locally (Windows, net10.0), 20 runs each: master 17 passed / 3 failed with `BadSecurityChecksFailed`; this branch 20 / 20 passed. - `WotRetainedConditionRefreshTests`, `WotPrivateEventCaptureTests`, `WotEventCapacityOwnershipTests` (123) and `WotEventModesLiveTests` (74) pass. --------- Co-authored-by: Marc Schier <marcschier@users.noreply.github.com> |
||
|
|
b72eb44a31 |
Redundancy: bound the Raft log with the engine's snapshots (#4836)
# Description **Draft: builds on #4835 and waits for a RaftCs release** containing marcschier/raft-cs#9, marcschier/raft-cs#10 and marcschier/raft-cs#11, anticipated as 1.1.4. It contains the commit of #4835; review only the second commit. `RaftSharedKeyValueStore` can capture and restore its state (#4794), but `IRaftConsensus` carries no log indices, so the store cannot bound the RaftCs log, which grows without limit. This change uses the engine's snapshots: - `IRaftSnapshotConsensus` (`CommittedItems` with log indices and snapshots, `CompactAsync`) and `RaftCommittedItem`. `DefaultRaftConsensus` implements it over `RaftNode.CommittedItems`, `CreateSnapshotAsync` and `CompactAsync`. - `RaftSharedKeyValueStore` gains a constructor overload with a compaction interval and an optional logger (the existing constructors are unchanged) and `AppliedIndex`. Over an `IRaftSnapshotConsensus` it applies the indexed stream. A snapshot on that stream (a lagging replica installing the leader's snapshot, or the first item after a restart) replaces the state and publishes the differences to watchers. Every compaction interval it snapshots its state into the engine, which discards the log behind it. A compaction that fails is logged and retried at the next interval. - The engine snapshot is a format marker followed by the snapshot encoding of #4794, so it carries the command position: the commands after it keep the positions they have on every replica, and `CreateSnapshotAsync`/`RestoreSnapshotAsync` keep working over it. No compaction runs while a store restored with `FromFirstCommand` still skips the replayed commands its snapshot holds. - A snapshot in another format stops the applier. Pending and later proposals and watches then fail with its error instead of waiting. - `AppliedIndex` is published once the state holds an item, before watchers and the proposer learn of it. - Registration: `RedundancyConsistencyOptions.RaftCompactionInterval` (server), and overloads of `AddRaftClientSharedStore` and `AddRedundantClientSharedStore` that take the interval (client). Zero, the default, never compacts. The store's logger comes from the registered `ITelemetryContext`, else `ILoggerFactory`. - `HighAvailability.md` describes the compaction and warns that a replica must not be rolled back to a release without engine snapshots once the cluster compacted its log. No public member is removed or changed. ## Related Issues - #4794 (store snapshots), #4835 (RaftCs 1.1.4) - marcschier/raft-cs#9, marcschier/raft-cs#10, marcschier/raft-cs#11 ## 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. (`RaftSharedKeyValueStoreCompactionTests`; registration tests in Server and Client.) - [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. (Redundancy test projects on .NET 10 against a RaftCs build of the three pull requests: Server 935, Client 135, Kubernetes 103, PubSub 45, Samples 17, all pass; the Server and Client test projects build for net48.) - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. (CI cannot restore RaftCs 1.1.4 until it is released.) - [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> |
||
|
|
2ba86e3a3b |
Server: queue the items of an event on several threads (#4859)
# Description
E2 of the event-heavy server load investigation (follows #4837 and
#4845).
### Problem
After #4845 (each event is evaluated once per distinct filter), the
event consumer of the Server node still ran on one core.
`MonitoredNode2` started the delivery of an event to every item and
awaited `Task.WhenAll`. Unless a permission check completed
asynchronously, every delivery ran synchronously on the consumer thread,
so one thread queued every item of every event. Each event also
allocated an `IEventMonitoredItem[]` snapshot
(`ConcurrentDictionary.Values`) and a `List<Task>`/`Task[]` sized to the
item count.
### Change (`MonitoredNode2`, multi-consumer nodes such as the Server
node)
- **Collect on the consumer.** Items are enumerated straight from
`EventMonitoredItems`. The audit filter and the #4845 group lookup run
there too. The results go into an `(item, key, evaluation)` struct
buffer that is reused for every batch and cleared afterwards, so the
per-event snapshot array and the per-item task list are gone.
- **Consumer keeps up (no notification waiting in the channel).** It
queues the event itself and does not wake pool threads. It splits only
what is left of an event after 1 ms (a very large event).
- **Consumer is behind (notifications waiting).** It also takes up to 3
more waiting events for the same items. It splits the batch at once into
ranges of at least 32 items, at most `min(ProcessorCount, 8)`, with one
range on the consumer thread. Each range queues the events of the batch
to its items one after the other. An event for different items (for
example an audit event that only reaches secure sessions) ends the batch
and is processed next; a fence or data change ends it too.
- **Ordering.** Every item still gets its events in order. A range waits
for its items' pending (asynchronous) permission checks before it queues
the next event of the batch, and the next batch is read only after all
ranges finished. A delivery that waits for a permission check still does
not hold up the other items.
The node-wide barrier per event now applies per batch: while one item
waits for its permission check, items in other ranges may already get
the next event of the same batch.
### Results
perf/ServerLoadHarness `events`: 50 sessions × 15 subscriptions,
publishing 100 ms, encrypt, `DOTNET_gcServer=1`. Paired A/B against
master
|
||
|
|
81c79f10a4 |
Merge master and reconcile xRegistry/WoT integration
Preserve the additive xRegistry 0.10.0 model and collection projection support while adopting upstream contextual deletes and NUnit fixes. Align new WoT parity pins with the retained xRegistry artifact and each model's core dependency; keep WoT's 0.7.0 minimum dependency. |
||
|
|
bd38870cf7 |
perf: pump the server child's stdout on a dedicated thread (#4855)
## Summary The 1-core client stall in the server load harness (`--client-cores 1`, write->notify p99 0.7-1.7 s) was caused by the harness, not the OPC UA client. `StartServerChild` started `PumpServerOutput` with `Task.Run`. The pump blocks in a synchronous `StreamReader.ReadLine` on the server's stdout for the whole run, so it held one thread-pool worker permanently. With `DOTNET_PROCESSOR_COUNT=1` the pool's minimum is one worker, so all other work items waited for the pool's starvation injection. That is why `DOTNET_ThreadPool_ForceMinWorkerThreads=8` and two processors both hid the stall. **Evidence:** 14 `dotnet-stack` samples of the constrained client during the measurement window. In every sample, the only blocked pool worker was in `ReadFile <- StreamReader.ReadLine <- Program.PumpServerOutput <- StartServerChild`, and the other workers were idle. No `Opc.Ua` frame was blocked. **Fix:** run the pump on a dedicated background thread. ## Numbers `run --scenario sub --sessions 25 --subs 20 --nodes 100 --pub 500 --write-interval 100 --duration 10 --warmup 6 --client-cores 1 --client-memory 512`, 5 alternating rounds (ms): | round | base write->notify p99 | fix write->notify p99 | base write p99 | fix write p99 | |---|---|---|---|---| | 1 | 659 | 61 | 619 | 12 | | 2 | 885 | 310 | 859 | 15 | | 3 | 306 | 303 | 52 | 23 | | 4 | 1717 | 455 | 408 | 81 | | 5 | 234 | 193 | 148 | 67 | The baseline stalled (> 600 ms) in 3 of 5 runs and the fix in 0 of 5 (max 492 ms). The remaining ~300 ms p99 comes from the 500 ms publishing interval. ## Test plan - [x] Harness Release build (net10.0) - [x] Alternating A/B runs above 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
50062cde1b |
PKCS#11 store: pair a certificate without a CKA_ID only with its own key (#4826)
# Description Follow-up to #4792. `Pkcs11CertificateStore.LoadPrivateKeyAsync` skipped a certificate whose CKA_ID cannot be read, but a certificate with an **empty** CKA_ID still reached `Pkcs11Token.FindPrivateKey`, which then selects a key by the configured object filters alone and, without any filter, returns the first private key of the token's key type. That key may belong to another certificate, so the store could return an identity whose signatures its certificate does not verify. A certificate without a CKA_ID is now paired with: - the key that carries its public key (the same lookup `AddAsync` uses to link a certificate it writes: RSA modulus and exponent, or EC point and curve), else - the key that the configured `ObjectLabel` / `ObjectId` selects, but only after that key signs a random challenge that the certificate's public key verifies (a shared label does not establish the pairing), else - no key. Before this change, a certificate without a CKA_ID on a token without object filters got the first key of its type. A certificate with a CKA_ID is handled as before. No public API changes. The package README describes the pairing. ## Related Issues Follow-up to #4792. ## 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. (Opc.Ua.Security.Pkcs11.Tests on .NET 10, 112 passed / 10 SoftHSM skipped; the library builds for net48, net8.0, net9.0, net10.0.) - [ ] 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> |
||
|
|
5f6faa60d7 |
Server: fewer allocations per single-value Read (isolation reservations, scope reuse, routing capture without restore) (#4856)
Removes per-request allocations on the server's single-value Read path
with small, internal changes. No wire-format change, no public API
change, no change of the execution model (the receive loop is
untouched). Exactly-once semantics of the isolation leases,
cancellation, timeouts, diagnostics and auditing are unchanged.
## Effect
Allocated bytes per single-value Read (perf harness, `native-read`, 10
clients x 100 in flight, `AsyncCustomNodeManager`, Server GC;
deterministic):
| | 1 core | 4 cores |
|---|---|---|
| master
|
||
|
|
65a67dd2f4 |
WoT runtime materialization, binding execution and native source emission (#4757)
# Description - WoT registry, prepared file store, immutable content, version leases, dependency-graph sharing (advanced graph rewrite), coordinator, materialization, views and atomic refresh. - Explicit caller-context propagation (`IWotRegistryService` / versioned registry / prepared host / publication capture) and the additive `IXRegistryProjectionContextualDelete` capability. - UA/HTTP/MQTT binding execution and compilation, plus the 1.02 FileType bridge and discovery. - WoT-specific native event, source-emission and readiness integration (`MasterNodeManager.SourceEmissions`, `EventManager` identity admission, external-value monitored sources, Fluent attach/detach) and event lease conservation. - All corresponding `WotCon.*` runtime tests, WotCon sample app code, WoT docs, CI feature budgets and Samples.Tests, plus the WotCon Design NodeSet/csv/csproj and ModelSourceParity tests. - Joint model metadata: XRegistry model 0.7.0 / 2026-09-12 (NamespaceVersion 63563, NamespacePublicationDate 63564), RequiredModel date 2025-01-08. - XML-doc work from the frozen WIP, applied to G12-owned declarations only. ## Related Issues - Fixes #4751. - Part of the WoT split tracked in #4511. ## 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. (Tests included, not yet run against current master.) - [ ] I have added all necessary documentation. - [ ] I have verified that my changes do not introduce (new) build or analyzer warnings. (Not run.) - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. (Not run.) - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. |
||
|
|
0edc3b55a6 |
Server: allocate data change notifications instead of pooling them (#4830)
Stacked on #4829 (publish timer sweep): this PR targets its branch, so the diff shows only this change. Retarget to master once #4829 merges. MonitoredItem.Publish rented every MonitoredItemNotification from the process-wide activator pool (BoundedObjectPool, bound 2 x monitored items, capped at 65536) and NotificationMessageLease returned it once the message was sent and acknowledged. Under load the pool costs more CPU than it saves: - A notification lives until its message is acknowledged, so a whole publishing cycle of notifications is in flight at once. With the publish sweep finishing within a few ms (fixed cadence + parallel remainder) the in-flight burst is far above the cap and most Gets miss anyway (MonitoredItemNotification 6 % -> 45 % of server allocation). - Raising the cap to 1M made it worse: 5.5 vs 4.4 us per notification, lock contention 16k vs 5k per window, delivery 93 vs 95.5 %. - Every hit touches an instance that went cold in the ~100-200 ms since it was returned, Reuse touches it once more, and the shared queue and counters bounce between cores. A fresh allocation comes from the thread's hot allocation context. The notifications are now allocated in MonitoredItem.Publish and left to the GC; RecyclePayload no longer reuses them, and the pool bound is only raised for EventFieldList. NotificationMessage, DataChangeNotification, EventNotificationList and EventFieldList pooling and the rented MonitoredItemNotification[] arrays are unchanged. Paired A/B on top of the publish sweep fix (perf/ServerLoadHarness, encrypted, alternating rounds, medians; runs repeated while processes outside the harness used more than 1.5 cores): | shape (sessions x subs x items) | GC | n | us/notif | server cores | delivered | GC pause / 15 s | p99 write->notify | |---------------------------------|--------|---|------------|--------------|--------------|-----------------|-------------------| | 50 x 15 x 600, pub 100, w 150 | server | 2 | 4.4 -> 3.1 | 11.9 -> 8.2 | 89 -> 89 % | 0.8 -> 1.5 s | 278 -> 325 ms | | 10 x 15 x 600, pub 100, w 150 | server | 3 | 5.1 -> 3.3 | 2.9 -> 2.0 | 100 -> 100 % | 15 -> 88 ms | 137 -> 141 ms | | 1000 x 1 x 1, pub 1000, w 500 | server | 7 | 158 -> 190 | 0.16 -> 0.19 | 50 -> 50 % | 15 -> 28 ms | 480 -> 490 ms | | 2 x 1 x 100, pub 100, w 150 | server | 3 | 59 -> 47 | 0.08 -> 0.06 | 100 -> 100 % | 7 -> 6 ms | 103 -> 114 ms | | 50 x 15 x 600, pub 100, w 150 | ws | 3*| 4.5 -> 3.5 | 10.5 -> 7.0 | 86 -> 96 % | 5.1 -> 8.6 s | 447 -> 373 ms | | 10 x 15 x 600, pub 100, w 150 | ws | 5*| 5.4 -> 3.6 | 3.3 -> 1.9 | 100 -> 100 % | 0.45 -> 1.1 s | 144 -> 155 ms | (*) workstation GC rows combine the A/B with the switch-build runs of the same comparison. Excluded: six runs (both builds) in which the 50-session shape collapsed with the server mostly idle (up to 16.7 s GC pause in the 15 s window, 0-4 % delivered), the GC suspension stall pattern seen before on this machine under external CPU saturation. The 1000-session cores differ by 0.03 with a same-build spread of 0.13-0.25 at ~1000 notifications/s. Trade-off: CPU per notification drops 25-40 % wherever notifications flow, but short-lived notifications now die young: gen0 GCs per 15 s 2 -> 24 (server GC, 10 sessions), gen1 35 -> 154 (workstation GC, 10 sessions), so GC pause time rises about 2x, and p99 latency rises with workstation GC at moderate load (+8 % at 10 sessions). When pooling could pay off again is tracked in #4819. 🤖 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> |
||
|
|
4ae2395f37 |
perf: client GC measurement, V2 client by default, container-like client constraints (#4844)
Extends the out-of-process load harness (`perf/`, not part of the product build) so it can measure client-side GC, run the V2 client by default, and emulate a container-sized client. No product code changes. ## Client GC measurement - New `client-gc:` result line from an in-process GC event listener (`GcListener.cs`, runtime GC keyword at informational level): GC mode, collections by condemned generation and trigger, pause and suspension p50/p99/max, bytes surviving gen0/gen1, pinned objects, gen2/LOH size, peak heap after a collection, committed bytes, heap limit, DATAS heap count and gen0 budget. `--gc-events 0` turns it off. - The latency recorder is now a fixed log-linear histogram (~1.6 % resolution). The old `ConcurrentBag<long>` kept ~25 MB of samples alive in the measured client's gen2 (client heap 29–31 MB vs 4 MB now). - **Fix:** GC, thread-pool and processor-count settings set for the harness (`DOTNET_GC*`, `COMPlus_GC*`, `DOTNET_ThreadPool_*`, `DOTNET_PROCESSOR_COUNT`) no longer leak into the server child. Before, `DOTNET_gcServer=1` switched the server too, although the README said it switched the client. `SERVER_`-prefixed variables still configure the server. - TraceAnalyzer `gc` decodes the condemn reasons and breaks each collection into phases (marking by root type with the bytes marked, plan/relocate/compact). ## V2 client by default - `--client` defaults to `managed` (`ManagedSession` + V2 subscription engine). The `events` and `counters` workloads gained V2 paths; `--pool-notifications 1` enables `WithPoolNotifications`. `client-scenarios.txt` lists V2 scenarios. `--client classic` and `shared` still work. ## Container-like client - `--client-cores N`: the harness runs itself again with `DOTNET_PROCESSOR_COUNT=N` on the last N processors and pins the server to the others. - `--client-memory MB`: the client's GC heap hard limit becomes 75 % of MB, the limit .NET derives from a container memory limit (native memory is not capped). ## What it found The report is published separately. Highlights: - Request path: a gen0 pause costs ~1 ms of fixed work (stack and card scanning, compaction), so pause time follows collection count. Read-1 allocates ~2 KB/request on the client, and its p99 is the GC pause (a 64 MB gen0 halves it). - V2 subscriptions and events never reach gen2 (classic `Subscription` with its message cache: 22 % of wall time paused for events). - Containers: each V2 monitored item costs 2.8–3.8 KB, mostly a per-item `ConcurrentDictionary` in `OptionsMonitor<T>` (one lock per processor) and a per-item `ConcurrentQueue`. 100k items do not fit in a 256 MB container. A 1-processor client stalls ~1 s unless the thread-pool minimum is raised. ## Testing - Harness builds in Release (`CustomTestTarget=net10.0`). I ran every changed scenario: read/write/browse/sub/events/counters on managed sessions, constrained runs with 1, 2 and 4 cores and 256 MB to 1 GB, and `TraceAnalyzer gc/alloc` on recorded traces. 🤖 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> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com> |
||
|
|
ad8c6ce59f |
TCP server: close the transport only after the terminal error message was sent (#4850)
### Why The weekly CTT regression run reported **Security Certificate Validation `029.js`** as a regression: the OpenSecureChannel with a CA certificate got `BadConnectionClosed` instead of `BadCertificateUseNotAllowed`. It passed in all 10 earlier full runs. When a channel faults (here a rejected client certificate during OpenSecureChannel), `ForceChannelFaultCore` sends the ERR message with `WriteMessageInline` and then closes the transport on the same stack. The send is not awaited. If it cannot complete synchronously, `Close()` cancels it or disposes the socket underneath it: - it may wait for the transport's send lock, for example while the queued writer is still finishing the Acknowledge; - or the socket send may still be pending. The client then sees the connection drop without the reason. OPC 10000-6 §7.1.5 requires the server to send the Error message and then close the connection. ### Change - `UaSCBinaryChannel.WriteMessageInline` keeps the terminal send task. - The new `UaSCBinaryChannel.CloseTransport` closes the transport at once when no terminal send is pending. Otherwise it closes once that send completes, after 2 s at the latest (`TimeProvider`). - `TcpListenerChannel.ChannelFaulted`, `ChannelClosed` and `UaSCBinaryChannel.Dispose` use it in place of `Transport.Close()`. - Nothing else changes: the channel still faults and is removed from the listener synchronously; only the socket close waits. ### Tests - New in `TcpServerChannelDeterministicTests`, both of which fail without the fix: - `FaultClosesTheTransportOnlyAfterTheErrorMessageWasSentAsync`: the ERR send is held, the transport must stay open, then close after the send. - `FaultClosesTheTransportWhenTheErrorMessageNeverCompletesAsync`: the ERR send never completes, so the transport closes after 2 s (FakeTimeProvider). - `Opc.Ua.Core.Tests` Stack.Transport/Stack.Tcp (net10.0): 858/858. - Opc.Ua.Core builds for net48. _Includes the master build fix of #4852 (cherry-picked so CI can run); it drops out on rebase once #4852 is merged._ 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6a156dfa2e |
Client: fewer allocations per request (shared timeout timer, pooled state machines, fewer buffer collections) (#4849)
# Description Reduces client allocations per request. A client GC investigation found that a gen0 pause on the request path costs about 1.1 ms however much survives. That makes the number of collections, and therefore the bytes allocated per request, what matters. At the default gen0 budget, each KB removed from a request removes about one collection in eight. Read-1 client bytes/request (`perf/ServerLoadHarness run --scenario read --nodes 1 --sessions 20`, builds run alternately, 3–4 rounds each, medians). Bytes/op repeated within ±5 B between rounds, while req/s varied 3x from other load on the machine, so throughput is not reported. | Commit | Change | classic | managed | |---|---|---|---| | master |
||
|
|
170d9460a0 |
plans/ctt-issues.md: C63 observed failure and CTT CI workaround (#4851)
Adds detail to the C63 entry, which only had a one-line description: - **What was seen.** In the weekly CTT run of 2026-10-18, A & C Refresh2 `Test_002.js`, `Test_003.js` and `cleanup.js` failed with `BadSubscriptionIdInvalid`, and the unit stopped after `Test_004.js`. - **How often.** With a 250 ms publish interval, the stall still hits about 1 in 5 A&C runs on the GitHub-hosted runner, sometimes inside a unit. - **The CTT CI workaround.** Each unit checks at its start that the publish thread is still alive. A part whose first attempt has regressions together with the CTT's own stall warning runs once more, and the second attempt is the result. No server change. Documentation only. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ac24123dde |
Await NUnit 5 Assert.ThrowsAsync/CatchAsync in the #4759 tests; fixes the master build (#4852)
master has failed to build since #4840 (NUnit 5) and #4759 (batch lifecycle and continuation tests) both landed: - #4759 calls `Assert.ThrowsAsync`/`Assert.CatchAsync` without `await`, which was the blocking NUnit 4 form. - Under NUnit 5 those methods return a `Task`. This PR adds the missing `await`, the same form as the rest of the migrated tests: - `NodeManagerLifecycleTests.Batch.cs`: six calls (`CS4014`, `NUnit2059`). - `ProductionDatasetClientTests.cs`: one call (`CS4014`). - `HistoryReadContinuationDispatchTests.FaultedRequestKeepsAssignedClientSuppliedContinuationPoint`: this test is synchronous, so it compiled. It discarded the returned `Task`, which means the assertion was never checked. It is now `async Task ...Async` and awaits it. No assertion changes. Tests (net10.0): - `UA.slnx` builds. - `NodeManagerLifecycleTests`: 317/317. - `ProductionDatasetClientTests`: 5/5. - `HistoryReadContinuationDispatchTests`: 6/6. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8fa6b7b60a | Merge master and adopt NUnit 5 async assertion updates | ||
|
|
2997cb8a2c |
SessionBindingTests: let readers confirm the live session before racing transfers (#4848)
## Problem `BindingLivenessRacingTransfersAndCloseNeverConfirmsReplacedBindingAsync` failed on the net48 CI runner (test-windows-net48 11/16, run 39250649914, on docs-only PR #4841) with `confirmed = 0`. The safety check (`errors`) was empty. The reader tasks were not scheduled until all 1000 transfers and the close had finished, so the liveness check never saw a live binding. ## Fix (test only) Before the first transfer, the test now waits until every reader has confirmed the live binding at least once (bounded: 30 s, then a clear `Assert.Fail`). The race still covers transfers and close, but the positive assertion no longer depends on thread-pool scheduling. The safety assertion is unchanged. ## Verification I ran the test with a temporary `[Repeat(100)]` in Release builds: - net10.0 and net48: 100 repeats each, no load - net10.0 and net48: 3 × 100 repeats each with 2 busy loops per core 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a6cc7c4bc9 |
Server: evaluate an event once per distinct event filter (#4845)
## Summary
The event consumer of a notifier (`MonitoredNode2`) evaluated the where
clause and read every select clause of a reported event once **per event
monitored item**, although items with an equal filter and equal session
locales get the same result. With 750 event items on the Server object
this single consumer thread capped event throughput (follow-up to #4837,
item E1 of the event load investigation).
Now each reported event is evaluated **once per distinct filter**:
- The consumer groups the items of an event by a key of their
`EventFilter` (value equality, generated structural
`IsEqual`/`GetHashCode`) and their session's `PreferredLocales`. The key
is cached per item and recreated when the item's filter or the session's
locales change (ModifyMonitoredItems, locale change). A key remembers
the equal key it was matched against, so after the first event a group
compares by reference.
- A per-event `SharedEventEvaluation` holds the where-clause result and
the field values. The first item that needs them evaluates them.
Evaluation is thread-safe, so a later partitioned fan-out (E2) can queue
a group's items on several threads without evaluating again.
- **Per item, unchanged:** duplicate check, permission and
audit-visibility checks, filtered-retain bookkeeping, queue size and
overflow events, and an own `EventFieldList` with its `ClientHandle` and
`Handle`. A trailing filtered-retain event reads its own fields through
`FilteredRetainTarget`. An item whose filter or locales changed after
the event was grouped evaluates the event itself.
- **No public API or wire change, no reflection.** The evaluation is
offered to the virtual `MonitoredItem.QueueEvent` for the duration of
the call (a `[ThreadStatic]` scope keyed by item and target). Derived
items that override `QueueEvent` therefore still see every event, and
they use the shared result only when they pass the same event on to the
base class. Detecting overrides by reflection was rejected because it is
not NativeAOT-safe.
- The shared `Variant[]` is never written after it is created.
`EventQueueHandler.Publish` only touches the `StatusResult` objects
inside it, which were already shared through the event node,
`TransformQueuedEvents` builds new lists, and pooled `EventFieldList`
reuse only reassigns its fields.
## Measurements
`perf/ServerLoadHarness`, `events` with 50 sessions × 15 subscriptions
(750 items on the Server object), publish interval 100 ms, encrypt,
server GC, `TC_QuickJitForLoops=0`/`OSR=0`. Paired A/B in 2 alternating
rounds; each run was repeated if processes outside the harness used more
than 1.5 cores. The base was #4837's tip (
|
||
|
|
d7c2ed6caf |
Client: smaller per-monitored-item footprint (listener array, lazy pending-change queue, shared logger) (#4847)
# Description Every V2 monitored item (`Opc.Ua.Client.Subscriptions`) cost about 3.8 KB of managed heap on a 24-core machine (2.8 KB on 1 core). Most of that was per-item infrastructure whose size grows with the processor count. With 100k items, a client limited to 256 MB (container-like) ran out of memory during subscription setup. - **`OptionsMonitor<T>`**: `OnChange` listeners are kept in a `Listener[]` that is replaced as a whole on each add or remove (`Interlocked.CompareExchange`), instead of a `ConcurrentDictionary`. That dictionary's default concurrency level allocated one lock object per processor for every monitored item (~1.4 KB on 24 cores). Notification, dispose and double-dispose behave as before; listeners are now called in registration order. - **`MonitoredItem`**: pending changes are kept in a `Queue<Change>` guarded by the existing `m_pendingChangesLock` instead of a per-item `ConcurrentQueue<Change>` (~810 B, 32-slot segment). The queue is created on enqueue and dropped once the last change completes, so idle items hold none. `HasPendingChanges`, `TryGetPendingChange` and `CompleteChange` now take the lock. `TryRequeue` used to enqueue without the lock and now enqueues under it. - **`DefaultSubscription`** shares one `ILogger` across its monitored items instead of creating one per item. ## Measurements These numbers come from the `ServerLoadHarness` client GC line (harness changes are in #4844): `run --scenario sub --sessions 25 --nodes 100 --pub 500 --write-interval 100 --duration 10 --warmup 6`. All figures are client gen2 MB. | Config | Items | Before | After | |---|---|---|---| | 24 cores, `--subs 20` | 50k | 190.0 / 188.7 | 62.9 / 64.0 | | `--client-cores 2 --client-memory 256`, `--subs 20` | 50k | 142.6 | 54.6 | | `--client-cores 2 --client-memory 256`, `--subs 40` | 100k | out of memory | 122.6 (limit 192) | That saves about 2.6 KB per item on 24 cores and about 1.8 KB on 2 cores. The whole client heap is now about 1.3–1.4 KB per item. ## 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. (`OptionsMonitorTests`: order, removal, duplicate callbacks, concurrent add/remove) - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. (`Opc.Ua.Client` Release build on all targets, including net48: 0 warnings) - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. (Only `Opc.Ua.Client.Tests` on net10.0 was run: 3231 passed, 0 failed, 2 skipped.) - [ ] 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> |
||
|
|
9bfc780e14 |
Update NuGet dependencies and migrate tests to NUnit 5 (#4840)
# Description Update 55 NuGet dependencies to the latest stable releases available on 2026-10-19, preserving documented compatibility pins and framework-specific version bands. Existing OpenUSD prereleases remain unchanged because no newer releases are available. Migrate existing tests to NUnit 5: await asynchronous assertions, propagate async through helpers, handle nullable predicates, and replace obsolete `Order` attributes with sequencing dependencies. Sequencing dependencies use `AllowFailure = true`; filtering a dependent test also runs prerequisites. Adjust fixture-level parallelism where NUnit dependency scheduling requires it. Keep Roslyn API/runtime pins intact; select newer Immutable/Metadata runtime packages only for test and profiling hosts requiring them. Update the approved SDK pin to 10.0.402 across local, CI, and container builds. Document NUnit 5 conventions. |