Jeff Dodson
Jeff Dodson

Reputation: 133

ASP.NET Core 2.2 SignalR Buffering Calls Instead of Invoking Asynchronously

I'm writing an ASP.NET Core 2.2 C# web application that uses SignalR to take calls from JavaScript in a web browser. On the server side, I initialize SignalR like this:

    public static void ConfigureServices(IServiceCollection services)
    {
        ...

        // Use SignalR
        services.AddSignalR(o =>
        {
            o.EnableDetailedErrors = true;
        });
    }

and

    public static void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        ...

        // Route to SignalR hubs
        app.UseSignalR(routes =>
        {
            routes.MapHub<ClientProxySignalR>("/clienthub");
        });

        ...
    }

My SignalR Hub class has a method like this:

public class ClientProxySignalR : Hub
{
    ...

    public async Task<IEnumerable<TagDescriptor>> GetRealtimeTags(string project)
    {
        return await _requestRouter.GetRealtimeTags(project).ConfigureAwait(false);
    }

    ...
}

and on the client side:

var connection = new signalR.HubConnectionBuilder()
                     .withUrl("/clienthub")
                     .configureLogging(signalR.LogLevel.Information)
                     .build();

connection.start().then(function () {
    ...
    // Enable buttons & stuff so you can click
    ...
}

document.getElementById("tagGetter").addEventListener("click", function (event) {
    connection.invoke("GetRealtimeTags", "Project1").then(data => {
        ...
        // use data
        ...
    }
}

This all works as far as it goes, and it does work asynchronously. So if I click the "tagGetter" button, it invokes the "GetRealtimeTags" method on my Hub and the "then" portion is invoked when the data comes back. It is also true that if this takes a while to run, and I click the "tagGetter" button again in the meantime, it makes the .invoke("GetRealtimeTags") call again...at least in the JavaScript.

However...this is where the problem occurs. Although the second call is made in the JavaScript, it will not trigger the corresponding method in my SignalR Hub class until the first call finishes. This doesn't match my understanding of what is supposed to happen. I thought that each invocation of a SignalR hub method back to the server would cause the creation of a new instance of the hub class to handle the call. Instead, the first call seems to be blocking the second.

If I create two different connections in my JavaScript code, then I am able to make two simultaneous calls on them without one blocking the other. But I know that isn't the right way to make this work.

So my question is: what am I doing wrong in this case?

Upvotes: 2

Views: 1402

Answers (2)

Khanh TO
Khanh TO

Reputation: 48972

This is by design of websockets to ensure messages are delivered in exact order.

You can refer to this for more information: https://hpbn.co/websocket/

Quoted:

The preceding example attempts to send application updates to the server, but only if the previous messages have been drained from the client’s buffer. Why bother with such checks? All WebSocket messages are delivered in the exact order in which they are queued by the client. As a result, a large backlog of queued messages, or even a single large message, will delay delivery of messages queued behind it—head-of-line blocking!

They also suggest a workaround solution:

To work around this problem, the application can split large messages into smaller chunks, monitor the bufferedAmount value carefully to avoid head-of-line blocking, and even implement its own priority queue for pending messages instead of blindly queuing them all on the socket.

Upvotes: 2

Lzh
Lzh

Reputation: 3635

Interesting question.

I think the button should be disabled and show a loading icon on the first time it is clicked. But maybe your UI enables more than one project to be loaded at once. Just thought for a second we might have an X-Y problem.

Anyways, to answer your question:

One way you can easily deal with this is to decouple the process of "requesting" data from the process of "getting and sending" data to the user when it is ready.

  • Don't await GetRealtimeTags and instead start a background task noting the connection id of the caller
  • Return nothing from GetRealtimeTags
  • Once the result is ready in the background task, call a new RealtimeTagsReady method that will call the JavaScript client with the results using the connection id kept earlier

Let me know if this helps.

Upvotes: 0

Related Questions