mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 14:43:08 +08:00
ManagedTransportChannelLease: non-blocking sync Dispose (deadlock under sync context)
Sync Dispose previously called
DisposeAsyncCore().AsTask().GetAwaiter().GetResult()
which ultimately performs network I/O (TCP FIN handshake) via
ChannelEntry.TearDownAsync → underlying.CloseAsync. On any host that
captures a SynchronizationContext (legacy ASP.NET, WPF, WinForms)
synchronous Dispose on the UI/request thread deadlocks. Even without
a sync context, a thread-pool thread is pinned on network I/O for the
close duration.
New Dispose() body:
- Atomically marks the lease released via
Interlocked.Exchange(ref m_active, 0) (idempotent — second call
short-circuits, so lease accounting cannot double-release)
- Pushes the actual Entry.ReleaseLeaseAsync(this) onto the thread
pool via Task.Run and returns IMMEDIATELY
- Swallows the async failure inside the fire-and-forget (sync Dispose
cannot surface async exceptions)
CloseAsync / DisposeAsyncCore (the async path) are unchanged and still
await the full teardown chain. Documented on Dispose() that callers
needing teardown observability must use CloseAsync().
This commit is contained in:
@@ -275,10 +275,42 @@ namespace Opc.Ua
|
||||
return DisposeAsyncCore();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <summary>
|
||||
/// 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 <see cref="CloseAsync(CancellationToken)"/>. The
|
||||
/// synchronous path exists only for compatibility with the
|
||||
/// <see cref="IDisposable"/> contract and the legacy
|
||||
/// <c>using</c> statement, and never blocks on network I/O —
|
||||
/// blocking would deadlock callers running under a synchronization
|
||||
/// context (legacy ASP.NET, WPF, WinForms).
|
||||
/// </summary>
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user