@@ -80,24 +80,7 @@ func newCircuitBreaker(endpoint string, config *Config) *CircuitBreaker {
80
80
// IsOpen returns true if the circuit breaker is open (rejecting requests)
81
81
func (cb * CircuitBreaker ) IsOpen () bool {
82
82
state := CircuitBreakerState (cb .state .Load ())
83
-
84
- if state == CircuitBreakerOpen {
85
- // Check if we should transition to half-open
86
- if cb .shouldAttemptReset () {
87
- if cb .state .CompareAndSwap (int32 (CircuitBreakerOpen ), int32 (CircuitBreakerHalfOpen )) {
88
- cb .requests .Store (0 )
89
- cb .successes .Store (0 )
90
- if cb .config != nil && cb .config .LogLevel .InfoOrAbove () {
91
- internal .Logger .Printf (context .Background (),
92
- "hitless: circuit breaker for %s transitioning to half-open" , cb .endpoint )
93
- }
94
- return false // Now in half-open state, allow requests
95
- }
96
- }
97
- return true // Still open
98
- }
99
-
100
- return false
83
+ return state == CircuitBreakerOpen
101
84
}
102
85
103
86
// shouldAttemptReset checks if enough time has passed to attempt reset
@@ -108,23 +91,36 @@ func (cb *CircuitBreaker) shouldAttemptReset() bool {
108
91
109
92
// Execute runs the given function with circuit breaker protection
110
93
func (cb * CircuitBreaker ) Execute (fn func () error ) error {
111
- // Fast path: if circuit is open, fail immediately
112
- if cb .IsOpen () {
113
- return ErrCircuitBreakerOpen
114
- }
115
-
94
+ // Single atomic state load for consistency
116
95
state := CircuitBreakerState (cb .state .Load ())
117
96
118
- // In half-open state, limit the number of requests
119
- if state == CircuitBreakerHalfOpen {
97
+ switch state {
98
+ case CircuitBreakerOpen :
99
+ if cb .shouldAttemptReset () {
100
+ // Attempt transition to half-open
101
+ if cb .state .CompareAndSwap (int32 (CircuitBreakerOpen ), int32 (CircuitBreakerHalfOpen )) {
102
+ cb .requests .Store (0 )
103
+ cb .successes .Store (0 )
104
+ state = CircuitBreakerHalfOpen // Update local state
105
+ if cb .config != nil && cb .config .LogLevel .InfoOrAbove () {
106
+ internal .Logger .Printf (context .Background (),
107
+ "hitless: circuit breaker for %s transitioning to half-open" , cb .endpoint )
108
+ }
109
+ } else {
110
+ return ErrCircuitBreakerOpen
111
+ }
112
+ } else {
113
+ return ErrCircuitBreakerOpen
114
+ }
115
+ case CircuitBreakerHalfOpen :
120
116
requests := cb .requests .Add (1 )
121
117
if requests > int64 (cb .maxRequests ) {
122
118
cb .requests .Add (- 1 ) // Revert the increment
123
119
return ErrCircuitBreakerOpen
124
120
}
125
121
}
126
122
127
- // Execute the function
123
+ // Execute the function with consistent state
128
124
err := fn ()
129
125
130
126
if err != nil {
@@ -221,46 +217,136 @@ type CircuitBreakerStats struct {
221
217
LastSuccessTime time.Time
222
218
}
223
219
220
+ // CircuitBreakerEntry wraps a circuit breaker with access tracking
221
+ type CircuitBreakerEntry struct {
222
+ breaker * CircuitBreaker
223
+ lastAccess atomic.Int64 // Unix timestamp
224
+ created time.Time
225
+ }
226
+
224
227
// CircuitBreakerManager manages circuit breakers for multiple endpoints
225
228
type CircuitBreakerManager struct {
226
- breakers sync.Map // map[string]*CircuitBreaker
227
- config * Config
229
+ breakers sync.Map // map[string]*CircuitBreakerEntry
230
+ config * Config
231
+ cleanupStop chan struct {}
232
+ cleanupMu sync.Mutex
233
+ lastCleanup atomic.Int64 // Unix timestamp
228
234
}
229
235
230
236
// newCircuitBreakerManager creates a new circuit breaker manager
231
237
func newCircuitBreakerManager (config * Config ) * CircuitBreakerManager {
232
- return & CircuitBreakerManager {
233
- config : config ,
238
+ cbm := & CircuitBreakerManager {
239
+ config : config ,
240
+ cleanupStop : make (chan struct {}),
234
241
}
242
+ cbm .lastCleanup .Store (time .Now ().Unix ())
243
+
244
+ // Start background cleanup goroutine
245
+ go cbm .cleanupLoop ()
246
+
247
+ return cbm
235
248
}
236
249
237
250
// GetCircuitBreaker returns the circuit breaker for an endpoint, creating it if necessary
238
251
func (cbm * CircuitBreakerManager ) GetCircuitBreaker (endpoint string ) * CircuitBreaker {
239
- if breaker , ok := cbm .breakers .Load (endpoint ); ok {
240
- return breaker .(* CircuitBreaker )
252
+ now := time .Now ().Unix ()
253
+
254
+ if entry , ok := cbm .breakers .Load (endpoint ); ok {
255
+ cbEntry := entry .(* CircuitBreakerEntry )
256
+ cbEntry .lastAccess .Store (now )
257
+ return cbEntry .breaker
241
258
}
242
259
243
- // Create new circuit breaker
260
+ // Create new circuit breaker with metadata
244
261
newBreaker := newCircuitBreaker (endpoint , cbm .config )
245
- actual , _ := cbm .breakers .LoadOrStore (endpoint , newBreaker )
246
- return actual .(* CircuitBreaker )
262
+ newEntry := & CircuitBreakerEntry {
263
+ breaker : newBreaker ,
264
+ created : time .Now (),
265
+ }
266
+ newEntry .lastAccess .Store (now )
267
+
268
+ actual , _ := cbm .breakers .LoadOrStore (endpoint , newEntry )
269
+ return actual .(* CircuitBreakerEntry ).breaker
247
270
}
248
271
249
272
// GetAllStats returns statistics for all circuit breakers
250
273
func (cbm * CircuitBreakerManager ) GetAllStats () []CircuitBreakerStats {
251
274
var stats []CircuitBreakerStats
252
275
cbm .breakers .Range (func (key , value interface {}) bool {
253
- breaker := value .(* CircuitBreaker )
254
- stats = append (stats , breaker .GetStats ())
276
+ entry := value .(* CircuitBreakerEntry )
277
+ stats = append (stats , entry . breaker .GetStats ())
255
278
return true
256
279
})
257
280
return stats
258
281
}
259
282
283
+ // cleanupLoop runs background cleanup of unused circuit breakers
284
+ func (cbm * CircuitBreakerManager ) cleanupLoop () {
285
+ ticker := time .NewTicker (5 * time .Minute ) // Cleanup every 5 minutes
286
+ defer ticker .Stop ()
287
+
288
+ for {
289
+ select {
290
+ case <- ticker .C :
291
+ cbm .cleanup ()
292
+ case <- cbm .cleanupStop :
293
+ return
294
+ }
295
+ }
296
+ }
297
+
298
+ // cleanup removes circuit breakers that haven't been accessed recently
299
+ func (cbm * CircuitBreakerManager ) cleanup () {
300
+ // Prevent concurrent cleanups
301
+ if ! cbm .cleanupMu .TryLock () {
302
+ return
303
+ }
304
+ defer cbm .cleanupMu .Unlock ()
305
+
306
+ now := time .Now ()
307
+ cutoff := now .Add (- 30 * time .Minute ).Unix () // 30 minute TTL
308
+
309
+ var toDelete []string
310
+ count := 0
311
+
312
+ cbm .breakers .Range (func (key , value interface {}) bool {
313
+ endpoint := key .(string )
314
+ entry := value .(* CircuitBreakerEntry )
315
+
316
+ count ++
317
+
318
+ // Remove if not accessed recently
319
+ if entry .lastAccess .Load () < cutoff {
320
+ toDelete = append (toDelete , endpoint )
321
+ }
322
+
323
+ return true
324
+ })
325
+
326
+ // Delete expired entries
327
+ for _ , endpoint := range toDelete {
328
+ cbm .breakers .Delete (endpoint )
329
+ }
330
+
331
+ // Log cleanup results
332
+ if len (toDelete ) > 0 && cbm .config != nil && cbm .config .LogLevel .InfoOrAbove () {
333
+ internal .Logger .Printf (context .Background (),
334
+ "hitless: circuit breaker cleanup removed %d/%d entries" , len (toDelete ), count )
335
+ }
336
+
337
+ cbm .lastCleanup .Store (now .Unix ())
338
+ }
339
+
340
+ // Shutdown stops the cleanup goroutine
341
+ func (cbm * CircuitBreakerManager ) Shutdown () {
342
+ close (cbm .cleanupStop )
343
+ }
344
+
260
345
// Reset resets all circuit breakers (useful for testing)
261
346
func (cbm * CircuitBreakerManager ) Reset () {
262
347
cbm .breakers .Range (func (key , value interface {}) bool {
263
- breaker := value .(* CircuitBreaker )
348
+ entry := value .(* CircuitBreakerEntry )
349
+ breaker := entry .breaker
264
350
breaker .state .Store (int32 (CircuitBreakerClosed ))
265
351
breaker .failures .Store (0 )
266
352
breaker .successes .Store (0 )
0 commit comments