> Какие варианты реализации взаимодействия фронтенда и бэкенда для долгих задач с отображением прогресса (Python)

Уровень: senior · Роль: backend · Язык: Python · Категория: Технические вопросы

Компании: JEDai

Стек: Python

> Пример ответа

Короткий ответ

Для долгих задач с отображением прогресса есть три основных подхода: polling, Server-Sent Events (SSE) и WebSocket. Polling - простейший, но создаёт лишнюю нагрузку. SSE - односторонний поток от сервера к клиенту, идеален для прогресса, работает поверх HTTP. WebSocket - двусторонний, подходит для интерактивных сценариев, но сложнее в инфраструктуре. Дополнительно можно использовать task queue (Celery, RQ) с брокером для выполнения фоновой работы и отдельный endpoint для получения статуса. Выбор зависит от требований к realtime и инфраструктурных ограничений.

Подробное объяснение

Основная проблема долгих задач - HTTP-запрос блокируется до завершения обработки. Клиент не получает промежуточных данных. Решения делятся на два класса: клиент опрашивает сервер (polling) или сервер сам отправляет обновления (push).

Polling - клиент периодически запрашивает статус задачи. Простой в реализации, работает с любым HTTP-стеком. Минусы: задержка между фактическим изменением статуса и его получением, лишние запросы при отсутствии обновлений. Long polling - вариант, где сервер держит соединение открытым до появления нового статуса или таймаута, уменьшая число пустых ответов.

SSE - сервер отправляет события через одно долгоживущее HTTP-соединение. Клиент использует EventSource в браузере. Автоматическое переподключение, встроенная обработка ошибок. Ограничение - только односторонняя связь, но для прогресса этого достаточно. Работает через прокси и load balancer, если настроен X-Accel-Buffering: no для nginx.

WebSocket - полнодуплексный канал. Даёт возможность клиенту отправлять команды (например, отмена задачи) в том же соединении. Требует отдельного протокола, поддержки на уровне инфраструктуры, управления heartbeat. Избыточен, если нужен только прогресс.

Архитектура с task queue - фоновая задача выполняется в worker-процессе (Celery, RQ, Dramatiq). Сервер хранит статус в Redis или БД. Клиент получает task_id при старте и опрашивает endpoint /tasks/{id} или получает обновления через SSE/WebSocket. Такой подход отделяет выполнение от HTTP-цикла, позволяет масштабировать worker'ы независимо.

Для Python-стека типичная схема: FastAPI/Flask + Celery + Redis. Celery обновляет состояние задачи (update_state), а отдельный endpoint отдаёт его. Для SSE в FastAPI можно использовать StreamingResponse или sse-starlette.

На практике

Для senior-позиции важно не просто перечислить варианты, а обосновать выбор. Критерии: количество одновременных задач, требуемая задержка обновления, инфраструктура (есть ли уже Redis), необходимость двусторонней связи, поведение при рестарте сервера.

Практический сценарий: пользователь загружает файл, сервер обрабатывает его 2-5 минут. Оптимально: клиент отправляет файл, получает task_id, открывает SSE-соединение на /tasks/{id}/events. Сервер публикует прогресс из Celery-задачи через Redis pub/sub или просто читает статус из Redis при каждом событии. Если нужна отмена - добавить WebSocket или отдельный POST endpoint.

Важный нюанс: при использовании нескольких worker'ов или рестарте сервера статус задачи должен быть устойчивым - хранить в Redis или БД, а не в памяти процесса. Также нужно продумать таймауты: SSE-соединение может оборваться, клиент должен уметь переподключаться и получать текущий статус.

Пример кода

Пример на FastAPI + Celery + Redis с SSE:

PYTHON
# tasks.py
from celery import Celery
celery_app = Celery('tasks', broker='redis://localhost:6379/0')
@celery_app.task(bind=True)
def long_task(self, total_steps: int):
for i in range(total_steps):
# имитация работы
time.sleep(1)
self.update_state(
state='PROGRESS',
meta={'current': i + 1, 'total': total_steps}
)
return {'status': 'done'}
PYTHON
# main.py
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import json
from tasks import long_task
app = FastAPI()
@app.post('/start')
async def start_task():
task = long_task.delay(10)
return {'task_id': task.id}
@app.get('/tasks/{task_id}/events')
async def task_events(task_id: str):
async def event_generator():
while True:
result = celery_app.AsyncResult(task_id)
if result.state == 'PROGRESS':
meta = result.info
yield f"data: {json.dumps(meta)}\n\n"
elif result.state == 'SUCCESS':
yield f"data: {json.dumps({'status': 'done'})}\n\n"
break
elif result.state == 'FAILURE':
yield f"data: {json.dumps({'status': 'error'})}\n\n"
break
await asyncio.sleep(1)
return StreamingResponse(event_generator(), media_type='text/event-stream')

Клиент на фронтенде:

JAVASCRIPT
const es = new EventSource(`/tasks/${taskId}/events`);
es.onmessage = (event) => {
const data = JSON.parse(event.data);
updateProgressBar(data.current, data.total);
};

Как отвечать на собеседовании

Начните с классификации: polling, SSE, WebSocket. Сразу уточните, что для отображения прогресса чаще всего достаточно polling или SSE, WebSocket - если нужна двусторонняя связь. Затем опишите архитектуру с task queue, подчеркните отделение выполнения от HTTP-цикла. Упомяните trade-off: polling проще, но создаёт нагрузку; SSE эффективнее, но требует поддержки инфраструктуры; WebSocket избыточен для одностороннего прогресса.

Приведите конкретный пример из практики: как бы вы реализовали обработку файла с прогрессом на FastAPI + Celery. Объясните, почему выбрали SSE, а не polling - меньше запросов, realtime-обновления. Упомяните, как храните статус (Redis) и что делаете при обрыве соединения.

Завершите обсуждением edge case: рестарт worker'а, потеря соединения, множественные worker'ы. Это покажет глубину понимания.

Что проверяет интервьюер

Интервьюер оценивает:

  • понимание ограничений HTTP и необходимости асинхронной обработки;
  • знание конкретных технологий и их trade-off;
  • умение проектировать архитектуру с учётом масштабирования и отказоустойчивости;
  • способность обосновать выбор подхода под конкретные требования;
  • знание деталей реализации: хранение статуса, обработка обрывов, работа через прокси.

Для senior-уровня важно показать, что вы не просто знаете варианты, а понимаете, когда какой применить, и умеете предвидеть проблемы.

Типичные ошибки

  • Предлагать WebSocket там, где достаточно SSE - усложнение без необходимости.
  • Не учитывать, что статус задачи должен переживать рестарт сервера - хранить в памяти процесса.
  • Игнорировать поведение при обрыве соединения: клиент должен уметь переподключаться и получать актуальный статус.
  • Использовать polling с маленьким интервалом при большом количестве задач - создавать избыточную нагрузку на сервер.
  • Не упоминать про настройку прокси для SSE (например, X-Accel-Buffering в nginx) - без этого соединение может буферизоваться.
  • Смешивать понятия: Celery - это task queue, а не транспорт для realtime-обновлений; для доставки прогресса нужен отдельный механизм.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью