# utils/task_tracker.py """ Tracked asyncio task creation utility. Replaces fire-and-forget asyncio.create_task() calls with error-logging wrappers so that exceptions in background tasks are never silently swallowed. """ import asyncio from typing import Optional, Coroutine, Set from utils.logger import get_logger logger = get_logger("task_tracker") # Keep references to running tasks so they aren't garbage-collected _active_tasks: Set[asyncio.Task] = set() def create_tracked_task( coro: Coroutine, task_name: Optional[str] = None, ) -> asyncio.Task: """ Create an asyncio task with automatic error logging. Unlike bare asyncio.create_task(), this wrapper: - Names the task for easier debugging - Logs any unhandled exception (with full traceback) instead of swallowing it - Keeps a strong reference so the task isn't garbage-collected mid-flight - Auto-cleans the reference set when the task finishes Args: coro: The coroutine to schedule. task_name: Human-readable name for log messages. Defaults to the coroutine's __qualname__. Returns: The created asyncio.Task (tracked internally). """ name = task_name or getattr(coro, "__qualname__", str(coro)) async def _wrapped(): try: await coro except asyncio.CancelledError: logger.debug(f"Task '{name}' was cancelled") raise # re-raise so Task.cancelled() works correctly except Exception: logger.error(f"Background task '{name}' failed", exc_info=True) task = asyncio.create_task(_wrapped(), name=name) _active_tasks.add(task) task.add_done_callback(_active_tasks.discard) return task