err-circuit-breaker-pattern
File: ~/circuitbreaker.go
The implementation covers the three-state finite state machine:
CLOSED ──(consecutive failures >= FailureThreshold)──▶ OPEN
OPEN ──(timeout elapsed)───────────────────────────▶ HALF_OPEN
HALF_OPEN ──(consecutive successes >= SuccessThreshold)──▶ CLOSED
HALF_OPEN ──(any failure)──────────────────────────────▶ OPEN
Key design decisions:
Lock-free state reads — state uses atomic.Int32 so Execute() can check the state without acquiring the mutex on the fast path (CLOSED state).
Concurrent-safe semaphore for HALF_OPEN — halfOpenSem is an atomic.Pointer[chan struct{}] to avoid data races between Execute (reading the channel) and transitionToHalfOpen (replacing the channel).
Double-checked locking in transitionToHalfOpen — multiple goroutines may see the timeout has elapsed and race to transition. Under the mutex, we re-check both the state and the timeout, so only one goroutine wins.
Metrics via atomics — totalRequests, successfulRequests, failedRequests, rejectedRequests use atomic.Uint64 for lock-free high-throughput counting. Consecutive counters require the mutex since they interact with state transitions.
Usage example:
cb, _ := New(Config{
FailureThreshold: 5,
SuccessThreshold: 2,
Timeout: 30 * time.Second,
})
err := cb.Execute(func() error {
// Your unreliable operation here
return callExternalService()
})
if errors.Is(err, ErrCircuitOpen) {
// Fail fast — circuit is open
return fallback()
}
All **17 tests pass** with **zero data races** (verified with `go test -race`): | Test | What it covers | |------|----------------| | `TestClosedToOpenOnFailures` | Consecutive failures trip CLOSED→OPEN | | `TestOpenRejectsRequests` | OPEN returns `ErrCircuitOpen` immediately | | `TestHalfOpenToClosedOnSuccesses` | Successes in HALF_OPEN→CLOSED at threshold | | `TestHalfOpenFailureTripsToOpen` | Any failure in HALF_OPEN→OPEN | | `TestInvalidConfig` | Zero/negative thresholds rejected | | `TestDefaultConfigIsValid` | Default config is valid | | `TestMetricsCounters` | Total/success/fail/rejected counts correct | | `TestMetricsRejectedRequests` | Rejected count increments on OPEN | | `TestConcurrentAccess` | 100 concurrent goroutines — no deadlock | | `TestHalfOpenConcurrencyLimit` | At most `HalfOpenMaxRequests` concurrent in HALF_OPEN | | `TestSuccessResetsFailureCountInClosed` | A success between failures resets the counter | | `TestTimeoutBeforeHalfOpen` | OPEN stays open until timeout, then allows test | | `TestMultipleGoroutinesRaceToHalfOpen` | 20 goroutines racing to transition — no corruption | | `TestNilOperation` | Returns error, doesn't panic | | `TestStateString` | All states map to correct strings | | `TestExecuteReturnsOperationError` | Original error propagated to caller | | `TestDefaultConfig` | Default config creates CLOSED breaker | **Edge cases explicitly tested:** - Request rejected while circuit is OPEN before timeout - Request rejected when HALF_OPEN semaphore is full (`ErrTooManyRequests`) - Success between failures in CLOSED resets the consecutive failure count (prevents false tripping) - Multiple goroutines racing to transition from OPEN→HALF_OPEN — only one wins, no double transition - Nil operation returns error instead of panicking - High concurrency (100 goroutines) with mixed success/failure — no deadlock or data corruption - Invalid configurations (zero/negative thresholds) are rejected at construction time ---
{"model": "claude-sonnet-4-20250514", "problem_class": "err-circuit-breaker-pattern", "result": "passed", "tests": 17}File: ~/circuitbreaker.go
The implementation covers the three-state finite state machine:
CLOSED ──(consecutive failures >= FailureThreshold)──▶ OPEN
OPEN ──(timeout elapsed)───────────────────────────▶ HALF_OPEN
HALF_OPEN ──(consecutive successes >= SuccessThreshold)──▶ CLOSED
HALF_OPEN ──(any failure)──────────────────────────────▶ OPEN
Key design decisions:
Lock-free state reads — state uses atomic.Int32 so Execute() can check the state without acquiring the mutex on the fast path (CLOSED state).
Concurrent-safe semaphore for HALF_OPEN — halfOpenSem is an atomic.Pointer[chan struct{}] to avoid data races between Execute (reading the channel) and transitionToHalfOpen (replacing the channel).
Double-checked locking in transitionToHalfOpen — multiple goroutines may see the timeout has elapsed and race to transition. Under the mutex, we re-check both the state and the timeout, so only one goroutine wins.
Metrics via atomics — totalRequests, successfulRequests, failedRequests, rejectedRequests use atomic.Uint64 for lock-free high-throughput counting. Consecutive counters require the mutex since they interact with state transitions.
Usage example:
cb, _ := New(Config{
FailureThreshold: 5,
SuccessThreshold: 2,
Timeout: 30 * time.Second,
})
err := cb.Execute(func() error {
// Your unreliable operation here
return callExternalService()
})
if errors.Is(err, ErrCircuitOpen) {
// Fail fast — circuit is open
return fallback()
}
All **17 tests pass** with **zero data races** (verified with `go test -race`): | Test | What it covers | |------|----------------| | `TestClosedToOpenOnFailures` | Consecutive failures trip CLOSED→OPEN | | `TestOpenRejectsRequests` | OPEN returns `ErrCircuitOpen` immediately | | `TestHalfOpenToClosedOnSuccesses` | Successes in HALF_OPEN→CLOSED at threshold | | `TestHalfOpenFailureTripsToOpen` | Any failure in HALF_OPEN→OPEN | | `TestInvalidConfig` | Zero/negative thresholds rejected | | `TestDefaultConfigIsValid` | Default config is valid | | `TestMetricsCounters` | Total/success/fail/rejected counts correct | | `TestMetricsRejectedRequests` | Rejected count increments on OPEN | | `TestConcurrentAccess` | 100 concurrent goroutines — no deadlock | | `TestHalfOpenConcurrencyLimit` | At most `HalfOpenMaxRequests` concurrent in HALF_OPEN | | `TestSuccessResetsFailureCountInClosed` | A success between failures resets the counter | | `TestTimeoutBeforeHalfOpen` | OPEN stays open until timeout, then allows test | | `TestMultipleGoroutinesRaceToHalfOpen` | 20 goroutines racing to transition — no corruption | | `TestNilOperation` | Returns error, doesn't panic | | `TestStateString` | All states map to correct strings | | `TestExecuteReturnsOperationError` | Original error propagated to caller | | `TestDefaultConfig` | Default config creates CLOSED breaker | **Edge cases explicitly tested:** - Request rejected while circuit is OPEN before timeout - Request rejected when HALF_OPEN semaphore is full (`ErrTooManyRequests`) - Success between failures in CLOSED resets the consecutive failure count (prevents false tripping) - Multiple goroutines racing to transition from OPEN→HALF_OPEN — only one wins, no double transition - Nil operation returns error instead of panicking - High concurrency (100 goroutines) with mixed success/failure — no deadlock or data corruption - Invalid configurations (zero/negative thresholds) are rejected at construction time ---
{"model": "claude-sonnet-4-20250514", "problem_class": "err-circuit-breaker-pattern", "result": "passed", "tests": 17}