feat(opcuaclient): subscription tuning, OperationLimits chunking, diagnostics counters, revocation options, DiscoveryUrl (#273 #275 #276 #277 #278)

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 14:40:20 -04:00
parent 55eac5f68b
commit ba62022e88
12 changed files with 1308 additions and 100 deletions
@@ -189,6 +189,86 @@ public sealed record OpcUaClientDriverOptions
/// </summary>
[Range(1, 60)]
public int ProbeTimeoutSeconds { get; init; } = 15;
// ---- #273 per-subscription tuning (defaults are the values that used to be hardcoded) ----
/// <summary>
/// Fixed publishing interval for every data-change subscription this driver creates. <c>null</c>
/// (default) keeps the historical behaviour: the interval the runtime asks for per
/// <c>SubscribeAsync</c> call, floored at 50 ms. When set it overrides the per-call value (the
/// 50 ms floor still applies); monitored-item sampling intervals follow it.
/// </summary>
public TimeSpan? SubscriptionPublishingInterval { get; init; }
/// <summary>
/// Publish cycles with no data before the server sends a keep-alive (default 10). Must be &gt;= 1.
/// </summary>
[Range(1, int.MaxValue)]
public uint SubscriptionKeepAliveCount { get; init; } = 10;
/// <summary>
/// Publish cycles with no Publish request before the server drops the subscription (default 1000).
/// Must be at least 3x <see cref="SubscriptionKeepAliveCount"/> (OPC UA Part 4 §5.13.2).
/// </summary>
[Range(3, int.MaxValue)]
public uint SubscriptionLifetimeCount { get; init; } = 1000;
/// <summary>Max notifications per Publish response; <c>0</c> (default) = server decides.</summary>
public uint SubscriptionMaxNotificationsPerPublish { get; init; }
/// <summary>Relative subscription priority 0-255 (default 0).</summary>
[Range(0, 255)]
public byte SubscriptionPriority { get; init; }
// ---- #275 ----
/// <summary>
/// When <c>true</c> (default) the driver reads the remote server's
/// <c>ServerCapabilities/OperationLimits</c> after connect and splits Read / Write / CreateMonitoredItems
/// requests that exceed them (a declared limit of 0 = unlimited). Set <c>false</c> to send every request
/// whole and let the server answer <c>BadTooManyOperations</c> itself.
/// </summary>
public bool HonorServerOperationLimits { get; init; } = true;
// ---- #277 ----
/// <summary>Certificate-revocation / issuer-store settings. Defaults preserve the SDK behaviour.</summary>
public OpcUaCertificateValidationOptions CertificateValidation { get; init; } = new();
// ---- #278 ----
/// <summary>
/// Optional discovery endpoint (a Local Discovery Server or the server itself), e.g.
/// <c>opc.tcp://lds.internal:4840</c>. When set, <c>InitializeAsync</c> calls <c>FindServers</c> and
/// <c>GetEndpoints</c> on it and tries the discovered URLs that match <see cref="SecurityPolicy"/> +
/// <see cref="SecurityMode"/> <i>before</i> the statically configured <see cref="EndpointUrls"/> /
/// <see cref="EndpointUrl"/>. A discovery failure is non-fatal: the static list is used. Discovery
/// itself always runs over an unsecured channel, as the spec requires. <c>null</c> (default) = off.
/// </summary>
public string? DiscoveryUrl { get; init; }
}
/// <summary>
/// Server-certificate revocation handling for the OPC UA Client driver (#277). The SDK reads CRLs from
/// the <c>crl</c> sub-folder of the trusted-peer / trusted-issuer stores; these settings only decide how
/// a certificate whose revocation status cannot be determined is treated and where the issuer store lives.
/// </summary>
public sealed record OpcUaCertificateValidationOptions
{
/// <summary>
/// When <c>true</c>, a server certificate whose revocation status is unknown (no CRL for its issuer, or a
/// stale one) is rejected. Default <c>false</c> keeps brownfield deployments without CRL infrastructure
/// connecting. A certificate that IS revoked is always rejected — even with
/// <see cref="OpcUaClientDriverOptions.AutoAcceptCertificates"/> set.
/// </summary>
public bool RejectUnknownRevocationStatus { get; init; }
/// <summary>
/// Directory used as the trusted-issuer certificate store. Its <c>crl/</c> sub-folder is where the SDK
/// looks for issuer CRLs (<c>.crl</c> files), so this is the knob for pointing the driver at an
/// operator-managed CRL set. <c>null</c>/empty (default) = <c>%LocalAppData%/OtOpcUa/pki/issuers</c>.
/// </summary>
public string? IssuerStorePath { get; init; }
}
/// <summary>