Resolve deadlock with `InvokeAsync` in `On` handler (#8334)
- Use a channel to unblock the dispatch loop - Added tests
This commit is contained in:
parent
a673be3b9a
commit
cfe0cc38ec
|
|
@ -688,7 +688,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<(bool close, Exception exception)> ProcessMessagesAsync(HubMessage message, ConnectionState connectionState)
|
private async Task<(bool close, Exception exception)> ProcessMessagesAsync(HubMessage message, ConnectionState connectionState, ChannelWriter<InvocationMessage> invocationMessageWriter)
|
||||||
{
|
{
|
||||||
Log.ResettingKeepAliveTimer(_logger);
|
Log.ResettingKeepAliveTimer(_logger);
|
||||||
ResetTimeout();
|
ResetTimeout();
|
||||||
|
|
@ -703,7 +703,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
break;
|
break;
|
||||||
case InvocationMessage invocation:
|
case InvocationMessage invocation:
|
||||||
Log.ReceivedInvocation(_logger, invocation.InvocationId, invocation.Target, invocation.Arguments);
|
Log.ReceivedInvocation(_logger, invocation.InvocationId, invocation.Target, invocation.Arguments);
|
||||||
await DispatchInvocationAsync(invocation);
|
await invocationMessageWriter.WriteAsync(invocation);
|
||||||
break;
|
break;
|
||||||
case CompletionMessage completion:
|
case CompletionMessage completion:
|
||||||
if (!connectionState.TryRemoveInvocation(completion.InvocationId, out irq))
|
if (!connectionState.TryRemoveInvocation(completion.InvocationId, out irq))
|
||||||
|
|
@ -903,6 +903,19 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
|
|
||||||
var uploadStreamSource = new CancellationTokenSource();
|
var uploadStreamSource = new CancellationTokenSource();
|
||||||
_uploadStreamToken = uploadStreamSource.Token;
|
_uploadStreamToken = uploadStreamSource.Token;
|
||||||
|
var invocationMessageChannel = Channel.CreateUnbounded<InvocationMessage>();
|
||||||
|
var invocationMessageReceiveTask = StartProcessingInvocationMessages(invocationMessageChannel.Reader);
|
||||||
|
|
||||||
|
async Task StartProcessingInvocationMessages(ChannelReader<InvocationMessage> invocationMessageChannelReader)
|
||||||
|
{
|
||||||
|
while (await invocationMessageChannelReader.WaitToReadAsync())
|
||||||
|
{
|
||||||
|
while (invocationMessageChannelReader.TryRead(out var invocationMessage))
|
||||||
|
{
|
||||||
|
await DispatchInvocationAsync(invocationMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -929,7 +942,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
Exception exception;
|
Exception exception;
|
||||||
|
|
||||||
// We have data, process it
|
// We have data, process it
|
||||||
(close, exception) = await ProcessMessagesAsync(message, connectionState);
|
(close, exception) = await ProcessMessagesAsync(message, connectionState, invocationMessageChannel.Writer);
|
||||||
if (close)
|
if (close)
|
||||||
{
|
{
|
||||||
// Closing because we got a close frame, possibly with an error in it.
|
// Closing because we got a close frame, possibly with an error in it.
|
||||||
|
|
@ -970,6 +983,8 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
invocationMessageChannel.Writer.TryComplete();
|
||||||
|
await invocationMessageReceiveTask;
|
||||||
timer.Stop();
|
timer.Stop();
|
||||||
uploadStreamSource.Cancel();
|
uploadStreamSource.Cancel();
|
||||||
}
|
}
|
||||||
|
|
@ -1233,8 +1248,7 @@ namespace Microsoft.AspNetCore.SignalR.Client
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Represents all the transient state about a connection
|
//TODO: Refactor all transient state about the connection into the ConnectionState class.
|
||||||
// This includes binding information because return type binding depends upon _pendingCalls
|
|
||||||
private class ConnectionState : IInvocationBinder
|
private class ConnectionState : IInvocationBinder
|
||||||
{
|
{
|
||||||
private volatile bool _stopping;
|
private volatile bool _stopping;
|
||||||
|
|
|
||||||
|
|
@ -247,6 +247,46 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
||||||
|
[LogLevel(LogLevel.Trace)]
|
||||||
|
public async Task CanInvokeFromOnHandler(string protocolName, HttpTransportType transportType, string path)
|
||||||
|
{
|
||||||
|
var protocol = HubProtocols[protocolName];
|
||||||
|
using (StartServer<Startup>(out var server))
|
||||||
|
{
|
||||||
|
const string originalMessage = "SignalR";
|
||||||
|
|
||||||
|
var connection = CreateHubConnection(server.Url, path, transportType, protocol, LoggerFactory);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await connection.StartAsync().OrTimeout();
|
||||||
|
|
||||||
|
var helloWorldTcs = new TaskCompletionSource<string>();
|
||||||
|
var echoTcs = new TaskCompletionSource<string>();
|
||||||
|
connection.On<string>("Echo", async (message) =>
|
||||||
|
{
|
||||||
|
echoTcs.SetResult(message);
|
||||||
|
helloWorldTcs.SetResult(await connection.InvokeAsync<string>(nameof(TestHub.HelloWorld)).OrTimeout());
|
||||||
|
});
|
||||||
|
|
||||||
|
await connection.InvokeAsync("CallEcho", originalMessage).OrTimeout();
|
||||||
|
|
||||||
|
Assert.Equal(originalMessage, await echoTcs.Task.OrTimeout());
|
||||||
|
Assert.Equal("Hello World!", await helloWorldTcs.Task.OrTimeout());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await connection.DisposeAsync().OrTimeout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
||||||
[LogLevel(LogLevel.Trace)]
|
[LogLevel(LogLevel.Trace)]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue