asyncio.shield: отменили ожидание, но задача ещё работает
Запрос инициировал завершение записи, но клиент отключился во время ожидания. Вокруг await появился shield, и разработчик решил, что теперь операция закончится сама. Однако вызывающая корутина всё равно получает CancelledError. Если потерять ссылку на внутреннюю задачу, становится непонятно, кто дождётся результата и увидит исключение.
Две разные отмены
shield разделяет две отмены. Отмена ожидающего внешнего кода не передаётся защищённой задаче через конкретное ожидание. Само внешнее ожидание при этом завершается отменой. Следовательно, после except у вас может остаться работающая task. Для корректного жизненного цикла её нужно сохранить в устойчивой переменной или реестре, а затем получить результат в определённом месте.
Полезно сначала проверить это на управляемых событиях, без догадок о длительности sleep. Событие started говорит, что внутренняя работа началась. Событие release разрешает ей завершиться. Мы отменяем внешнего waiter после started, убеждаемся, что внутренняя task не отменена, и только затем открываем release. Так порядок теста выражает причину каждого наблюдения.
Здесь завершение принадлежит функции main. Она хранит task и после отмены waiter явно ожидает её. Это подходит для объяснения локального механизма. В сервисе владельцем может быть менеджер фоновых заданий с ограничением числа работ и процедурой остановки. Один shield внутри HTTP-обработчика такого менеджера не создаёт.
Сохранить и дождаться task
import asyncio
async def main():
started = asyncio.Event()
release = asyncio.Event()
saved = []
async def commit():
started.set()
await release.wait()
saved.append("written")
return len(saved)
task = asyncio.create_task(commit())
async def wait_for_commit():
return await asyncio.shield(task)
waiter = asyncio.create_task(wait_for_commit())
await started.wait()
waiter.cancel()
try:
await waiter
except asyncio.CancelledError:
pass
else:
raise AssertionError("waiter was not cancelled")
assert not task.done()
assert not task.cancelled()
release.set()
result = await task
assert result == 1 and saved == ["written"]
print("waiter cancelled:", waiter.cancelled())
print("commit result:", result)
asyncio.run(main())
В выводе внешнее ожидание отменено, а результат commit равен одному. Программа не подавляет отмену во всей системе: она сначала принимает отмену waiter и затем показывает выбранное завершение конкретной внутренней задачи. Если кто-то отдельно вызовет task.cancel(), shield не запретит это действие. Если внутренняя корутина сама завершится ошибкой, её результат также потребуется получить.
Определить владельца завершения
Не защищайте весь запрос автоматически. Клиент мог уйти до начала полезной работы, и продолжение дорогого обращения к зависимости лишь расходует ресурсы. Выберите короткий этап, завершение которого действительно требуется по правилам приложения, и определите его верхнюю границу времени. Для длительной устойчивой работы обычно нужен сохранённый job с независимым исполнителем, чтобы перезапуск процесса не уничтожил задачу.
Если функция перехватывает CancelledError ради очистки, после нужных действий обычно сохраняется семантика отмены вызывающей операции. Иначе верхний уровень может принять частично выполненную работу за обычное успешное завершение. В примере отменённый waiter остаётся отменённым, а main отдельно получает результат commit. Такой интерфейс делает две судьбы видимыми.
При остановке приложения сначала прекратите принимать новые задания, затем дождитесь принадлежащих ему задач в выбранном бюджете. shield не удержит event loop живым после завершения главной корутины. Особенно важно не запускать защищённую корутину без сохранённой ссылки: стандартная библиотека прямо требует сохранять сильную ссылку на задачу, которую вы хотите довести до конца.
Добавьте сценарий ошибки commit и проверьте, где она наблюдается после отмены waiter. Затем проверьте отдельную отмену самой внутренней задачи. Эти случаи защищают модель владения лучше, чем единственная проверка «функция успела записать значение» с произвольной задержкой.
Продолжить практику
В курсе «Python для профессионалов» есть async/await и цикл событий asyncio. Откройте программу и продолжите с организацией асинхронной работы: кто создаёт задачу, кто получает её результат и где заканчивается срок жизни ресурса. На этой основе отмена становится частью контракта операции.
Продолжить на практике
- Python для профессионалов ↗
Python для профессионалов — курс для людей, стремящихся углубить свои знания языка. Вы изучите итераторы, генераторы, декораторы, современные возможности Python (3.8+), освоите метапрограммирование, асинхронность, внутреннее устройство интерпретатора, инструменты оптимизации и профилирования, а также получите уверенность в использовании продвинутых возможностей стандартной библиотеки.
