Implementing Basic Circuit Breakers
Let’s start by implementing a simple but effective circuit breaker in Go.
Simple Counter-Based Circuit Breaker
The most straightforward implementation uses a counter to track failures:
package circuitbreaker
import (
"errors"
"sync"
"time"
)
var ErrCircuitOpen = errors.New("circuit breaker is open")
// SimpleCircuitBreaker implements a basic counter-based circuit breaker
type SimpleCircuitBreaker struct {
failureThreshold int
resetTimeout time.Duration
failureCount int
lastFailureTime time.Time
state State
mutex sync.RWMutex
}
// NewSimpleCircuitBreaker creates a new circuit breaker
func NewSimpleCircuitBreaker(failureThreshold int, resetTimeout time.Duration) *SimpleCircuitBreaker {
return &SimpleCircuitBreaker{
failureThreshold: failureThreshold,
resetTimeout: resetTimeout,
state: Closed,
}
}
// Execute runs the given function with circuit breaker protection
func (cb *SimpleCircuitBreaker) Execute(fn func() error) error {
if !cb.AllowRequest() {
return ErrCircuitOpen
}
err := fn()
cb.RecordResult(err == nil)
return err
}
// AllowRequest checks if a request should be allowed through
func (cb *SimpleCircuitBreaker) AllowRequest() bool {
cb.mutex.RLock()
defer cb.mutex.RUnlock()
switch cb.state {
case Closed:
return true
case Open:
// Check if reset timeout has elapsed
if time.Since(cb.lastFailureTime) > cb.resetTimeout {
// Transition to half-open state must be done with a write lock
cb.mutex.RUnlock()
cb.mutex.Lock()
defer cb.mutex.Unlock()
// Double-check state after acquiring write lock
if cb.state == Open {
cb.state = HalfOpen
}
return cb.state == HalfOpen
}
return false
case HalfOpen:
// In half-open state, allow only one request through
cb.mutex.RUnlock()
cb.mutex.Lock()
defer cb.mutex.Unlock()
// Only the first request should be allowed
if cb.state == HalfOpen {
return true
}
return false
default:
return false
}
}
// RecordResult records the result of a request
func (cb *SimpleCircuitBreaker) RecordResult(success bool) {
cb.mutex.Lock()
defer cb.mutex.Unlock()
if success {
switch cb.state {
case HalfOpen:
// On success in half-open state, reset and close the circuit
cb.failureCount = 0
cb.state = Closed
case Closed:
// Reset failure count on success
cb.failureCount = 0
}
} else {
cb.lastFailureTime = time.Now()
switch cb.state {
case HalfOpen:
// On failure in half-open state, reopen the circuit
cb.state = Open
case Closed:
// Increment failure count and check threshold
cb.failureCount++
if cb.failureCount >= cb.failureThreshold {
cb.state = Open
}
}
}
}
// State returns the current state of the circuit breaker
func (cb *SimpleCircuitBreaker) State() State {
cb.mutex.RLock()
defer cb.mutex.RUnlock()
return cb.state
}
Using the Simple Circuit Breaker
Here’s how to use our simple circuit breaker:
package main
import (
"fmt"
"net/http"
"time"
"example.com/circuitbreaker"
)
func main() {
// Create a circuit breaker that trips after 5 failures and resets after 10 seconds
cb := circuitbreaker.NewSimpleCircuitBreaker(5, 10*time.Second)
// Create an HTTP client with the circuit breaker
client := &http.Client{
Timeout: 5 * time.Second,
}
// Function to make HTTP request with circuit breaker protection
makeRequest := func(url string) (*http.Response, error) {
var resp *http.Response
err := cb.Execute(func() error {
var err error
resp, err = client.Get(url)
return err
})
return resp, err
}
// Example usage
for i := 0; i < 10; i++ {
resp, err := makeRequest("https://api.example.com/endpoint")
if err != nil {
if errors.Is(err, circuitbreaker.ErrCircuitOpen) {
fmt.Println("Circuit is open, skipping request")
continue
}
fmt.Printf("Request failed: %v\n", err)
continue
}
fmt.Printf("Request succeeded with status: %d\n", resp.StatusCode)
resp.Body.Close()
}
}
Sliding Window Circuit Breaker
A more sophisticated approach uses a sliding window to track failure rates:
package circuitbreaker
import (
"errors"
"sync"
"time"
)
// Result represents the outcome of a request
type Result struct {
Success bool
Time time.Time
}
// SlidingWindowCircuitBreaker implements a circuit breaker with a sliding window
type SlidingWindowCircuitBreaker struct {
windowSize time.Duration
failureRateThreshold float64
minimumRequests int
resetTimeout time.Duration
results []Result
state State
lastStateChange time.Time
mutex sync.RWMutex
}
// NewSlidingWindowCircuitBreaker creates a new sliding window circuit breaker
func NewSlidingWindowCircuitBreaker(
windowSize time.Duration,
failureRateThreshold float64,
minimumRequests int,
resetTimeout time.Duration,
) *SlidingWindowCircuitBreaker {
return &SlidingWindowCircuitBreaker{
windowSize: windowSize,
failureRateThreshold: failureRateThreshold,
minimumRequests: minimumRequests,
resetTimeout: resetTimeout,
state: Closed,
results: make([]Result, 0, 100), // Pre-allocate some capacity
}
}
// Execute runs the given function with circuit breaker protection
func (cb *SlidingWindowCircuitBreaker) Execute(fn func() error) error {
if !cb.AllowRequest() {
return ErrCircuitOpen
}
err := fn()
cb.RecordResult(err == nil)
return err
}
// AllowRequest checks if a request should be allowed through
func (cb *SlidingWindowCircuitBreaker) AllowRequest() bool {
cb.mutex.RLock()
defer cb.mutex.RUnlock()
switch cb.state {
case Closed:
return true
case Open:
if time.Since(cb.lastStateChange) > cb.resetTimeout {
cb.mutex.RUnlock()
cb.mutex.Lock()
defer cb.mutex.Unlock()
if cb.state == Open {
cb.state = HalfOpen
cb.lastStateChange = time.Now()
}
return cb.state == HalfOpen
}
return false
case HalfOpen:
// In half-open state, allow limited requests
return true
default:
return false
}
}
// RecordResult records the result of a request
func (cb *SlidingWindowCircuitBreaker) RecordResult(success bool) {
cb.mutex.Lock()
defer cb.mutex.Unlock()
now := time.Now()
// Add the new result
cb.results = append(cb.results, Result{
Success: success,
Time: now,
})
// Remove results outside the window
cutoff := now.Add(-cb.windowSize)
newStart := 0
for i, result := range cb.results {
if result.Time.After(cutoff) {
newStart = i
break
}
}
cb.results = cb.results[newStart:]
// Calculate failure rate
if len(cb.results) >= cb.minimumRequests {
failures := 0
for _, result := range cb.results {
if !result.Success {
failures++
}
}
failureRate := float64(failures) / float64(len(cb.results))
switch cb.state {
case Closed:
if failureRate >= cb.failureRateThreshold {
cb.state = Open
cb.lastStateChange = now
}
case HalfOpen:
if !success {
cb.state = Open
cb.lastStateChange = now
} else if len(cb.results) >= cb.minimumRequests && failureRate < cb.failureRateThreshold {
cb.state = Closed
cb.lastStateChange = now
}
}
}
}
// State returns the current state of the circuit breaker
func (cb *SlidingWindowCircuitBreaker) State() State {
cb.mutex.RLock()
defer cb.mutex.RUnlock()
return cb.state
}