fix: terminally dispose SDK1 server ownership once

This commit is contained in:
Joseph Doherty
2026-11-10 16:53:34 -05:00
parent 84bfe6cd20
commit 10edc44f94
@@ -173,6 +173,9 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable
private ApplicationInstance? _application;
private StandardServer? _server;
private ImpersonateEventHandler? _impersonateHandler;
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>
@@ -200,16 +203,18 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task StartAsync(StandardServer server, CancellationToken cancellationToken)
{
_server = server;
// 1.5.378 requires an ITelemetryContext on the ApplicationInstance ctor (the parameterless ctor
// is obsolete). 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(DefaultTelemetry.Create(_ => { }))
lock (_lifetimeLock)
{
ApplicationName = _options.ApplicationName,
ApplicationType = ApplicationType.Server,
ConfigSectionName = "OtOpcUa",
};
ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposeStarted) != 0, this);
_server = server;
// 1.5.378 requires telemetry on ApplicationInstance; the empty builder keeps SDK tracing separate.
_application = new ApplicationInstance(DefaultTelemetry.Create(_ => { }))
{
ApplicationName = _options.ApplicationName,
ApplicationType = ApplicationType.Server,
ConfigSectionName = "OtOpcUa",
};
}
_ = await BuildConfigurationAsync(cancellationToken);
await EnsureApplicationCertificateAsync(cancellationToken).ConfigureAwait(false);
@@ -657,25 +662,60 @@ 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()
{
if (Interlocked.CompareExchange(ref _disposeStarted, 1, 0) == 0)
_ = CompleteDisposalAsync();
return new ValueTask(_disposeCompletion.Task);
}
private async Task CompleteDisposalAsync()
{
Exception? failure = null;
try
{
// An explicit SDK stop may already have halted CurrentInstance; still continue disposal.
if (_impersonateHandler is not null && _server?.CurrentInstance?.SessionManager is { } sessionManager)
sessionManager.ImpersonateUser -= _impersonateHandler;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "OpcUaApplicationHost: detaching ImpersonateUser threw");
}
_impersonateHandler = null;
ApplicationInstance? application;
StandardServer? server;
ImpersonateEventHandler? handler;
lock (_lifetimeLock)
{
application = _application;
server = _server;
handler = _impersonateHandler;
_application = null;
_server = null;
_impersonateHandler = null;
}
if (_application is not null)
try
{
try
{
// An explicit SDK stop may already have halted CurrentInstance; terminal disposal still follows.
if (handler is not null && server?.CurrentInstance?.SessionManager is { } sessionManager)
sessionManager.ImpersonateUser -= handler;
}
catch (Exception ex) { _logger.LogWarning(ex, "OpcUaApplicationHost: detaching ImpersonateUser threw"); }
if (application is not null)
{
try { await application.StopAsync().ConfigureAwait(false); }
catch (Exception ex) { _logger.LogWarning(ex, "OpcUaApplicationHost: Stop threw on dispose"); }
}
}
finally
{
// SDK1 Stop does not release the server's request queue or terminal event registrations (#807).
try { server?.Dispose(); }
catch (Exception ex) { _logger.LogWarning(ex, "OpcUaApplicationHost: server disposal threw"); }
}
}
catch (Exception error) { failure = error; }
finally
{
// 1.5.378: ApplicationInstance.Stop() → StopAsync().
try { await _application.StopAsync().ConfigureAwait(false); }
catch (Exception ex) { _logger.LogWarning(ex, "OpcUaApplicationHost: Stop threw on dispose"); }
// All callers observe completion, including when a logger or cleanup callback throws.
if (failure is null) _disposeCompletion.TrySetResult();
else _disposeCompletion.TrySetException(failure);
}
}
}