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>
48 lines
1.5 KiB
Python
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
|