mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-27 04:59:55 +08:00
WebSocket transport: a sender queued across Close fails as closed (#4832)
## Problem `WebSocketByteTransportTests.CloseFailsQueuedSendInsteadOfOrphaningItAsync` fails intermittently. One example is test-linux-net10.0 (3/16) in run 39114639155 on #4831, an unrelated MachineTool change. The queued send failed with a raw `WebSocketException` ("…transitioned into an invalid state") instead of `ServiceResultException` with `BadConnectionClosed`. ## Cause This is a product race in `WebSocketByteTransportBase`. - `Close()` cancels the shared send token. That wakes both the in-flight send and the queued `SemaphoreSlim.WaitAsync`. - `SemaphoreSlim` removes a cancelled waiter on a later continuation. If the in-flight sender's `finally` releases the lock first, the release *grants* the queued waiter despite its cancelled token. - `EnterSendAsync` then returned normally, and the queued sender called `SendAsync` on the already aborted socket. - That socket error is not an `OperationCanceledException`, so it reached the caller untranslated. ## Fix After acquiring the lock, `EnterSendAsync` checks whether the transport closed. If it did, it gives the lock back and throws `BadConnectionClosed`. `Close()` sets the closed flag before it cancels, so every waiter woken by `Close()` sees it. ## Tests - The existing test body moved into a helper. New `CloseFailsQueuedSendEvenWhenTheLockIsGrantedAsync` repeats the interleaving 2000 times (about 140 ms). - Without the fix it fails 3/3 runs within about 100 ms, with the CI exception. With the fix it passes 3/3. - `Opc.Ua.Core.Tests` `~WebSocket` (net10.0): 39/39 passed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0005b0d5e6
commit
014f99a53a
@@ -393,6 +393,17 @@ namespace Opc.Ua.Bindings
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
await m_sendLock.WaitAsync(linkedCts.Token).ConfigureAwait(false);
|
await m_sendLock.WaitAsync(linkedCts.Token).ConfigureAwait(false);
|
||||||
|
// SemaphoreSlim can still grant a waiter whose token was cancelled when a
|
||||||
|
// release reaches it before the cancellation removes it. A sender queued
|
||||||
|
// across Close would then write to the aborted socket and fail with the
|
||||||
|
// socket's own error, so it gives the lock back and fails as closed.
|
||||||
|
if (Volatile.Read(ref m_closed) != 0)
|
||||||
|
{
|
||||||
|
m_sendLock.Release();
|
||||||
|
throw ServiceResultException.Create(
|
||||||
|
StatusCodes.BadConnectionClosed,
|
||||||
|
"The WebSocket transport is closed.");
|
||||||
|
}
|
||||||
return linkedCts;
|
return linkedCts;
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
|
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
|
||||||
|
|||||||
@@ -503,7 +503,27 @@ namespace Opc.Ua.Core.Tests.Stack.Transport
|
|||||||
/// sender forever.
|
/// sender forever.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Test]
|
[Test]
|
||||||
public async Task CloseFailsQueuedSendInsteadOfOrphaningItAsync()
|
public Task CloseFailsQueuedSendInsteadOfOrphaningItAsync()
|
||||||
|
{
|
||||||
|
return CloseWithQueuedSendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Close can wake the in-flight sender, whose lock release then grants
|
||||||
|
/// the queued waiter before its cancellation removes it. The queued
|
||||||
|
/// sender must still fail as closed rather than write to the aborted
|
||||||
|
/// socket. The interleaving depends on the thread pool, so it repeats.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task CloseFailsQueuedSendEvenWhenTheLockIsGrantedAsync()
|
||||||
|
{
|
||||||
|
for (int iteration = 0; iteration < 2000; iteration++)
|
||||||
|
{
|
||||||
|
await CloseWithQueuedSendAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CloseWithQueuedSendAsync()
|
||||||
{
|
{
|
||||||
using var socket = new BlockingSendWebSocket();
|
using var socket = new BlockingSendWebSocket();
|
||||||
var transport = new WebSocketServerByteTransport(
|
var transport = new WebSocketServerByteTransport(
|
||||||
|
|||||||
Reference in New Issue
Block a user