Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OpcUaApplicationHostQuotaTests.cs
T
Joseph Doherty 1578efd4a9 fix(opcua): write-path robustness, injected clock, configurable quotas (B4)
Review 01 L3/H5/H4/P6/P4.

- L3/H5 OnEquipmentTagWrite's dispatch is factored into DispatchDeviceWrite:
  a gateway that throws synchronously is refused inline with
  BadDeviceFailure (it used to escape the handler under the SDK Lock), and
  a faulted/cancelled dispatch task has its exception observed and logged
  and carries the real reason into the revert/audit event instead of
  surfacing as UnobservedTaskException at finalization.
- H4 every timestamp the manager stamps (condition quality, timed-shelve
  expiry, method-transition events, carry-over, revert, revalidation)
  reads the injected Clock; RemainingShelveMs takes the clock reading.
- P6 OpcUa:Quotas:* (OpcUaQuotaOptions on OpcUaApplicationHostOptions):
  Min/MaxRequestThreadCount, MaxQueuedRequestCount with the former
  hard-coded 5/100/200 as defaults, and the TransportQuotas members as
  nullable overrides over the SDK defaults.
- P4 the SDK line exposes IHistoryReadAsyncNodeManager but CustomNodeManager2
  offers no async virtual to override (adopting it means re-implementing
  the base dispatcher), so the lever is the admission cap:
  MaxConcurrentHistoryReadBatches defaults to clamp(2*ProcessorCount, 4, 16).
  The Host still overrides it from ServerHistorianOptions (default 16) —
  follow-up F3.

Tests: NodeManagerWriteDispatchTests (sync throw refused; faulted task
reverts; success leaves the value; clock-stamped revert),
OpcUaApplicationHostQuotaTests.

Refs #732

Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 10:47:32 -04:00

75 lines
2.8 KiB
C#

using Opc.Ua;
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
/// <summary>Review 01 P6 — the SDK request and transport quotas are configurable (<c>OpcUa:Quotas:*</c>) and
/// default to exactly what used to be hard-coded.</summary>
public sealed class OpcUaApplicationHostQuotaTests
{
[Fact]
public void Default_request_quotas_are_the_former_hard_coded_values()
{
var config = new ServerConfiguration();
OpcUaApplicationHost.ApplyRequestQuotas(config, new OpcUaQuotaOptions());
config.MinRequestThreadCount.ShouldBe(5);
config.MaxRequestThreadCount.ShouldBe(100);
config.MaxQueuedRequestCount.ShouldBe(200);
}
[Fact]
public void Configured_request_quotas_are_applied()
{
var config = new ServerConfiguration();
OpcUaApplicationHost.ApplyRequestQuotas(config, new OpcUaQuotaOptions
{
MinRequestThreadCount = 8, MaxRequestThreadCount = 250, MaxQueuedRequestCount = 1000,
});
config.MinRequestThreadCount.ShouldBe(8);
config.MaxRequestThreadCount.ShouldBe(250);
config.MaxQueuedRequestCount.ShouldBe(1000);
}
[Fact]
public void Unset_transport_quotas_keep_the_sdk_defaults()
{
var sdkDefault = new TransportQuotas();
var built = OpcUaApplicationHost.BuildTransportQuotas(new OpcUaQuotaOptions());
built.OperationTimeout.ShouldBe(sdkDefault.OperationTimeout);
built.MaxStringLength.ShouldBe(sdkDefault.MaxStringLength);
built.MaxByteStringLength.ShouldBe(sdkDefault.MaxByteStringLength);
built.MaxArrayLength.ShouldBe(sdkDefault.MaxArrayLength);
built.MaxMessageSize.ShouldBe(sdkDefault.MaxMessageSize);
built.MaxBufferSize.ShouldBe(sdkDefault.MaxBufferSize);
built.ChannelLifetime.ShouldBe(sdkDefault.ChannelLifetime);
built.SecurityTokenLifetime.ShouldBe(sdkDefault.SecurityTokenLifetime);
}
[Fact]
public void Configured_transport_quotas_replace_only_what_is_set()
{
var sdkDefault = new TransportQuotas();
var built = OpcUaApplicationHost.BuildTransportQuotas(new OpcUaQuotaOptions
{
MaxMessageSize = 8 * 1024 * 1024, OperationTimeoutMs = 45_000, MaxArrayLength = 1_000_000,
});
built.MaxMessageSize.ShouldBe(8 * 1024 * 1024);
built.OperationTimeout.ShouldBe(45_000);
built.MaxArrayLength.ShouldBe(1_000_000);
built.MaxStringLength.ShouldBe(sdkDefault.MaxStringLength);
built.ChannelLifetime.ShouldBe(sdkDefault.ChannelLifetime);
}
[Fact]
public void History_read_batch_cap_default_scales_with_the_machine()
{
OtOpcUaNodeManager.DefaultMaxConcurrentHistoryReadBatches
.ShouldBe(Math.Clamp(Environment.ProcessorCount * 2, 4, 16));
}
}