Long Polling: kiedy serwer opóźnia odpowiedź
Polling ma prosty problem:
client -> anything new?
server <- no
client -> anything new?
server <- no
Serwer odpowiada natychmiast, mimo że klient nie potrzebuje odpowiedzi „brak zmian”.
Long polling zmienia czas odpowiedzi.
Request pozostaje otwarty
Klient wysyła:
GET /events?after=1842
Jeżeli nie ma nowych danych, backend nie odpowiada od razu.
client -> request ────────────────┐
│
wait │
│
event! │
client <- response ───────────────┘
Przykładowa odpowiedź:
{
"id": 1843,
"type": "order.completed",
"orderId": 42
}
Po jej odebraniu klient natychmiast rozpoczyna kolejny request:
async function listen(after) {
while (true) {
const response =
await fetch(`/events?after=${after}`);
const event = await response.json();
handle(event);
after = event.id;
}
}
To nadal jest HTTP request/response.
Nie powstał trwały kanał wiadomości. Po prostu odpowiedź może nadejść dużo później.
Timeout jest częścią protokołu
Żaden request nie powinien wisieć bez końca.
Reverse proxy, load balancer albo sam backend może mieć limit długości requestu.
Dlatego typowy model wygląda tak:
request
|
v
event appears -> respond
or
timeout -> empty response
Klient w obu przypadkach rozpoczyna następny request.
Przykład:
t=0 request A
t=25 timeout
t=25 request B
t=41 event
t=41 response B
t=41 request C
Long polling jest więc ciągiem długich requestów, a nie jednym nieskończonym requestem.
Musimy wiedzieć, co już widzieliśmy
Reconnect tworzy istotny problem.
Załóżmy:
event 1843 generated
response sent
network breaks
Czy klient otrzymał event?
Backend nie może tego pewnie wywnioskować tylko z faktu wysłania odpowiedzi.
Dlatego klient często przekazuje cursor:
GET /events?after=1842
Po odebraniu 1843 następny request będzie:
GET /events?after=1843
To wprowadza pojęcia, które wrócą później:
event identity
cursor
resume
duplicate delivery
Transport i delivery semantics to dwie różne rzeczy.
Mniej requestów, ale więcej otwartych requestów
Polling:
few open requests
many total requests
Long polling:
many open requests
fewer total requests
Przy dużej liczbie klientów backend i infrastruktura muszą dobrze obsługiwać dużą liczbę jednocześnie oczekujących operacji I/O.
W modelu thread-per-request może to być kosztowne.
W event-driven/non-blocking I/O tysiące oczekujących socketów są znacznie bardziej naturalne.
Mechanizm komunikacji wpływa więc bezpośrednio na model wykonania backendu.
Dlaczego to działało tak dobrze?
Long polling daje serwerowi możliwość niemal natychmiastowego dostarczenia zmiany bez wymagania osobnego protokołu realtime.
Przy prawidłowo zestawionym keep-alive kolejne requesty mogą wykorzystywać istniejące połączenie transportowe.
To ważne rozróżnienie:
HTTP request lifetime
!=
TCP connection lifetime
Kończymy request, niekoniecznie fizyczne połączenie.
Ale nadal kończymy response
Jeżeli serwer ma trzy zdarzenia:
event A
event B
event C
long polling zwykle wykonuje cykl:
request
<- event A
request
<- event B
request
<- event C
To prowadzi do prostego pytania:
skoro HTTP response body może być przesyłane stopniowo, dlaczego musimy kończyć response po pierwszym zdarzeniu?
Nie musimy.
Jedna odpowiedź może być strumieniem danych.
To jest HTTP streaming.