ADR-0027: The SSE events topic carries data, with Last-Event-ID replay
- Status: accepted
- Date: 2026-09-04
- Deciders: Mahdi Amirabdollahi
Context
ADR-0018 built the SSE hub on the principle that events are change signals, not data — {topic,clusterId,ts}, and the client refetches the matching TanStack Query key. That is right for every poll-derived topic: there is a server-side resource behind topology / health / queues, so "something changed, refetch" is enough.
Phase 4's broker events are different. A live feed of "consumer X on address Y, node Z, at T" has no query key to refetch — there is no server-side list endpoint whose contents just changed, there is a stream of facts. And the consumers / sessions / connections resource views that a notification implies are stale are served by live per-node broker reads (PagedListService.fanOut), so a signal per notification would be one Jolokia call per node per notification — an uncoalesced chatty broker turning push into a self-inflicted DoS (non-negotiable #1).
Decision
We will extend ADR-0018, not edit it. Its decision — SSE, one endpoint, named events, two-failure fallback, signal topics carry no data — stays binding for every topic except events.
- The
eventstopic carries the fullBrokerEventpayload and sets the SSEid:line to thebroker_event.seq.SseHub.publishgains apublish(clusterId, topic, data, eventId)overload; the existingpublish(clusterId, topic)delegates with nulls and the signal envelope is untouched. - A reconnecting client replays what it missed.
GET /api/v1/streamreads theLast-Event-IDheader; when present and the client wantsevents, the controller flushesBrokerEventService.since(clusterId, lastId, 500)before live delivery. The replay is bounded — a client gone longer than the cap or the retention window gets at most the 500 most-recent missed events. - Derived staleness is coalesced.
TopicCoalescercollapses theconsumers/sessions/connections/queuessignals a burst of notifications implies into at most one signal per(cluster, topic)per coalescing window (artemis-studio.events.coalesce-window-millis, default 1000). The firsttouchin a window schedules the signal; later touches are absorbed.StreamSignals' signature machinery is for poll dedup and is not reused — push events are already edge-triggered. KNOWN_TOPICSgainsevents,consumers,sessions,connections. An unknown topic in the CSV is still filtered out; a request that matches nothing falls back to the three signal defaults (not all known topics —eventsis heavier and opt-in).- The frontend
Topicunion gains the four names in the same change, because the controller filters unknown topics rather than rejecting them — a name that exists on only one side fails silently.
Consequences
- The events screen and a reconnecting browser get history without a cross-instance mechanism; the
broker_eventtable (ADR-0028) is the backing store andseqis both the PK and the cursor. - One more
SseHuboverload and a coalescer bean. No WebFlux, no STOMP, ADR-0018's mechanism is unchanged. - The coalescing window is a latency/throughput knob: too short and a reconnect storm still costs; too long and the resource views feel stale. 1s is the starting point, tunable at runtime.
Alternatives considered
- Signal-only + a paged
/eventsendpoint the client refetches — stays literally inside ADR-0018 but a live feed refetching a paged endpoint on every event is wasteful and races the live edge. - One signal per notification — a 500-consumer reconnect storm becomes 500 × (nodes) Jolokia calls.
- Editing ADR-0018 — its "events are signals" decision is still correct for every other topic; superseding it wholesale would misrepresent the design.