Files
BanGUI/backend/app/utils/async_utils.py
Lukas 7ec80fdeec refactor(logging): replace structlog with stdlib logging compat layer
- Remove structlog dependency from backend/pyproject.toml
- Add app.utils.logging_compat shim for keyword-arg logging API
- Add app.utils.json_formatter for JSON log output with extra fields
- Update all backend modules to use logging_compat.get_logger()
- Update docstrings in log_sanitizer.py and json_formatter.py
- Update test comment in test_async_utils.py
- Record 406 failing tests in Docs/Tasks.md for tracking
2026-05-10 13:37:54 +02:00

75 lines
2.0 KiB
Python

"""Async execution utilities.
Provides a shared thread-backed executor abstraction and helpers for
running blocking callables without stalling the FastAPI event loop.
"""
from __future__ import annotations
import asyncio
import functools
from collections.abc import Callable, Coroutine
from concurrent.futures import ThreadPoolExecutor
from typing import Any, ParamSpec, TypeVar
from app.utils.logging_compat import get_logger
P = ParamSpec("P")
T = TypeVar("T")
log = get_logger(__name__)
DEFAULT_BLOCKING_EXECUTOR: ThreadPoolExecutor = ThreadPoolExecutor(
max_workers=16,
thread_name_prefix="bangui-blocking",
)
async def run_blocking(
func: Callable[P, T],
*args: P.args,
executor: ThreadPoolExecutor | None = None,
**kwargs: P.kwargs,
) -> T:
"""Run a blocking callable in the shared thread pool.
Args:
func: Blocking callable to execute.
*args: Positional arguments for the callable.
executor: Optional custom executor. Defaults to the shared pool.
**kwargs: Keyword arguments for the callable.
Returns:
The callable return value.
"""
loop = asyncio.get_running_loop()
executor = DEFAULT_BLOCKING_EXECUTOR if executor is None else executor
if kwargs:
func = functools.partial(func, *args, **kwargs)
return await loop.run_in_executor(executor, func)
return await loop.run_in_executor(executor, func, *args)
async def logged_task(
coro: Coroutine[Any, Any, Any],
name: str,
) -> None:
"""Execute a coroutine with automatic exception logging.
Wraps fire-and-forget tasks to ensure exceptions are always logged and
do not become unhandled task exceptions. Use with asyncio.create_task():
asyncio.create_task(
logged_task(some_coroutine(), "task_name"),
name="task_name"
)
Args:
coro: Coroutine to execute.
name: Task name for logging context.
"""
try:
await coro
except Exception: # noqa: BLE001
log.exception("background_task_failed", task_name=name)