O mneSkúsenostiBlogKontakt
LinkedInGitHubGitLabWhatsApp
Všetky články

3 min čítania

Prázdny polling

architecturesseangularspring-bootreal-time

Kolega vám niečo pošle. Nemali by ste kvôli tomu obnovovať stránku.

Prvá verzia to riešila pollingom: prehliadač sa každých pár sekúnd pýtal „niečo nové?“ a väčšinou počul „nie“. Skrátenie intervalu len znásobí prázdne požiadavky a notifikácia aj tak mešká až o celý interval. Polling škáluje cenu pýtania sa, nie rýchlosť dozvedenia sa.

Problém je jednosmerný: server vie, kedy sa niečo stane, klient sa to len potrebuje dozvedieť. To je vysielanie, nie rozhovor.

Server-Sent Events

SSE sedí presne: jedno dlho žijúce HTTP spojenie, do ktorého server posiela dáta. Žiadny polling, žiadny WebSocket handshake pre kanál, ktorý tečie len jedným smerom. V Spring Boote je to jeden SseEmitter na používateľa, držaný v registri:

@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter stream(@AuthenticationPrincipal User user) {
  SseEmitter emitter = new SseEmitter(Duration.ofMinutes(30).toMillis());
  registry.add(user.id(), emitter);
  emitter.onCompletion(() -> registry.remove(user.id(), emitter));
  emitter.onTimeout(() -> registry.remove(user.id(), emitter));
  return emitter;
}

void notify(UserId recipient, Notification event) {
  for (SseEmitter emitter : registry.get(recipient)) {
    try {
      emitter.send(SseEmitter.event().name("stream").data(event));
    } catch (IOException dead) {
      registry.remove(recipient, emitter);
    }
  }
}

Pasca s vlastnou hlavičkou

Stream je za rovnakou autentifikáciou ako všetko ostatné: za hlavičkou Authorization. Natívny EventSource v prehliadači ju poslať nedokáže.

event-source-polyfill hovorí rovnakým protokolom, ale prijme skutočné hlavičky. Práve tá hlavička bola jediný dôvod siahnuť po ňom.

Opätovné pripojenie je samostatná téma: natívny EventSource aj polyfill sa pripájajú znova sami. Tu je ich predvolené opakovanie nahradené obmedzeným exponenciálnym backoffom:

import { EventSourcePolyfill } from "event-source-polyfill";

connect(retry = 0): void {
  const stream = new EventSourcePolyfill("/api/stream", {
    headers: { Authorization: `Bearer ${this.token}` },
  });

  stream.addEventListener("stream", (event) => {
    this.show(JSON.parse((event as MessageEvent).data));
    retry = 0;
  });

  stream.onerror = () => {
    stream.close();
    const delay = Math.min(30_000, 1_000 * 2 ** retry);
    window.setTimeout(() => this.connect(retry + 1), delay);
  };
}

Práve ten backoff je rozdiel medzi demom a niečím, na čo sa dá spoľahnúť celý pracovný deň: spadnuté spojenie sa zotaví samo a nikto nemusí obnovovať stránku.

Tú istú myšlienku, aktualizácie posielané zo servera cez jedno dlho žijúce spojenie, som neskôr spracoval do malej Kotlin knižnice: stream-pulse, publikovanej na JitPacku.

Výsledok

Notifikácie prichádzajú v momente, keď kolega koná, nie s oneskorením o interval. Server neodpovedá na žiadne prázdne polly. Spadnuté spojenie sa pripojí samo.

Nástroj bol malý. Inžinierstvo bolo všetko okolo neho.