tg icon indicating copy to clipboard operation
tg copied to clipboard

SSE: single EventSource per browser for the same origin

Open 01es opened this issue 2 years ago • 5 comments

Description

Issue #1835 addressed the problem of multiple EventSource instances per web client, running in a single window/tab, by establishing a single EventSource per web client. However, for cases where users open multiple tabs, this turned out to perform worse than the original approach due to the fact that SSE channel would be initialised immediately (i.e. without opening a component such as an Entity Centre that may actually need it). This problem is especially critical for deployments that still rely on HTTP 1.1, where browsers would permit opening only upto 6 tabs, actually hanging on the 6ths.

To address this situation, it is proposed to develop a mechanism to utilise a single EventSource per browser. This way only one EvenSource instance would be used to communicate with the server side, but it would need to pass all SSE events to all instances of the same web client, running all other browser tabs/windows. If a client instance, which established an SSE channel dies, one of the remaining instances should establish a new SSE channel and take over that role of broadcasting SSE events to the rest.

Implementation details

Each web client generated a GUID, which it provides when establishing an SSE connection. A pair of <username, GUID> uniquely identifies each web client, running in a separate tab/window. This is why currently, every client that gets loaded in a separate tab/window by the same user, establishes its own SSE channel (i.e., GUID is different). This needs to be changed.

  • [x] 1. A first instantiated web client should generate and record <username, GUID> pair into localStorage. This value is to be used by all web clients, started by the same user in the same browser, when establishing an SSE channel. It is important to ensure that the server-side logic, responsible for accepting SSE requests, refuses to accept any duplicate requests, responding with a "close" command, so that the EventSource instance would not attempt to reconnect. Implementing just that would ensure that only one EventSource instance associated with a single tab could exist, even if multiple client instances were loaded in different tabs/windows by the same user.

A web client instance that established an SSE channel (let's call it "main") becomes responsible for broadcasting all SSE events to all other instances, running in different tabs/windows. This can be achieved by writing SSE events to localStorage when an SSE event is received by "main", and for all other instances to listen to the relevant storage events (https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event). This way, all clients would receive an SSE event – the main client via an SSE channel, and all other clients via a storage event – and would be able to process that event as per usual.

  • [x] 2. All instantiated web clients, including the main one, should sign up for a storage event with a handler which delegates an SSE event to the current handler. The main client is responsible for writing a received SSE event to the local storage to communicate it to the rest of the clients and to itself (this way there would a uniform processing of SSE events).

It is possible that the main client would get closed or even crashed. It is important to establish a mechanism (heartbeat) by which one of the remaining web clients would become "main".

  • [x] 3. The main client is responsible for self-identification to the rest of the clients by periodically (e.g., every 10 seconds – 5 seconds is used by the server) writing a heartbeat to localStorage. All clients except the main one, should sign up for a heartbeat storage event. In case if a heartbeat is missed, other clients should assume that the main one is no longer functioning and should try to establish an SSE channel with the server-end, by re-using an established <username, GUID> pair. Only one client should succeed as per item 1 above.

Expected outcome

Even more scalable SSE mechanism, involving a single EventSource instance at the client-side, running multiple client instances.

01es avatar Feb 03 '23 08:02 01es

The core idea, described in this issue, is sound – having a single SSE channel per web client, regardless of the number of instances open in different tabs/windows. However, there are 2 main problems with the current implementation, which need to be resolved for this issue to be considered further:

  1. The SSE registration logic at the server end need to ensure that the check for an SSE UID presence and creation of a corresponding emmuter is atomic. In introducing a reentrant lock would solve this problem, but could also introduce a bottle neck. Load testing is required to better understand the limitation of this approach.

  2. The logic for deciding if a new EventSource needs to be created upon application loading, relies on localStorage to check whether any other instance, loaded previously in a separate tab/window, had already created an EventSource. Unfortunately, localStorage does not provide any concurrency guarantees regarding read/write, which may lead to unpredictable behaviour if multiple tabs/windows are open simultaneously. Something like Java's ConcurrentHashMap.computeIfAbsent or the ability for a pessimistic lock, would solve this problem. However, there is nothing standard to support such operations. Alternatives need to be investigated.

01es avatar Feb 10 '23 01:02 01es

A question regarding item 2 has been posted on Stackoverflow at https://stackoverflow.com/questions/75406271/how-would-one-implement-localstorage-setitem-with-the-semantics-of-concurrent

01es avatar Feb 10 '23 02:02 01es

Also refer https://stackoverflow.com/questions/22001112/is-localstorage-thread-safe for a useful discusson. Note a newer answers.

01es avatar Feb 10 '23 02:02 01es

@oleh-maikovych, please research if we could use IndexDB instead of localStorage. Not sure if it can be used for messaging between tabs/windows, but should definitely be possible to use it for ensuring that only a single key value exists as per comments at https://stackoverflow.com/questions/75406271/how-would-one-implement-localstorage-setitem-with-the-semantics-of-concurrent?noredirect=1#comment133052825_75406271

01es avatar Feb 10 '23 03:02 01es

Exploration of IndexDB is also important for supporting offline operations in TG-based systems, which is on our technology roadmap (https://github.com/fieldenms/tg/issues/530).

Some useful links: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API https://www.w3.org/TR/IndexedDB/#dfn-mode

01es avatar Feb 10 '23 03:02 01es