Skip to content

Commit da4bd70

Browse files
Don't wait for client methods to complete when stopping connection (#20104)
1 parent 3fe9012 commit da4bd70

File tree

2 files changed

+50
-4
lines changed

2 files changed

+50
-4
lines changed

src/SignalR/clients/csharp/Client.Core/src/HubConnection.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,7 +1128,9 @@ private async Task ReceiveLoop(ConnectionState connectionState)
11281128
var uploadStreamSource = new CancellationTokenSource();
11291129
connectionState.UploadStreamToken = uploadStreamSource.Token;
11301130
var invocationMessageChannel = Channel.CreateUnbounded<InvocationMessage>(_receiveLoopOptions);
1131-
var invocationMessageReceiveTask = StartProcessingInvocationMessages(invocationMessageChannel.Reader);
1131+
1132+
// We can't safely wait for this task when closing without introducing deadlock potential when calling StopAsync in a .On method
1133+
connectionState.InvocationMessageReceiveTask = StartProcessingInvocationMessages(invocationMessageChannel.Reader);
11321134

11331135
async Task StartProcessingInvocationMessages(ChannelReader<InvocationMessage> invocationMessageChannelReader)
11341136
{
@@ -1223,9 +1225,6 @@ async Task StartProcessingInvocationMessages(ChannelReader<InvocationMessage> in
12231225
await timerTask;
12241226
uploadStreamSource.Cancel();
12251227
await HandleConnectionClose(connectionState);
1226-
1227-
// await after the connection has been closed, otherwise could deadlock on a user's .On callback(s)
1228-
await invocationMessageReceiveTask;
12291228
}
12301229
}
12311230

@@ -1658,6 +1657,9 @@ private class ConnectionState : IInvocationBinder
16581657
public Exception CloseException { get; set; }
16591658
public CancellationToken UploadStreamToken { get; set; }
16601659

1660+
// We store this task so we can view it in a dump file, but never await it
1661+
public Task InvocationMessageReceiveTask { get; set; }
1662+
16611663
// Indicates the connection is stopping AND the client should NOT attempt to reconnect even if automatic reconnects are enabled.
16621664
// This means either HubConnection.DisposeAsync/StopAsync was called OR a CloseMessage with AllowReconnects set to false was received.
16631665
public bool Stopping

src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,50 @@ public async Task ClosedEventRaisedWhenTheClientIsStopped()
7878
Assert.Null(await closedEventTcs.Task);
7979
}
8080

81+
[Fact]
82+
public async Task StopAsyncCanBeCalledFromOnHandler()
83+
{
84+
var connection = new TestConnection();
85+
var hubConnection = CreateHubConnection(connection, loggerFactory: LoggerFactory);
86+
87+
var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
88+
hubConnection.On("method", async () =>
89+
{
90+
await hubConnection.StopAsync().OrTimeout();
91+
tcs.SetResult(null);
92+
});
93+
94+
await hubConnection.StartAsync().OrTimeout();
95+
96+
await connection.ReceiveJsonMessage(new { type = HubProtocolConstants.InvocationMessageType, target= "method", arguments = new object[] { } }).OrTimeout();
97+
98+
Assert.Null(await tcs.Task.OrTimeout());
99+
}
100+
101+
[Fact]
102+
public async Task StopAsyncDoesNotWaitForOnHandlers()
103+
{
104+
var connection = new TestConnection();
105+
var hubConnection = CreateHubConnection(connection, loggerFactory: LoggerFactory);
106+
107+
var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
108+
var methodCalledTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
109+
hubConnection.On("method", async () =>
110+
{
111+
methodCalledTcs.SetResult(null);
112+
await tcs.Task;
113+
});
114+
115+
await hubConnection.StartAsync().OrTimeout();
116+
117+
await connection.ReceiveJsonMessage(new { type = HubProtocolConstants.InvocationMessageType, target = "method", arguments = new object[] { } }).OrTimeout();
118+
119+
await methodCalledTcs.Task.OrTimeout();
120+
await hubConnection.StopAsync().OrTimeout();
121+
122+
tcs.SetResult(null);
123+
}
124+
81125
[Fact]
82126
public async Task PendingInvocationsAreCanceledWhenConnectionClosesCleanly()
83127
{

0 commit comments

Comments
 (0)