Votre tâche asyncio peut mourir en plein travail

De nombreux services Python utilisent ce modèle pour exécuter des tâches de fond :

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

L'objectif est d'envoyer un webhook sans faire attendre le client. Cela fonctionne en développement. En production, cela échoue de manière aléatoire.

Le problème est le garbage collection.

Lorsque vous appelez asyncio.create_task sans sauvegarder le résultat, la tâche devient éligible au garbage collection. La boucle d'événements (event loop) ne conserve que des références faibles (weak references) vers les tâches. Si rien d'autre ne pointe vers cette tâche, Python peut la récupérer alors qu'elle est encore en cours d'exécution.

Votre travail s'arrête, tout simplement. Il n'y a aucune erreur dans votre outil de suivi. Le code est parfait. La tâche disparaît, tout simplement.

Vous pourriez voir ce log dans votre stderr : Task was destroyed but it is pending!

Cette erreur est difficile à trouver car elle apparaît souvent bien plus tard que l'échec réel.

Comment le corriger :

Utilisez un ensemble (set) de tâches de fond pour conserver les références.

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

L'ensemble maintient la tâche en vie. Le callback la supprime une fois le travail terminé. Cela évite les fuites de mémoire et empêche le garbage collector de tuer vos tâches.

Pour Python 3.11 et les versions ultérieures, vous pouvez utiliser les TaskGroups. Un TaskGroup conserve des références fortes vers chaque tâche qu'il contient. Notez qu'un TaskGroup attendra que toutes les tâches soient terminées avant de quitter le bloc. Utilisez cette méthode si le travail doit être achevé avant de répondre à l'utilisateur.

Pour les travaux de fond intensifs, utilisez un modèle de superviseur (supervisor pattern) :

  • Créez une tâche à longue durée de vie au démarrage.
  • Utilisez une file d'attente (queue) pour lui envoyer du travail.
  • Le gestionnaire de requête ajoute simplement des éléments à la file d'attente.

Cela garantit la survie de votre travail et vous permet d'ajouter des tentatives de réessai (retries) plus tard.

En Python, vous devez être le propriétaire de vos tâches. Si vous ne conservez pas de référence, l'environnement d'exécution (runtime) ne les protégera pas.

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