fix(dcl): async OPC UA endpoint discovery bounded by OperationTimeoutMs/verify budget; adapter connect never runs on the actor thread (review 2026-10-21 02/P2)
This commit is contained in:
@@ -957,7 +957,14 @@ public class DataConnectionActor : UntypedActor, IWithTimers
|
||||
// is still in flight swaps the adapter and bumps the counter, and the OUTGOING
|
||||
// adapter's answer must never be applied to its replacement.
|
||||
var generation = _adapterGeneration;
|
||||
_adapter.ConnectAsync(_connectionDetails).ContinueWith(t =>
|
||||
// Started on the thread pool, never inline: everything an adapter's ConnectAsync does
|
||||
// before its first YIELDING await would otherwise run on this actor's dispatcher thread
|
||||
// — OPC UA endpoint discovery was synchronous there and parked the thread for the SDK's
|
||||
// operation timeout on every reconnect tick to a black-holed endpoint (review 2026-10-21
|
||||
// 02/P2). Adapter and details are captured here, on the actor thread.
|
||||
var adapter = _adapter;
|
||||
var details = _connectionDetails;
|
||||
Task.Run(() => adapter.ConnectAsync(details)).ContinueWith(t =>
|
||||
{
|
||||
if (t.IsCompletedSuccessfully)
|
||||
return new ConnectResult(true, null, generation);
|
||||
|
||||
@@ -96,19 +96,42 @@ public class RealOpcUaClient : IOpcUaClient
|
||||
/// <inheritdoc />
|
||||
public bool IsConnected => _session?.Connected ?? false;
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronous endpoint discovery bounded by <paramref name="appConfig"/>'s
|
||||
/// <c>TransportQuotas.OperationTimeout</c> (the connection's <c>OperationTimeoutMs</c>) and by
|
||||
/// <paramref name="ct"/> (review 2026-10-21 02/P2). It replaces the synchronous
|
||||
/// <c>DiscoveryClient.Create(uri).GetEndpoints(null)</c>, which ran on whatever thread called
|
||||
/// the connect — the actor thread, when <c>ValidateAsync</c> completed synchronously — under
|
||||
/// the SDK's default 120 s operation timeout, and could not be cancelled by a verify probe's
|
||||
/// budget.
|
||||
/// </summary>
|
||||
private static async Task<EndpointDescriptionCollection> DiscoverEndpointsAsync(
|
||||
ApplicationConfiguration appConfig, string endpointUrl, CancellationToken ct)
|
||||
{
|
||||
var discoveryConfig = EndpointConfiguration.Create(appConfig);
|
||||
using var discoveryClient = await DiscoveryClient.CreateAsync(
|
||||
appConfig, new Uri(endpointUrl), discoveryConfig, DiagnosticsMasks.None, ct).ConfigureAwait(false);
|
||||
return await discoveryClient.GetEndpointsAsync(null, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>Raised when the OPC UA connection is lost.</summary>
|
||||
public event Action? ConnectionLost;
|
||||
|
||||
/// <summary>Selects exactly the requested protection mode; protected discovery failures fail closed.</summary>
|
||||
/// <param name="url">The discovery URL to query.</param>
|
||||
/// <param name="requested">The security mode that must be matched exactly.</param>
|
||||
/// <param name="discover">Delegate that returns the server's advertised endpoints.</param>
|
||||
/// <param name="discover">Delegate that returns the server's advertised endpoints, honouring the token.</param>
|
||||
/// <param name="ct">
|
||||
/// Bounds the discovery. A cancellation is rethrown — never mistaken for "discovery
|
||||
/// unavailable", which would otherwise fall back to an unsecured endpoint and carry on.
|
||||
/// </param>
|
||||
/// <returns>The endpoint matching the requested security mode.</returns>
|
||||
internal static EndpointDescription DiscoverEndpoint(string url, MessageSecurityMode requested,
|
||||
Func<EndpointDescriptionCollection> discover)
|
||||
internal static async Task<EndpointDescription> DiscoverEndpointAsync(string url, MessageSecurityMode requested,
|
||||
Func<CancellationToken, Task<EndpointDescriptionCollection>> discover, CancellationToken ct = default)
|
||||
{
|
||||
EndpointDescriptionCollection endpoints;
|
||||
try { endpoints = discover(); }
|
||||
try { endpoints = await discover(ct).ConfigureAwait(false); }
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (requested != MessageSecurityMode.None)
|
||||
@@ -163,13 +186,8 @@ public class RealOpcUaClient : IOpcUaClient
|
||||
}
|
||||
|
||||
// Discover endpoints from the server, pick the preferred security mode
|
||||
var endpoint = DiscoverEndpoint(endpointUrl, preferredSecurityMode, () =>
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
using var discoveryClient = DiscoveryClient.Create(new Uri(endpointUrl));
|
||||
return discoveryClient.GetEndpoints(null);
|
||||
#pragma warning restore CS0618
|
||||
});
|
||||
var endpoint = await DiscoverEndpointAsync(endpointUrl, preferredSecurityMode,
|
||||
token => DiscoverEndpointsAsync(appConfig, endpointUrl, token), cancellationToken);
|
||||
|
||||
// The server advertises its own base address, which is frequently unroutable from here
|
||||
// (a 0.0.0.0 wildcard bind, or an internal container/NAT hostname). Swap it back to the
|
||||
@@ -391,13 +409,9 @@ public class RealOpcUaClient : IOpcUaClient
|
||||
await appConfig.ValidateAsync(ApplicationType.Client);
|
||||
|
||||
// Discover endpoints, pick the preferred security mode (same logic as ConnectAsync).
|
||||
var endpoint = DiscoverEndpoint(endpointUrl, preferredSecurityMode, () =>
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
using var discoveryClient = DiscoveryClient.Create(new Uri(endpointUrl));
|
||||
return discoveryClient.GetEndpoints(null);
|
||||
#pragma warning restore CS0618
|
||||
});
|
||||
// Bounded by the probe's own budget (linkedCts), not just the SDK operation timeout.
|
||||
var endpoint = await DiscoverEndpointAsync(endpointUrl, preferredSecurityMode,
|
||||
token => DiscoverEndpointsAsync(appConfig, endpointUrl, token), linkedCts.Token);
|
||||
|
||||
// Same reachability rewrite as ConnectAsync — a probe against a server advertising a
|
||||
// 0.0.0.0 / NAT base address must dial the reachable host, not the advertised one.
|
||||
|
||||
Reference in New Issue
Block a user