diff --git a/Stack/Opc.Ua.Core/Stack/Client/Channels/Internal/ManagedTransportChannelLease.cs b/Stack/Opc.Ua.Core/Stack/Client/Channels/Internal/ManagedTransportChannelLease.cs index ba5201c227..86d7731287 100644 --- a/Stack/Opc.Ua.Core/Stack/Client/Channels/Internal/ManagedTransportChannelLease.cs +++ b/Stack/Opc.Ua.Core/Stack/Client/Channels/Internal/ManagedTransportChannelLease.cs @@ -275,10 +275,42 @@ namespace Opc.Ua return DisposeAsyncCore(); } - /// + /// + /// Synchronous dispose. Begins lease teardown asynchronously on + /// a thread-pool thread and returns immediately. Callers that + /// need to observe teardown completion or surface failures MUST + /// use . The + /// synchronous path exists only for compatibility with the + /// contract and the legacy + /// using statement, and never blocks on network I/O — + /// blocking would deadlock callers running under a synchronization + /// context (legacy ASP.NET, WPF, WinForms). + /// public void Dispose() { - DisposeAsyncCore().AsTask().GetAwaiter().GetResult(); + // Sync Dispose is best-effort. Mark the lease released + // immediately (preventing any further SendRequestAsync / + // ReconnectAsync calls), then push the actual network I/O + // onto the thread pool. This decouples the synchronous + // caller from the TCP FIN handshake — see + // ChannelEntry.TearDownAsync — and avoids deadlocking + // callers running under a synchronization context. + if (Interlocked.Exchange(ref m_active, 0) == 0) + { + return; + } + ChannelEntry entry = Entry; + _ = Task.Run(async () => + { + try + { + await entry.ReleaseLeaseAsync(this).ConfigureAwait(false); + } + catch + { + // best effort — sync Dispose cannot surface async failures + } + }); } private async ValueTask DisposeAsyncCore()