# retoor from __future__ import annotations import time from dataclasses import dataclass from typing import Callable, Hashable from devplacepy.services.nodes.spec import SPEC LIMITS = SPEC.limits CLOSED = "closed" OPEN = "open" HALF_OPEN = "half_open" @dataclass class _Circuit: failures: int = 0 opened_at: float = 0.0 probing: bool = False class CircuitBreaker: def __init__( self, failures: int = LIMITS.breaker_failures, open_seconds: float = LIMITS.breaker_open_seconds, clock: Callable[[], float] = time.monotonic, wall: Callable[[], float] = time.time, ) -> None: self.failures = max(1, failures) self.open_seconds = open_seconds self._clock = clock self._wall = wall self._circuits: dict[Hashable, _Circuit] = {} self.dirty = False def state(self, key: Hashable) -> str: circuit = self._circuits.get(key) if circuit is None or circuit.failures < self.failures: return CLOSED if self._clock() - circuit.opened_at < self.open_seconds: return OPEN return HALF_OPEN def allows(self, key: Hashable) -> bool: state = self.state(key) if state == CLOSED: return True if state == OPEN: return False return not self._circuits[key].probing def begin(self, key: Hashable) -> None: if self.state(key) == HALF_OPEN: self._circuits[key].probing = True def success(self, key: Hashable) -> None: if self._circuits.pop(key, None) is not None: self.dirty = True def failure(self, key: Hashable) -> None: circuit = self._circuits.setdefault(key, _Circuit()) circuit.probing = False circuit.failures += 1 if circuit.failures >= self.failures: circuit.opened_at = self._clock() self.dirty = True def forget(self, key: Hashable) -> None: if self._circuits.pop(key, None) is not None: self.dirty = True def open_count(self) -> int: return sum(1 for key in list(self._circuits) if self.state(key) != CLOSED) def snapshot(self) -> list[dict]: now_clock, now_wall = self._clock(), self._wall() rows = [] for key, circuit in list(self._circuits.items()): state = self.state(key) rows.append( { "key": ":".join(str(part) for part in key) if isinstance(key, tuple) else str(key), "failures": circuit.failures, "state": state, "opened_at": now_wall - (now_clock - circuit.opened_at) if circuit.failures >= self.failures else 0.0, "seconds_left": max(0.0, self.open_seconds - (now_clock - circuit.opened_at)) if state == OPEN else 0.0, } ) rows.sort(key=lambda item: (-item["failures"], item["key"])) return rows def restore(self, rows: list[dict]) -> int: now_clock, now_wall = self._clock(), self._wall() restored = 0 for row in rows: key = tuple(str(row["key"]).split(":", 1)) if len(key) != 2: continue failures = int(row.get("failures") or 0) opened_wall = float(row.get("opened_at") or 0.0) if failures < self.failures or opened_wall <= 0: continue age = max(0.0, now_wall - opened_wall) if age >= self.open_seconds * 2: continue circuit = _Circuit(failures=failures, opened_at=now_clock - age) self._circuits[key] = circuit restored += 1 return restored