diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs
index 6a806fab..73272688 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs
@@ -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;
/// Initializes a new instance of the OPC UA application host.
/// The host configuration options.
@@ -200,16 +203,18 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable
/// A task that represents the asynchronous operation.
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
/// Disposes the application host and cleans up resources.
/// A task that represents the asynchronous operation.
- 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);
}
}
}