Completed
Branch develop (9f3dfb)
by Fabian
52s
created

circuitbreaker.CircuitBreaker.call_generator()   A

Complexity

Conditions 3

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 4
nop 4
dl 0
loc 9
ccs 4
cts 4
cp 1
crap 3
rs 10
c 0
b 0
f 0
1
# -*- coding: utf-8 -*-
2 1
from __future__ import unicode_literals
3 1
from __future__ import division
4 1
from __future__ import print_function
5 1
from __future__ import absolute_import
6
7 1
from functools import wraps
8 1
from datetime import datetime, timedelta
9 1
from inspect import isgeneratorfunction
10 1
from typing import AnyStr, Iterable
11
12 1
STATE_CLOSED = 'closed'
13 1
STATE_OPEN = 'open'
14 1
STATE_HALF_OPEN = 'half_open'
15
16
17 1
class CircuitBreaker(object):
18 1
    FAILURE_THRESHOLD = 5
19 1
    RECOVERY_TIMEOUT = 30
20 1
    EXPECTED_EXCEPTION = Exception
21 1
    FALLBACK_FUNCTION = None
22
23 1
    def __init__(self,
24
                 failure_threshold=None,
25
                 recovery_timeout=None,
26
                 expected_exception=None,
27
                 name=None,
28
                 fallback_function=None):
29 1
        self._last_failure = None
30 1
        self._failure_count = 0
31 1
        self._failure_threshold = failure_threshold or self.FAILURE_THRESHOLD
32 1
        self._recovery_timeout = recovery_timeout or self.RECOVERY_TIMEOUT
33 1
        self._expected_exception = expected_exception or self.EXPECTED_EXCEPTION
34 1
        self._fallback_function = fallback_function or self.FALLBACK_FUNCTION
35 1
        self._name = name
36 1
        self._state = STATE_CLOSED
37 1
        self._opened = datetime.utcnow()
38
39 1
    def __call__(self, wrapped):
40 1
        return self.decorate(wrapped)
41
42 1
    def __enter__(self):
43 1
        return None
44
45 1
    def __exit__(self, exc_type, exc_value, traceback):
46 1
        if exc_type and issubclass(exc_type, self._expected_exception):
47
            # exception was raised and is our concern
48 1
            self._last_failure = exc_value
49 1
            self.__call_failed()
50
        else:
51 1
            self.__call_succeeded()
52 1
        return False  # return False to raise exception if any
53
54 1
    def decorate(self, function):
55
        """
56
        Applies the circuit breaker to a function
57
        """
58 1
        if self._name is None:
59 1
            self._name = function.__name__
60
61 1
        CircuitBreakerMonitor.register(self)
62
63 1
        if isgeneratorfunction(function):
64 1
            call = self.call_generator
65
        else:
66 1
            call = self.call
67
68 1
        @wraps(function)
69
        def wrapper(*args, **kwargs):
70 1
            if self.opened:
71 1
                if self.fallback_function:
72 1
                    return self.fallback_function(*args, **kwargs)
73 1
                raise CircuitBreakerError(self)
74 1
            return call(function, *args, **kwargs)
75
76 1
        return wrapper
77
78 1
    def call(self, func, *args, **kwargs):
79
        """
80
        Calls the decorated function and applies the circuit breaker
81
        rules on success or failure
82
        :param func: Decorated function
83
        """
84 1
        with self:
85 1
            return func(*args, **kwargs)
86
87 1
    def call_generator(self, func, *args, **kwargs):
88
        """
89
        Calls the decorated generator function and applies the circuit breaker
90
        rules on success or failure
91
        :param func: Decorated genrator function
92
        """
93 1
        with self:
94 1
            for el in func(*args, **kwargs):
95 1
                yield el
96
97 1
    def __call_succeeded(self):
98
        """
99
        Close circuit after successful execution and reset failure count
100
        """
101 1
        self._state = STATE_CLOSED
102 1
        self._last_failure = None
103 1
        self._failure_count = 0
104
105 1
    def __call_failed(self):
106
        """
107
        Count failure and open circuit, if threshold has been reached
108
        """
109 1
        self._failure_count += 1
110 1
        if self._failure_count >= self._failure_threshold:
111 1
            self._state = STATE_OPEN
112 1
            self._opened = datetime.utcnow()
113
114 1
    @property
115
    def state(self):
116 1
        if self._state == STATE_OPEN and self.open_remaining <= 0:
117 1
            return STATE_HALF_OPEN
118 1
        return self._state
119
120 1
    @property
121
    def open_until(self):
122
        """
123
        The datetime, when the circuit breaker will try to recover
124
        :return: datetime
125
        """
126 1
        return self._opened + timedelta(seconds=self._recovery_timeout)
127
128 1
    @property
129
    def open_remaining(self):
130
        """
131
        Number of seconds remaining, the circuit breaker stays in OPEN state
132
        :return: int
133
        """
134 1
        return (self.open_until - datetime.utcnow()).total_seconds()
135
136 1
    @property
137
    def failure_count(self):
138 1
        return self._failure_count
139
140 1
    @property
141
    def closed(self):
142 1
        return self.state == STATE_CLOSED
143
144 1
    @property
145
    def opened(self):
146 1
        return self.state == STATE_OPEN
147
148 1
    @property
149
    def name(self):
150 1
        return self._name
151
152 1
    @property
153
    def last_failure(self):
154 1
        return self._last_failure
155
156 1
    @property
157
    def fallback_function(self):
158 1
        return self._fallback_function
159
160 1
    def __str__(self, *args, **kwargs):
161 1
        return self._name
162
163
164 1
class CircuitBreakerError(Exception):
165 1
    def __init__(self, circuit_breaker, *args, **kwargs):
166
        """
167
        :param circuit_breaker:
168
        :param args:
169
        :param kwargs:
170
        :return:
171
        """
172 1
        super(CircuitBreakerError, self).__init__(*args, **kwargs)
173 1
        self._circuit_breaker = circuit_breaker
174
175 1
    def __str__(self, *args, **kwargs):
176 1
        return 'Circuit "%s" OPEN until %s (%d failures, %d sec remaining) (last_failure: %r)' % (
177
            self._circuit_breaker.name,
178
            self._circuit_breaker.open_until,
179
            self._circuit_breaker.failure_count,
180
            round(self._circuit_breaker.open_remaining),
181
            self._circuit_breaker.last_failure,
182
        )
183
184
185 1
class CircuitBreakerMonitor(object):
186 1
    circuit_breakers = {}
187
188 1
    @classmethod
189
    def register(cls, circuit_breaker):
190 1
        cls.circuit_breakers[circuit_breaker.name] = circuit_breaker
191
192 1
    @classmethod
193
    def all_closed(cls):
194
        # type: () -> bool
195 1
        return len(list(cls.get_open())) == 0
196
197 1
    @classmethod
198
    def get_circuits(cls):
199
        # type: () -> Iterable[CircuitBreaker]
200 1
        return cls.circuit_breakers.values()
201
202 1
    @classmethod
203
    def get(cls, name):
204
        # type: (AnyStr) -> CircuitBreaker
205 1
        return cls.circuit_breakers.get(name)
206
207 1
    @classmethod
208
    def get_open(cls):
209
        # type: () -> Iterable[CircuitBreaker]
210 1
        for circuit in cls.get_circuits():
211 1
            if circuit.opened:
212 1
                yield circuit
213
214 1
    @classmethod
215
    def get_closed(cls):
216
        # type: () -> Iterable[CircuitBreaker]
217 1
        for circuit in cls.get_circuits():
218 1
            if circuit.closed:
219 1
                yield circuit
220
221
222 1
def circuit(failure_threshold=None,
223
            recovery_timeout=None,
224
            expected_exception=None,
225
            name=None,
226
            fallback_function=None,
227
            cls=CircuitBreaker):
228
229
    # if the decorator is used without parameters, the
230
    # wrapped function is provided as first argument
231 1
    if callable(failure_threshold):
232 1
        return cls().decorate(failure_threshold)
233
    else:
234 1
        return cls(
235
            failure_threshold=failure_threshold,
236
            recovery_timeout=recovery_timeout,
237
            expected_exception=expected_exception,
238
            name=name,
239
            fallback_function=fallback_function)
240