Task asyncio của bạn có thể bị dừng đột ngột khi đang chạy
Nhiều dịch vụ Python sử dụng mô hình này để chạy các tác vụ nền:
async def create_order(order):
await save(order)
asyncio.create_task(send_webhook(order))
return {"status": "created"}
Mục tiêu là gửi một webhook mà không bắt client phải chờ. Điều này hoạt động tốt trong môi trường phát triển. Nhưng trong môi trường production, nó lại lỗi ngẫu nhiên.
Vấn đề nằm ở garbage collection (thu gom rác).
Khi bạn gọi asyncio.create_task mà không lưu lại kết quả, task đó sẽ trở thành đối tượng có thể bị garbage collection thu hồi. Event loop chỉ giữ các tham chiếu yếu (weak references) tới các task. Nếu không có gì khác trỏ đến task đó, Python có thể thu hồi nó ngay cả khi nó vẫn đang chạy.
Công việc của bạn đột ngột dừng lại. Không có lỗi nào trong trình theo dõi (tracker) của bạn. Mã nguồn hoàn toàn hoàn hảo. Task đó đơn giản là biến mất.
Bạn có thể thấy dòng log này trong stderr:
Task was destroyed but it is pending!
Lỗi này rất khó tìm vì nó thường xuất hiện muộn hơn nhiều so với thời điểm lỗi thực sự xảy ra.
Cách khắc phục:
Sử dụng một set (tập hợp) các background task để giữ các tham chiếu.
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 này giúp giữ task luôn tồn tại. Callback sẽ xóa task khỏi set sau khi công việc hoàn tất. Điều này giúp ngăn chặn rò rỉ bộ nhớ (memory leaks) và ngăn bộ thu gom rác tiêu diệt các task của bạn.
Đối với Python 3.11 trở lên, bạn có thể sử dụng TaskGroups. Một TaskGroup sẽ giữ các tham chiếu mạnh (strong references) tới mọi task bên trong nó. Lưu ý rằng một TaskGroup sẽ đợi tất cả các task hoàn thành trước khi thoát khỏi khối lệnh. Hãy sử dụng cách này nếu công việc bắt buộc phải hoàn tất trước khi bạn phản hồi người dùng.
Đối với các công việc nền nặng, hãy sử dụng mô hình supervisor (mô hình giám sát):
- Tạo một task tồn tại lâu dài khi khởi động.
- Sử dụng một queue để gửi công việc tới task đó.
- Trình xử lý yêu cầu (request handler) chỉ việc thêm các mục vào queue.
Điều này đảm bảo công việc của bạn được duy trì và cho phép bạn thêm cơ chế thử lại (retries) sau này.
Trong Python, bạn phải làm chủ các task của mình. Nếu bạn không giữ một tham chiếu, runtime sẽ không bảo vệ chúng.
Source: https://dev.to/r9v/your-asyncio-task-can-be-garbage-collected-mid-flight-3kg1
