"""Per-host circuit breaker. One 403, 429, 503 or CAPTCHA page opens the breaker for that host for ELEC_BREAKER_COOLDOWN_HOURS. While it is open the host is not requested at all and its products are collected from web search results instead. There is no retry-with-a-different-identity: a block is an answer. """ from __future__ import annotations import threading import time from typing import Callable, Dict, Optional, Tuple from app.infrastructure.settings import ELEC_BREAKER_COOLDOWN_HOURS class CircuitBreaker: def __init__( self, cooldown_seconds: float = ELEC_BREAKER_COOLDOWN_HOURS * 3600, on_trip: Optional[Callable[[str, str, float], None]] = None, clock: Callable[[], float] = time.time, ) -> None: self.cooldown = cooldown_seconds self.on_trip = on_trip self._clock = clock self._open: Dict[str, Tuple[float, str]] = {} self._lock = threading.Lock() @staticmethod def _key(host: str) -> str: host = host.lower() return host[4:] if host.startswith("www.") else host def preload(self, host: str, until_epoch: float, reason: str) -> None: """Restore a breaker that was opened in an earlier run (elec.site).""" if until_epoch > self._clock(): with self._lock: self._open[self._key(host)] = (until_epoch, reason) def trip(self, host: str, reason: str) -> None: until = self._clock() + self.cooldown with self._lock: self._open[self._key(host)] = (until, reason) if self.on_trip: self.on_trip(self._key(host), reason, until) def is_open(self, host: str) -> bool: key = self._key(host) with self._lock: entry = self._open.get(key) if not entry: return False if entry[0] <= self._clock(): del self._open[key] return False return True def reason(self, host: str) -> Optional[str]: entry = self._open.get(self._key(host)) return entry[1] if entry else None