mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-29 15:11:53 +08:00
Server: size isolation execution capacity from the worker limit
- CreateRuntimePlan sizes RequestExecution from MaxRequestThreadCount instead of at least 100, so ordinary requests cannot occupy every worker past the reserved floors; too few workers fail at startup. - Count parked requests explicitly for the parked gauge. - Forward --isolation to the native benchmark server child. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f80e7e0634
commit
fc77ad7bea
@@ -593,7 +593,9 @@ bootstrap, reconnect, control and trusted floors, so the shared capacity equals
|
||||
the configured value. For example, with `MaxQueuedRequestCount = 10000`, ten
|
||||
clients can each keep 1,000 requests outstanding. Balanced then sizes the
|
||||
RequestQueue stage at 10,003 entries. RequestExecution keeps its floors inside
|
||||
the worker limit, so an idle worker remains available for protected requests.
|
||||
the worker limit, so an idle worker remains available for protected requests:
|
||||
its total is `MaxRequestThreadCount`, and startup fails when that leaves no
|
||||
worker for ordinary requests (Balanced needs at least 4).
|
||||
An explicit `Stages[].Capacity` is still an absolute total that includes the
|
||||
floors, and it cannot exceed the default total.
|
||||
|
||||
|
||||
@@ -413,7 +413,9 @@ namespace Opc.Ua.Perf.ServerLoadHarness
|
||||
FormattableString.Invariant(
|
||||
$"--counters {(counters ? Program.Arg("nodes", "1") : "0")} --sampling {Program.Arg("sampling", "10")} ") +
|
||||
FormattableString.Invariant(
|
||||
$"--counter-lock {Program.Arg("counter-lock", "1")} --async-node-manager {Program.Arg("async-node-manager", "1")}"),
|
||||
$"--counter-lock {Program.Arg("counter-lock", "1")} --async-node-manager {Program.Arg("async-node-manager", "1")} ") +
|
||||
FormattableString.Invariant(
|
||||
$"--isolation {Program.Arg("isolation", "default")}"),
|
||||
// the benchmark's .NET workers run with Server GC; SERVER_DOTNET_gcServer=0 overrides it
|
||||
new Dictionary<string, string>(StringComparer.Ordinal) { ["DOTNET_gcServer"] = "1" });
|
||||
string ready = handle.m_lines.Take();
|
||||
|
||||
@@ -124,7 +124,7 @@ namespace Opc.Ua
|
||||
description: "Requests executing and holding a permit or worker.");
|
||||
m_meter.CreateObservableGauge(
|
||||
"opcua.server.request_queue.parked",
|
||||
() => Math.Max(0, Volatile.Read(ref m_processingCount) - Volatile.Read(ref m_activeThreadCount)),
|
||||
() => Volatile.Read(ref m_parkedCount),
|
||||
description: "Requests parked (for example held Publish requests) after releasing their permit or worker.");
|
||||
|
||||
m_workers = [];
|
||||
@@ -330,6 +330,7 @@ namespace Opc.Ua
|
||||
if (!processing.IsCompleted)
|
||||
{
|
||||
await Task.WhenAny(processing, parkSink.ParkedTask).ConfigureAwait(false);
|
||||
CountWhileParked(processing);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -553,6 +554,7 @@ namespace Opc.Ua
|
||||
if (!processing.IsCompleted)
|
||||
{
|
||||
await Task.WhenAny(processing, parkSink.ParkedTask).ConfigureAwait(false);
|
||||
CountWhileParked(processing);
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -566,6 +568,26 @@ namespace Opc.Ua
|
||||
// required here because ProcessRequestSafeAsync observes all faults.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Counts a request that released its permit or worker at the park point as
|
||||
/// parked until its handler finishes.
|
||||
/// </summary>
|
||||
private void CountWhileParked(Task processing)
|
||||
{
|
||||
if (processing.IsCompleted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Interlocked.Increment(ref m_parkedCount);
|
||||
// ProcessRequestSafeAsync never throws, so the continuation only counts.
|
||||
_ = processing.ContinueWith(
|
||||
static (_, state) => Interlocked.Decrement(ref ((RequestQueue)state!).m_parkedCount),
|
||||
this,
|
||||
CancellationToken.None,
|
||||
TaskContinuationOptions.ExecuteSynchronously,
|
||||
TaskScheduler.Default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the server request handler and traps any unexpected error the same
|
||||
/// way the legacy inline path did, guaranteeing the request is always faulted
|
||||
@@ -793,6 +815,12 @@ namespace Opc.Ua
|
||||
/// </summary>
|
||||
private int m_processingCount;
|
||||
|
||||
/// <summary>
|
||||
/// Number of handlers that parked and released their permit or worker,
|
||||
/// changed with <see cref="Interlocked"/>.
|
||||
/// </summary>
|
||||
private int m_parkedCount;
|
||||
|
||||
/// <summary>
|
||||
/// Number of requests executing rather than parked or waiting: the permits in
|
||||
/// use, changed under <see cref="m_workerGate"/>, or the busy fair-queue
|
||||
|
||||
@@ -281,7 +281,9 @@ namespace Opc.Ua.Server
|
||||
? rateLimits.MaxConcurrentSessionEstablishment
|
||||
: ServerRateLimitOptions.DefaultMaxConcurrentSessionEstablishment;
|
||||
int queuedCapacity = Math.Max(100, server.MaxQueuedRequestCount);
|
||||
int executionCapacity = Math.Max(100, Math.Max(server.MinRequestThreadCount, server.MaxRequestThreadCount));
|
||||
// the request queue starts at most this many workers, so the execution stage cannot
|
||||
// promise protected classes more than the workers can run
|
||||
int executionCapacity = Math.Max(1, Math.Max(server.MinRequestThreadCount, server.MaxRequestThreadCount));
|
||||
long retainedRequestSlots = 2L * queuedCapacity + executionCapacity;
|
||||
if (retainedRequestSlots > long.MaxValue / quotas.MaxMessageSize)
|
||||
{
|
||||
@@ -382,6 +384,13 @@ namespace Opc.Ua.Server
|
||||
SubtractFloor(ref remaining, owner.Floors[ii]);
|
||||
trustedTotal += owner.Floors[ii];
|
||||
}
|
||||
if (remaining < unit && ii == (int)ResourceIsolationStage.RequestExecution && item?.Capacity == null)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"MaxRequestThreadCount ({executionCapacity}) must exceed the requests reserved for " +
|
||||
"bootstrap, reconnect, control and trusted owners. Increase MaxRequestThreadCount or " +
|
||||
"reduce the RequestExecution reservations.");
|
||||
}
|
||||
if (remaining < unit || hardLimit < Math.Max(Math.Max(bootstrapFloor, reconnectFloor), controlFloor))
|
||||
{
|
||||
throw new ArgumentException($"The {(ResourceIsolationStage)ii} pools cannot hold an operation.");
|
||||
|
||||
@@ -60,6 +60,39 @@ namespace Opc.Ua.Server.Tests
|
||||
.And.Message.Contains("two minutes"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The execution stage holds no more requests than the request queue has workers,
|
||||
/// so reserved classes cannot be crowded out of the workers by ordinary requests.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RuntimePlanExecutionCapacityIsMaxRequestThreadCount()
|
||||
{
|
||||
ApplicationConfiguration configuration = Configuration();
|
||||
configuration.ServerConfiguration.MinRequestThreadCount = 1;
|
||||
configuration.ServerConfiguration.MaxRequestThreadCount = 4;
|
||||
|
||||
ResourceIsolationStagePlan execution = Options()
|
||||
.CreateRuntimePlan(configuration, new ServerRateLimitOptions())
|
||||
.GetStage(ResourceIsolationStage.RequestExecution);
|
||||
|
||||
Assert.That(execution.Capacity, Is.EqualTo(4));
|
||||
Assert.That(execution.SharedCapacity, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Too few workers for the reserved requests are named at startup.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RuntimePlanRejectsMaxRequestThreadCountBelowReservations()
|
||||
{
|
||||
ApplicationConfiguration configuration = Configuration();
|
||||
configuration.ServerConfiguration.MinRequestThreadCount = 1;
|
||||
configuration.ServerConfiguration.MaxRequestThreadCount = 3;
|
||||
|
||||
Assert.That(() => Options().CreateRuntimePlan(configuration, new ServerRateLimitOptions()),
|
||||
Throws.ArgumentException.With.Message.Contains("MaxRequestThreadCount (3)"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task StartupRejectsInvalidTimeoutWithoutInstallingOrDisposingProvidersAsync(
|
||||
[Values] ServerResourceIsolationMode mode,
|
||||
|
||||
Reference in New Issue
Block a user