Skip to content

Commit

Permalink
Merge branch 'release/1.2.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
fabfuel committed Jun 30, 2019
2 parents 6a4cde4 + 7490b2b commit f28c440
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 15 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ __pycache__
.cache
.tox
/env
/venv
/circuitbreaker.egg-info
.coverage
/dist
Expand Down
18 changes: 10 additions & 8 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ CircuitBreaker
.. image:: https://scrutinizer-ci.com/g/fabfuel/circuitbreaker/badges/quality-score.png?b=master
:target: https://scrutinizer-ci.com/g/fabfuel/circuitbreaker

This is an Python implementation of the "Circuit Breaker" Pattern (http://martinfowler.com/bliki/CircuitBreaker.html).
This is a Python implementation of the "Circuit Breaker" Pattern (http://martinfowler.com/bliki/CircuitBreaker.html).
Inspired by Michael T. Nygard's highly recommendable book *Release It!* (https://pragprog.com/book/mnee/release-it).


Expand All @@ -40,7 +40,7 @@ This is the simplest example. Just decorate a function with the ``@circuit`` dec
This decorator sets up a circuit breaker with the default settings. The circuit breaker:

- monitors the function execution and counts failures
- resets the failure count after every successful execution (while is is closed)
- resets the failure count after every successful execution (while it is closed)
- opens and prevents further executions after 5 subsequent failures
- switches to half-open and allows one test-execution after 30 seconds recovery timeout
- closes if the test-execution succeeded
Expand All @@ -50,7 +50,7 @@ This decorator sets up a circuit breaker with the default settings. The circuit

What does *failure* mean?
=========================
A *failure* is a raised exception, which was not cought during the function call.
A *failure* is a raised exception, which was not caught during the function call.
By default, the circuit breaker listens for all exceptions based on the class ``Exception``.
That means, that all exceptions raised during the function call are considered as an
"expected failure" and will increase the failure count.
Expand All @@ -65,7 +65,7 @@ end is unavailable. So e.g. if there is an ``ConnectionError`` or a request ``Ti
If you are e.g. using the requests library (http://docs.python-requests.org/) for making HTTP calls,
its ``RequestException`` class would be a great choice for the ``expected_exception`` parameter.

All recognized exceptions will be reraised anyway, but the goal is, to let the circuit breaker only
All recognized exceptions will be re-raised anyway, but the goal is, to let the circuit breaker only
recognize those exceptions which are related to the communication to your integration point.

When it comes to monitoring (see Monitoring_), it may lead to falsy conclusions, if a
Expand All @@ -86,19 +86,21 @@ The following configuration options can be adjusted via decorator parameters. Fo

failure threshold
=================
By default, the circuit breaker opens after 5 subsequent failures. You can adjust this value via the ``failure_threshold`` parameter.
By default, the circuit breaker opens after 5 subsequent failures. You can adjust this value with the ``failure_threshold`` parameter.

recovery timeout
================
By default, the circuit breaker stays open for 30 seconds to allow the integration point to recover. You can adjust this value via the ``recovery_timeout`` parameter.
By default, the circuit breaker stays open for 30 seconds to allow the integration point to recover.
You can adjust this value with the ``recovery_timeout`` parameter.

expected exception
==================
By default, the circuit breaker listens for all exceptions which are based on the ``Exception`` class. You can adjust this via the ``expected_exception`` parameter.
By default, the circuit breaker listens for all exceptions which are based on the ``Exception`` class.
You can adjust this with the ``expected_exception`` parameter. It can be either an exception class or a tuple of exception classes.

name
====
By default, the circuit breaker name is the name of the function it decorates. You can adjust the name via parameter ``name``.
By default, the circuit breaker name is the name of the function it decorates. You can adjust the name with parameter ``name``.


Advanced Usage
Expand Down
14 changes: 11 additions & 3 deletions circuitbreaker.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def __init__(self,
recovery_timeout=None,
expected_exception=None,
name=None):
self._last_failure = None
self._failure_count = 0
self._failure_threshold = failure_threshold or self.FAILURE_THRESHOLD
self._recovery_timeout = recovery_timeout or self.RECOVERY_TIMEOUT
Expand Down Expand Up @@ -59,7 +60,8 @@ def call(self, func, *args, **kwargs):
raise CircuitBreakerError(self)
try:
result = func(*args, **kwargs)
except self._expected_exception:
except self._expected_exception as e:
self._last_failure = e
self.__call_failed()
raise

Expand All @@ -71,6 +73,7 @@ def __call_succeeded(self):
Close circuit after successful execution and reset failure count
"""
self._state = STATE_CLOSED
self._last_failure = None
self._failure_count = 0

def __call_failed(self):
Expand Down Expand Up @@ -120,6 +123,10 @@ def opened(self):
def name(self):
return self._name

@property
def last_failure(self):
return self._last_failure

def __str__(self, *args, **kwargs):
return self._name

Expand All @@ -136,11 +143,12 @@ def __init__(self, circuit_breaker, *args, **kwargs):
self._circuit_breaker = circuit_breaker

def __str__(self, *args, **kwargs):
return 'Circuit "%s" OPEN until %s (%d failures, %d sec remaining)' % (
return 'Circuit "%s" OPEN until %s (%d failures, %d sec remaining) (last_failure: %r)' % (
self._circuit_breaker.name,
self._circuit_breaker.open_until,
self._circuit_breaker.failure_count,
round(self._circuit_breaker.open_remaining)
round(self._circuit_breaker.open_remaining),
self._circuit_breaker.last_failure,
)


Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ def readme():

setup(
name='circuitbreaker',
version='1.1.0',
version='1.2.0',
url='https://github.com/fabfuel/circuitbreaker',
download_url='https://github.com/fabfuel/circuitbreaker/archive/1.1.0.tar.gz',
download_url='https://github.com/fabfuel/circuitbreaker/archive/1.2.0.tar.gz',
license='BSD',
author='Fabian Fuelling',
author_email='pypi@fabfuel.de',
Expand Down
30 changes: 29 additions & 1 deletion tests/test_unit.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
try:
from unittest.mock import Mock
except ImportError:
from mock import Mock

from mock.mock import patch
from pytest import raises

from circuitbreaker import CircuitBreaker, CircuitBreakerError, circuit

Expand All @@ -10,10 +16,32 @@ def test_circuitbreaker__str__():

def test_circuitbreaker_error__str__():
cb = CircuitBreaker(name='Foobar')
cb._last_failure = Exception()
error = CircuitBreakerError(cb)

assert str(error).startswith('Circuit "Foobar" OPEN until ')
assert str(error).endswith('(0 failures, 30 sec remaining)')
assert str(error).endswith('(0 failures, 30 sec remaining) (last_failure: Exception())')


def test_circuitbreaker_should_save_last_exception_on_failure_call():
cb = CircuitBreaker(name='Foobar')

func = Mock(side_effect=IOError)

with raises(IOError):
cb.call(func)

assert isinstance(cb.last_failure, IOError)


def test_circuitbreaker_should_clear_last_exception_on_success_call():
cb = CircuitBreaker(name='Foobar')
cb._last_failure = IOError()
assert isinstance(cb.last_failure, IOError)

cb.call(lambda: True)

assert cb.last_failure is None


@patch('circuitbreaker.CircuitBreaker.decorate')
Expand Down
8 changes: 7 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
[tox]
envlist = py27, py35, flake8
envlist = py27, py35, py36, py37, flake8

[testenv:py27]
basepython = python2.7

[testenv:py35]
basepython = python3.5

[testenv:py36]
basepython = python3.6

[testenv:py37]
basepython = python3.7

[testenv:flake8]
basepython=python3
deps=flake8
Expand Down

0 comments on commit f28c440

Please sign in to comment.