Программирование PythonКонкурентность и asyncioРазработчик серверных приложений на Python

В веб обработчике отменяется внешняя задача запроса, но начатую операцию нужно довести до конца. Что именно...

В веб-обработчике отменяется внешняя задача запроса, но начатую операцию нужно довести до конца. Что именно изменяет asyncio.shield в отношении отмены?

Проходите собеседования с ИИ помощником Hintsage

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

asyncio.shield не даёт отмене внешней задачи автоматически отменить защищаемую внутреннюю задачу. Внешняя корутина при этом всё равно получает CancelledError и может завершиться, пока внутренняя операция продолжает выполняться.

shield защищает только от отмены через конкретное внешнее ожидание. Он не делает операцию неотменяемой и не гарантирует её выполнение после закрытия event loop.

Исторический контекст

Асинхронные операции часто образуют цепочку ожиданий: обработчик ждёт внутреннюю задачу, а вызывающий код может отменить обработчик. Обычная передача отмены по этой цепочке удобна для освобождения ресурсов, но опасна для коротких критичных операций, например фиксации результата или записи аудита.

asyncio.shield существует как примитив для разделения жизненного цикла внешней задачи и конкретной внутренней операции. Он не отменяет кооперативную модель asyncio, а позволяет явно выбрать, какая часть работы должна пережить отмену ожидания.

Постановка проблемы

Без защиты отмена обработчика обычно распространяется на задачу, которую он ожидает. Если операция уже отправила данные или изменила состояние, её прерывание может оставить систему в промежуточном состоянии.

Противоположная ошибка — считать shield гарантией завершения. Защищённая задача всё ещё может быть отменена напрямую, завершиться с исключением или быть прервана закрытием event loop. Кроме того, если внешняя корутина немедленно завершится после отмены, результат внутренней задачи может остаться необработанным.

Подробное решение

При ожидании через asyncio.shield отменяется объект внешнего ожидания, но не сама внутренняя задача. Внешняя корутина получает CancelledError и должна решить, как поступить: сразу завершиться, дождаться внутренней операции перед повторным выбрасыванием исключения или передать работу отдельному надёжному механизму.

import asyncio async def write_audit(): await asyncio.sleep(1) print("audit saved") async def handler(): task = asyncio.create_task(write_audit()) try: await asyncio.shield(task) except asyncio.CancelledError: await task raise

Здесь отмена handler не отменяет write_audit. Обработчик сначала дожидается завершения записи, затем повторно сообщает вызывающему коду об отмене. Для реального сервиса такое ожидание обычно ограничивают отдельным тайм-аутом.

Если вместо задачи передать в shield корутину, asyncio создаст для неё задачу. На практике лучше заранее сохранить объект задачи в сильной ссылке: event loop хранит задачи слабо, поэтому без другой ссылки фоновая задача может быть собрана сборщиком мусора до завершения.

shield не защищает от task.cancel(), вызванного непосредственно для внутренней задачи. Также он не переживает остановку event loop: при завершении приложения нужно отдельно организовать graceful shutdown и дождаться важных задач.

Ситуация из практики

Сервис принимает платёж и после ответа внешней платёжной системы должен сохранить аудиторскую запись. Если клиент закрыл соединение, веб-фреймворк отменяет задачу обработчика.

Вариант с обычным await прост, но отмена может прервать запись аудита. Полное игнорирование отмены опасно: обработчик может долго удерживать ресурсы и нарушить остановку сервиса. Запуск записи без сохранения задачи тоже ненадёжен: исключение может остаться необработанным, а задача — завершиться при закрытии event loop.

Выбранное решение — создать отдельную задачу, ожидать её через asyncio.shield, после отмены обработчика дождаться её в пределах короткого бюджета времени и затем повторно выбросить CancelledError. Для действительно обязательной фиксации лучше использовать надёжную внешнюю очередь или транзакционное хранилище, потому что shield защищает только от одной формы отмены и не заменяет механизм долговечности данных.

Что кандидаты часто упускают

  1. Вопрос: Отменит ли asyncio.shield внутреннюю задачу, если вызывающий код напрямую вызовет для неё cancel()?

Ответ: Да, внутренняя задача будет отменена. shield перехватывает только отмену внешнего объекта ожидания и не блокирует прямое управление внутренней задачей. Поэтому доступ к объекту задачи и правила её отмены должны контролироваться отдельно.

  1. Вопрос: Скрывает ли asyncio.shield отмену от внешней корутины?

Ответ: Нет. Если внешняя задача отменена во время ожидания shield, она получает CancelledError. Отличие состоит в том, что внутренняя задача продолжает выполнение. Если внешняя корутина не обработает исключение, она завершится отменённой, а защищённая задача может закончиться позже.

  1. Вопрос: Что произойдёт с защищённой задачей при тайм-ауте внешнего ожидания?

Ответ: Если тайм-аут применяется к asyncio.shield(task), тайм-аут обычно отменяет защищающий объект ожидания, а не саму task. Внутренняя задача продолжит работу, поэтому её нужно сохранить, дождаться позже или явно отменить, иначе она может стать неконтролируемой фоновой работой. При завершении event loop она не получает гарантии на завершение.