signalr-hubasp.net-core-signalr

SignalR async Hub method blocks client connection


The ASP.NET SignalR Hubs API Guide - Server (https://learn.microsoft.com/en-us/aspnet/signalr/overview/guide-to-the-api/hubs-api-guide-server) states the following:

Making a Hub method asynchronous avoids blocking the connection when it uses the WebSocket transport. When a Hub method executes synchronously and the transport is WebSocket, subsequent invocations of methods on the Hub from the same client are blocked until the Hub method completes.

Based on that I assumed when the method is async, method calls on the Hub from the same client are not blocked.

I created a template Angular/ASP.NET Core app (dotnet new angular)(.Net Core 2.2.108), and added the following:

Server:

public class ChatHub : Hub
{
    public async Task<string> GetMessage(string name)
    {
        Console.WriteLine(DateTime.Now + " GetMessage " + name);
        await Task.Delay(3000);
        return "Hello " + name;
    }
}

Client:

export class AppComponent {
  title = 'app';

  private hubConnection: signalR.HubConnection;

  ngOnInit() {

    this.hubConnection = new signalR.HubConnectionBuilder()
      .withUrl('/chatHub')
      .configureLogging(signalR.LogLevel.Debug)
      .build();

    this.hubConnection
      .start()
      .then(() => {
        console.log('Connection started');
        this.logMessage('a');
        this.logMessage('b');
      })
      .catch(err => console.log('Error while starting connection: ' + err));
  }

  private logMessage(name) {
    this.hubConnection.invoke("getMessage", name)
      .then(val => console.log(new Date().toLocaleString() + ": " + val));
    console.log(new Date().toLocaleString() + ": getMessage " + name + " invoked");
  }
}

I got the following output on the client:

[2019-08-08T13:51:09.872Z] Information: Normalizing '/chatHub' to 'https://localhost:44389/chatHub'.
Utils.js:209 [2019-08-08T13:51:09.875Z] Information: Normalizing '/chatHub' to 'https://localhost:44389/chatHub'.
Utils.js:213 [2019-08-08T13:51:09.876Z] Debug: Starting HubConnection.
Utils.js:213 [2019-08-08T13:51:09.878Z] Debug: Starting connection with transfer format 'Text'.
Utils.js:213 [2019-08-08T13:51:09.880Z] Debug: Sending negotiation request: https://localhost:44389/chatHub/negotiate.
core.js:3121 Angular is running in the development mode. Call enableProdMode() to enable the production mode.
Utils.js:213 [2019-08-08T13:51:09.930Z] Debug: Selecting transport 'WebSockets'.
Utils.js:209 [2019-08-08T13:51:09.994Z] Information: WebSocket connected to wss://localhost:44389/chatHub?id=mCisfcGtLdYEBMSIix6tvQ.
Utils.js:213 [2019-08-08T13:51:09.995Z] Debug: Sending handshake request.
Utils.js:209 [2019-08-08T13:51:09.996Z] Information: Using HubProtocol 'json'.
Utils.js:213 [2019-08-08T13:51:10.040Z] Debug: Server handshake complete.
app.component.ts:28 Connection started
app.component.ts:38 8/8/2019, 3:51:10 PM: getMessage a invoked
app.component.ts:38 8/8/2019, 3:51:10 PM: getMessage b invoked
app.component.ts:37 8/8/2019, 3:51:13 PM: Hello a
app.component.ts:37 8/8/2019, 3:51:16 PM: Hello b

And on the server (AspNetCore messages stripped):

signalr-test> 2019-08-08 3:51:10 PM GetMessage a
signalr-test> 2019-08-08 3:51:13 PM GetMessage b

There's a 3 sec delay in the second server call and so in the client reply as well. Debugging also shows that the second call is placed on the hub only after the first one finished (the Task completes). It looks like SignalR uses the WebSockets transport, but the server processes the messages sequentially, so the method calls from the same client are blocked, even though the hub method is async.

What do I miss? Or what did I misunderstand?

My experiments:


Solution

  • It is done by design (issue at github). By default a client is only allowed to invoke a single Hub method at a time.

    However, starting from ASP.NET Core SignalR 5.0 there is option for it (default is 1).

    HubOptions:

    public int MaxParallelInvocationsPerClient { get; set; }
    

    HubConnectionContextOptions:

    public int MaximumParallelInvocations { get; set; }