Как обновлять состояние документа в UI для отображения прогресса формирования файла
Что проверяют
Понимаете ли вы, что долгую генерацию файла нельзя делать внутри HTTP-запроса, и как состояние задачи доходит до интерфейса. Следом обычно спрашивают, что будет при падении воркера и как пользователь получит готовый файл после перезагрузки страницы.
Как отвечать
Каркас для веб-приложения на Python такой:
- вынести работу в фон: запрос на генерацию создаёт запись документа со статусом
pendingи ставит задачу в очередь (Celery, RQ, Dramatiq, arq). Ответ приходит сразу,202 Acceptedс идентификатором задачи или документа; - писать прогресс в общее хранилище: воркер по ходу обновляет статус и процент в базе или Redis. В Celery для этого есть
self.update_state(state="PROGRESS", meta={"done": i, "total": n})в задаче сbind=True, прочитать можно черезAsyncResult(task_id).info; - доставить в UI: самый простой вариант это опрос
GET /documents/{id}раз в секунду или две. Без лишних запросов работают Server-Sent Events, когда поток идёт только от сервера, или WebSocket (Django Channels, WebSocket-эндпоинт в FastAPI), если нужна двусторонняя связь; - завершение: статус
readyи ссылка на файл, например подписанная ссылка на объект в S3-совместимом хранилище, илиfailedс понятной причиной.
Состояние живёт в базе, а не в памяти процесса, поэтому пользователь увидит прогресс и после перезагрузки страницы, а на нескольких экземплярах приложения всё продолжит работать.
Если спрашивают про десктоп (Tkinter, PyQt), принцип тот же: работа идёт в отдельном потоке, а виджеты обновляет только главный поток, через root.after() в Tkinter или сигналы в Qt.
Где спотыкаются
Генерируют файл прямо в обработчике запроса. Запрос упирается в таймаут прокси или сервера приложений, а пользователь смотрит на крутилку без информации.
Берут BackgroundTasks из FastAPI для тяжёлой работы. Задача выполняется в том же процессе после ответа, теряется при перезапуске и не видна другим экземплярам, так что прогресс хранить негде.
Трогают виджеты Tkinter из фонового потока. Tkinter рассчитан на работу из потока с mainloop(), а такие вызовы дают плавающие ошибки вроде RuntimeError: main thread is not in main loop.