fix: share SDK2 host disposal completion and close unadopted servers
This commit is contained in:
@@ -175,6 +175,9 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable
|
||||
private StandardServer? _server;
|
||||
private readonly ITelemetryContext _telemetry = DefaultTelemetry.Create(_ => { });
|
||||
private readonly List<IUserTokenAuthenticator> _identityAuthenticators = [];
|
||||
private readonly object _lifetimeLock = new();
|
||||
private readonly TaskCompletionSource _disposeCompletion = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
private int _disposeStarted;
|
||||
|
||||
/// <summary>Initializes a new instance of the OPC UA application host.</summary>
|
||||
/// <param name="options">The host configuration options.</param>
|
||||
@@ -202,18 +205,21 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task StartAsync(StandardServer server, CancellationToken cancellationToken)
|
||||
{
|
||||
if (server is not OtOpcUaSdkServer sdkServer)
|
||||
throw new ArgumentException("The server must support application authentication before its listeners open.", nameof(server));
|
||||
_server = server;
|
||||
// ApplicationInstance receives the host's ITelemetryContext. DefaultTelemetry.Create wires
|
||||
// the SDK's internal logging; an empty builder keeps
|
||||
// the SDK's trace off our ILogger (the host keeps its own _logger) — sufficient for the bootstrap.
|
||||
_application = new ApplicationInstance(_telemetry)
|
||||
OtOpcUaSdkServer sdkServer;
|
||||
lock (_lifetimeLock)
|
||||
{
|
||||
ApplicationName = _options.ApplicationName,
|
||||
ApplicationType = ApplicationType.Server,
|
||||
ConfigSectionName = "OtOpcUa",
|
||||
};
|
||||
ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposeStarted) != 0, this);
|
||||
sdkServer = server as OtOpcUaSdkServer
|
||||
?? throw new ArgumentException("The server must support application authentication before its listeners open.", nameof(server));
|
||||
_server = server;
|
||||
// The empty telemetry builder keeps SDK tracing separate from the host's ILogger.
|
||||
_application = new ApplicationInstance(_telemetry)
|
||||
{
|
||||
ApplicationName = _options.ApplicationName,
|
||||
ApplicationType = ApplicationType.Server,
|
||||
ConfigSectionName = "OtOpcUa",
|
||||
};
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
@@ -640,16 +646,63 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable
|
||||
|
||||
/// <summary>Disposes the application host and cleans up resources.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async ValueTask DisposeAsync()
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
// Authenticators remain installed until the SDK closes every listener and disposes its registry.
|
||||
if (_application is { } application)
|
||||
if (Interlocked.CompareExchange(ref _disposeStarted, 1, 0) == 0)
|
||||
_ = CompleteDisposalAsync();
|
||||
return new ValueTask(_disposeCompletion.Task);
|
||||
}
|
||||
|
||||
private async Task CompleteDisposalAsync()
|
||||
{
|
||||
Exception? failure = null;
|
||||
try
|
||||
{
|
||||
_application = null;
|
||||
try { await application.DisposeAsync().ConfigureAwait(false); }
|
||||
catch (Exception ex) { _logger.LogWarning(ex, "OpcUaApplicationHost: disposal threw"); }
|
||||
ApplicationInstance? application;
|
||||
StandardServer? server;
|
||||
bool adopted;
|
||||
lock (_lifetimeLock)
|
||||
{
|
||||
application = _application;
|
||||
server = _server;
|
||||
// SDK2 adopts the server only when ApplicationInstance.StartAsync is reached.
|
||||
adopted = server is not null && ReferenceEquals(application?.Server, server);
|
||||
_application = null;
|
||||
}
|
||||
try
|
||||
{
|
||||
if (application is not null)
|
||||
{
|
||||
try { await application.DisposeAsync().ConfigureAwait(false); }
|
||||
catch (Exception ex) { _logger.LogWarning(ex, "OpcUaApplicationHost: disposal threw"); }
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Failed configuration/certificate work may precede the application's ownership handoff.
|
||||
// An adopted server remains solely owned by ApplicationInstance.DisposeAsync.
|
||||
if (!adopted)
|
||||
{
|
||||
try { if (server is not null) await server.DisposeAsync().ConfigureAwait(false); }
|
||||
catch (Exception ex) { _logger.LogWarning(ex, "OpcUaApplicationHost: unadopted server disposal threw"); }
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception error) { failure = error; }
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
// Only the cleanup owner clears adapters after every listener and registry has closed.
|
||||
_identityAuthenticators.Clear();
|
||||
_server = null;
|
||||
}
|
||||
catch (Exception error) { failure = failure is null ? error : new AggregateException(failure, error); }
|
||||
finally
|
||||
{
|
||||
if (failure is null) _disposeCompletion.TrySetResult();
|
||||
else _disposeCompletion.TrySetException(failure);
|
||||
}
|
||||
}
|
||||
_identityAuthenticators.Clear();
|
||||
_server = null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user