Il tuo task asyncio può interrompersi durante l'esecuzione

Molti servizi Python utilizzano questo pattern per eseguire operazioni in background:

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

L'obiettivo è inviare un webhook senza far attendere il client. Questo funziona in fase di sviluppo, ma in produzione fallisce in modo casuale.

Il problema è il garbage collection.

Quando chiami asyncio.create_task e non salvi il risultato, il task diventa idoneo per il garbage collection. L'event loop mantiene solo riferimenti deboli (weak references) ai task. Se nient'altro punta a quel task, Python potrebbe recuperare la memoria mentre è ancora in esecuzione.

Il tuo lavoro si interrompe e basta. Non ci sono errori nel tuo tracker. Il codice è perfetto. Il task svanisce e basta.

Potresti vedere questo log nel tuo stderr: Task was destroyed but it is pending!

Questo errore è difficile da individuare perché spesso appare molto tempo dopo il guasto effettivo.

Come risolvere:

Usa un set di background task per mantenere i riferimenti.

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

Il set mantiene il task in vita. La callback lo rimuove una volta terminata l'operazione. Questo previene i memory leak e impedisce al garbage collector di terminare i tuoi task.

Per Python 3.11 e versioni successive, puoi usare i TaskGroups. Un TaskGroup mantiene riferimenti forti (strong references) a ogni task al suo interno. Nota che un TaskGroup attenderà che tutti i task finiscano prima di uscire dal blocco. Usa questo approccio se l'operazione deve completarsi prima di rispondere all'utente.

Per operazioni pesanti in background, usa un pattern supervisor:

  • Crea un task a lunga durata all'avvio.
  • Usa una coda per inviargli il lavoro.
  • Il gestore della richiesta (request handler) aggiunge solo elementi alla coda.

Questo garantisce che il tuo lavoro sopravviva e ti permette di aggiungere tentativi di ripristino (retries) in seguito.

In Python, devi essere il proprietario dei tuoi task. Se non mantieni un riferimento, il runtime non li proteggerà.

Fonte: https://dev.to/r9v/your-asyncio-task-can-be-garbage-collected-mid-flight-3kg1