משימת ה-asyncio שלך עלולה למות באמצע העבודה

שירותי Python רבים משתמשים בתבנית הזו כדי להריץ עבודה ברקע:

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

המטרה היא לשלוח webhook מבלי לגרום ללקוח לחכות. זה עובד בסביבת פיתוח. בסביבת ייצור (production), זה נכשל באופן אקראי.

הבעיה היא ה-garbage collection.

כשאתה קורא ל-asyncio.create_task ואינך שומר את התוצאה, המשימה הופכת להיות מועמדת ל-garbage collection. ה-event loop שומר רק הפניות חלשות (weak references) למשימות. אם שום דבר אחר אינו מצביע על המשימה הזו, Python עלולה לשחרר אותה בזמן שהיא עדיין רצה.

העבודה שלך פשוט נעצרת. אין שגיאות במערכת הניטור (tracker) שלך. הקוד מושלם. המשימה פשוט נעלמת.

ייתכן שתראה את הלוג הזה ב-stderr שלך: Task was destroyed but it is pending!

קשה למצוא את השגיאה הזו כי היא מופיעה לעיתים קרובות הרבה יותר מאוחר מהכישלון בפועל.

איך לתקן את זה:

השתמש ב-set של משימות רקע כדי להחזיק הפניות.

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

ה-set שומר על המשימה בחיים. ה-callback מסיר אותה ברגע שהעבודה מסתיימת. זה מונע דליפות זיכרון (memory leaks) ומונע מה-garbage collector להרוג את המשימות שלך.

עבור Python 3.11 ומעלה, ניתן להשתמש ב-TaskGroups. TaskGroup מחזיק הפניות חזקות (strong references) לכל משימה בתוכו. שים לב ש-TaskGroup ימתין לסיום כל המשימות לפני שהוא יוצא מהבלוק. השתמש בזה אם העבודה חייבת להסתיים לפני שתגיב למשתמש.

לעבודה כבדה ברקע, השתמש בתבנית supervisor:

  • צור משימה אחת ארוכת-טווח בעת ההפעלה.
  • השתמש בתור (queue) כדי לשלוח אליה עבודה.
  • מטפל הבקשות (request handler) רק מוסיף פריטים לתור.

זה מבטיח שהעבודה שלך תשרוד ומאפשר לך להוסיף ניסיונות חוזרים (retries) מאוחר יותר.

ב-Python, אתה חייב להיות ה"בעלים" של המשימות שלך. אם לא תחזיק בהן הפניה, ה-runtime לא יגן עליהן.

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