forked from GoCodeAlone/modular
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcircuit_breaker_test.go
More file actions
246 lines (195 loc) · 7.1 KB
/
circuit_breaker_test.go
File metadata and controls
246 lines (195 loc) · 7.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
package reverseproxy
import (
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestNewCircuitBreaker(t *testing.T) {
// Test constructor function
cb := NewCircuitBreaker("test-backend", nil)
assert.Equal(t, StateClosed, cb.GetState(), "New circuit breaker should start in closed state")
assert.Equal(t, 0, cb.failureCount, "New circuit breaker should have zero failures")
assert.Equal(t, 5, cb.failureThreshold, "Failure threshold should be set correctly")
assert.Equal(t, 10*time.Second, cb.resetTimeout, "Reset timeout should be set correctly")
}
func TestCircuitBreakerRecordSuccess(t *testing.T) {
cb := NewCircuitBreaker("test-backend", nil)
// Record some failures but not enough to open circuit
for i := 0; i < 3; i++ {
cb.RecordFailure()
}
// Record a success, which should reset the counter
cb.RecordSuccess()
assert.Equal(t, 0, cb.failureCount, "Success should reset failure counter")
assert.Equal(t, StateClosed, cb.GetState(), "Circuit should remain closed")
// Test transition from half-open to closed
cb.state = StateHalfOpen
cb.RecordSuccess()
assert.Equal(t, StateClosed, cb.GetState(), "Success in half-open state should close the circuit")
}
func TestCircuitBreakerRecordFailure(t *testing.T) {
cb := NewCircuitBreaker("test-backend", nil)
// Record failures up to threshold
for i := 0; i < 4; i++ {
cb.RecordFailure()
assert.Equal(t, StateClosed, cb.GetState(), "Circuit should remain closed before threshold")
assert.Equal(t, i+1, cb.failureCount, "Failure count should be incremented")
}
// One more failure should trip the circuit
cb.RecordFailure()
assert.Equal(t, StateOpen, cb.GetState(), "Circuit should open after threshold failures")
assert.Equal(t, 5, cb.failureCount, "Failure count should be at threshold")
// Further failures don't change state
cb.RecordFailure()
assert.Equal(t, StateOpen, cb.GetState(), "Circuit should remain open on additional failures")
assert.Equal(t, 5, cb.failureCount, "Failure count should remain at threshold")
}
func TestCircuitBreakerIsOpen(t *testing.T) {
cb := NewCircuitBreaker("test-backend", nil)
// Override resetTimeout for testing
cb.resetTimeout = 10 * time.Millisecond
// When closed
assert.False(t, cb.IsOpen(), "New circuit should not be open")
// Trip the circuit
for i := 0; i < 5; i++ {
cb.RecordFailure()
}
// Should be open now
assert.True(t, cb.IsOpen(), "Circuit should be open after failures")
// Wait for timeout to expire
time.Sleep(20 * time.Millisecond)
// First call after timeout should transition to half-open and return false
assert.False(t, cb.IsOpen(), "Circuit should transition to half-open after timeout")
assert.Equal(t, StateHalfOpen, cb.GetState(), "State should be half-open")
// Test half-open state
assert.False(t, cb.IsOpen(), "Half-open circuit should report as not open")
}
func TestCircuitBreakerReset(t *testing.T) {
cb := NewCircuitBreaker("test-backend", nil)
// Trip the circuit
for i := 0; i < 5; i++ {
cb.RecordFailure()
}
assert.Equal(t, StateOpen, cb.GetState(), "Circuit should be open")
// Reset the circuit
cb.Reset()
assert.Equal(t, StateClosed, cb.GetState(), "Circuit should be closed after reset")
assert.Equal(t, 0, cb.failureCount, "Failure count should be reset")
}
func TestCircuitBreakerConcurrency(t *testing.T) {
cb := NewCircuitBreaker("test-backend", nil)
// Override the failure threshold for this test
cb.failureThreshold = 100
// Test concurrent access to the circuit breaker
done := make(chan bool)
// Start 10 goroutines to record failures
for i := 0; i < 10; i++ {
go func() {
for j := 0; j < 10; j++ {
cb.RecordFailure()
}
done <- true
}()
}
// Wait for all goroutines to complete
for i := 0; i < 10; i++ {
<-done
}
assert.Equal(t, StateOpen, cb.GetState(), "Circuit should be open after concurrent failures")
// Reset and test concurrent successes
cb.Reset()
// Trip the circuit part way
for i := 0; i < 50; i++ {
cb.RecordFailure()
}
// Start 10 goroutines to record successes
for i := 0; i < 10; i++ {
go func() {
for j := 0; j < 5; j++ {
cb.RecordSuccess()
}
done <- true
}()
}
// Wait for all goroutines to complete
for i := 0; i < 10; i++ {
<-done
}
assert.Equal(t, StateClosed, cb.GetState(), "Circuit should be closed after concurrent successes")
assert.Equal(t, 0, cb.failureCount, "Failure count should be reset")
}
func TestCircuitBreakerConfiguration(t *testing.T) {
// Create a composite handler with a test backend
backends := []*Backend{
{
ID: "api1",
URL: "http://example.com/api1",
Client: http.DefaultClient,
},
{
ID: "api2",
URL: "http://example.com/api2",
Client: http.DefaultClient,
},
}
handler := NewCompositeHandler(backends, StrategyMerge, 5*time.Second)
// Initially, circuit breakers should be nil
for _, b := range backends {
assert.Nil(t, handler.circuitBreakers[b.ID], "Circuit breaker should be nil initially")
}
// Setup configuration
globalConfig := CircuitBreakerConfig{
Enabled: true,
FailureThreshold: 10,
OpenTimeout: 60 * time.Second,
}
// Backend-specific configuration for api2
backendConfig := map[string]CircuitBreakerConfig{
"api2": {
Enabled: true,
FailureThreshold: 3,
OpenTimeout: 15 * time.Second,
},
}
// Apply the configuration
handler.ConfigureCircuitBreakers(globalConfig, backendConfig)
// Check that circuit breakers are configured correctly
assert.NotNil(t, handler.circuitBreakers["api1"], "Circuit breaker for api1 should be created")
assert.NotNil(t, handler.circuitBreakers["api2"], "Circuit breaker for api2 should be created")
// Check that global config was used for api1
assert.Equal(t, StateClosed, handler.circuitBreakers["api1"].GetState(), "Circuit breaker should start closed")
// Check that specific config was used for api2
assert.Equal(t, StateClosed, handler.circuitBreakers["api2"].GetState(), "Circuit breaker should start closed")
// Now test the disabled case
disabledConfig := map[string]CircuitBreakerConfig{
"api1": {
Enabled: false,
},
}
// Reset and apply new configuration
handler = NewCompositeHandler(backends, StrategyMerge, 5*time.Second)
handler.ConfigureCircuitBreakers(globalConfig, disabledConfig)
// api1 should be disabled, api2 should use global config
assert.Nil(t, handler.circuitBreakers["api1"], "Circuit breaker for api1 should be disabled")
assert.NotNil(t, handler.circuitBreakers["api2"], "Circuit breaker for api2 should be created")
}
func TestGlobalCircuitBreakerDisabled(t *testing.T) {
// Create a composite handler with a test backend
backends := []*Backend{
{
ID: "api1",
URL: "http://example.com/api1",
Client: http.DefaultClient,
},
}
handler := NewCompositeHandler(backends, StrategyMerge, 5*time.Second)
// Create configuration with circuit breaker disabled globally
globalConfig := CircuitBreakerConfig{
Enabled: false,
}
// Apply the configuration
handler.ConfigureCircuitBreakers(globalConfig, nil)
// Circuit breakers should be nil since they're disabled globally
assert.Nil(t, handler.circuitBreakers["api1"], "Circuit breaker should be disabled")
}