Skip to content
This repository has been archived by the owner on Dec 18, 2018. It is now read-only.

SSE transport times out behind IIS due to idle connection #835

Closed
BrennanConroy opened this issue Sep 8, 2017 · 5 comments
Closed

SSE transport times out behind IIS due to idle connection #835

BrennanConroy opened this issue Sep 8, 2017 · 5 comments

Comments

@BrennanConroy
Copy link
Member

After 120 seconds of no messages through the SSE transport it will be closed by IIS as an idle connection. Adding keep-alives could fix this.

@Externaluse
Copy link

I have tried this as illustrated in my question on SO: .Net Core SignalR - connection timeout - heartbeat timer - connection state change handling

However, I don't manage to restart the connection after net::ERR_NETWORK_IO_SUSPENDED (e.g. when resuming after hibernation).

What I've tried to do is

var connection = null;
var heartBeatTockTimer;
registerSignalRConnection();
function registerSignalRConnection() {
    if (connection !== null) {
        try {
            connection.stop().catch(err => console.log(err));
        } catch (exception) {
            console.log(exception);
        }
    }
    connection = new signalR.HubConnection("/signalr", { transport: SignalR.TransportType.ServerSentEvents });
    connection.on("Heartbeat", serverTime => { console.log(serverTime); });
    connection.start().then(args => {
        heartBeatTockTimer = setInterval(sendHeartBeatTock, 10000);
     }).catch(args => console.log("Error connecting", args));
     return connection.connection.connectionState;

The client heartbeat then looks like this

function sendHeartBeatTock() {
try {
    console.log("Standard attempt HeartBeatTock");
    connection.invoke("HeartBeatTock").catch(err => {
        console.log("HeartbeatTock Standard Error", err);
            setTimeout(function() {
                registerSignalRConnection();
                    connection.invoke("HeartBeatTock").catch(err => {
                        console.log("Error after HeartbeatTock Restart", err);
                        clearInterval(heartBeatTockTimer);
                });    
            }, 5000);                        
       });
} catch (exception) {
    console.log("caught exception on standard attempt", exception);
}
}

Is there a better way to check/restore a connection when invoke fails?

@moozzyk
Copy link
Contributor

moozzyk commented Nov 10, 2017

You did not show what error you get so I am just shooting in the dark - when you are starting after hibernation you may want to try a few start attempts with a timeout.

@Externaluse
Copy link

The error occurs when connected to a Hub, then putting the computer to sleep. When resuming, you'd get "POST https://localhost:44311/signalr?id=df54e3b8-e395-4a2f-89f4-8345bb0df99c net::ERR_NETWORK_IO_SUSPENDED" in signalr-client-1.0.0-alpha2-funal.js:159
This gets caught as HttpError at XMLHttpRequest.xhr.onerror
Using my retry method, the next call results into a 404 because the connectionId is gone; HttpError: Not Found at XMLHttpRequest.xhr.onload

At this point, I wanted to wait for 10 seconds and try again, but it seems I'm getting confused in how to set this up properly due to the asynchronous nature of the library. Beginner, sorry!

This is what I am trying:

var connection = null;
var heartBeatTockTimer; // It's named tock as opposed to tick from the server...
var retryAttempts = 5;
var retryWaitSeconds = 10;
var heartBeatTockTimerSeconds = 10;
// helper function to delay execution
$.wait = function(miliseconds) {
    var defer = $.Deferred();
    setTimeout(function() { defer.resolve(); }, miliseconds);
    return defer;
};
registerSignalRConnection();

function registerSignalRConnection() {
    if (connection !== null) {
        console.log("registerSignalRConnection was not null", connection);
        connection.stop().catch(err => console.log(err));                
    }
    console.log("Creating new connection");
    connection = new signalR.HubConnection("/signalr", { transport: signalR.TransportType.ServerSentEvents });
    connection.start().then(() => {
        console.log("Connection started, starting timer.");
        heartBeatTockTimer = setInterval(sendHeartBeatTock, heartBeatTockTimerSeconds * 1000);
        return connection;
    }).catch(exception => {
        console.log("Error connecting", exception, connection);
        return null;
    });

The function HeartBeatTock that is registered with the timer is supposed to detect when errors occur, and then try for a number of attempts to reconnect to the Hub. At some point I'd like to display a notification after the max number of retries are exceeded:

function sendHeartBeatTock() {
    console.log("Standard attempt HeartBeatTock");
    connection.invoke("HeartBeatTock").then(() => { console.log("HeartbeatTock worked.")}).catch(err => {
        console.log("HeartbeatTock Standard Error", err);       
        // retryInterval is the ID returned by setInterval to stop it once successful
        let retryInterval = 0;
        let attempts = 0;
        retryInterval = setInterval(function () {                                                    
            $.wait(retryWaitSeconds * 1000).then(function () {
                ++attempts;
                console.log("executing attempt #" + attempts.toString());
                // I tried to return connection from registerSignalRConnection() in order to use .then(), but that didn't quite work out. Lost here now... and if it returned null it'd need checking I guess - where?
                registerSignalRConnection().then(() => {
                    connection.invoke("HeartBeatTock").then(() => { console.log("HeartbeatTock worked, Attempt #".attempts.toString()) }).catch(err => {
                        console.log("Error after HeartbeatTock Restart #" + attempts.toString(), err);
                         clearInterval(heartBeatTockTimer);
                     });
                });
                // success or give up?
                console.log("Connection: ", connection.connection.state, "attempt: ", attempts);
                if (connection.connection.state == 2 || attempts > retryAttempts) {
                    clearInterval(retryInterval);
                }
           });
        }, retryWaitSeconds * 1000);
    });
}

Because this all invoked and deferred, I think I'm resetting attempts and timers all the time. The main purpose is achieved - the connection is kept alive forever, but I am unable to make it reconnect after the computer went to sleep. What you are suggesting is what I am trying to do, but seem unable to figure out how. I did notice that a few issues appeared regarding Websockets behind IIS and the timeouts of SSE, so I thought this may help others as well - would be nice to see an example in a sample.
In any case, thank you for your time and help.

@Externaluse
Copy link

I think I have figured out a solution now. My error above was that I was trying to re-register the connection at the same time (or with the same delay) as the next invocation of sendHeartBeat was running; essentially creating an error loop.
I have summarised it on SO https://stackoverflow.com/questions/47208844/net-core-signalr-connection-timeout-heartbeat-timer-connection-state-chan

@davidfowl davidfowl modified the milestone: 2.1.0-preview2 Feb 17, 2018
@davidfowl
Copy link
Member

We have keep alives, this should fix things.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants