Repository: fabfuel/circuitbreaker

Issue:
Possibility to add a fallback call instead of raise CircuitBreakerError
It could add a parameter like 

```python
def fallback_function():
  return True

@circuit(fallback=fallback_function)
def external_call():
    ...
```

So, when circuit is open, the fallback function is called instead of raise CircuitBreakerError

Discussion:

Patch:
diff --git a/circuitbreaker.py b/circuitbreaker.py
index 5e4a99b..f81fd7f 100644
--- a/circuitbreaker.py
+++ b/circuitbreaker.py
@@ -17,17 +17,20 @@ class CircuitBreaker(object):
     FAILURE_THRESHOLD = 5
     RECOVERY_TIMEOUT = 30
     EXPECTED_EXCEPTION = Exception
+    FALLBACK_FUNCTION = None
 
     def __init__(self,
                  failure_threshold=None,
                  recovery_timeout=None,
                  expected_exception=None,
-                 name=None):
+                 name=None,
+                 fallback_function=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
         self._expected_exception = expected_exception or self.EXPECTED_EXCEPTION
+        self._fallback_function = fallback_function or self.FALLBACK_FUNCTION
         self._name = name
         self._state = STATE_CLOSED
         self._opened = datetime.utcnow()
@@ -57,7 +60,10 @@ class CircuitBreaker(object):
         :param func: Decorated function
         """
         if self.opened:
-            raise CircuitBreakerError(self)
+            if self.fallback_function:
+                self.fallback_function(*args, **kwargs)
+            else:
+                raise CircuitBreakerError(self)
         try:
             result = func(*args, **kwargs)
         except self._expected_exception as e:
@@ -127,6 +133,10 @@ class CircuitBreaker(object):
     def last_failure(self):
         return self._last_failure
 
+    @property
+    def fallback_function(self):
+        return self._fallback_function
+
     def __str__(self, *args, **kwargs):
         return self._name
 
@@ -193,6 +203,7 @@ def circuit(failure_threshold=None,
             recovery_timeout=None,
             expected_exception=None,
             name=None,
+            fallback_function=None,
             cls=CircuitBreaker):
 
     # if the decorator is used without parameters, the
@@ -204,4 +215,5 @@ def circuit(failure_threshold=None,
             failure_threshold=failure_threshold,
             recovery_timeout=recovery_timeout,
             expected_exception=expected_exception,
-            name=name)
+            name=name,
+            fallback_function=fallback_function)


Test patch:
diff --git a/tests/test_unit.py b/tests/test_unit.py
index 8b6d97c..66793c8 100644
--- a/tests/test_unit.py
+++ b/tests/test_unit.py
@@ -44,6 +44,35 @@ def test_circuitbreaker_should_clear_last_exception_on_success_call():
     assert cb.last_failure is None
 
 
+def test_circuitbreaker_should_call_fallback_function_if_open():
+    fallback = Mock(return_value=True)
+
+    func = Mock(return_value=False)
+
+    CircuitBreaker.opened = lambda self: True
+    
+    cb = CircuitBreaker(name='WithFallback', fallback_function=fallback)
+    cb.call(func)
+    fallback.assert_called_once_with()
+
+def mocked_function(*args, **kwargs):
+    pass
+
+def test_circuitbreaker_call_fallback_function_with_parameters():
+    fallback = Mock(return_value=True)
+
+    cb = circuit(name='with_fallback', fallback_function=fallback)
+
+    # mock opened prop to see if fallback is called with correct parameters.
+    cb.opened = lambda self: True
+    func_decorated = cb.decorate(mocked_function)
+
+    func_decorated('test2',test='test')
+
+    # check args and kwargs are getting correctly to fallback function
+    
+    fallback.assert_called_once_with('test2', test='test')
+
 @patch('circuitbreaker.CircuitBreaker.decorate')
 def test_circuit_decorator_without_args(circuitbreaker_mock):
     function = lambda: True
@@ -54,10 +83,12 @@ def test_circuit_decorator_without_args(circuitbreaker_mock):
 @patch('circuitbreaker.CircuitBreaker.__init__')
 def test_circuit_decorator_with_args(circuitbreaker_mock):
     circuitbreaker_mock.return_value = None
-    circuit(10, 20, KeyError, 'foobar')
+    function_fallback = lambda: True
+    circuit(10, 20, KeyError, 'foobar', function_fallback)
     circuitbreaker_mock.assert_called_once_with(
         expected_exception=KeyError,
         failure_threshold=10,
         recovery_timeout=20,
-        name='foobar'
+        name='foobar',
+        fallback_function=function_fallback
     )