Bridge broker events onto SSE streams and replay history on reconnect - #48
Merged
Conversation
Viewers can now open GET /data/projects/{projectId}/events as a
server-sent-events stream. A registry keeps the open streams per project
and fans incoming payloads out on an executor, evicting dead emitters on
send failure; a scheduled comment heartbeat keeps idle streams alive
through nginx's 60-second proxy read timeout, and the path rides the
existing /data/ route so no proxy changes are needed (response buffering
is disabled per-response with X-Accel-Buffering).
Authorization at stream-open mirrors the STOMP subscribe check: the
caller must hold ViewProject on the project, with identity resolution
isolated in one seam that #305 replaces with a short-lived redeemable
ticket. The Last-Event-ID header (and a lastEventId query fallback for
fresh loads, which never send the header) is plumbed to a catch-up seam
whose real replay implementation arrives with the sequence work; streams
also get a finite lifetime so every reconnect re-authorizes.
Nothing publishes into the registry yet — the broker bridge lands
separately once events carry real sequence numbers (#296).
Part of protegeproject/webprotege-gwt-ui#304
(epic protegeproject/webprotege-gwt-ui#303).
…tags Every pushed event window carried the hardcoded tags 0 to 1, so after the first window advanced a client's bookmark past 1, the client's stale- window guard dropped every subsequent push — the websocket went silent after one event and delivery quietly fell back to the 10-second poll. That is the direct cause of the ~9.5s median UI latency measured in the epic's baseline. The emitter now consumes the sequenced event that the event-history service publishes after archiving a change bundle, and pushes it with truthful tags: startTag is the previous ordinal, endTag is the bundle's ordinal. Because that event is emitted strictly after persistence, anything pushed is already fetchable through the pull path. The wire payload shape is unchanged, so the client deserializes it exactly as before. The error log now includes the exception so failures are diagnosable. The gateway half of protegeproject/webprotege-gwt-ui#296 (part of protegeproject/webprotege-gwt-ui#303).
The sequenced change events the gateway consumes now fan out to the open SSE streams as well as the STOMP topic, carrying the per-project sequence ordinal as the SSE event id and the same truthful tags in the payload. Both sinks send the identical payload object, so the bytes a client sees are the same regardless of transport, and the registry hands each emitter write to its dispatch executor so a slow stream can never stall the broker listener thread. STOMP delivery stays in place until the old transport is retired. Part of protegeproject/webprotege-gwt-ui#304 (epic protegeproject/webprotege-gwt-ui#303).
A stream that opens with a last-seen event id now gets exact catch-up: the gateway queries the durable event archive for everything after that id, sends the result as a single replay frame whose id is the archive's end position, and only then releases live delivery. Live events arriving during the replay are buffered per subscriber and flushed in order with anything at or below the replay position dropped, so nothing is missed and nothing is delivered twice regardless of how the broker and the query interleave. The buffer is bounded; on overflow or a failed history query the stream stays alive and falls back to live-only, where the client's gap detection recovers the difference. Fresh connections carry no id and start at now — project open downloads no history on this transport either. Part of protegeproject/webprotege-gwt-ui#304 (epic protegeproject/webprotege-gwt-ui#303).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Commits 2–3 of protegeproject/webprotege-gwt-ui#304 (part of protegeproject/webprotege-gwt-ui#303). Stacked on #47.
Bridge: the sequenced change events fan out to open SSE streams alongside STOMP (identical payload bytes on both sinks; STOMP retired by #308), with the per-project sequence as the SSE
idand emitter writes on the dispatch executor — a slow stream can never stall the broker listener.Replay: a reconnect with
Last-Event-ID(or?lastEventId=) queries the event archive for everything after that id, sends one replay frame (id = archive end position), then flushes per-subscriber-buffered live events dropping anything ≤ the replay position — nothing missed, nothing double-delivered, regardless of interleaving. Bounded buffer; failed/overflowing catch-up degrades to live-only and the client's #297 gap detection recovers.Tests: full gateway suite 45/45 under JDK 17, including a Testcontainers context-load proving the wiring. Known deliberate tradeoff: the history query blocks the request thread during catch-up (same pattern as the existing permission check); noted in code for a future non-blocking pass.