Skip to content

Add configurable server resource isolation - #4541

Merged
marcschier merged 24 commits into
masterfrom
feat/server-resource-isolation
Sep 27, 2026
Merged

marcschier merged 24 commits into
masterfrom
feat/server-resource-isolation

Conversation

@marcschier

@marcschier marcschier commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Add configurable server resource isolation on top of the merged #4522 foundation.

  • Coordinate connection, handshake, retained-message, session-establishment, queue, execution, request-cost and parked-request limits through one server-scoped provider.
  • Keep configured startup, continuity/reconnect, control and trusted-owner reservations inside existing totals. Shared capacity remains usable up to explicit owner ceilings.
  • Use authoritative Session membership for reassembly continuity capacity without granting unrelated resource or rate privileges. Preserve explicit owner mappings and bounded tracking.
  • Cache immutable classification evidence while retaining current Session and classifier checks.
  • Support weighted request ordering or FIFO independently of resource accounting. Installed providers enforce decoded-request limits in both modes.
  • Preserve fail-fast reassembly, valid Abort handling, fixed deadlines, active-work protection, reverse-connect handoff and bounded OpenAPI processing.
  • Support custom-handler parking through IRequestParkingPolicy, WithRequestParking(...), and RequestLifetime.ParkSink. Parking releases execution capacity only, keeping retained-data and parked charges until completion. Built-in Publish and the global opt-out remain unchanged.
  • Provide direct, fluent and DI configuration, public incoming-request parking support, effective startup limit logs and user-facing documentation.

Balanced remains the default. SharedOnly without an explicit provider retains shared behavior for intentionally incompatible/unlimited configurations. Reservations are finite and are not one full-message allowance per Session.

Related Issues

Builds on merged #4522. Fixes #4554.

Review and CI corrections

All three previously missed findings in review 5326734718 are addressed in 9e25e4ecc:

  1. A custom FIFO provider receives RequestQueue, RequestQueueBytes, RequestExecution and ParkedRequest accounting. A blocked head cannot be overtaken.
  2. IResourceIsolationReassemblyProvider replaces the sessionless threshold independently of UseFairScheduling. The aggregate byte ceiling remains enforced.
  3. Runtime ReservedReassemblyBytes includes all configured protected stage floors, including ControlReserved.

The review's two already-resolved changes remain intact.

The CI collection race was fixed at its source in e1a994a4b: ArrayOf.ToArrayOf treats a separately read collection count as an initial capacity hint and grows/trims for the actual enumeration. Deterministic tests cover growth, shrinkage and stale-zero counts.

The subsequent recovery-test race was fixed in 05753ce77: fake time advances only after a gated keepalive response has been processed and reset its timer, not merely after request arrival. Recovery-order and timeout assertions remain intact, with additional response-state assertions. Instrumentation reproduced the original failure twice; both fixed variants passed 400 executions per framework.

CodeQL alert 388 was verified and dismissed as a false positive. Startup logs contain aggregate numeric capacities, not credentials or caller identities.

Validation

Scope Result
Latest automated checks at 05753ce7775a58801b4ba9d8f6d7781b709d0b89 167 successful, one package-publication check skipped, no failures
Remaining external prerequisite license/cla pending: Contributor License Agreement is not signed yet
FIFO/weighted queues, custom parking and server-channel selection 175 passed on each net10.0/net48
Isolation, configuration and parking-hosting selection 259 passed net10.0; 257 passed net48
ArrayOf conversion tests 73 passed on each net10.0/net48
Recovery-audit and keepalive fixtures 40 passed on each net10.0/net48
NativeAOT smoke from preceding updates Published and executed successfully

No tests, assertions, coverage thresholds, or required checks were weakened. The remaining CLA prerequisite must be completed through CLA Assistant.

This is not CTT certification or an unconditional network-DDoS, whole-process-memory or fleet-wide guarantee.

Checklist

  • I have signed the CLA and read the CONTRIBUTING doc.
  • I have added tests that prove my fix is effective or that my feature works and increased code coverage.
  • I have added all necessary documentation.
  • I have verified that my changes do not introduce (new) build or analyzer warnings.
  • I ran all tests locally using the UA.slnx solution against at least .net framework and .net 10, and all passed.
  • I fixed all failing and flaky tests in the CI pipelines and all CodeQL warnings.
  • I have addressed all PR feedback received.

Base automatically changed from fix/incomplete-message-buffering to master September 24, 2026 18:13
…e-isolation

# Conflicts:
#	docs/README.md
#	docs/RateLimiting.md
Wire server-wide stage and owner admission, protected byte/slot/rate reservations, validated classification, fair decoded-request scheduling, transport startup and lease lifetimes, hosting configuration, bounded diagnostics, and regression coverage. Preserve fail-fast reassembly and SharedOnly compatibility.
@marcschier marcschier changed the title Add configurable server resource isolation (staged) Add configurable server resource isolation Sep 24, 2026
@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Code coverage

✅ Coverage gate passed.

Check Result Threshold
✅ Project line rate 87.70% (275002/313569 lines) >= 70.00%
✅ Project branch rate 78.10% >= 60.00%
✅ Patch coverage 87.64% (2191/2500 changed lines) >= 75.00% (> 100 changed lines)
ℹ️ Baseline delta (advisory) +14.10 pp 73.60% recorded
Uncovered changed lines
  • src/Opc.Ua.Core/Stack/Tcp/UaScConnectionAdmission.cs: 190, 196, 199, 221, 223, 224, 228, 267, 277, 282, 291, 300, 301, 311, 319, 339, 365, 596, 598, 599
  • src/Opc.Ua.Core/Stack/Server/ServerBase.RequestQueue.cs: 77, 163, 178, 179, 203, 205, 206, 275, 277, 278, 444, 446, 447, 448, 471, 473, 474
  • src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs: 800, 802, 803, 913, 915, 916, 920
  • src/Opc.Ua.Core/Stack/Server/EndpointBase.cs: 98
  • src/Opc.Ua.Server/ResourceIsolation/DefaultServerResourceIsolationProvider.cs: 68, 148, 152, 153, 246, 250, 254, 259, 312, 316, 340, 352, 388, 391, 393, 394, 395, 397, 398, 592
  • src/Opc.Ua.Core/Stack/Server/IServerResourceIsolationProvider.cs: 172, 176, 180, 184, 191, 222
  • src/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs: 884, 886, 887, 1392, 1410
  • src/Opc.Ua.Server/Server/StandardServer.cs: 3714
  • src/Opc.Ua.Server/ResourceIsolation/ServerResourceIsolationOptions.cs: 195, 196, 198, 200, 203, 204, 206, 208, 212, 239, 243, 252, 259, 276, 286, 299, 320, 321, 352, 365, 372, 385, 421, 434, 435, 437, 447, 465
  • src/Opc.Ua.Server/Hosting/OpcUaServerBuilderExtensions.ResourceIsolation.cs: 73, 77, 93, 95, 97, 98, 117, 164, 182
  • src/Opc.Ua.Core/Stack/Server/FairRequestQueue.cs: 64, 124, 135, 136, 239, 330, 681
  • src/Opc.Ua.Server/ResourceIsolation/ServerResourceIsolationPlan.cs: 166, 170
  • src/Opc.Ua.Types/BuiltIn/ArrayOf.cs: 938
  • src/Opc.Ua.Server/RateLimiting/ResourceIsolationConnectionRateLimiter.cs: 188, 189
  • src/Opc.Ua.Bindings.Https/Https/HttpsTransportListener.cs: 650, 652, 657, 710, 711, 712, 713, 714, 715, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 780, 782, 785, 786, 787, 866, 868, 869, 872, 873, 920, 922, 923, 925, 926, 927, 953, 1002, 1004, 1005, 1006, 1008, 1009, 1036, 1037, 1038, 1039, 1040, 1041, 1077, 1078, 1080, 1082, 1083, 1084, 1085, 1086, 1088, 1115, 1116, 1122, 1140, 1142, 1143, 1160, 1161, 1162, 1165, 1185, 1186, 1187, 1188, 1191, 1192, 1280, 1284, 1294, 1295, 1366, 1368, 1394, 1397, 1428, 1430, 1431, 1432, 1433, 1434, 1435, 1436, 1437, 1442, 1516, 1517, 1518, 1519, 1520, 1521, 1540, 1541, 1542, 1543, 1550, 1588, 1590, 1591, 1621, 1622, 1623, 1624, 1625, 1626, 1627, 1628, 1629, 1630, 1631, 1632, 1633, 1634, 1635, 1636, 1637, 1638, 1639, 1640, 1641, 1642, 1643, 1645, 1646, 1649, 1650, 1651, 1652, 1657, 1658, 1659, 1660, 1661, 1662, 1665, 1668, 1669, 1670, 1671, 1672, 1673, 1739, 1740, 1824, 1918, 2096, 2097, 2303, 2304, 2411, 2412, 2423, 2424, 2493, 2495, 2496, 2578, 2580, 2581, 2582, 2600, 2602, 2603, 2632, 2634, 2665, 2667, 2668, 2669, 2799, 2800, 2801, 2807, 2813, 2901, 2903, 2904

Coverage is above the recorded baseline - consider ratcheting coverage-thresholds.json.

Thresholds live in coverage-thresholds.json. Whole report before exclusions: line 86.80%, branch 77.25%.

@codecov

codecov Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.48345% with 241 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.86%. Comparing base (46fa7d3) to head (05753ce).

Files with missing lines Patch % Lines
...esourceIsolation/ServerResourceIsolationOptions.cs 76.65% 27 Missing and 26 partials ⚠️
...solation/DefaultServerResourceIsolationProvider.cs 88.26% 20 Missing and 22 partials ⚠️
...c/Opc.Ua.Core/Stack/Tcp/UaScConnectionAdmission.cs 85.02% 20 Missing and 11 partials ⚠️
...pc.Ua.Core/Stack/Server/ServerBase.RequestQueue.cs 81.53% 17 Missing and 7 partials ⚠️
src/Opc.Ua.Core/Stack/Server/FairRequestQueue.cs 92.60% 7 Missing and 14 partials ⚠️
.../OpcUaServerBuilderExtensions.ResourceIsolation.cs 75.34% 9 Missing and 9 partials ⚠️
...e/Stack/Server/IServerResourceIsolationProvider.cs 29.41% 6 Missing and 6 partials ⚠️
src/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs 88.54% 5 Missing and 6 partials ⚠️
src/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs 89.47% 7 Missing and 3 partials ⚠️
...r/ResourceIsolation/ServerResourceIsolationPlan.cs 73.33% 2 Missing and 2 partials ⚠️
... and 9 more
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #4541      +/-   ##
==========================================
+ Coverage   81.82%   81.86%   +0.04%     
==========================================
  Files        2074     2084      +10     
  Lines      311884   313569    +1685     
  Branches    54158    54560     +402     
==========================================
+ Hits       255200   256706    +1506     
- Misses      38342    38413      +71     
- Partials    18342    18450     +108     
Flag Coverage Δ
actions 81.86% <86.48%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
....Ua.Bindings.Https/Https/HttpsTransportListener.cs 72.65% <ø> (+3.08%) ⬆️
....Bindings.Https/Tcp/KestrelTcpConnectionHandler.cs 91.42% <100.00%> (+0.51%) ⬆️
...ack/Server/EndpointBase.EndpointIncomingRequest.cs 85.29% <100.00%> (+0.44%) ⬆️
.../Opc.Ua.Core/Stack/Server/IRequestParkingPolicy.cs 100.00% <100.00%> (ø)
src/Opc.Ua.Core/Stack/Server/RequestParkSink.cs 100.00% <ø> (ø)
src/Opc.Ua.Core/Stack/Tcp/ChannelQuotas.cs 100.00% <100.00%> (ø)
....Core/Stack/Transport/TransportListenerSettings.cs 100.00% <100.00%> (ø)
....Ua.Server/Hosting/OpcUaServerBuilderExtensions.cs 74.16% <100.00%> (+0.09%) ⬆️
...r/RateLimiting/DefaultServerRateLimiterProvider.cs 91.83% <100.00%> (+3.20%) ⬆️
...c.Ua.Server/RateLimiting/ServerRateLimitOptions.cs 100.00% <ø> (ø)
... and 21 more

... and 37 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread docs/DependencyInjection.md Outdated
Comment thread docs/ResourceIsolation.md Outdated
Comment thread docs/ResourceIsolation.md Outdated
Comment thread docs/ResourceIsolation.md Outdated
Comment thread docs/ResourceIsolation.md Outdated
Comment thread src/Opc.Ua.Bindings.Https/Https/HttpsTransportListener.cs Outdated
Comment thread src/Opc.Ua.Bindings.Https/Https/HttpsTransportListener.cs Outdated
Comment thread src/Opc.Ua.Core/Stack/Server/ServerBase.RequestQueue.cs Outdated
Comment thread src/Opc.Ua.Core/Stack/Server/ServerBase.RequestQueue.cs Outdated
Comment thread src/Opc.Ua.Server/ResourceIsolation/DefaultServerResourceIsolationProvider.cs Outdated
Bound execution wakeups by stage, avoid repeated owner/hash work during revalidation, distinguish live classification changes from missing sessions, correct fixture channel capacity, and clarify the profiles and enforcement contract without weakening configured guarantees.
@marcschier
marcschier marked this pull request as ready for review September 26, 2026 05:26
Copilot AI lite review requested due to automatic review settings September 26, 2026 05:26
Comment thread docs/DependencyInjection.md Outdated
Comment thread docs/MigrationGuide.md Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Two moderate review findings remain unresolved.

Review effort: Lite
Findings: None

What changed in this PR

Adds configurable, server-scoped resource isolation across transports, queues, rate limiting, hosting, and documentation.

Changes:

  • Adds isolation profiles, capacity planning, classification, and protected reservations.
  • Integrates admission, reassembly, scheduling, shutdown, and WebSocket handling.
  • Adds configuration, diagnostics, documentation, and extensive regression coverage.
File Summary
tests/​Opc.Ua.Sessions.Tests/​WssTransportIntegrationTests.cs WebSocket transport isolation coverage.
tests/​Opc.Ua.Sessions.Tests/​WssReverseConnectIntegrationTests.cs Reverse-connect isolation coverage.
tests/​Opc.Ua.Sessions.Tests/​WssOpenApiIntegrationTests.cs OpenAPI WebSocket integration coverage.
tests/​Opc.Ua.Sessions.Tests/​WssJsonTransportIntegrationTests.cs JSON WebSocket transport coverage.
tests/​Opc.Ua.Sessions.Tests/​SharedKestrelHostIntegrationTests.cs Shared Kestrel host scenarios.
tests/​Opc.Ua.Sessions.Tests/​KestrelTcpIntegrationTests.cs Kestrel TCP integration scenarios.
tests/​Opc.Ua.Sessions.Tests/​HttpsWebApiIntegrationTests.cs HTTPS Web API integration coverage.
tests/​Opc.Ua.Sessions.Tests/​HttpsJsonTransportIntegrationTests.cs HTTPS JSON transport coverage.
tests/​Opc.Ua.Server.Tests/​ResourceIsolation/​ServerResourceIsolationOptionsTests.cs Resource-isolation option validation.
tests/​Opc.Ua.Server.Tests/​Hosting/​ConfigurationFileHostingTests.cs Configuration-file hosting coverage.
tests/​Opc.Ua.Server.Tests/​ChunkReassemblyBudgetServerTests.cs Chunk reassembly budget scenarios.
tests/​Opc.Ua.Server.TestFramework/​ServerFixture.cs Shared server test fixture updates.
tests/​Opc.Ua.Core.Tests/​Stack/​Transport/​WssAdmissionTests.cs WebSocket admission tests.
tests/​Opc.Ua.Core.Tests/​Stack/​Transport/​UaScConnectionAdmissionTests.cs Secure-channel admission tests.
tests/​Opc.Ua.Core.Tests/​Stack/​Transport/​TcpTransportListenerDeterministicTests.cs Deterministic TCP listener tests.
tests/​Opc.Ua.Core.Tests/​Stack/​Transport/​TcpServerChannelBufferTests.cs TCP channel buffer tests.
tests/​Opc.Ua.Core.Tests/​Stack/​Transport/​TcpAdmissionLifetimeRegressionTests.cs Admission lifetime regressions.
tests/​Opc.Ua.Core.Tests/​Stack/​Transport/​KestrelAdmissionTests.cs Kestrel admission tests.
tests/​Opc.Ua.Core.Tests/​Stack/​Server/​RequestQueueTests.cs Request queue and fairness tests.
tests/​Opc.Ua.Core.Tests/​Stack/​Server/​EndpointIncomingRequestTests.cs Endpoint request admission tests.
tests/​Opc.Ua.Client.TestFramework/​ClientTestFramework.cs Client test framework support.
tests/​Opc.Ua.Aot.Tests/​RateLimitingAotTests.cs NativeAOT rate-limiting coverage.
src/​Opc.Ua.Server/​Server/​StandardServer.cs Server provider lifecycle integration.
src/​Opc.Ua.Server/​ResourceIsolation/​ServerResourceIsolationPlan.cs Immutable capacity planning.
src/​Opc.Ua.Server/​ResourceIsolation/​ServerResourceIsolationMode.cs Isolation profile definitions.
src/​Opc.Ua.Server/​ResourceIsolation/​ResourceIsolationStageOptions.cs Stage and reservation options.
src/​Opc.Ua.Server/​ResourceIsolation/​IResourceIsolationClassifier.cs Classification contract.
src/​Opc.Ua.Server/​RateLimiting/​ServerRateLimitOptions.cs Rate-limit configuration.
src/​Opc.Ua.Server/​RateLimiting/​ResourceIsolationConnectionRateLimiter.cs Protected connection rate limiting.
src/​Opc.Ua.Server/​RateLimiting/​IServerRateLimiterProvider.cs Rate-limiter provider contract.
src/​Opc.Ua.Server/​RateLimiting/​DefaultServerRateLimiterProvider.cs Default provider implementation.
src/​Opc.Ua.Server/​Hosting/​OpcUaServerOptions.cs Server hosting options.
src/​Opc.Ua.Server/​Hosting/​OpcUaServerHostedService.cs Hosted-service integration.
src/​Opc.Ua.Server/​Hosting/​OpcUaServerBuilderExtensions.ResourceIsolation.cs Resource-isolation builder setup.
src/​Opc.Ua.Server/​Hosting/​OpcUaServerBuilderExtensions.cs General hosting extensions.
src/​Opc.Ua.Server/​EventIds.cs Server diagnostic event identifiers.
src/​Opc.Ua.Core/​Stack/​Transport/​TransportListenerSettings.cs Transport listener settings.
src/​Opc.Ua.Core/​Stack/​Transport/​IConnectionRateLimiter.cs Connection rate-limiter contract.
src/​Opc.Ua.Core/​Stack/​Tcp/​UaSCBinaryChannel.cs Secure-channel admission and reassembly.
src/​Opc.Ua.Core/​Stack/​Tcp/​TcpTransportListener.cs TCP listener admission.
src/​Opc.Ua.Core/​Stack/​Tcp/​TcpServerChannel.cs TCP server channel resource handling.
src/​Opc.Ua.Core/​Stack/​Tcp/​TcpListenerChannel.cs TCP listener channel lifecycle.
src/​Opc.Ua.Core/​Stack/​Tcp/​ChannelQuotas.cs Channel quota definitions.
src/​Opc.Ua.Core/​Stack/​Server/​ServerBase.RequestQueue.cs Queue admission and scheduling.
src/​Opc.Ua.Core/​Stack/​Server/​ServerBase.cs Core server resource integration.
src/​Opc.Ua.Core/​Stack/​Server/​IServerResourceIsolationProvider.cs Resource-isolation provider contract.
src/​Opc.Ua.Core/​Stack/​Server/​EndpointBase.cs Endpoint resource handling.
src/​Opc.Ua.Core/​EventIds.cs Core diagnostic event identifiers.
src/​Opc.Ua.Bindings.Https/​UaScConnectionAdmission.cs HTTPS secure-channel admission.
src/​Opc.Ua.Bindings.Https/​Tcp/​KestrelTcpTransportListener.cs Kestrel transport integration.
src/​Opc.Ua.Bindings.Https/​Tcp/​KestrelTcpConnectionHandler.cs Kestrel connection handling.
docs/​README.md Documentation index updates.
docs/​RateLimiting.md Rate-limiting guidance.
docs/​MigrationGuide.md Migration guidance.
docs/​DependencyInjection.md Dependency-injection configuration guidance.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docs/MigrationGuide.md Outdated
Comment thread docs/MigrationGuide.md Outdated
Comment thread docs/RateLimiting.md
Comment thread docs/RateLimiting.md Outdated
Comment thread docs/RateLimiting.md Outdated
Comment thread docs/ResourceIsolation.md Outdated
Comment thread docs/ResourceIsolation.md Outdated
Comment thread docs/ResourceIsolation.md Outdated
Comment thread docs/ResourceIsolation.md Outdated
Comment thread docs/ResourceIsolation.md Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The default Balanced validation leaves an existing explicit load-test configuration unable to start.

Review effort: Balanced
Findings: 1 High severity · 1 Low severity

Open (2)

Comment thread src/Opc.Ua.Server/ResourceIsolation/ServerResourceIsolationPlan.cs Outdated
Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com>
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ marcschier
❌ Copilot
You have signed the CLA already but the status is still pending? Let us recheck it.

Comment thread docs/ResourceIsolation.md Outdated
Comment thread docs/ResourceIsolation.md Outdated
Comment thread docs/ResourceIsolation.md Outdated
Comment thread docs/ResourceIsolation.md Outdated
Comment thread docs/ResourceIsolation.md

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Reassembly capability handling and decoded-request accounting have unresolved contract violations.

Review effort: Balanced
Findings: None

Resolved since last review (2)
Previously missed (3)

In code that hasn't changed since last review

Medium severity Disabling fair scheduling bypasses decoded-request admission

src/​Opc.Ua.Core/​Stack/​Server/​ServerBase.RequestQueue.cs:98

UseFairScheduling is documented as selecting weighted ordering, but this branch also disables every decoded-request admission call when it is false. An injected custom provider that wants FIFO therefore never sees RequestQueue, RequestQueueBytes, RequestExecution, or ParkedRequest, despite being installed as the server resource provider. Preserve FIFO independently from whether these stages are accounted, or expose a separate explicit capability for bypassing decoded-request isolation.

Medium severity Custom reassembly providers incorrectly require fair scheduling

src/​Opc.Ua.Core/​Stack/​Tcp/​UaSCBinaryChannel.cs:638

The optional reassembly capability is incorrectly coupled to fair request scheduling here. A custom provider can implement IResourceIsolationReassemblyProvider while returning UseFairScheduling == false; it classifies and charges the chunk, but this expression still applies the legacy sessionless threshold, so its protected pre-session owner cannot use the provider's reserved reassembly capacity. The capability itself should be sufficient to replace that threshold, as documented for custom providers.

Medium severity Reserved reassembly bytes omit the control-reserved floor

src/​Opc.Ua.Server/​ResourceIsolation/​ServerResourceIsolationPlan.cs:127

ReservedReassemblyBytes omits the reassembly stage's ControlReserved floor. CreateRuntimePlan permits an explicit nonzero control floor for this stage, while UnreservedReassemblyBytes uses SharedCapacity, which does subtract it; the public totals then no longer add up to MaxReassemblyBytes. Derive the reserved value from the runtime stage when available (or include its control floor explicitly).

Comment thread src/Opc.Ua.Server/ResourceIsolation/DefaultServerResourceIsolationProvider.cs Dismissed
Use an available count only as an initial capacity hint, growing and trimming for the actual enumeration. Prevents concurrent queue growth from overrunning the array and shrinkage from emitting trailing defaults. Add deterministic growth, shrinkage and empty-count regressions.
Address all three previously missed findings in review 5326734718: account custom FIFO requests, honor optional reassembly classification without weighted scheduling, and include control reserves in runtime reassembly totals.
Wait for each gated keepalive response to reset its timer before advancing the fake clock. Preserve recovery ordering and progress assertions while removing the proven request-arrival versus response-completion race.
@marcschier
marcschier enabled auto-merge (squash) September 27, 2026 14:51
@marcschier
marcschier merged commit f989202 into master Sep 27, 2026
168 of 169 checks passed
@marcschier
marcschier deleted the feat/server-resource-isolation branch September 29, 2026 09:32
romanett pushed a commit that referenced this pull request Oct 6, 2026
…ssionlessInvocation (#4659)

Marc answered the five design questions (2026-10-05). This update
implements the answers; see "Design decisions" at the end.

# Description

OPC 10000-4 §6.3 lets a client invoke a subset of the services without
CreateSession/ActivateSession. The request then carries either an Access
Token in `RequestHeader.authenticationToken` or no token at all. The
server today only offers the low-level
`ISessionManager.ValidateSessionLessRequest` event. Without a handler,
every request whose token names no session is answered with
`Bad_SessionIdInvalid`, including a request without a token. That is the
code a client of an expired session expects; for a session-less request,
§6.3.1 has a server that does not support the invocation answer
`Bad_ServiceUnsupported`.

This PR adds built-in session-less invocation behind an opt-in and keeps
the existing event as the way to take over completely.

## Changes

### `AddSessionlessInvocation()`
(`src/Opc.Ua.Server/Hosting/OpcUaServerBuilderExtensions.cs`)

```csharp
services.AddOpcUa()
    .AddServer(options => { /* … */ })
    .AddSessionlessInvocation(o => o.AllowAnonymous = false)  // the default
    .AddJwtIssuer(/* … */);   // validates the Access Tokens
```

- Registers `SessionlessInvocationOptions`.
`DependencyInjectionStandardServer` applies them to the `SessionManager`
in use, including one created by a registered session manager factory,
unless that manager already has options.
- Without dependency injection: `SessionManager.SessionlessInvocation =
new SessionlessInvocationOptions()`.
- Without either, nothing changes apart from the
`Bad_ServiceUnsupported` answer below.

### `SessionlessInvocationOptions` (new, `src/Opc.Ua.Server/Session/`)

- `AcceptAccessTokens` (default `true`): an Access Token in
`RequestHeader.authenticationToken`, carried as a String NodeId, is
handed to the server's identity registry (`IServerIdentityRegistry`, the
one ActivateSession uses) as an issued JWT token (OPC 10000-6
§7.6.5.2.3). The resulting identity runs the request. §6.3.1 requires an
encrypted channel for the token, so it is only accepted over
SignAndEncrypt or HTTPS.
- `AllowAnonymous` (default `false`): a request without a token runs as
an anonymous user. Without it, such a request is rejected with
`Bad_IdentityTokenInvalid`.
- `MaxConcurrentRequests` (default 64) and
`MaxConcurrentRequestsPerChannel` (default 16): the budget for
session-less requests, see below. Zero does not limit; a negative value
is rejected.

### Budget for session-less requests (`SessionlessRequestBudget`, new,
internal)

#4541 bounds channels and incomplete messages. A session-less request
has no Session to be accounted for, but it occupies a worker and keeps
its channel active. It gets a budget of its own:

- **Two limits**, checked and taken together under one
`System.Threading.Lock`: concurrent requests on the server, and on one
channel. A request refused by the channel limit takes no server-wide
place. The channel is the SecureChannel on opc.tcp. The HTTPS bindings
use one SecureChannel id per listener, so their requests are grouped by
peer address (IPv4-mapped IPv6 addresses count as IPv4).
- **Answer**: `Bad_ServerTooBusy` ("The Server does not have the
resources to process the request at this time."), the code #4541's
request queue uses when it has no room. `Bad_TooManyOperations` means
too many operations in one request, so it does not fit.
- **Lifetime**: the place is taken before the identity is checked, which
also bounds the validations that run at the same time. The lease is
attached to the `OperationContext` and returned when the context is
disposed, which every service does with `using`, however the request
ends (completed, failed, cancelled). A rejected token, a handler error
or exception, and a cancelled validation return it at once. A lease
releases once, however often it is disposed. Channel entries are removed
with their last request.
- **Scope**: it applies to every session-less request on a manager that
has `SessionlessInvocation` set, including requests a
`ValidateSessionLessRequest` handler decides. A handler without
`AddSessionlessInvocation` runs without a limit, as before. Requests
with a Session token and services outside the §6.3.1 set are not
counted.
- The limits are read for each request, so a change of the options
applies to the next request.

### `SessionManager`

- **Service sets:** a request without a session is limited to the
service sets §6.3.1 allows: View (without
RegisterNodes/UnregisterNodes), Attribute, Method, NodeManagement and
Query. Every other service keeps answering `Bad_SessionIdInvalid`.
`IsSessionlessService(RequestType)` is public.
- **Request without a token:** with session-less invocation not enabled
and no `ValidateSessionLessRequest` handler, it is answered with
`Bad_ServiceUnsupported` (§6.3.1). A token that names no session stays
`Bad_SessionIdInvalid`.
- **Precedence:** a registered `ValidateSessionLessRequest` handler
still decides first, unchanged apart from the service-set limit.
`ValidateSessionlessRequestAsync` is `protected virtual` for servers
that need their own rules.
- **Roles:** the identity of a session-less request goes through the
role resolution ActivateSession applies to the effective identity of a
Session (the `ResolveRoles` part of `AddMandatoryRoles`: the
`MustChangePassword` restriction, `IRoleManager.ResolveGrantedRoles`, so
mapped Access Token claims and the built-in AuthenticatedUser role).
This includes an identity a `ValidateSessionLessRequest` handler sets.
There is no client application certificate without a Session, so no
TrustedApplication role and no application-based mapping.
- **Cancellation:** cancelling an Access Token validation propagates as
`OperationCanceledException` so the endpoint maps it to the status of
the request lifetime (previously it became `Bad_UnexpectedError`). The
budget place is still returned.
- **Log:** a rejected Access Token is logged with a dedicated
`[LoggerMessage]` (`ServerEventIds.SessionManager + 18`, the next free
id after #4632).

### Documentation

- `docs/SessionlessInvocation.md` (new, linked from `docs/README.md`):
which services, enabling it, caller identity, the answers without the
feature, the request limits, the handler, calling the server over
opc.tcp and the REST binding.
- `docs/ResourceIsolation.md`: a short section "Requests without a
Session" pointing to the budget.
- `docs/DependencyInjection.md`: `AddSessionlessInvocation` in the
identity extension tables.

## Tests

- `Opc.Ua.Server.Tests/SessionlessInvocationTests` (mocked server
internals):
- `SessionlessRequestIsUnsupportedUnlessEnabled`,
`AnUnknownSessionTokenStaysBadSessionIdInvalid`,
`ServicesOutsideTheSessionlessSetStillNeedASession` (7 services),
`TheServiceSetLimitAppliesBeforeAValidateSessionLessRequestHandler`,
`AValidateSessionLessRequestHandlerTakesPrecedenceAsync`;
-
`SessionlessRequestWithoutIdentityIsRejectedUnlessAnonymousIsAllowedAsync`,
`SessionlessAccessTokenIsValidatedByTheIdentityRegistryAsync`,
`AnAccessTokenNoAuthenticatorHandlesIsRejected`,
`SessionlessAccessTokenRequiresAnEncryptedChannel` (None, Sign),
`SessionlessAccessTokenIsAcceptedOverSignAndEncryptAsync`,
`AccessTokensCanBeDisabled`,
`IsSessionlessServiceFollowsTheServiceSetsOfPartFour` (21 cases),
`ValidateSessionlessRequestRejectsMissingArguments`;
- wiring: `AddSessionlessInvocationRegistersTheConfiguredOptions`,
`AddSessionlessInvocationRejectsANullBuilder`,
`TheHostedServerAppliesTheOptionsToItsSessionManager`,
`TheHostedServerAppliesTheOptionsToAFactoryCreatedSessionManager`,
`TheHostedServerKeepsOptionsTheSessionManagerAlreadyHas`.
- End to end against the reference server,
`Opc.Ua.Sessions.Tests/SessionlessServiceInvocationTests` (opc.tcp,
`SessionClient` on a channel without CreateSession):
- `ReadWithAnAccessTokenNeedsNoSessionAsync` (SignAndEncrypt),
`AnAccessTokenOnAnUnencryptedChannelIsRejectedAsync`,
`ARequestWithoutAnIdentityIsRejectedAsync`,
`SubscriptionsStillNeedASessionAsync`;
- one sample per service set: `ViewServicesRunWithoutASessionAsync`
(Browse, TranslateBrowsePathsToNodeIds),
`AttributeServicesRunWithoutASessionAsync` (Write, HistoryRead),
`MethodServicesRunWithoutASessionAsync` (Call),
`NodeManagementServicesRunWithoutASessionAsync` (AddNodes). They check
that the service runs and reports its operation result, not a missing
session or an internal error, since #4632 hardened only
Browse/BrowseNext against a missing session. Query is not sampled:
`StandardServer` does not implement QueryFirst/QueryNext.
- REST binding,
`HttpsWebApiIntegrationTests.SessionlessReadOverRestRunsWithAnAccessTokenAsync`
(Compact and Verbose): `POST /read` with the Access Token as String
NodeId (`"AuthenticationToken": "s=<token>"`), and the same request
without a token.
- **Budget**, `Opc.Ua.Server.Tests/SessionlessRequestBudgetTests` (22
tests):
- the budget itself: server limit and release, channel limit that takes
nothing from the server, server limit checked first, zero does not
limit, a lease releases once, HTTPS grouped by peer address (IPv4-mapped
form too), 200 concurrent callers against the limits (exactly the limit
granted, none above the channel limit, parallel release leaves zero and
no tracked channel), 16 callers acquiring and releasing in a loop
(high-water mark never above the limit);
- through `SessionManager.ValidateRequestAsync`: `Bad_ServerTooBusy`
over the server and the channel limit, disposing a context twice
releases once, places returned after a rejected Access Token, an
unencrypted channel, a missing identity, a cancelled validation (while
it waits, the place is taken), a handler error and a handler exception;
the budget also limits requests a handler decides, and a handler-only
server has none; requests outside the service set and requests a
disabled server refuses take no place; changed limits apply to the next
request and new options start a new budget;
  - options: finite defaults, negative values rejected.
- **Budget end to end**,
`Opc.Ua.Sessions.Tests/SessionlessServiceInvocationTests.SessionlessRequestsRunInABudgetOfTheirOwnAsync`
(opc.tcp, requests held in the Access Token validation): the third
request over a limit of two gets `Bad_ServerTooBusy`; after release,
five sequential reads and five reads of an unknown node (operation
result bad) all pass; a channel limit of one refuses the second request
on that channel while another channel's request is admitted.
`ARequestWithoutATokenIsUnsupportedWhileTheFeatureIsOffAsync`:
`Bad_ServiceUnsupported` for a request without token on a server without
the feature, `Bad_SessionIdInvalid` for a token that names no Session.
- **Fail without the change**: the places-returned tests fail when the
budget does not release on failure (4 of 22 fail with the release in
`SessionManager` removed; the other budget tests use API this change
adds). `SessionlessRequestIsUnsupportedUnlessEnabled` and
`TheServiceSetLimitAppliesBeforeAValidateSessionLessRequestHandler` are
unchanged from the first version.
- **Review follow-up (`e9a35980b`)**:
`SessionlessAccessTokenReceivesTheRolesMappedFromItsClaimsAsync`,
`SessionlessAccessTokenWithoutTheMappedClaimIsNotGrantedTheRoleAsync`,
`AnAnonymousSessionlessRequestIsGrantedTheAnonymousRoleOnlyAsync`,
`TheIdentityOfAValidateSessionLessRequestHandlerReceivesTheMappedRolesAsync`,
`AValidateSessionLessRequestHandlerThatSetsNoIdentityKeepsNoIdentityAsync`;
end to end
`ARoleMappedNodeIsReadWithTheRolesMappedFromTheAccessTokenAsync` (the
ServerDiagnosticsSummary, readable for SecurityAdmin only, is denied for
a token whose claim is not mapped, readable once the rule maps it,
denied for another token) and
`ACancelledAccessTokenValidationAnswersTheTimeoutAndReturnsItsPlaceAsync`.
The anonymous and no-identity tests guard unchanged behavior. With the
`SessionManager.cs` change removed the three claim-mapping tests, the
end-to-end role test (`Bad_UserAccessDenied`), the end-to-end
cancellation test (`Bad_UnexpectedError` instead of `Bad_Timeout`) and
`ACancelledValidationReturnsItsPlaceAsync` fail. Lines of this commit
were not re-measured for patch coverage; the figure below is for
`496f84fce`.
- **`ClientTest`**: the three assertions that accepted
`Bad_ServiceUnsupported` too are back to master (`Bad_SessionIdInvalid`
only), so the PR no longer touches that file. Since #4646 a closed
`Session` refuses a request without authenticationToken itself; the
server code is not reached. The three tests pass with the single code
(12 cases on net10.0).
- Results on macOS: `Opc.Ua.Server.Tests` full run on net10.0 8076
passed / 9 skipped / 1 failed
(`ACancelledValidationReturnsItsPlaceAsync`, which asserted the old
`Bad_UnexpectedError` mapping; updated to the propagated cancellation
and passing); the session-less, SessionManager and role filters 368
passed / 0 failed on net10.0 and on net8.0, and the session-less
invocation and budget tests 73 passed on net10.0 after the last test was
added. `Opc.Ua.Sessions.Tests` with the
HTTPS/WebApi/WSS/OpenApi/Sessionless/ClientTest filter: 400 passed / 241
skipped / 0 failed on net10.0 and on net8.0 (the earlier host-name
failures were an environment issue of the test machine and are gone).
- Builds: `Opc.Ua.Server`, `Opc.Ua.Server.Tests` and
`Opc.Ua.Sessions.Tests` for net48, net8.0, net9.0 and net10.0 and
`Opc.Ua.Server` in Release net10.0: 0 errors, no new warnings (the
CA2213 in `SamplingGroup.cs` and the CA1861/CA2025 in other test files
are on master too).
- `dotnet format` (whitespace, style, analyzers `--severity info`, one
`--include` per changed file): no finding on a changed line. What
remains is on master lines (`SessionManager.cs` 552, 1374, 2205 and
`DependencyInjectionStandardServer.cs` 228) and the line-ending finding
of a macOS checkout.
- Coverage (`.azurepipelines/check-coverage.ps1` against master, only
the session-less tests of `Opc.Ua.Server.Tests` on net10.0): patch
105/116 changed lines (90.5 %). The 11 uncovered lines are Session-bound
lines of `ValidateRequestAsync` and the lockout logger that moved with
the change; the rest of the suite covers them. The project floor is not
meaningful for this subset.
- Fork CI and CodeQL: previous head `0fba9f21d` (master incl. #4674
merged): [CI run
37307824769](https://github.com/biancode/UA-.NETStandard/actions/runs/37307824769)
green (one net48 timing test, `KeepAliveHandlerV2Async`, failed in the
first attempt and passed on the job rerun; the PR does not touch
subscriptions), [CodeQL run
37307829138](https://github.com/biancode/UA-.NETStandard/actions/runs/37307829138)
green. Current head `e9a35980b`: [CI run
37364178979](https://github.com/biancode/UA-.NETStandard/actions/runs/37364178979),
[CodeQL run
37364182697](https://github.com/biancode/UA-.NETStandard/actions/runs/37364182697)
(results pending).

## Design decisions

Marc's answers (2026-10-05) and what the PR does:

1. **Built in** when `AddSessionlessInvocation` was used. Done.
2. **Service-set limit for all paths**: it applies before a
`ValidateSessionLessRequest` handler too. Done, unchanged.
3. **`Bad_ServiceUnsupported`** for a request without a token on a
server without the feature. Done, unchanged. A token that names no
Session stays `Bad_SessionIdInvalid`.
4. **Identity model**: an Access Token in the RequestHeader, validated
as an issued JWT token through the identity registry, anonymous only by
opt-in. Unchanged. The REST authentication is #4674, now in master.
5. **Separate budget** for session-less requests: added, see "Budget for
session-less requests". Open: no limit per identity yet (identities are
claims-defined, so the keys would be unbounded); say if you want one.

## Related Issues

- Fixes #4658
- Relates to #4657 (the REST binding's session-less service set), #4646
(a closed `Session` no longer sends requests without token), #4541
(resource isolation) and #4674 (REST authentication)

## 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. (Affected
suites on net10.0 and net8.0 locally on macOS; .NET Framework is covered
by the fork CI's Windows net48 legs.)
- [ ] 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>
marcschier pushed a commit that referenced this pull request Oct 13, 2026
…ed (#4749)

The `[Explicit]` Sessions `LoadTest` fixture has not started since #4541
(also on the 2.0.0 tag): it set `MaxChannelCount = MaxSessionCount =
10100`, and resource isolation refuses to start unless the channel cap
covers the sessions plus the bootstrap and reconnect floors plus one.
Every LoadTest case failed in `OneTimeSetUp`. Running the fixture again,
and profiling it and the `perf/ServerLoadHarness`, found several
bottlenecks that shipped in 2.0.

## Changes

**Test**
- `MaxChannelCount = MaxSessionCount + 3`, the same margin as the
framework defaults (100 / 103).
- Notifications are counted with per-node `Interlocked` counters. The
shared `ConcurrentDictionary<NodeId, int>`, updated by 750
subscriptions, took 13 % of the process CPU in bucket-lock contention.
- After the writer stops, and after the last event, the tests wait until
the received count stops growing instead of a fixed delay, and log the
drain time.

**Server / client**
- **Detached monitored item recovery** (#4094): every node a NodeManager
added (`AddPredefinedNodeAsync`, recursively for children) scanned every
monitored item of every subscription for detached items.
`CreateSubscription` adds ~30 diagnostics nodes, so with 450k monitored
items subscription creation was quadratic: 69 % of CPU, and
`CreateSubscription` timed out. `MonitoredItem` now keeps an exact
process-wide count of live detached or deleted items, and
`MasterNodeManager` skips recovery while it is zero. All flag changes go
through one helper under the item lock; a disposed item does not count,
because it cannot be attached again.
- **Client channel gate** (#4232):
`UaSCUaBinaryClientChannel.SendRequestAsync` entered the gate with a
blocking `Gate.Enter()` inside an async method, and `SemaphoreSlim.Wait`
spun for 19 % of CPU with many requests in flight on one channel. It now
awaits `Gate.EnterAsync(ct)`, like the server side.
- **ResourceManager**
- Status code and symbolic id translation looked up their maps under the
manager lock and translated while holding it. Sessions that request
diagnostics do this for every notification: 83 % of lock contention. The
maps are copy-on-write and read without the lock.
- Translation tables were `SortedDictionary<string, string>` with the
default, culture-sensitive comparer: up to 1 µs per Browse display name,
22 % of Browse server CPU. They now use an ordinal `Dictionary`.
- **Namespace default permissions**: every permission check read
`DefaultAccessRestrictions` / `DefaultRolePermissions` /
`DefaultUserRolePermissions` of the node's namespace through two
server-wide locks and deep-copied the role permission array, per node.
That was 99 % of lock contention for Read/Write of 100 values. The
values are now read once per namespace per service request and
remembered on the `OperationContext`. Contexts that are not a service
request (monitored item and session contexts, which can live long)
remember nothing, so a changed default applies to them at once.

## Results

`perf/ServerLoadHarness`, paired A/B against `master` (3 alternating
rounds, medians, 20 sessions, 24 cores). The harness numbers predate the
recovery, client gate and status code translation commits, which those
scenarios do not exercise:

| Scenario | Throughput | Server CPU / op | Server alloc / op | Lock
contention |
|---|---|---|---|---|
| Read, 100 values | +19 % | −28 % | −18 % | −96 % |
| Read, 100 values, SignAndEncrypt | +27 % | −26 % | −18 % | −98 % |
| Write, 100 values | noise ¹ | −19 % | −17 % | −88 % |
| Browse (Server object) | +3 % | −16 % | −10 % | −2 % ² |
| Read, 1 value | +12 % | −4 % | ±0 | ±0 |

¹ Throughput varied run to run on the shared machine; per-op CPU and
allocation are the reliable signal.
² Browse contention is the Server node's browse lock in
`NodeState.CreateBrowser` (all sessions browse the same node); not
changed here.

`LoadTest` (opc.tcp, Release, in-process client and server):

| Case | master | this PR |
|---|---|---|
| `ServerReadLoadTestAsync` | setup failure | passes (50M value reads, 0
errors) |
| `ServerManySessionsLoadTestAsync(500)` | setup failure | passes |
| `ServerSubscribeLoadTest_Classic` | setup failure; on 2026-09-26
(before #4541): `CreateSubscription` `BadRequestTimeout` | 76–85 %
received, **fails** (needs 99 %) |
| `ServerSubscribeLoadTest_ManagedSessionV2` | setup failure;
2026-09-26: 1,885 notifications received | 35–45 M notifications, 77–85
% received, **fails** |
| `ServerEventSubscribeLoadTestAsync` | setup failure; 2026-09-26: 34.6
% | passes: 100 % delivered, drained ~40 s after the last event |

The subscribe cases now lose notifications rather than deliver them
late: delivery stops ~2 s after the last write. Every monitored item has
queue size 1 and the writer is not paced by delivery, so whatever
exceeds the publish rate (~585k notifications/s here, SignAndEncrypt
with the client in the same process) is overwritten.

The event test lost nothing either: it waited a fixed 2 s after the last
event and counted the backlog as lost. It now waits until delivery
stops. Its throughput is bound by garbage collection: the event consumer
of the Server node spends ~90 % of its wall time suspended for
workstation GCs of the shared test process. With `DOTNET_gcServer=1` it
generates 702 instead of ~210 events/s and drains in 7 s instead of ~40
s, and the subscribe cases reach 87 % (Classic) and 93 %
(ManagedSessionV2) at 78–106 M notifications.

## Testing
- `Opc.Ua.Server.Tests` 8468 passed / 5 skipped, `Opc.Ua.Client.Tests`
2788 passed, `Opc.Ua.Sessions.Tests` 851 passed / 379 skipped,
`Opc.Ua.Core.Tests` Tcp/Channel 797 passed (net10.0).
- New `OperationContextDefaultPermissionsCacheTests`.
- `Opc.Ua.Server` builds for net48; `dotnet format whitespace` clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose cooperative request parking for custom service handlers

6 participants