Files
musicseerr/backend/infrastructure/degradation.py
T
Harvey a69a26852e Cut down unnecessary logging (#48)
* Cut down unnecessary logging

* fix format etc

* fix checks

* fix tests
2026-04-14 00:02:38 +01:00

89 lines
2.7 KiB
Python

"""Request-scoped degradation context via ``contextvars``.
A :class:`DegradationContext` is created per HTTP request by the
:class:`DegradationMiddleware` and can be accessed from *any* layer
(repository, service, route) without threading extra arguments through
call signatures.
"""
from __future__ import annotations
import contextvars
from typing import Literal
from infrastructure.integration_result import IntegrationResult, IntegrationStatus
_degradation_ctx_var: contextvars.ContextVar[DegradationContext | None] = (
contextvars.ContextVar("degradation_ctx", default=None)
)
class DegradationContext:
"""Accumulates per-source integration status within a single request."""
__slots__ = ("_services",)
def __init__(self) -> None:
self._services: dict[str, IntegrationStatus] = {}
def record(self, result: IntegrationResult) -> None: # type: ignore[type-arg]
"""Record an integration result, keeping the worst status per source."""
prev = self._services.get(result.source)
if prev is None or _severity(result.status) > _severity(prev):
self._services[result.source] = result.status
def summary(self) -> dict[str, str]:
"""Return ``{source: status}`` for all recorded integrations."""
return dict(self._services)
def degraded_summary(self) -> dict[str, str]:
"""Return only sources that are *not* ``ok``."""
return {k: v for k, v in self._services.items() if v != "ok"}
def has_degradation(self) -> bool:
return any(v != "ok" for v in self._services.values())
def init_degradation_context() -> DegradationContext:
"""Create a fresh context and install it in the current ``ContextVar``."""
ctx = DegradationContext()
_degradation_ctx_var.set(ctx)
return ctx
def get_degradation_context() -> DegradationContext:
"""Return the current request's context.
Raises :class:`RuntimeError` when called outside a request scope.
"""
ctx = _degradation_ctx_var.get()
if ctx is None:
raise RuntimeError(
"get_degradation_context() called outside a request scope"
)
return ctx
def try_get_degradation_context() -> DegradationContext | None:
"""Return the current context, or ``None`` if none is active.
Safe for background tasks / startup code where no request is in
flight.
"""
return _degradation_ctx_var.get()
def clear_degradation_context() -> None:
"""Remove the current context (end-of-request cleanup)."""
_degradation_ctx_var.set(None)
_SEVERITY: dict[IntegrationStatus, int] = {"ok": 0, "degraded": 1, "error": 2}
def _severity(status: IntegrationStatus) -> int:
return _SEVERITY[status]