Ваше asyncio-завдання може перерватися під час виконання

Багато сервісів на Python використовують цей патерн для виконання фонових завдань:

async def create_order(order):
    await save(order)
    asyncio.create_task(send_webhook(order))
    return {"status": "created"}

Мета полягає в тому, щоб надіслати вебхук, не змушуючи клієнта чекати. У режимі розробки це працює. У продакшені ж це призводить до випадкових збоїв.

Проблема полягає у збиранні сміття (garbage collection).

Коли ви викликаєте asyncio.create_task і не зберігаєте результат, завдання стає доступним для збирання сміття. Цикл подій (event loop) зберігає лише слабкі посилання (weak references) на завдання. Якщо ніщо інше не посилається на це завдання, Python може видалити його, поки воно ще виконується.

Ваша робота просто зупиняється. У вашому трекері помилок немає. Код ідеальний. Завдання просто зникає.

Ви можете побачити такий запис у вашому stderr: Task was destroyed but it is pending!

Цю помилку важко знайти, тому що вона часто з'являється набагато пізніше, ніж стається фактичний збій.

Як це виправити:

Використовуйте множину (set) для фонових завдань, щоб утримувати посилання на них.

background_tasks = set()

def fire_and_forget(coro):
    task = asyncio.create_task(coro)
    background_tasks.add(task)
    task.add_done_callback(background_tasks.discard)
    return task

Множина (set) підтримує завдання живим. Callback видаляє його, щойно робота завершиться. Це запобігає витоку пам'яті та не дає збирачу сміття видаляти ваші завдання.

Для Python 3.11 і новіших версій можна використовувати TaskGroups. TaskGroup утримує сильні посилання на кожне завдання всередині нього. Зауважте, що TaskGroup чекатиме на завершення всіх завдань перед виходом із блоку. Використовуйте це, якщо робота має бути завершена до того, як ви відповісте користувачеві.

Для важких фонових завдань використовуйте патерн «супервайзер» (supervisor pattern):

  • Створіть одне довгоживуче завдання під час запуску.
  • Використовуйте чергу (queue) для надсилання завдань у нього.
  • Обробник запитів лише додає елементи до черги.

Це гарантує, що ваша робота не перерветься, і дозволить вам пізніше додати механізм повторних спроб (retries).

У Python ви повинні володіти своїми завданнями. Якщо ви не утримуєте посилання на них, середовище виконання (runtime) не захистить їх.

Джерело: https://dev.to/r9v/your-asyncio-task-can-be-garbage-collected-mid-flight-3kg1