Introduction to Circuit Breakers

In the world of microservices, where multiple services collaborate to handle requests, the risk of cascading failures is ever-present. Imagine a scenario where one service is down or responding slowly, causing a chain reaction that brings down the entire system. This is where the Circuit Breaker pattern comes into play, acting as a guardian that prevents such catastrophic failures.

What is a Circuit Breaker?

A Circuit Breaker is a design pattern that prevents a network or service failure from cascading to other services. It works similarly to an electrical circuit breaker, which trips and breaks the circuit when the current exceeds a certain threshold, preventing further damage.

States of a Circuit Breaker

A Circuit Breaker has three primary states:

Closed

In the Closed state, the circuit breaker allows all requests to pass through to the service. If a certain number of consecutive requests fail (reaching a failure threshold), the circuit breaker switches to the Open state.

Open

In the Open state, all requests are immediately blocked, and an error is returned to the caller without attempting to contact the failing service. This gives the service time to recover and prevents it from being overwhelmed.

Half-Open

After a predefined recovery period, the circuit breaker transitions to the Half-Open state. Here, it allows a limited number of test requests to see if the service has recovered. If these requests succeed, the circuit breaker transitions back to the Closed state. If any of these requests fail, it goes back to the Open state.

stateDiagram-v2 state "Closed" as Closed state "Open" as Open state "Half-Open" as HalfOpen Closed --> Open: Consecutive failures exceed threshold Open --> HalfOpen: Recovery period expires HalfOpen --> Closed: Test requests succeed HalfOpen --> Open: Test requests fail

Implementing a Circuit Breaker in Go

To implement a Circuit Breaker in Go, you can either use an existing library like gobreaker or create your own from scratch.

Using the gobreaker Library

The gobreaker library is a popular choice for implementing Circuit Breakers in Go. Here’s how you can use it:

package main

import (
    "fmt"
    "log"
    "net/http"
    "time"

    "github.com/sony/gobreaker"
)

func main() {
    var st gobreaker.Settings
    st.Name = "Circuit Breaker PoC"
    st.Timeout = time.Second * 5
    st.MaxRequests = 2
    st.ReadyToTrip = func(counts gobreaker.Counts) bool {
        return counts.ConsecutiveFailures >= 1
    }

    cb := gobreaker.NewCircuitBreaker(st)

    url := "http://localhost:8080/success"
    cb.Execute(func() (int, error) { return Get(url) })
    fmt.Println("Circuit Breaker state:", cb.State()) // closed!

    url = "http://localhost:8080/failure"
    cb.Execute(func() (int, error) { return Get(url) })
    fmt.Println("Circuit Breaker state:", cb.State()) // open!

    time.Sleep(time.Second * 6)

    url = "http://localhost:8080/success"
    cb.Execute(func() (int, error) { return Get(url) })
    fmt.Println("Circuit Breaker state:", cb.State()) // half-open!

    url = "http://localhost:8080/success"
    cb.Execute(func() (int, error) { return Get(url) })
    fmt.Println("Circuit Breaker state:", cb.State()) // closed!
}

func Get(url string) (int, error) {
    r, _ := http.Get(url)
    if r.StatusCode != http.StatusOK {
        return r.StatusCode, fmt.Errorf("failed to get %s", url)
    }
    return r.StatusCode, nil
}

Creating a Circuit Breaker from Scratch

If you prefer to create your own Circuit Breaker, here’s a simple example:

package main

import (
    "fmt"
    "log"
    "net/http"
    "sync"
    "time"
)

const (
    Closed = "closed"
    Open   = "open"
    HalfOpen = "half-open"
)

type circuitBreaker struct {
    mu sync.Mutex
    state string
    failureCount int
    lastFailureTime time.Time
    halfOpenSuccessCount int
    failureThreshold int
    recoveryTime time.Duration
    halfOpenMaxRequests int
    timeout time.Duration
}

func NewCircuitBreaker(failureThreshold int, recoveryTime time.Duration, halfOpenMaxRequests int, timeout time.Duration) *circuitBreaker {
    return &circuitBreaker{
        state: Closed,
        failureThreshold: failureThreshold,
        recoveryTime: recoveryTime,
        halfOpenMaxRequests: halfOpenMaxRequests,
        timeout: timeout,
    }
}

func (cb *circuitBreaker) Call(fn func() (any, error)) (any, error) {
    cb.mu.Lock()
    defer cb.mu.Unlock()

    switch cb.state {
    case Closed:
        return cb.handleClosedState(fn)
    case Open:
        return cb.handleOpenState()
    case HalfOpen:
        return cb.handleHalfOpenState(fn)
    default:
        return nil, fmt.Errorf("unknown circuit state")
    }
}

func (cb *circuitBreaker) handleClosedState(fn func() (any, error)) (any, error) {
    result, err := fn()
    if err != nil {
        cb.failureCount++
        cb.lastFailureTime = time.Now()
        if cb.failureCount >= cb.failureThreshold {
            cb.state = Open
        }
        return nil, err
    }
    cb.failureCount = 0
    return result, nil
}

func (cb *circuitBreaker) handleOpenState() (any, error) {
    if time.Since(cb.lastFailureTime) > cb.recoveryTime {
        cb.state = HalfOpen
    }
    return nil, fmt.Errorf("circuit is open")
}

func (cb *circuitBreaker) handleHalfOpenState(fn func() (any, error)) (any, error) {
    result, err := fn()
    if err != nil {
        cb.state = Open
        return nil, err
    }
    cb.halfOpenSuccessCount++
    if cb.halfOpenSuccessCount >= cb.halfOpenMaxRequests {
        cb.state = Closed
    }
    return result, nil
}

func main() {
    cb := NewCircuitBreaker(3, time.Second*5, 2, time.Second*1)

    url := "http://localhost:8080/success"
    result, err := cb.Call(func() (any, error) { return Get(url) })
    fmt.Println("Circuit Breaker state:", cb.state)

    url = "http://localhost:8080/failure"
    result, err = cb.Call(func() (any, error) { return Get(url) })
    fmt.Println("Circuit Breaker state:", cb.state)

    time.Sleep(time.Second * 6)

    url = "http://localhost:8080/success"
    result, err = cb.Call(func() (any, error) { return Get(url) })
    fmt.Println("Circuit Breaker state:", cb.state)
}

func Get(url string) (int, error) {
    r, _ := http.Get(url)
    if r.StatusCode != http.StatusOK {
        return r.StatusCode, fmt.Errorf("failed to get %s", url)
    }
    return r.StatusCode, nil
}

Benefits and Challenges

Benefits

  • Prevents Cascading Failures: By tripping the circuit when a service is failing, you prevent other services from being overwhelmed with requests.
  • Reduces Latency: Immediate failure in the Open state reduces the latency caused by waiting for a failing service to respond.
  • Improves Resilience: Allows the system to recover more quickly by giving the failing service time to recover without additional load.

Challenges

  • Choosing Thresholds: It can be challenging to choose the right failure thresholds and recovery times without introducing false positives or excessive latency.
  • Complexity: Implementing a Circuit Breaker can add complexity to your codebase, especially if you are creating it from scratch.

Real-World Example

Let’s consider a real-world example where we have a microservice architecture with multiple services interacting with each other. One of these services, UserService, depends on another service, PaymentService, to process payments.

sequenceDiagram participant UserService participant PaymentService participant CircuitBreaker UserService->>CircuitBreaker: Request to process payment CircuitBreaker->>PaymentService: Forward request alt PaymentService is available PaymentService->>CircuitBreaker: Successful response CircuitBreaker->>UserService: Forward response else PaymentService is failing PaymentService->>CircuitBreaker: Failure response CircuitBreaker->>CircuitBreaker: Trip circuit CircuitBreaker->>UserService: Immediate failure response end

In this scenario, if PaymentService starts failing, the Circuit Breaker will trip and prevent further requests from reaching PaymentService, thus preventing cascading failures.

Conclusion

The Circuit Breaker pattern is a powerful tool in the arsenal of any microservice architect. By preventing cascading failures and reducing latency, it significantly improves the resilience of your system. Whether you choose to use an existing library like gobreaker or implement your own Circuit Breaker from scratch, the benefits are well worth the effort.

So the next time your services start to feel like they’re in a game of dominoes, just remember: a Circuit Breaker can be the safety net that keeps everything standing tall. Happy coding