Merge pull request '[opcuaclient] OpcUaClient — Per-subscription tuning' (#331) from auto/opcuaclient/1 into auto/driver-gaps
This commit was merged in pull request #331.
This commit is contained in:
@@ -858,22 +858,24 @@ public sealed class OpcUaClientDriver(OpcUaClientDriverOptions options, string d
|
|||||||
var id = Interlocked.Increment(ref _nextSubscriptionId);
|
var id = Interlocked.Increment(ref _nextSubscriptionId);
|
||||||
var handle = new OpcUaSubscriptionHandle(id);
|
var handle = new OpcUaSubscriptionHandle(id);
|
||||||
|
|
||||||
// Floor the publishing interval at 50ms — OPC UA servers routinely negotiate
|
// Floor the publishing interval — OPC UA servers routinely negotiate
|
||||||
// minimum-supported intervals up anyway, but sending sub-50ms wastes negotiation
|
// minimum-supported intervals up anyway, but sending sub-floor values wastes
|
||||||
// bandwidth on every subscription create.
|
// negotiation bandwidth on every subscription create. Floor is configurable via
|
||||||
var intervalMs = publishingInterval < TimeSpan.FromMilliseconds(50)
|
// OpcUaSubscriptionDefaults.MinPublishingIntervalMs (default 50ms).
|
||||||
? 50
|
var subDefaults = _options.Subscriptions;
|
||||||
|
var intervalMs = publishingInterval < TimeSpan.FromMilliseconds(subDefaults.MinPublishingIntervalMs)
|
||||||
|
? subDefaults.MinPublishingIntervalMs
|
||||||
: (int)publishingInterval.TotalMilliseconds;
|
: (int)publishingInterval.TotalMilliseconds;
|
||||||
|
|
||||||
var subscription = new Subscription(telemetry: null!, new SubscriptionOptions
|
var subscription = new Subscription(telemetry: null!, new SubscriptionOptions
|
||||||
{
|
{
|
||||||
DisplayName = $"opcua-sub-{id}",
|
DisplayName = $"opcua-sub-{id}",
|
||||||
PublishingInterval = intervalMs,
|
PublishingInterval = intervalMs,
|
||||||
KeepAliveCount = 10,
|
KeepAliveCount = (uint)subDefaults.KeepAliveCount,
|
||||||
LifetimeCount = 1000,
|
LifetimeCount = subDefaults.LifetimeCount,
|
||||||
MaxNotificationsPerPublish = 0,
|
MaxNotificationsPerPublish = subDefaults.MaxNotificationsPerPublish,
|
||||||
PublishingEnabled = true,
|
PublishingEnabled = true,
|
||||||
Priority = 0,
|
Priority = subDefaults.Priority,
|
||||||
TimestampsToReturn = TimestampsToReturn.Both,
|
TimestampsToReturn = TimestampsToReturn.Both,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -975,15 +977,16 @@ public sealed class OpcUaClientDriver(OpcUaClientDriverOptions options, string d
|
|||||||
// match in O(1) without re-parsing on every event.
|
// match in O(1) without re-parsing on every event.
|
||||||
var sourceFilter = new HashSet<string>(sourceNodeIds, StringComparer.Ordinal);
|
var sourceFilter = new HashSet<string>(sourceNodeIds, StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var alarmDefaults = _options.Subscriptions;
|
||||||
var subscription = new Subscription(telemetry: null!, new SubscriptionOptions
|
var subscription = new Subscription(telemetry: null!, new SubscriptionOptions
|
||||||
{
|
{
|
||||||
DisplayName = $"opcua-alarm-sub-{id}",
|
DisplayName = $"opcua-alarm-sub-{id}",
|
||||||
PublishingInterval = 500, // 500ms — alarms don't need fast polling; the server pushes
|
PublishingInterval = 500, // 500ms — alarms don't need fast polling; the server pushes
|
||||||
KeepAliveCount = 10,
|
KeepAliveCount = (uint)alarmDefaults.KeepAliveCount,
|
||||||
LifetimeCount = 1000,
|
LifetimeCount = alarmDefaults.LifetimeCount,
|
||||||
MaxNotificationsPerPublish = 0,
|
MaxNotificationsPerPublish = alarmDefaults.MaxNotificationsPerPublish,
|
||||||
PublishingEnabled = true,
|
PublishingEnabled = true,
|
||||||
Priority = 0,
|
Priority = alarmDefaults.AlarmsPriority,
|
||||||
TimestampsToReturn = TimestampsToReturn.Both,
|
TimestampsToReturn = TimestampsToReturn.Both,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -134,8 +134,58 @@ public sealed class OpcUaClientDriverOptions
|
|||||||
/// browse forever.
|
/// browse forever.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int MaxBrowseDepth { get; init; } = 10;
|
public int MaxBrowseDepth { get; init; } = 10;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Per-subscription tuning knobs applied when the driver creates data + alarm
|
||||||
|
/// subscriptions on the upstream session. Defaults preserve the previous hard-coded
|
||||||
|
/// values so existing deployments see no behaviour change.
|
||||||
|
/// </summary>
|
||||||
|
public OpcUaSubscriptionDefaults Subscriptions { get; init; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tuning surface for OPC UA subscriptions created by <see cref="OpcUaClientDriver"/>.
|
||||||
|
/// Lifted from the per-call hard-coded literals so operators can tune publish cadence,
|
||||||
|
/// keep-alive ratio, and alarm-vs-data prioritisation without recompiling the driver.
|
||||||
|
/// Defaults match the original hard-coded values (KeepAlive=10, Lifetime=1000,
|
||||||
|
/// MaxNotifications=0 unlimited, Priority=0, MinPublishingInterval=50ms).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="KeepAliveCount">
|
||||||
|
/// Number of consecutive empty publish cycles before the server sends a keep-alive
|
||||||
|
/// response. Default 10 — high enough to suppress idle traffic, low enough that the
|
||||||
|
/// client notices a stalled subscription within ~5x the publish interval.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="LifetimeCount">
|
||||||
|
/// Number of consecutive missed publish responses before the server tears down the
|
||||||
|
/// subscription. Must be ≥3×<see cref="KeepAliveCount"/> per OPC UA spec; default 1000
|
||||||
|
/// gives ~100 keep-alives of slack which is conservative on flaky networks.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="MaxNotificationsPerPublish">
|
||||||
|
/// Cap on notifications returned per publish response. <c>0</c> = unlimited (the OPC UA
|
||||||
|
/// spec sentinel). Lower this to bound publish-message size on bursty servers.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="Priority">
|
||||||
|
/// Subscription priority for data subscriptions (0..255). Higher = scheduled ahead of
|
||||||
|
/// lower. Default 0 matches the SDK's default for ordinary tag subscriptions.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="MinPublishingIntervalMs">
|
||||||
|
/// Floor (ms) applied to <c>publishingInterval</c> requests. Sub-floor values are
|
||||||
|
/// clamped up so wire-side negotiations don't waste round-trips on intervals the server
|
||||||
|
/// will only round up anyway. Default 50ms.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="AlarmsPriority">
|
||||||
|
/// Subscription priority for the alarm subscription (0..255). Higher than
|
||||||
|
/// <see cref="Priority"/> by default (1 vs 0) so alarm publishes aren't starved during
|
||||||
|
/// data-tag bursts.
|
||||||
|
/// </param>
|
||||||
|
public sealed record OpcUaSubscriptionDefaults(
|
||||||
|
int KeepAliveCount = 10,
|
||||||
|
uint LifetimeCount = 1000,
|
||||||
|
uint MaxNotificationsPerPublish = 0,
|
||||||
|
byte Priority = 0,
|
||||||
|
int MinPublishingIntervalMs = 50,
|
||||||
|
byte AlarmsPriority = 1);
|
||||||
|
|
||||||
/// <summary>OPC UA message security mode.</summary>
|
/// <summary>OPC UA message security mode.</summary>
|
||||||
public enum OpcUaSecurityMode
|
public enum OpcUaSecurityMode
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -67,6 +67,45 @@ public sealed class OpcUaClientDriverScaffoldTests
|
|||||||
health.LastError.ShouldNotBeNull();
|
health.LastError.ShouldNotBeNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Default_subscription_tuning_matches_prior_hard_coded_values()
|
||||||
|
{
|
||||||
|
// PR #273: lifted hard-coded Subscription parameters into options; defaults MUST
|
||||||
|
// remain wire-identical so existing deployments see no behaviour change.
|
||||||
|
var subs = new OpcUaClientDriverOptions().Subscriptions;
|
||||||
|
subs.KeepAliveCount.ShouldBe(10);
|
||||||
|
subs.LifetimeCount.ShouldBe(1000u);
|
||||||
|
subs.MaxNotificationsPerPublish.ShouldBe(0u, "0 = unlimited per OPC UA spec");
|
||||||
|
subs.Priority.ShouldBe((byte)0);
|
||||||
|
subs.MinPublishingIntervalMs.ShouldBe(50);
|
||||||
|
subs.AlarmsPriority.ShouldBe((byte)1, "alarms get a higher priority than data tags so they aren't starved during bursts");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Subscription_defaults_are_overridable_via_options()
|
||||||
|
{
|
||||||
|
// Operators tuning a flaky-network deployment should be able to bump LifetimeCount /
|
||||||
|
// lower MaxNotificationsPerPublish without recompiling the driver. Verify the record
|
||||||
|
// is overridable end-to-end.
|
||||||
|
var opts = new OpcUaClientDriverOptions
|
||||||
|
{
|
||||||
|
Subscriptions = new OpcUaSubscriptionDefaults(
|
||||||
|
KeepAliveCount: 25,
|
||||||
|
LifetimeCount: 5000u,
|
||||||
|
MaxNotificationsPerPublish: 200u,
|
||||||
|
Priority: 7,
|
||||||
|
MinPublishingIntervalMs: 100,
|
||||||
|
AlarmsPriority: 9),
|
||||||
|
};
|
||||||
|
|
||||||
|
opts.Subscriptions.KeepAliveCount.ShouldBe(25);
|
||||||
|
opts.Subscriptions.LifetimeCount.ShouldBe(5000u);
|
||||||
|
opts.Subscriptions.MaxNotificationsPerPublish.ShouldBe(200u);
|
||||||
|
opts.Subscriptions.Priority.ShouldBe((byte)7);
|
||||||
|
opts.Subscriptions.MinPublishingIntervalMs.ShouldBe(100);
|
||||||
|
opts.Subscriptions.AlarmsPriority.ShouldBe((byte)9);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Reinitialize_against_unreachable_endpoint_re_throws()
|
public async Task Reinitialize_against_unreachable_endpoint_re_throws()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user