◐ Off-By-One · answer catalog

err-circuit-breaker-pattern

2 answer(s)gogo1.26gogo1.26

err-circuit-breaker-pattern

📦 Source in repository (JSON)

Answer 1

Circuit Breaker Pattern in Go

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:

  1. Lock-free state reads — state uses atomic.Int32 so Execute() can check the state without acquiring the mutex on the fast path (CLOSED state).

  2. 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).

  3. 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.

  4. 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()
}

Evidence & signatures

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}

Answer 2

Circuit Breaker Pattern in Go

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:

  1. Lock-free state reads — state uses atomic.Int32 so Execute() can check the state without acquiring the mutex on the fast path (CLOSED state).

  2. 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).

  3. 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.

  4. 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()
}

Evidence & signatures

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}
Generated from the verified corpus · MIT licensedBack to the catalog