Passed
Pull Request — develop (#34)
by Fabian
01:19
created

CircuitBreakerMonitor.get_circuits()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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