Files
UA-.NETStandard/tests/TestThreadPoolSetup.cs
70e9873328 Server: leave the ThreadPool alone and add request queue metrics (#4917)
Q4 and Q5 of the request-queue comparison with ASP.NET Core/Kestrel (Q1
is #4914). Two commits, one per item.

## Q4: stop changing the process ThreadPool

`RequestQueue` called `ThreadPool.SetMinThreads` / `SetMaxThreads` for
the whole process every time a queue was created, including a throwaway
10-worker queue that the `ServerBase` constructor built and
`InitializeRequestQueue` disposed again at start.

- The calls are removed. Nothing in the repo (src, tests, samples)
depends on them. Queue workers are asynchronous and do not hold a pool
thread while they wait. `SetMaxThreads` never had an effect, because the
default maximum (32767) is always above the configured counts.
- The constructor no longer builds the throwaway queue. Until the server
starts, `ServerBase.ScheduleIncomingRequest` completes a request with
`BadServerHalted`. That request used to reach `ProcessRequestAsync` of a
server that had not started yet.

### Release note (behaviour change)
> The server no longer raises the process-wide thread pool minimum to
`MinRequestThreadCount` (default 10). On machines with fewer logical
processors than `MinRequestThreadCount`, the thread pool now starts with
its own default minimum (the processor count). Hosts whose node managers
block threads synchronously, and that relied on the old minimum, can set
it themselves with `ThreadPool.SetMinThreads` or the
`System.Threading.ThreadPool.MinThreads` runtime setting.

### Test hosts
CI showed that test hosts depended on the old minimum. On the small
linux runners, tests that block pool threads in parallel left a queued
request waiting more than 5 s for a thread (`RequestQueueTests`,
`RequestLifetimeTests`). `tests/TestThreadPoolSetup.cs` is an
assembly-wide NUnit `SetUpFixture` that `tests/Directory.Build.targets`
compiles into every NUnit test project. It restores the minimum of at
least 10 worker and I/O threads, for the test hosts only.

## Q5: request queue metrics

The SharedOnly path only logged `RequestQueueFull` (with a `// TODO:
make a metric`). The queue now publishes instruments in both the shared
and the isolation mode. They follow the style of
`DefaultServerResourceIsolationProvider` and are modelled on Kestrel's
`kestrel.queued_requests`, `http.server.active_requests` and
`aspnetcore.rate_limiting.request.time_in_queue`. The meter comes from
the server's `ITelemetryContext` (`CreateMeter()`, named after
`Opc.Ua.Core`) and is disposed once shutdown has drained the queue.

| Instrument | Type |
|---|---|
| `opcua.server.request_queue.queued` | observable gauge |
| `opcua.server.request_queue.executing` | observable gauge |
| `opcua.server.request_queue.parked` | observable gauge |
| `opcua.server.request_queue.time_in_queue` | histogram, seconds |
| `opcua.server.request_queue.rejections` | counter, tag `status` |

Cost on the request path:
- The gauges are observed from counters that already exist, so they add
no work per request.
- The time in queue uses a `Stopwatch` timestamp taken at enqueue, and
only while the histogram has a listener, on both paths. The fair queue
stamps an `Entry` only after its admission succeeds. Once the Q2
enqueue/received timestamp lands, it can replace this one.
- Rejections are tagged with the interned symbolic id of the status code
through the single-tag `Add` overload, so no tag array is allocated.
- Rejections include queue full, halted, cancelled while queued, failed
revalidation and every fair-admission rejection, because the fair
queue's completion callback now goes through `RejectRequest`. Handler
failures are not rejections.

`docs/ServerScalability.md` lists the instruments.

## Tests
- `RequestQueueTests`: request before start is halted, ThreadPool limits
unchanged, shared queue metrics (queued, executing, parked, rejections
by status, time in queue), parked gauge, fair queue metrics.
- `Opc.Ua.Core.Tests` `Stack.Server`: 1946 passed, 0 failed (net10.0).
- `Opc.Ua.Server.Tests`: 9653 passed. The 8 failures are symlink tests
(`FileLinksRejectReadWriteAndCopy` and similar) that fail on this
machine with "insufficient privileges" and are unrelated to this change.
- net48 build of `Opc.Ua.Core.Tests` compiles.

Conflicts to expect: the Q2/Q3 session (time in queue against
TimeoutHint, retry-after) touches the dequeue and reject spots of
`ServerBase.RequestQueue.cs`.

🤖 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>
2026-10-28 03:43:11 +01:00

60 lines
2.5 KiB
C#

/* ========================================================================
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.Threading;
using NUnit.Framework;
#pragma warning disable CA1050 // A SetUpFixture outside any namespace applies to the whole test assembly.
/// <summary>
/// Raises the thread pool minimum of the test host for every test assembly.
/// </summary>
/// <remarks>
/// The server no longer changes the process-wide thread pool. Up to 2.0.1 every
/// <c>ServerBase</c> raised the minimum to at least its <c>MinRequestThreadCount</c>
/// (10 by default), and test hosts on small CI runners depend on that minimum: tests
/// running in parallel block pool threads, and a queued request could otherwise wait
/// seconds for the thread pool to inject a thread.
/// </remarks>
[SetUpFixture]
public sealed class TestThreadPoolSetup
{
private const int kMinThreads = 10;
[OneTimeSetUp]
public void RaiseThreadPoolMinimum()
{
ThreadPool.GetMinThreads(out int workerThreads, out int completionPortThreads);
ThreadPool.SetMinThreads(
Math.Max(workerThreads, kMinThreads),
Math.Max(completionPortThreads, kMinThreads));
}
}