Completed
Branch develop (641d1e)
by Fabian
01:02
created

pseudo_remote_call()   A

Complexity

Conditions 1

Size

Total Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
c 1
b 0
f 0
dl 0
loc 2
rs 10
1
from time import sleep
2
3
from mock.mock import patch, Mock
4
from pytest import raises
5
6
from circuitbreaker import CircuitBreaker, CircuitBreakerError, \
7
    CircuitBreakerMonitor, STATE_CLOSED, STATE_HALF_OPEN, STATE_OPEN
8
9
10
def pseudo_remote_call():
11
    return True
12
13
14
@CircuitBreaker()
15
def circuit_success():
16
    return pseudo_remote_call()
17
18
19
@CircuitBreaker(failure_threshold=1, name="circuit_failure")
20
def circuit_failure():
21
    raise ConnectionError()
22
23
24
@CircuitBreaker(failure_threshold=1, name="threshold_1")
25
def circuit_threshold_1():
26
    return pseudo_remote_call()
27
28
29
@CircuitBreaker(failure_threshold=2, recover_timeout=1, name="threshold_2")
30
def circuit_threshold_2_timeout_1():
31
    return pseudo_remote_call()
32
33
34
@CircuitBreaker(failure_threshold=3, recover_timeout=1, name="threshold_3")
35
def circuit_threshold_3_timeout_1():
36
    return pseudo_remote_call()
37
38
39
def test_circuit_pass_through():
40
    assert circuit_success() is True
41
42
43
def test_circuitbreaker_monitor():
44
    assert CircuitBreakerMonitor.all_closed() is True
45
    assert len(list(CircuitBreakerMonitor.get_circuits())) == 5
46
    assert len(list(CircuitBreakerMonitor.get_closed())) == 5
47
    assert len(list(CircuitBreakerMonitor.get_open())) == 0
48
49
    with raises(ConnectionError):
50
        circuit_failure()
51
52
    assert CircuitBreakerMonitor.all_closed() is False
53
    assert len(list(CircuitBreakerMonitor.get_circuits())) == 5
54
    assert len(list(CircuitBreakerMonitor.get_closed())) == 4
55
    assert len(list(CircuitBreakerMonitor.get_open())) == 1
56
57
58
@patch('test_functional.pseudo_remote_call', return_value=True)
59
def test_threshold_hit_prevents_consequent_calls(mock_remote: Mock):
60
    mock_remote.side_effect = ConnectionError('Connection refused')
61
    circuitbreaker = CircuitBreakerMonitor.get('threshold_1')
62
63
    assert circuitbreaker.closed
64
65
    with raises(ConnectionError):
66
        circuit_threshold_1()
67
68
    assert circuitbreaker.opened
69
70
    with raises(CircuitBreakerError):
71
        circuit_threshold_1()
72
73
    mock_remote.assert_called_once()
74
75
76
@patch('test_functional.pseudo_remote_call', return_value=True)
77
def test_circuitbreaker_recover_half_open(mock_remote: Mock):
78
    circuitbreaker = CircuitBreakerMonitor.get('threshold_3')
79
80
    # initial state: closed
81
    assert circuitbreaker.closed
82
    assert circuitbreaker.state == STATE_CLOSED
83
84
    # no exception -> success
85
    assert circuit_threshold_3_timeout_1()
86
87
    # from now all subsequent calls will fail
88
    mock_remote.side_effect = ConnectionError('Connection refused')
89
90
    # 1. failed call -> original exception
91
    with raises(ConnectionError):
92
        circuit_threshold_3_timeout_1()
93
    assert circuitbreaker.closed
94
    assert circuitbreaker.failure_count == 1
95
96
    # 2. failed call -> original exception
97
    with raises(ConnectionError):
98
        circuit_threshold_3_timeout_1()
99
    assert circuitbreaker.closed
100
    assert circuitbreaker.failure_count == 2
101
102
    # 3. failed call -> original exception
103
    with raises(ConnectionError):
104
        circuit_threshold_3_timeout_1()
105
106
    # Circuit breaker opens, threshold has been reached
107
    assert circuitbreaker.opened
108
    assert circuitbreaker.state == STATE_OPEN
109
    assert circuitbreaker.failure_count == 3
110
    assert 0 < circuitbreaker.open_remaining <= 1
111
112
    # 4. failed call -> not passed to function -> CircuitBreakerError
113
    with raises(CircuitBreakerError):
114
        circuit_threshold_3_timeout_1()
115
    assert circuitbreaker.opened
116
    assert circuitbreaker.failure_count == 3
117
    assert 0 < circuitbreaker.open_remaining <= 1
118
119
    # 5. failed call -> not passed to function -> CircuitBreakerError
120
    with raises(CircuitBreakerError):
121
        circuit_threshold_3_timeout_1()
122
    assert circuitbreaker.opened
123
    assert circuitbreaker.failure_count == 3
124
    assert 0 < circuitbreaker.open_remaining <= 1
125
126
    # wait for 1 second (recover timeout)
127
    sleep(1)
128
129
    # circuit half-open -> next call will be passed through
130
    assert circuitbreaker.closed
131
    assert circuitbreaker.open_remaining < 0
132
    assert circuitbreaker.state == STATE_HALF_OPEN
133
134
    # State half-open -> function is executed -> original exception
135
    with raises(ConnectionError):
136
        circuit_threshold_3_timeout_1()
137
    assert circuitbreaker.opened
138
    assert circuitbreaker.failure_count == 4
139
    assert 0 < circuitbreaker.open_remaining <= 1
140
141
    # State open > not passed to function -> CircuitBreakerError
142
    with raises(CircuitBreakerError):
143
        circuit_threshold_3_timeout_1()
144
145
146
@patch('test_functional.pseudo_remote_call', return_value=True)
147
def test_circuitbreaker_reopens_after_successful_call(mock_remote: Mock):
148
    circuitbreaker = CircuitBreakerMonitor.get('threshold_2')
149
150
    assert str(circuitbreaker) == 'threshold_2'
151
152
    # initial state: closed
153
    assert circuitbreaker.closed
154
    assert circuitbreaker.state == STATE_CLOSED
155
    assert circuitbreaker.failure_count == 0
156
157
    # successful call -> no exception
158
    assert circuit_threshold_2_timeout_1()
159
160
    # from now all subsequent calls will fail
161
    mock_remote.side_effect = ConnectionError('Connection refused')
162
163
    # 1. failed call -> original exception
164
    with raises(ConnectionError):
165
        circuit_threshold_2_timeout_1()
166
    assert circuitbreaker.closed
167
    assert circuitbreaker.failure_count == 1
168
169
    # 2. failed call -> original exception
170
    with raises(ConnectionError):
171
        circuit_threshold_2_timeout_1()
172
173
    # Circuit breaker opens, threshold has been reached
174
    assert circuitbreaker.opened
175
    assert circuitbreaker.state == STATE_OPEN
176
    assert circuitbreaker.failure_count == 2
177
    assert 0 < circuitbreaker.open_remaining <= 1
178
179
    # 4. failed call -> not passed to function -> CircuitBreakerError
180
    with raises(CircuitBreakerError):
181
        circuit_threshold_2_timeout_1()
182
    assert circuitbreaker.opened
183
    assert circuitbreaker.failure_count == 2
184
    assert 0 < circuitbreaker.open_remaining <= 1
185
186
    # from now all subsequent calls will succeed
187
    mock_remote.side_effect = None
188
189
    # but recover timeout has not been reached -> still open
190
    # 5. failed call -> not passed to function -> CircuitBreakerError
191
    with raises(CircuitBreakerError):
192
        circuit_threshold_2_timeout_1()
193
    assert circuitbreaker.opened
194
    assert circuitbreaker.failure_count == 2
195
    assert 0 < circuitbreaker.open_remaining <= 1
196
197
    # wait for 1 second (recover timeout)
198
    sleep(1)
199
200
    # circuit half-open -> next call will be passed through
201
    assert circuitbreaker.closed
202
    assert circuitbreaker.failure_count == 2
203
    assert circuitbreaker.open_remaining < 0
204
    assert circuitbreaker.state == STATE_HALF_OPEN
205
206
    # successful call
207
    assert circuit_threshold_2_timeout_1()
208
209
    # circuit closed and reset'ed
210
    assert circuitbreaker.closed
211
    assert circuitbreaker.state == STATE_CLOSED
212
    assert circuitbreaker.failure_count == 0
213
214
    # some another successful calls
215
    assert circuit_threshold_2_timeout_1()
216
    assert circuit_threshold_2_timeout_1()
217
    assert circuit_threshold_2_timeout_1()
218
219
220
221
222
223
224
# @CircuitBreaker(failure_threshold=2, recover_timeout=4)
225
# def external_call(call_id):
226
#     if call_id in (2, 3, 6, 7, 10, 12, 15):
227
#         raise ConnectionError('Connection refused')
228
#     return 'SUCCESS'
229
#
230
# for i in range(0, 20):
231
#     try:
232
#         print('CALL: %d' % i)
233
#         print(' ## %s ' % external_call(i))
234
#     except Exception as e:
235
#         print('  \__ %s ' % e)
236
#
237
#     sleep(0.5)
238