Completed
Push — develop ( fe5492...19b1e9 )
by Fabian
01:32
created

CircuitBreaker.__call_succeeded()   A

Complexity

Conditions 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
dl 0
loc 6
ccs 3
cts 3
cp 1
crap 1
rs 9.4285
c 1
b 0
f 0
1 1
from functools import wraps
2 1
from datetime import datetime, timedelta
3
4 1
STATE_CLOSED = 'closed'
5 1
STATE_OPEN = 'open'
6 1
STATE_HALF_OPEN = 'half_open'
7
8
9 1
class CircuitBreaker(object):
10 1
    FAILURE_THRESHOLD = 5
11 1
    RECOVERY_TIMEOUT = 30
12 1
    EXPECTED_EXCEPTION = Exception
13
14 1
    def __init__(self,
15
                 failure_threshold=None,
16
                 recovery_timeout=None,
17
                 expected_exception=None,
18
                 name=None):
19 1
        self._failure_count = 0
20 1
        self._failure_threshold = failure_threshold or self.FAILURE_THRESHOLD
21 1
        self._recovery_timeout = recovery_timeout or self.RECOVERY_TIMEOUT
22 1
        self._expected_exception = expected_exception or self.EXPECTED_EXCEPTION
23 1
        self._name = name
24 1
        self._state = STATE_CLOSED
25 1
        self._opened = datetime.utcnow()
26
27 1
    def __call__(self, wrapped):
28 1
        return self.decorate(wrapped)
29
30 1
    def decorate(self, function):
31
        """
32
        Applies the circuit breaker to a function
33
        """
34 1
        if self._name is None:
35 1
            self._name = function.__name__
36
37 1
        CircuitBreakerMonitor.register(self)
38
39 1
        @wraps(function)
40
        def wrapper(*args, **kwargs):
41 1
            return self.call(function, *args, **kwargs)
42
43 1
        return wrapper
44
45 1
    def call(self, func, *args, **kwargs):
46
        """
47
        Calls the decorated function and applies the circuit breaker
48
        rules on success or failure
49
        :param func: Decorated function
50
        """
51 1
        if self.opened:
52 1
            raise CircuitBreakerError(self)
53 1
        try:
54 1
            result = func(*args, **kwargs)
55 1
        except self._expected_exception:
56 1
            self.__call_failed()
57 1
            raise
58
59 1
        self.__call_succeeded()
60 1
        return result
61
62 1
    def __call_succeeded(self):
63
        """
64
        Close circuit after successful execution and reset failure count
65
        """
66 1
        self._state = STATE_CLOSED
67 1
        self._failure_count = 0
68
69 1
    def __call_failed(self):
70
        """
71
        Count failure and open circuit, if threshold has been reached
72
        """
73 1
        self._failure_count += 1
74 1
        if self._failure_count >= self._failure_threshold:
75 1
            self._state = STATE_OPEN
76 1
            self._opened = datetime.utcnow()
77
78 1
    @property
79
    def state(self):
80 1
        if self._state == STATE_OPEN and self.open_remaining <= 0:
81 1
            return STATE_HALF_OPEN
82 1
        return self._state
83
84 1
    @property
85
    def open_until(self):
86
        """
87
        The datetime, when the circuit breaker will try to recover
88
        :return: datetime
89
        """
90 1
        return self._opened + timedelta(seconds=self._recovery_timeout)
91
92 1
    @property
93
    def open_remaining(self):
94
        """
95
        Number of seconds remaining, the circuit breaker stays in OPEN state
96
        :return: int
97
        """
98 1
        return (self.open_until - datetime.utcnow()).total_seconds()
99
100 1
    @property
101
    def failure_count(self):
102 1
        return self._failure_count
103
104 1
    @property
105
    def closed(self):
106 1
        return self.state == STATE_CLOSED
107
108 1
    @property
109
    def opened(self):
110 1
        return self.state == STATE_OPEN
111
112 1
    @property
113
    def name(self):
114 1
        return self._name
115
116 1
    def __str__(self, *args, **kwargs):
117 1
        return self._name
118
119
120 1
class CircuitBreakerError(Exception):
121 1
    def __init__(self, circuit_breaker, *args, **kwargs):
122
        """
123
        :param circuit_breaker:
124
        :param args:
125
        :param kwargs:
126
        :return:
127
        """
128 1
        super().__init__(*args, **kwargs)
129 1
        self._circuit_breaker = circuit_breaker
130
131 1
    def __str__(self, *args, **kwargs):
132 1
        return 'Circuit "%s" OPEN until %s (%d failures, %d sec remaining)' % (
133
            self._circuit_breaker.name,
134
            self._circuit_breaker.open_until,
135
            self._circuit_breaker.failure_count,
136
            round(self._circuit_breaker.open_remaining)
137
        )
138
139
140 1
class CircuitBreakerMonitor(object):
141 1
    circuit_breakers = {}
142
143 1
    @classmethod
144
    def register(cls, circuit_breaker):
145 1
        cls.circuit_breakers[circuit_breaker.name] = circuit_breaker
146
147 1
    @classmethod
148 1
    def all_closed(cls) -> bool:
149 1
        return len(list(cls.get_open())) == 0
150
151 1
    @classmethod
152 1
    def get_circuits(cls) -> [CircuitBreaker]:
153 1
        return cls.circuit_breakers.values()
154
155 1
    @classmethod
156 1
    def get(cls, name) -> CircuitBreaker:
157 1
        return cls.circuit_breakers.get(name)
158
159 1
    @classmethod
160 1
    def get_open(cls) -> [CircuitBreaker]:
161 1
        for circuit in cls.get_circuits():
162 1
            if circuit.opened:
163 1
                yield circuit
164
165 1
    @classmethod
166 1
    def get_closed(cls) -> [CircuitBreaker]:
167 1
        for circuit in cls.get_circuits():
168 1
            if circuit.closed:
169 1
                yield circuit
170
171
172 1
def circuit(failure_threshold=None,
173
            recovery_timeout=None,
174
            expected_exception=None,
175
            name=None,
176
            cls=CircuitBreaker):
177
178
    # if the decorator is used without parameters, the
179
    # wrapped function is provided as first argument
180 1
    if callable(failure_threshold):
181 1
        return cls().decorate(failure_threshold)
182
    else:
183 1
        return cls(
184
            failure_threshold=failure_threshold,
185
            recovery_timeout=recovery_timeout,
186
            expected_exception=expected_exception,
187
            name=name)
188