pybreaker circuit breaker closed, open and half-open transitions: working versions (tested 2026-10-16)
A payment service guarded by a pybreaker CircuitBreaker, with offline tests for tripping on failures, open-state short-circuiting, half-open trial calls, success thresholds and excluded business exceptions.
passed6 passed · 0 failed2026-10-16 09:57 UTC- Status
- passed
- Last passed
- 2026-10-16 09:57 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions2
pybreaker@1.4.1pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38883899161
The code
Exactly the files that passed. Your agent can pull them with get_recipe("pybreaker-circuit-breaker-states") (install), or see them on GitHub.
requirements.txt
pybreaker==1.4.1 pytest==9.1.1
breaker.py
from __future__ import annotations
from pybreaker import CircuitBreaker, CircuitBreakerListener
class GatewayDown(Exception):
"""Backend failure: counts against the breaker."""
class InvalidCharge(Exception):
"""Business error: must never trip the breaker."""
class FakeGateway:
"""In-memory stand-in for a flaky payment gateway (no network)."""
def __init__(self) -> None:
self.up = True
self.calls = 0
def charge(self, amount: int) -> str:
self.calls += 1
if amount <= 0:
raise InvalidCharge("amount must be positive")
if not self.up:
raise GatewayDown(f"gateway unavailable for {amount}")
return f"charged:{amount}"
class StateLog(CircuitBreakerListener):
"""Records every transition as "old->new" so tests can assert on them."""
def __init__(self) -> None:
self.transitions: list[str] = []
def state_change(self, cb, old_state, new_state) -> None:
old = old_state.name if old_state is not None else "none"
self.transitions.append(f"{old}->{new_state.name}")
class ChargeService:
"""Charges through a breaker so a sick gateway cannot keep taking traffic."""
def __init__(self, gateway: FakeGateway, breaker: CircuitBreaker) -> None:
self.gateway = gateway
self.breaker = breaker
def charge(self, amount: int) -> str:
return self.breaker.call(self.gateway.charge, amount)
tests/test_breaker.py
import pytest
from pybreaker import CircuitBreaker, CircuitBreakerError
from breaker import ChargeService, FakeGateway, GatewayDown, InvalidCharge, StateLog
def make_service(gateway, **kwargs):
log = StateLog()
breaker = CircuitBreaker(listeners=[log], name="payments", **kwargs)
return ChargeService(gateway, breaker), log
@pytest.fixture
def gateway():
return FakeGateway()
def test_trip_raises_breaker_error_not_original(gateway):
svc, log = make_service(gateway, fail_max=2, reset_timeout=60)
gateway.up = False
with pytest.raises(GatewayDown):
svc.charge(10)
with pytest.raises(CircuitBreakerError) as exc:
svc.charge(10)
assert "opened" in str(exc.value)
assert svc.breaker.current_state == "open"
assert log.transitions == ["closed->open"]
def test_open_short_circuits_without_calling_backend(gateway):
svc, _ = make_service(gateway, fail_max=1, reset_timeout=60)
gateway.up = False
with pytest.raises(CircuitBreakerError):
svc.charge(10)
assert gateway.calls == 1
with pytest.raises(CircuitBreakerError):
svc.charge(10)
assert gateway.calls == 1
assert svc.breaker.current_state == "open"
def test_elapsed_timeout_half_opens_then_closes(gateway):
svc, log = make_service(gateway, fail_max=1, reset_timeout=0)
gateway.up = False
with pytest.raises(CircuitBreakerError):
svc.charge(10)
gateway.up = True
assert svc.charge(10) == "charged:10"
assert svc.breaker.current_state == "closed"
assert log.transitions == ["closed->open", "open->half-open", "half-open->closed"]
def test_failed_trial_reopens(gateway):
svc, log = make_service(gateway, fail_max=1, reset_timeout=0)
gateway.up = False
with pytest.raises(CircuitBreakerError):
svc.charge(10)
with pytest.raises(CircuitBreakerError):
svc.charge(10)
assert svc.breaker.current_state == "open"
assert log.transitions == ["closed->open", "open->half-open", "half-open->open"]
def test_half_open_needs_success_threshold(gateway):
svc, _ = make_service(gateway, fail_max=1, reset_timeout=0, success_threshold=2)
gateway.up = False
with pytest.raises(CircuitBreakerError):
svc.charge(10)
gateway.up = True
svc.charge(10)
assert svc.breaker.current_state == "half-open"
svc.charge(10)
assert svc.breaker.current_state == "closed"
def test_excluded_exception_never_trips(gateway):
svc, _ = make_service(gateway, fail_max=1, reset_timeout=60, exclude=[InvalidCharge])
for _ in range(3):
with pytest.raises(InvalidCharge):
svc.charge(0)
assert svc.breaker.current_state == "closed"
assert svc.breaker.fail_counter == 0
pytest.ini
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.