haiku.rag/haiku_rag_slim/haiku/rag/circuit_breaker.py
Yiorgis Gozadinos d781335868
Move CircuitBreaker to a shared module
Relocate CircuitBreaker from ingester/pollers to haiku/rag/circuit_breaker
so non-ingester callers (docling-serve provider) can reuse it without
depending on the ingester package.

Co-Authored-By: bryan davis <bryan@monkeytronics.org>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 11:03:30 +03:00

48 lines
1.5 KiB
Python

import time
from collections.abc import Callable
from haiku.rag.config import CircuitBreakerConfig
class CircuitBreaker:
"""Three-state breaker over repeated failures of a single target.
- closed: failures are counted; threshold flips to open.
- open: probes are blocked until cooldown elapses, then a single probe
is allowed; success closes the breaker, another failure re-opens it.
`now_fn` is injectable so tests don't need monkeypatching of time.time.
"""
def __init__(
self,
config: CircuitBreakerConfig | None = None,
*,
now_fn: Callable[[], float] = time.monotonic,
):
self._config = config or CircuitBreakerConfig()
self._now = now_fn
self._consecutive_failures = 0
self._opened_at: float | None = None
@property
def is_open(self) -> bool:
if self._opened_at is None:
return False
if self._now() - self._opened_at >= self._config.cooldown_s:
# cooldown elapsed; let the next call probe
return False
return True
def record_success(self) -> None:
self._consecutive_failures = 0
self._opened_at = None
def record_failure(self) -> None:
self._consecutive_failures += 1
if self._consecutive_failures >= self._config.failure_threshold:
self._opened_at = self._now()
@property
def consecutive_failures(self) -> int:
return self._consecutive_failures