fix(tasks): replace fire-and-forget asyncio.create_task with create_tracked_task
Add utils/task_tracker.py with create_tracked_task() that wraps background tasks with error logging, cancellation handling, and reference tracking. Replace all 17 fire-and-forget asyncio.create_task() calls across 7 files: - bot/bot.py (5 interjection checks) - bot/utils/autonomous.py (2 check-and-act/react tasks) - bot/utils/bipolar_mode.py (3 argument tasks) - bot/commands/uno.py (1 game loop task) - bot/utils/voice_receiver.py (3 STT/interruption callbacks) - bot/utils/persona_dialogue.py (4 dialogue turn/interjection tasks) Previously-tracked tasks (voice_audio.py, voice_manager.py) were left as-is since they already store task references for cancellation. Closes #1
This commit is contained in:
54
bot/utils/task_tracker.py
Normal file
54
bot/utils/task_tracker.py
Normal file
@@ -0,0 +1,54 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user