SSE connecting twice if session terminated at server

I’m doing testing with my code, thus the reason for finding these problems!

I’ve noticed that if I have a session running and then terminate that session on the server (using ServerEvents.UnRegisterAsync(request.Id)), the JavaScript client successfully reconnects, however it then connects again so two sessions are created on the server. The first one then drops off after timing out.

I removed the ss-utils.js file and used the native EventSource and this behaviour doesn’t seem to occur?

The SSE Client library has auto reconnection the vanilla EventSource object which it wraps does not. When you unregister you’re removing that SSE connection subscription from the server so it no longer exists & any attempt to access it will result in an error.

Ok, so I’m seeing the following sequence of events:

/event-heartbeat?id=jAFOPfYPkN4x9mBiUm0O
= This request returns 404 not found (session has been deleted from server)

/event-stream?t=1591100656386&channels=0c328a71c14f4a208ebe7bc9672089ed
= This is the reconnection after the session has been deleted. This connection stays open until…

/event-stream?t=1591100656386&channels=0c328a71c14f4a208ebe7bc9672089ed
= The reconnection gets dropped, and this one is opened. This means there are two sessions on the server until the ‘reconnection’ gets timed out.

Does that make sense?

How many onConnect / onReconnect events are fired?

$(source).handleServerEvents({
    handlers: {
        onConnect: function(o) { console.log('onConnect',o) },
        onReconnect: function(o) { console.log('onReconnect',o) }
    }
});

Ok, I saw the following run of events:

onConnect (initial connection)
[Session is deleted on server]
onConnect (reconnect?)
onReconnect
onConnect (second connection)

Not able to repro this, I’m only seeing the expected single reconnection in both the network connections:

And the callbacks:

No worries, I’ll delve a bit deeper, must be something conflicting or making it behave strange.

Very odd. I’ve stripped the page and code back to nothing and I’m still seeing the issue.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <title>SSE Test Harness</title>
    <meta charset="UTF-8" />
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>

<body>
    <script src="/display/legacy/js/jquery.min.js"></script>
    <script src="/js/ss-utils.js"></script>
    <script>
        $(function() {
            var source = new EventSource('/event-stream?channel=0c328a71c14f4a208ebe7bc9672089ed&t=' + new Date().getTime());
                $(source).handleServerEvents({
                    handlers: {
                        onConnect: function (o) { console.log('onConnect',o) },
                        onReconnect: function(o) { console.log('onReconnect',o) },
                        onJoin: function (user) { },
                        onLeave: function (user) { }
                    }
                });
        });
    </script>
</body>
</html>

Exact same behavior, only 1 connection:

I’ve replaced the jquery ref in your page to use the latest jquery:

<script src="https://code.jquery.com/jquery-3.5.1.min.js" 
   integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" 
   crossorigin="anonymous"></script>

Are you using the latest v5.8.1 on MyGet? The latest v5.8.1 also has the updated ss-utils.js which uses sendBeacon().

I haven’t changed the entire ServiceStack package to 5.8.1, I’m just using the updating ss-utils.js.

Let me try the new jquery.

Same behaviour with later jQuery:

onConnect
POST http://127.0.0.1:8090/event-heartbeat?id=6NmVl8DLrQht8za7vWdX 404 (Subscription '6NmVl8DLrQht8za7vWdX' does not exist)
onReconnect {readyState: 4, getResponseHeader: ƒ, getAllResponseHeaders: ƒ, setRequestHeader: ƒ, overrideMimeType: ƒ, …}
onConnect {userId: "-4", isAuthenticated: "false", displayName: "user4", channels: "0c328a71c14f4a208ebe7bc9672089ed", createdAt: "1591108312711", …}
onReconnect Event {isTrusted: true, type: "error", target: EventSource, currentTarget: EventSource, eventPhase: 0, …}
onConnect {userId: "-5", isAuthenticated: "false", displayName: "user5", channels: "0c328a71c14f4a208ebe7bc9672089ed", createdAt: "1591108321241", …}

I’ve stripped most of the code, the only difference is I’m adding an extra entry in the subscription on creation:

Plugins.Add(new ServerEventsFeature
{
OnCreated = (sub, req) => { sub.ServerArgs["lastHeartbeat"] = DateTime.Now.ToString(); }
});

Will need a stand-alone repro to be able to investigate any further. I’m calling ServerEvents.Reset() to remove the connection from the server. e.g:

public void Any(Reset request) => ServerEvents.Reset();

I’ll see what I can put together, I’m using:

await ServerEvents.UnRegisterAsync(request.Id);

Ok, using .Reset() stops this issue from occurring.

Am I using the wrong call to remove a subscription/connection?

You’re forcibly killing the connection on the server, the right way is for the client to close their own connection, e.g:

$.ss.disposeServerEvents();

In certain environments/browsers we have seen SSE sessions stay open and due to operational requirements, there is a need for a user to be able to forcibly remove the session from the admin UI.

Is there no way to accomplish this?

Call the unRegisterUrl which is stored on the subscription.ConnectArgs["unRegisterUrl"].

Don’t understand, are you saying that there are zombie server connections that stay forever after the client is gone or that you have operational requirements to kill active connections from the server?

We’ve found zombie sessions that stay when the client has gone. The deletion needs to be done at the server.

I don’t understand the purpose of the UnRegisterAsync function? I thought this would wipe unregister the subscription and the client would gracefully reconnect if still active?

The public endpoint is the unregisterUrl which does call UnRegisterAsync, this is the same URL the client uses to unregister itself.

The graceful way to close a connection is always have the client close their own connection, the client auto reconnects as part of its built in resilience to recover from a broken connection.

IMO I’d be investigating why the zombie connections remain open when the clients have disappeared, if there’s nothing keeping it alive with heartbeats it should be closed by the server. What’s the state of its IEventSubscription? (i.e. LastPulseAt/IsDisposed/Disposing/Response.IsClosed) what happens if you send an event to it, (e.g. PublishRawAsync("\n\n") etc.