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
