"""Fire-and-forget asyncio tasks that are kept alive until they finish.""" from __future__ import annotations import asyncio import logging log = logging.getLogger(__name__) _BACKGROUND: set[asyncio.Task] = set() def _done(task: asyncio.Task) -> None: _BACKGROUND.discard(task) if not task.cancelled() and task.exception() is not None: # class name only: exception text may carry paths/URLs/keys log.error("background task failed: %s", type(task.exception()).__name__) def spawn(coro) -> asyncio.Task: """create_task + strong reference (the loop only keeps weak ones).""" task = asyncio.create_task(coro) _BACKGROUND.add(task) task.add_done_callback(_done) return task