Use Server-sent events to add live updates for files, players, and markers#819
Conversation
…rkers SSE Implementation: - A `SseConnectionManager` is created per-map in the `MapRequestHandler`. - Whenever a client connects to the `live/sse` endpoint, a new `SseConnection` is created for it and is added to the `SseConnectionManager`. - The `SseConnectionManager` receives requests to push updates and sends them to all known `SseConnections`. If sending fails, the connection is cleaned up and removed. - Sending happens on a background thread so normal server operations are not blocked on clients picking up events. Backend events: - Tile updates are event-based: when a new tile is rendered an event is emitted which causes the tile's X+Y and lod to be broadcasted to all connected clients. - Players and other markers are still polling-based, but the polling is done server-side (and only once per endpoint) and connected clients are notified only when the data changes. - Polling was implemented by replacing the `CachedRateLimitDataSupplier` with a `LiveDataSupplierBroadcaster`. This `Supplier` wrapper can be used in the same way where requests to `get()` it's data are rate-limited and cached. However, it also runs a background thread that (when listeners are added) will automatically poll the supplier and send events when the data changes. Frontend changes: - On page load, an `EventSource` connects to the `<map>/live/sse` endpoint and watches for events. - "tile" updates cause the specified tiles to be force-reloaded - "player" updates are fed directly into the existing `playerMarkerManager` - "marker" updates are fed directly into the existing `markerFileManager` - Since this takes the place of the marker managers polling, the marker managers were updated to be able to be paused, and are started in a paused state. - In order to ensure that markers are still updated if the SSE endpoint can't be contacted, the existing polling marker managers will be unpaused if the SSE connection fails to connect or disconnects. If the SSE connection returns, the marker managers will be paused again. - Tile force-reloading is handled by removing the specified tile from the `revalidatedUrls` cache, the calling `load` on it. This forces the normal tile loading mechanism to update the tile, reloading it.
This is an Nginx-specific header, but may be supported by other reverse proxies as well. At the very lest, it can't hurt?
The polling of data at a fixed interval is now scheduled by the BlueMap.SCHEDULER and the work of doing the updates is run in the BlueMap.THREAD_POOL. This makes the LiveDataSupplierBroadcaster only responsible for starting/stopping the scheduled task when listeners connect/disconnect. This avoids the overhead of having to spin up a polling thread for every supplier * every map, as well is a much simpler implementation.
Previously a single slow consumer of events would block all other consumers. This was because the `SseConnectionManager` would handle sending data to each connection in a single worker thread. This commit refactors the code so that the `SseConnectionManager` is no longer responsible for actually sending the data to each connection. Instead, it simply enqueues events to every managed `SseConnection` which handles sending them. Details: - Each `SseConnection` now has a queue and a virtual thread that pulls events off it and sends them to the consumer. This means that a slow consumer now only blocks its own connection. - The per-connection queues now limit how many events can be queued to send. After the limit is reached, any new events are simply dropped to avoid slow clients forcing the server to buffer an unlimited number of old events. - An `onClose` callback can now be registered on the `SseConnection` class. This allows it to own its entire lifecycle, while still allowing the `SseConnectionManager` to be notified when it's been closed and needs removing from the set of managed connections.
|
Doing the latest SSE connection refactor made me realize that the way that the SSE data is written could probably be optimized, though it would take some changing of code more on the HTTP sever side of things. Currently the This first event buffer was required before the refactor in order to ensure that we could write an event and move on to the next connection without waiting for the client to consume the data. However, since the I think it would save the creation of a virtual thread, 2 buffers, some cross-thread overhead, and would avoid having to make this wider change. However, it would involve some amount of changes more on the HTTP server side which might be a bit out of scope for at least this initial version. |
@pR0Ps I already had the exact same thought! Another idea i had was moving the tile-change listeners from the HiresModelManager and the LowresTileManager, over to the Storage implementation. Which would imho be cleaner and a lot more powerful, since Addons then could use Listeners on the GridStorage/ItemStorage as well, and i am not yet 100% happy with having these listeners inside HiresModelManager/LowresTileManager.. |
TBlueF
left a comment
There was a problem hiding this comment.
Alright, i think i would be fine with merging it like this for now, and then any improvements can be made in follow-up PR's :)
If you agree with that, and with the latest changes i made, then i would press that merge button 👍
|
Agreed, I think this is good to go as-is. All your changes look good, thanks for adding SSE support to the CLI version! |
SSE Implementation:
SseConnectionManageris created per-map in theMapRequestHandler.live/sseendpoint, a newSseConnectionis created for it and is added to theSseConnectionManager.SseConnectionManagerreceives requests to push updates and sends them to all knownSseConnections. If sending fails, the connection is cleaned up and removed.Backend events:
CachedRateLimitDataSupplierwith aLiveDataSupplierBroadcaster. ThisSupplierwrapper can be used in the same way where requests toget()it's data are rate-limited and cached. However, it also runs a background thread that (when listeners are added) will automatically poll the supplier and send events when the data changes.Frontend changes:
EventSourceconnects to the<map>/live/sseendpoint and watches for events.playerMarkerManagermarkerFileManagerrevalidatedUrlscache, the callingloadon it. This forces the normal tile loading mechanism to update the tile, reloading it.