Polling — клиент спрашивает «готово?» через интервалы. Long polling — спрашивает один раз, сервер ждёт изменения и отвечает, после чего клиент спрашивает снова. Оба работают через обычный HTTP. Главные решения аналитика: что именно читаем, как долго ждём, как продолжаем после обрыва и когда прекращаем попытки.
Заказали справку в приложении. Генерация занимает минуту, но HTTP-запрос создания не обязан висеть всё это время. Сервер принимает работу, возвращает её идентификатор, а приложение отдельно наблюдает за статусом. Я бы не начинал такой сценарий с постоянного сокета: сначала посчитал бы, сколько стоит простая проверка.
Как устроен обычный polling?
POST /api/reports
→ 202 Accepted
Location: /api/reports/r-42
GET /api/reports/r-42
→ 200 OK
{"id":"r-42","status":"running","progress":40}
через 5 секунд: GET /api/reports/r-42
→ 200 OK
{"id":"r-42","status":"succeeded","downloadUrl":"/api/reports/r-42/file"}
Это учебный контракт: названия статусов и интервал выбирает команда. Ответ 202 означает, что работу приняли, а не закончили. Состояния succeeded, failed и cancelled считаем терминальными: после них опрос прекращается. Ошибка сети — отдельная ось, она не превращает выполняющийся отчёт в failed.
Не запускайте новый запрос по таймеру, пока предыдущий ещё выполняется. Иначе на медленной сети запросы наложатся, а запоздалый ответ может затереть более свежий. Проще назначать следующую проверку после завершения предыдущей. При уходе со страницы отменяем ожидание; при возврате сразу перечитываем актуальное состояние.
Сколько стоит «раз в пять секунд»?
Допустим, одновременно открыто 10 000 экранов. Интервал — пять секунд, запросы быстрые. Получаем примерно 10 000 / 5 = 2 000 запросов в секунду, даже если ничего не меняется. Если изменение происходит равномерно между проверками, среднее ожидание следующего опроса — около 2,5 секунды, плюс сеть и обработка. Это оценка для наших допущений, не обещание SLA.
Условный GET с ETag может уменьшить тело ответа, если ресурс не менялся, но не убирает сам запрос. Для нашей справки полезнее ограничить время наблюдения, снижать частоту долгого ожидания и не опрашивать скрытый экран без необходимости. Разбор лимитов — в главе rate limiting.
Что меняет long polling?
Клиент отправляет запрос. Если новые события есть, сервер отвечает сразу. Если нет — держит запрос открытым до появления данных или согласованного срока. Получив ответ, клиент начинает следующий запрос. В отличие от SSE, здесь каждый ответ заканчивается, а не продолжается новыми событиями.
sequenceDiagram participant C as Браузер participant S as Сервер C->>S: GET events after=c17 wait=25 Note over S: Нет новых событий, ждём S-->>C: 200 events + nextCursor=c18 C->>S: GET events after=c18 wait=25 Note over S: Время ожидания истекло S-->>C: 200 empty + nextCursor=c18 C->>S: GET events after=c18 wait=25
Браузер ждёт изменения после курсора c17. Сервер отдаёт событие и новый курсор c18. Следующее ожидание заканчивается без событий; курсор остаётся прежним, клиент продолжает с него. Пауза между запросами безопасна только если сервер сохраняет события для последующего чтения. Ограничения долгих запросов и посредников разобраны в RFC 6202.
Как выглядит контракт ожидания?
GET /api/report-events?after=c17&wait=25
200 OK
Cache-Control: no-store
Content-Type: application/json
{
"events": [
{"id":"e18","reportId":"r-42","revision":8,"status":"succeeded"}
],
"nextCursor":"c18"
}
Без событий: 200 {"events":[],"nextCursor":"c17"}
Курсор уже удалён: 410 {"code":"CURSOR_EXPIRED"}
И 200 с пустым массивом, и 410 здесь — наш выбор API, а не встроенные правила long polling. Можно договориться о 204 без тела, но тогда нельзя одновременно требовать JSON в этом ответе. Параметр wait сервер ограничивает сверху; клиент не должен удерживать ресурсы произвольное время.
В учебном варианте ждём на сервере не более 25 секунд. Клиентский таймаут — 35 секунд, таймаут посредника — больше серверного ожидания с запасом. Реальные числа проверяем на всей цепочке. Это не универсальные настройки для любого балансировщика.
Зачем курсор, если есть дата события?
Два события могут иметь одинаковое время, а часы разных узлов — расходиться. Курсор обозначает позицию в упорядоченном журнале в рамках конкретной подписки. В нашем контракте он непрозрачный: клиент не прибавляет к нему единицу и не строит его из timestamp. Авторизация проверяется независимо от курсора.
После применения всей пачки клиент сохраняет nextCursor. Если ответ потерялся, повторяет чтение со старой позиции: сервер может вернуть ту же пачку. Поэтому у события есть стабильный id, а у состояния — ревизия. При истечении срока хранения клиент получает новый снимок с согласованным курсором и начинает чтение хвоста. Такая пересинхронизация сохраняет текущее состояние, но не восстанавливает удалённую историю — если нужна каждая запись, требования к хранению другие.
Как не устроить шторм повторов?
Пустой ответ после полного ожидания — нормальное завершение, следующий запрос можно делать сразу. Сетевая ошибка, 429 и временный 503 — причина отступить с растущей задержкой, случайным разбросом и ограничением числа попыток или общего времени. Учитываем Retry-After, если он предусмотрен контрактом. На 401 обновляем авторизацию по правилам приложения, на 403 прекращаем подписку. Не повторяем любую ошибку в бесконечном цикле.
Мало запросов не значит мало ресурсов
10 000 ожидающих клиентов — это 10 000 открытых запросов, плюс подписки и буферы. Long polling уменьшает частоту пустых ответов, но требует измерять память и лимиты соединений. После массового перезапуска клиенты не должны переподключиться все в одну миллисекунду.
Что проверить аналитику?
- Событие появилось между ответом и следующим запросом: клиент получит его по курсору.
- Ответ потерялся: повтор не дублирует запись на экране.
- Событий нет: сервер завершает ожидание в срок, JSON соответствует договорённости.
- Курсор устарел: есть явная пересинхронизация вместо тихого пропуска.
- Закрыли экран: сервер освободил ожидающий запрос и связанные ресурсы.
- Отозвали доступ, вернули 429, оборвали сеть: поведение различается и видно пользователю.
Если обновления идут часто, сравните затраты с WebSocket и SSE. Общая развилка — в главе выбор способа доставки.
Частые вопросы
Таймаут ожидания означает, что операция не выполнилась?
Нет. Закончился запрос наблюдения. Сама операция может продолжаться. Её результат узнаём из ресурса состояния, а не угадываем по состоянию сети.
При long polling сервер сам открывает соединение с браузером?
Нет. Соединение инициировал клиент своим запросом. Сервер только откладывает ответ. Это не вебхук.
Можно опрашивать только последнее состояние?
Да, если промежуточные изменения не нужны. Процент выполнения можно перечитать. Историю действий таким способом гарантированно не восстановить: нужно читать события, а не снимок.