Ihre asyncio-Task kann mitten in der Arbeit absterben

Viele Python-Services nutzen dieses Muster, um Hintergrundaufgaben auszuführen:

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

Das Ziel ist es, einen Webhook zu senden, ohne den Client warten zu lassen. In der Entwicklung funktioniert das. In der Produktion schlägt es jedoch sporadisch fehl.

Das Problem ist die Garbage Collection.

Wenn Sie asyncio.create_task aufrufen und das Ergebnis nicht speichern, wird die Task für die Garbage Collection freigegeben. Der Event Loop behält nur schwache Referenzen (weak references) auf die Tasks. Wenn nichts anderes auf diese Task verweist, kann Python sie zurückfordern, während sie noch läuft.

Ihre Arbeit bricht einfach ab. In Ihrem Error-Tracker erscheinen keine Fehler. Der Code ist perfekt. Die Task verschwindet einfach.

Möglicherweise sehen Sie diesen Log-Eintrag in Ihrer stderr: Task was destroyed but it is pending!

Dieser Fehler ist schwer zu finden, da er oft viel später auftritt als das eigentliche Problem.

So beheben Sie es:

Verwenden Sie ein Set für Hintergrund-Tasks, um Referenzen zu halten.

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

Das Set hält die Task am Leben. Der Callback entfernt sie, sobald die Arbeit abgeschlossen ist. Dies verhindert Memory Leaks und sorgt dafür, dass der Garbage Collector Ihre Tasks nicht beendet.

Für Python 3.11 und neuer können Sie TaskGroups verwenden. Eine TaskGroup hält starke Referenzen auf jede darin enthaltene Task. Beachten Sie, dass eine TaskGroup wartet, bis alle Tasks abgeschlossen sind, bevor sie den Block verlässt. Verwenden Sie dies, wenn die Arbeit abgeschlossen sein muss, bevor Sie dem Benutzer antworten.

Für rechenintensive Hintergrundaufgaben verwenden Sie ein Supervisor-Pattern:

  • Erstellen Sie beim Start eine langlebige Task.
  • Verwenden Sie eine Queue, um Aufgaben an diese zu senden.
  • Der Request-Handler fügt nur Elemente zur Queue hinzu.

Dies stellt sicher, dass Ihre Arbeit überlebt, und ermöglicht es Ihnen, später Retries (Wiederholungsversuche) hinzuzufügen.

In Python müssen Sie die Verantwortung für Ihre Tasks übernehmen. Wenn Sie keine Referenz halten, wird die Runtime sie nicht schützen.

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