Mostafa Asgari
Mostafa Asgari

Mostafa Asgari

State Design Pattern: Let's build a circuit breaker

Mostafa Asgari's photo
Mostafa Asgari

Published on Aug 20, 2021

4 min read

Definition of State pattern:

The state pattern is a behavioral software design pattern that allows an object to alter its behavior when its internal state changes. This pattern is close to the concept of finite-state machines. The state pattern can be interpreted as a strategy pattern, which is able to switch a strategy through invocations of methods defined in the pattern's interface.

The state pattern is used in computer programming to encapsulate varying behavior for the same object, based on its internal state. This can be a cleaner way for an object to change its behavior at runtime without resorting to conditional statements and thus improve maintainability.

Definition of Circuit breaker:

Circuit breaker is a design pattern used in software development. It is used to detect failures and encapsulates the logic of preventing a failure from constantly recurring, during maintenance, temporary external system failure or unexpected system difficulties.

gobreaker is a Go library that implements circuit breaker. I read the source code, and I saw that there are if/switch cases all over the code to handle each state. For instance this is part of the source code:

func (cb *CircuitBreaker) onSuccess(state State, now time.Time) {
    switch state {
    case StateClosed:
        cb.counts.onSuccess()
    case StateHalfOpen:
        cb.counts.onSuccess()
        if cb.counts.ConsecutiveSuccesses >= cb.maxRequests {
            cb.setState(StateClosed, now)
        }
    }
}

func (cb *CircuitBreaker) onFailure(state State, now time.Time) {
    switch state {
    case StateClosed:
        cb.counts.onFailure()
        if cb.readyToTrip(cb.counts) {
            cb.setState(StateOpen, now)
        }
    case StateHalfOpen:
        cb.setState(StateOpen, now)
    }
}

func (cb *CircuitBreaker) currentState(now time.Time) (State, uint64) {
    switch cb.state {
    case StateClosed:
        if !cb.expiry.IsZero() && cb.expiry.Before(now) {
            cb.toNewGeneration(now)
        }
    case StateOpen:
        if cb.expiry.Before(now) {
            cb.setState(StateHalfOpen, now)
        }
    }
    return cb.state, cb.generation
}

I don't want to criticize this library. It is a great library and it's doing its job perfectly. I just wanted to use State pattern in a real-world example. That's why I rewrite it with State pattern. In the state pattern, all the logic and details for each state, encapsulated in its own object, eliminating the need for if/switch cases and consequently resulted in more readable code.

States in circuit breaker

The circuit breaker has 3 steps:

  • Closed
  • Open
  • Half-open

The initial state is closed. In this state, all requests allow reaching the backend service. If the number of consecutive failures reached a certain threshold, it switches to the open state. The circuit remains in this state for a predefined period of time and rejects all the requests with an error, saving time and increase response time for a failed service. After this predefined time, the circuit goes to half-open. If a request fails in this state, it immediately goes to the open state again. Otherwise, it goes to the closed state (The number of successful requests in order to move to the closed state is configurable, the default is 1). The picture below depicts the idea:

0_1LY6sVQA9PzuHqXO.png

State design pattern

State_Design_Pattern_UML_Class_Diagram.svg.png In Golang we don't have inheritance. So instead of having a base class for all states, I declared all the common features for each state in an interface:

type state interface {
    execute(func() (interface{}, error)) (interface{}, error)
    getCounts() Counts
    getType() StateType

    // for allocation resources
    onEnter()
    // for releasing resources
    onLeave()
}

And we have 3 structs that implement this interface:

// These are states of CircuitBreaker.
var (
    closed   *closedState
    halfOpen *halfOpenState
    open     *openState
)

Let's focus more on the state interface. execute method is the primary logic of the state. It accepts a function that returns eighter error or result and it executes this function. If an error occurs it counts as a failure, otherwise counts as successful and returns the result to the caller. Each state implements the execute logic differently. For instance, in the open state, it just simply returns an error indicating that the circuit is open:

func (s *openState) execute(req func() (interface{}, error)) (interface{}, error) {
    return nil, ErrOpenState
}

The execute function for closed and halfOpen states have more logic.

getCounts() returns statistics about the current state like total requests or total failure. One thing to bear in mind is that if the state changes for whatever reason, all counts will be reset to zero, and new generation will be started:

generation (1).png

getType() simply returns the state type. Either: Closed, Open or HalfOpen.

onEnter() and onLeave() methods are events fired for each state. Imaging a transition from closed to open:

state-onEnter-onLeave (1).png First onLeave event for closed state fires, then onEnter for open state.

Changing the state

In the UML for state pattern, each state holds a reference to state context and they use this context to notify that state is changing. In my design:

type CircuitBreaker struct {
   ...
}
func (cb *CircuitBreaker) changeState(newState state) {
  ...
}

CircuitBreaker is the state context and each state holds a reference to it:

func NewClosedState(cb *CircuitBreaker) *closedState
func NewHalfOpenState(cb *CircuitBreaker) *halfOpenState
func NewOpenState(cb *CircuitBreaker) *openState

Last word

The state design pattern is a great pattern when you have many states, and each state has a different logic. State pattern moves the logic for each state to its own class, eliminating the program from if conditions and switch/cases. You can find the full source code on the github page.

 
Share this