.Net Core SignalR - connection timeout - heartbeat timer - connection state change handling

ExternalUse picture ExternalUse · Nov 9, 2017 · Viewed 7.9k times · Source

just to be clear up-front, this questions is about .Net Core SignalR, not the previous version.

The new SignalR has an issue with WebSockets behind IIS (I can't get them to work on Chrome/Win7/IIS express). So instead I'm using Server Sent Events (SSE). However, the problem is that those time out after about 2 minutes, the connection state goes from 2 to 3. Automatic reconnect has been removed (apparently it wasn't working really well anyway in previous versions).

I'd like to implement a heartbeat timer now to stop clients from timing out, a tick every 30 seconds may well do the job.

Update 10 November

I have now managed to implement the server side Heartbeat, essentially taken from Ricardo Peres' https://weblogs.asp.net/ricardoperes/signalr-in-asp-net-core

  • in startup.cs, add to public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider serviceProvider)
    app.UseSignalR(routes =>  
    {  
        routes.MapHub<TheHubClass>("signalr");  
    });

    TimerCallback SignalRHeartBeat = async (x) => {   
    await serviceProvider.GetService<IHubContext<TheHubClass>>().Clients.All.InvokeAsync("Heartbeat", DateTime.Now); };
    var timer = new Timer(SignalRHeartBeat).Change(TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(30));            
  • HubClass

For the HubClass, I have added public async Task HeartBeat(DateTime now) => await Clients.All.InvokeAsync("Heartbeat", now);

Obviously, both the timer, the data being sent (I'm just sending a DateTime) and the client method name can be different.

Update .Net Core 2.1+

See the comment below; the timer callback should no longer be used. I've now implemented an IHostedService (or rather the abstract BackgroundService) to do that:

public class HeartBeat : BackgroundService
{
    private readonly IHubContext<SignalRHub> _hubContext;
    public HeartBeat(IHubContext<SignalRHub> hubContext)
    {
        _hubContext = hubContext;
    }
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await _hubContext.Clients.All.SendAsync("Heartbeat", DateTime.Now, stoppingToken);
            await Task.Delay(30000, stoppingToken);
        }            
    }
}

In your startup class, wire it in after services.AddSignalR();:

services.AddHostedService<HeartBeat>();
  • Client
    var connection = new signalR.HubConnection("/signalr", { transport: signalR.TransportType.ServerSentEvents });
    connection.on("Heartbeat", serverTime => { console.log(serverTime); });

Remaining pieces of the initial question

What is left is how to properly reconnect the client, e.g. after IO was suspended (the browser's computer went to sleep, lost connection, changed Wifis or whatever)

I have implemented a client side Heartbeat that is working properly, at least until the connection breaks:

  • Hub Class: public async Task HeartBeatTock() => await Task.CompletedTask;
  • Client:

    var heartBeatTockTimer; function sendHeartBeatTock() { connection.invoke("HeartBeatTock"); } connection.start().then(args => { heartBeatTockTimer = setInterval(sendHeartBeatTock, 10000); });

After the browser suspends IO for example, the invoke method would throw an exception - which cannot be caught by a simple try/catch because it is async. What I tried to do for my HeartBeatTock was something like (pseudo-code):

function sendHeartBeatTock
    try connection.invoke("HeartbeatTock)
    catch exception
         try connection.stop()
         catch exception (and ignore it)
         finally
              connection = new HubConnection().start()
    repeat try connection.invoke("HeartbeatTock")
    catch exception
        log("restart did not work")
        clearInterval(heartBeatTockTimer)
        informUserToRefreshBrowser()

Now, this does not work for a few reasons. invoke throws the exception after the code block executes due to being run asynchronous. It looks as though it exposes a .catch() method, but I'm not sure how to implement my thoughts there properly. The other reason is that starting a new connection would require me to re-implement all server calls like "connection.on("send"...) - which appears silly.

Any hints as to how to properly implement a reconnecting client would be much appreciated.

Answer

Pawel picture Pawel · Nov 9, 2017

This is an issue when running SignalR Core behind IIS. IIS will close idle connections after 2 minutes. The long term plan is to add keep alive messages which, as a side effect, will prevent IIS from closing the connection. To work around the problem for now you can:

  • send periodically a message to the clients
  • change the idle-timeout setting in IIS as described here
  • restart the connection on the client side if it gets closed
  • use a different transport (e.g. long polling since you cannot use webSockets on Win7/Win2008 R2 behind IIS)