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
}