Операция формирования отчёта занимает несколько минут. Как спроектировать API, чтобы клиент не удерживал синхронный запрос открытым?
Используйте асинхронный API: запрос запускает операцию и сразу возвращает 202 Accepted с идентификатором задания. Клиент отдельно проверяет состояние задания или получает уведомление о завершении, а готовый отчёт скачивает по отдельной ссылке.
Так сервер не удерживает соединение несколько минут, а длительная работа выполняется фоновым обработчиком через надёжную очередь.
Синхронная модель хорошо подходит для коротких операций: клиент отправляет запрос, сервер обрабатывает его и возвращает результат в рамках одного соединения. По мере роста времени обработки такая модель стала плохо сочетаться с ограничениями тайм-аутов, количеством соединений и масштабированием веб-серверов.
Асинхронный шаблон «принять задание — обработать позже — получить результат» решает исходную проблему длительных операций: жизненный цикл HTTP-запроса отделяется от жизненного цикла бизнес-задачи.
Формирование отчёта может занимать минуты из-за чтения большого объёма данных, агрегаций или подготовки файла. Если удерживать исходный запрос открытым, соединение и серверный рабочий ресурс будут заняты всё это время.
Это повышает риск тайм-аутов, ограничивает число одновременно обслуживаемых клиентов и создаёт неясность при сбое соединения: отчёт мог завершиться на сервере, хотя клиент не получил ответ. Неверно спроектированный асинхронный API тоже опасен: потеря задания или отсутствие понятного статуса приводит к «зависшим» операциям.
При получении запроса API создаёт долговечную запись задания со статусом, например queued, и публикует работу в очередь. Ответ содержит идентификатор задания и адрес ресурса, где можно узнать его состояние.
Минимальный контракт может выглядеть так:
Фоновый обработчик забирает задание, переводит его в running, формирует отчёт и сохраняет результат. После успешного завершения он переводит задание в completed; при неисправимой ошибке — в failed с безопасным для клиента описанием причины.
Состояние задания должно храниться надёжно, отдельно от памяти конкретного экземпляра сервиса. Очередь должна поддерживать повторную доставку или иной механизм восстановления, потому что сбой обработчика после получения задания не должен приводить к его незаметной потере.
API следует определить для промежуточных и конечных состояний: клиенту нужны признаки выполнения, ошибки, готовности результата и, при необходимости, срок доступности файла. Для защиты ресурсов стоит ограничивать размер отчёта, частоту запуска, время жизни задания и количество одновременно выполняемых работ.
202 Accepted не означает, что отчёт готов: он означает только, что запрос принят для дальнейшей обработки. Если результат большой, его обычно сохраняют в объектном хранилище и отдают через временную ссылку, а не через API-сервис.
Основной компромисс — сложность против устойчивости. Асинхронная схема требует очереди, хранилища состояния, очистки старых результатов и обработки повторных доставок, зато изолирует длительные операции от пользовательских запросов и лучше масштабируется.
В аналитическом сервисе экспорт отчёта занимал от 30 секунд до 8 минут. Синхронный вариант был прост для клиента, но часто завершался тайм-аутом, а занятые соединения ухудшали работу остальных методов.
Рассматривались три варианта. Увеличить тайм-аут — быстро, но это не устраняло потребление соединений и делало сбои более дорогими. Перенести экспорт в отдельный сервис без ресурса задания — уменьшало влияние на основной API, но клиент не получал надёжного способа узнать итог. Асинхронное задание с очередью и отдельным ресурсом состояния было сложнее, зато давало наблюдаемость и контролируемое ограничение нагрузки.
Выбрали третий вариант: API возвращал идентификатор задания, обработчики масштабировались по длине очереди, а готовые файлы хранились отдельно с ограниченным сроком жизни. В результате пользовательские запросы перестали зависеть от длительности экспорта, а перегрузка ограничивалась числом фоновых обработчиков и глубиной очереди.
1. Что именно гарантирует ответ 202 Accepted?
Он гарантирует принятие запроса на обработку, но не успешное завершение и не готовность результата. Поэтому контракт должен предоставлять ресурс состояния задания и явно описывать конечные статусы, включая ошибку и отмену, если отмена поддерживается.
2. Как избежать зависших заданий после сбоя обработчика?
Нужно хранить время последнего прогресса и использовать lease или тайм-аут обработки. Если задание долго остаётся в состоянии running, диспетчер может вернуть его в очередь или перевести в ошибочное состояние; повторная обработка должна быть безопасной для результата.
3. Когда вместо опроса состояния использовать уведомление?
Опрос проще внедрить и не требует от сервера хранить доступный клиентский канал, но создаёт лишний трафик и задержку между завершением работы и обнаружением результата. Webhook или push-уведомление уменьшает этот трафик, однако требует повторной доставки, подтверждений, защиты endpoint и обработки недоступности клиента; поэтому часто оставляют ресурс состояния как надёжный резервный способ проверки.