python.uncancelled_tasks
Stability
Medium
Detects async tasks that are created but never properly cancelled during shutdown, leading to resource leaks and hung processes.
Why It Matters
Section titled “Why It Matters”Uncancelled tasks cause:
- Resource leaks — Tasks hold connections, file handles, memory
- Hung shutdowns — Process waits for tasks that never complete
- Orphaned operations — Tasks continue after they should stop
- Zombie processes — Incomplete cleanup on termination
Example
Section titled “Example”# ❌ Before (tasks never tracked/cancelled)async def start_background_work(): asyncio.create_task(monitor_metrics()) asyncio.create_task(process_queue()) # Tasks are orphaned - no way to cancel them# ✅ After (tasks tracked and cancelled)class BackgroundWorker: def __init__(self): self.tasks: set[asyncio.Task] = set()
def create_task(self, coro): task = asyncio.create_task(coro) self.tasks.add(task) task.add_done_callback(self.tasks.discard) return task
async def shutdown(self): for task in self.tasks: task.cancel() await asyncio.gather(*self.tasks, return_exceptions=True)
worker = BackgroundWorker()
async def start_background_work(): worker.create_task(monitor_metrics()) worker.create_task(process_queue())
async def cleanup(): await worker.shutdown()What Unfault Detects
Section titled “What Unfault Detects”asyncio.create_task()without task tracking- Tasks created without cancellation handling
- Missing cleanup in shutdown handlers
- Fire-and-forget task patterns
Auto-Fix
Section titled “Auto-Fix”Unfault generates patches that add task tracking:
# TaskGroup (Python 3.11+)async def main(): async with asyncio.TaskGroup() as tg: tg.create_task(task1()) tg.create_task(task2()) # All tasks automatically cancelled on exitBest Practices
Section titled “Best Practices”# For long-running servicesimport signal
shutdown_event = asyncio.Event()
async def graceful_shutdown(): shutdown_event.set() # Give tasks time to finish await asyncio.sleep(5) # Cancel remaining tasks tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()] for task in tasks: task.cancel() await asyncio.gather(*tasks, return_exceptions=True)
# Register signal handlersloop = asyncio.get_event_loop()loop.add_signal_handler(signal.SIGTERM, lambda: asyncio.create_task(graceful_shutdown()))