Twoje zadanie asyncio może zostać przerwane w trakcie pracy

Wiele usług napisanych w Pythonie używa tego wzorca do wykonywania pracy w tle:

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

Celem jest wysłanie webhooka bez zmuszania klienta do czekania. W środowisku deweloperskim to działa. Na produkcji błąd występuje losowo.

Problemem jest garbage collection.

Kiedy wywołujesz asyncio.create_task i nie zapisujesz wyniku, zadanie staje się podatne na garbage collection. Pętla zdarzeń (event loop) przechowuje jedynie słabe referencje (weak references) do zadań. Jeśli nic innego nie wskazuje na to zadanie, Python może je zwolnić, podczas gdy wciąż będzie ono wykonywane.

Twoja praca po prostu się zatrzymuje. W systemie śledzenia błędów nie pojawiają się żadne błędy. Kod jest idealny. Zadanie po prostu znika.

W stderr możesz zobaczyć taki log: Task was destroyed but it is pending!

Ten błąd jest trudny do znalezienia, ponieważ często pojawia się znacznie później niż faktyczna awaria.

Jak to naprawić:

Użyj zestawu (set) zadań w tle, aby przechowywać referencje.

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

Zestaw utrzymuje zadanie przy życiu. Callback usuwa je, gdy praca zostanie zakończona. Zapobiega to wyciekom pamięci i chroni przed zabijaniem zadań przez garbage collector.

W wersji Python 3.11 i nowszych możesz użyć TaskGroups. TaskGroup przechowuje silne referencje (strong references) do każdego zadania wewnątrz niego. Pamiętaj, że TaskGroup będzie czekać na zakończenie wszystkich zadań przed wyjściem z bloku. Użyj tego, jeśli praca musi zostać ukończona, zanim odpowiesz użytkownikowi.

W przypadku ciężkiej pracy w tle użyj wzorca nadzorcy (supervisor pattern):

  • Utwórz jedno długożyjące zadanie podczas uruchamiania.
  • Użyj kolejki (queue), aby przesyłać do niego zadania.
  • Obsługujący żądanie (request handler) jedynie dodaje elementy do kolejki.

Zapewnia to przetrwanie Twojej pracy i pozwala na dodanie mechanizmu ponowień (retries) w przyszłości.

W Pythonie musisz brać odpowiedzialność za swoje zadania. Jeśli nie przechowujesz do nich referencji, środowisko uruchomieniowe ich nie ochroni.

Źródło: https://dev.to/r9v/your-asyncio-task-can-be-garbage-collected-mid-flight-3kg1