Skip to content

Commit 2ba434a

Browse files
committed
add __init__ tests
1 parent cfb06b5 commit 2ba434a

File tree

1 file changed

+121
-0
lines changed

1 file changed

+121
-0
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
from unittest import TestCase
2+
from unittest.mock import ANY, MagicMock, patch
3+
4+
from django.conf import settings
5+
from redis.client import Pipeline
6+
7+
from sentry.ratelimits.sliding_windows import Quota, RedisSlidingWindowRateLimiter
8+
from sentry.testutils.helpers.datetime import freeze_time
9+
from sentry.utils.circuit_breaker2 import CircuitBreaker, CircuitBreakerConfig
10+
11+
# Note: These need to be relatively big. If the limit is too low, the RECOVERY quota isn't big
12+
# enough to be useful, and if the window is too short, redis (which doesn't seem to listen to the
13+
# @freezetime decorator) will expire the state keys.
14+
DEFAULT_CONFIG: CircuitBreakerConfig = {
15+
"error_limit": 200,
16+
"error_limit_window": 3600, # 1 hr
17+
"broken_state_duration": 120, # 2 min
18+
}
19+
20+
21+
@freeze_time()
22+
class CircuitBreakerTest(TestCase):
23+
def setUp(self) -> None:
24+
self.config = DEFAULT_CONFIG
25+
self.breaker = CircuitBreaker("dogs_are_great", self.config)
26+
27+
# Clear all existing keys from redis
28+
self.breaker.redis_pipeline.flushall()
29+
self.breaker.redis_pipeline.execute()
30+
31+
def test_sets_default_values(self):
32+
breaker = self.breaker
33+
34+
assert breaker.__dict__ == {
35+
"key": "dogs_are_great",
36+
"broken_state_key": "dogs_are_great.circuit_breaker.broken",
37+
"recovery_state_key": "dogs_are_great.circuit_breaker.in_recovery",
38+
"error_limit": 200,
39+
"recovery_error_limit": 20,
40+
"window": 3600,
41+
"window_granularity": 180,
42+
"broken_state_duration": 120,
43+
"recovery_duration": 7200,
44+
# These can't be compared with a simple equality check and therefore are tested
45+
# individually below
46+
"limiter": ANY,
47+
"primary_quota": ANY,
48+
"recovery_quota": ANY,
49+
"redis_pipeline": ANY,
50+
}
51+
assert isinstance(breaker.limiter, RedisSlidingWindowRateLimiter)
52+
assert isinstance(breaker.primary_quota, Quota)
53+
assert isinstance(breaker.recovery_quota, Quota)
54+
assert breaker.primary_quota.__dict__ == {
55+
"window_seconds": 3600,
56+
"granularity_seconds": 180,
57+
"limit": 200,
58+
"prefix_override": "dogs_are_great.circuit_breaker.ok",
59+
}
60+
assert breaker.recovery_quota.__dict__ == {
61+
"window_seconds": 3600,
62+
"granularity_seconds": 180,
63+
"limit": 20,
64+
"prefix_override": "dogs_are_great.circuit_breaker.recovery",
65+
}
66+
assert isinstance(breaker.redis_pipeline, Pipeline)
67+
68+
@patch("sentry.utils.circuit_breaker2.logger")
69+
def test_fixes_too_loose_recovery_limit(self, mock_logger: MagicMock):
70+
config: CircuitBreakerConfig = {
71+
**DEFAULT_CONFIG,
72+
"error_limit": 200,
73+
"recovery_error_limit": 400,
74+
}
75+
76+
for settings_debug_value, expected_log_function in [
77+
(True, mock_logger.error),
78+
(False, mock_logger.warning),
79+
]:
80+
settings.DEBUG = settings_debug_value
81+
breaker = CircuitBreaker("dogs_are_great", config)
82+
83+
expected_log_function.assert_called_with(
84+
"Circuit breaker '%s' has a recovery error limit (%d) greater than or equal"
85+
+ " to its primary error limit (%d). Using the stricter error-limit-based"
86+
+ " default (%d) instead.",
87+
breaker.key,
88+
400,
89+
200,
90+
20,
91+
)
92+
assert breaker.recovery_error_limit == 20
93+
94+
@patch("sentry.utils.circuit_breaker2.logger")
95+
def test_fixes_mismatched_state_durations(self, mock_logger: MagicMock):
96+
config: CircuitBreakerConfig = {
97+
**DEFAULT_CONFIG,
98+
"error_limit_window": 600,
99+
"broken_state_duration": 100,
100+
"recovery_duration": 200,
101+
}
102+
for settings_debug_value, expected_log_function in [
103+
(True, mock_logger.error),
104+
(False, mock_logger.warning),
105+
]:
106+
settings.DEBUG = settings_debug_value
107+
breaker = CircuitBreaker("dogs_are_great", config)
108+
109+
expected_log_function.assert_called_with(
110+
"Circuit breaker '%s' has BROKEN and RECOVERY state durations (%d and %d sec, respectively)"
111+
+ " which together are less than the main error limit window (%d sec). This can lead to the"
112+
+ " breaker getting tripped unexpectedly, until the original spike in errors clears the"
113+
+ " main time window. Extending RECOVERY period to %d seconds, to give the primary quota time"
114+
+ " to clear.",
115+
breaker.key,
116+
100,
117+
200,
118+
600,
119+
500,
120+
)
121+
assert breaker.recovery_duration == 500

0 commit comments

Comments
 (0)