github.com/blend/go-sdk@v1.20220411.3/breaker/breaker.go (about)

     1  /*
     2  
     3  Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file.
     5  
     6  */
     7  
     8  package breaker
     9  
    10  import (
    11  	"context"
    12  	"sync"
    13  	"sync/atomic"
    14  	"time"
    15  
    16  	"github.com/blend/go-sdk/async"
    17  	"github.com/blend/go-sdk/ex"
    18  )
    19  
    20  var (
    21  	_ async.Interceptor = (*Breaker)(nil)
    22  )
    23  
    24  type (
    25  	// OnStateChangeHandler is called when the state changes.
    26  	OnStateChangeHandler func(ctx context.Context, from, to State, generation int64)
    27  	// ShouldOpenProvider returns if the breaker should open.
    28  	ShouldOpenProvider func(ctx context.Context, counts Counts) bool
    29  	// NowProvider returns the current time.
    30  	NowProvider func() time.Time
    31  )
    32  
    33  // New creates a new breaker with the given options.
    34  func New(options ...Option) *Breaker {
    35  	b := Breaker{
    36  		ClosedExpiryInterval: DefaultClosedExpiryInterval,
    37  		OpenExpiryInterval:   DefaultOpenExpiryInterval,
    38  		HalfOpenMaxActions:   DefaultHalfOpenMaxActions,
    39  		OpenFailureThreshold: DefaultOpenFailureThreshold,
    40  	}
    41  	for _, opt := range options {
    42  		opt(&b)
    43  	}
    44  	return &b
    45  }
    46  
    47  // Breaker is a state machine to prevent performing actions that are likely to fail.
    48  type Breaker struct {
    49  	sync.Mutex
    50  	// OpenAction is an optional actioner to be called when the breaker is open (i.e. preventing calls
    51  	// to intercepted action(er)s)
    52  	OpenAction Actioner
    53  	// OnStateChange is an optional handler called when the breaker transitions state.
    54  	OnStateChange OnStateChangeHandler
    55  	// ShouldOpenProvider is called optionally to determine if we should open the breaker.
    56  	ShouldOpenProvider ShouldOpenProvider
    57  	// NowProvider lets you optionally inject the current time for testing.
    58  	NowProvider NowProvider
    59  
    60  	// OpenFailureThreshold is the default failure threshold
    61  	// before the breaker enters the open state. It is how many actions
    62  	// have to fail consecutively.
    63  	OpenFailureThreshold int64
    64  	// HalfOpenMaxActions is the maximum number of requests
    65  	// we can make when the state is HalfOpen.
    66  	HalfOpenMaxActions int64
    67  	// ClosedExpiryInterval is the cyclic period of the closed state for the CircuitBreaker to clear the internal Counts.
    68  	// If Interval is 0, the CircuitBreaker doesn't clear internal Counts during the closed state.
    69  	ClosedExpiryInterval time.Duration
    70  	// OpenExpiryInterval is the period of the open state,
    71  	// after which the state of the CircuitBreaker becomes half-open.
    72  	// If Timeout is 0, the timeout value of the CircuitBreaker is set to 60 seconds.
    73  	OpenExpiryInterval time.Duration
    74  	// Counts are stats for the breaker.
    75  	Counts Counts
    76  
    77  	// state is the current Breaker state (Closed, HalfOpen, Open etc.)
    78  	state State
    79  	// generation is the current state generation.
    80  	generation int64
    81  	// stateExpiresAt is the time when the current state will expire.
    82  	// It is set when we change state according to the interval
    83  	// and the current time.
    84  	stateExpiresAt time.Time
    85  }
    86  
    87  // Intercept implements the breaker by returning a wrapper for a given action(er).
    88  /*
    89  It returns an error instantly if the Breaker rejects the request, otherwise,
    90  it returns the result of the request.
    91  
    92  If a panic occurs in the request, the Breaker handles it as an error.
    93  */
    94  func (b *Breaker) Intercept(action Actioner) Actioner {
    95  	return ActionerFunc(func(ctx context.Context, args interface{}) (res interface{}, err error) {
    96  		var generation int64
    97  		generation, err = b.beforeAction(ctx)
    98  		if err != nil {
    99  			if b.OpenAction != nil {
   100  				res, err = b.OpenAction.Action(ctx, args)
   101  				return
   102  			}
   103  			return
   104  		}
   105  		defer func() {
   106  			if r := recover(); r != nil {
   107  				b.afterAction(ctx, generation, false)
   108  			} else {
   109  				b.afterAction(ctx, generation, err == nil)
   110  			}
   111  		}()
   112  		res, err = action.Action(ctx, args)
   113  		return
   114  	})
   115  }
   116  
   117  // EvaluateState returns the current state of the CircuitBreaker.
   118  //
   119  // It takes a context because there is a chance that evaluating
   120  // the state causes the state to change, which would
   121  // result in calling the `OnStateChange` handler.
   122  func (b *Breaker) EvaluateState(ctx context.Context) State {
   123  	b.Lock()
   124  	defer b.Unlock()
   125  
   126  	now := time.Now()
   127  	state, _ := b.evaluateStateUnsafe(ctx, now)
   128  	return state
   129  }
   130  
   131  //
   132  // internal methods
   133  //
   134  
   135  func (b *Breaker) beforeAction(ctx context.Context) (int64, error) {
   136  	b.Lock()
   137  	defer b.Unlock()
   138  
   139  	now := b.now()
   140  	state, generation := b.evaluateStateUnsafe(ctx, now)
   141  
   142  	if state == StateOpen {
   143  		return generation, ex.New(ErrOpenState)
   144  	} else if state == StateHalfOpen && b.Counts.Requests >= b.HalfOpenMaxActions {
   145  		return generation, ex.New(ErrTooManyRequests)
   146  	}
   147  
   148  	atomic.AddInt64(&b.Counts.Requests, 1)
   149  	return generation, nil
   150  }
   151  
   152  func (b *Breaker) afterAction(ctx context.Context, currentGeneration int64, success bool) {
   153  	b.Lock()
   154  	defer b.Unlock()
   155  
   156  	now := b.now()
   157  	state, generation := b.evaluateStateUnsafe(ctx, now)
   158  	if generation != currentGeneration {
   159  		return
   160  	}
   161  	if success {
   162  		b.success(ctx, state, now)
   163  		return
   164  	}
   165  	b.failure(ctx, state, now)
   166  }
   167  
   168  func (b *Breaker) success(ctx context.Context, state State, now time.Time) {
   169  	switch state {
   170  	case StateClosed:
   171  		atomic.AddInt64(&b.Counts.TotalSuccesses, 1)
   172  		atomic.AddInt64(&b.Counts.ConsecutiveSuccesses, 1)
   173  		atomic.StoreInt64(&b.Counts.ConsecutiveFailures, 0)
   174  	case StateHalfOpen:
   175  		atomic.AddInt64(&b.Counts.TotalSuccesses, 1)
   176  		atomic.AddInt64(&b.Counts.ConsecutiveSuccesses, 1)
   177  		atomic.StoreInt64(&b.Counts.ConsecutiveFailures, 0)
   178  		if b.Counts.ConsecutiveSuccesses >= b.HalfOpenMaxActions {
   179  			b.setStateUnsafe(ctx, StateClosed, now)
   180  		}
   181  	}
   182  }
   183  
   184  func (b *Breaker) failure(ctx context.Context, state State, now time.Time) {
   185  	switch state {
   186  	case StateClosed:
   187  		atomic.AddInt64(&b.Counts.TotalFailures, 1)
   188  		atomic.AddInt64(&b.Counts.ConsecutiveFailures, 1)
   189  		atomic.StoreInt64(&b.Counts.ConsecutiveSuccesses, 0)
   190  		if b.shouldOpen(ctx) {
   191  			b.setStateUnsafe(ctx, StateOpen, now)
   192  		}
   193  	case StateHalfOpen:
   194  		b.setStateUnsafe(ctx, StateOpen, now)
   195  	}
   196  }
   197  
   198  func (b *Breaker) evaluateStateUnsafe(ctx context.Context, t time.Time) (state State, generation int64) {
   199  	switch b.state {
   200  	case StateClosed:
   201  		if !b.stateExpiresAt.IsZero() && b.stateExpiresAt.Before(t) {
   202  			b.incrementGeneration(t)
   203  		}
   204  	case StateOpen:
   205  		if b.stateExpiresAt.Before(t) {
   206  			b.setStateUnsafe(ctx, StateHalfOpen, t)
   207  		}
   208  	}
   209  	return b.state, b.generation
   210  }
   211  
   212  func (b *Breaker) setStateUnsafe(ctx context.Context, state State, now time.Time) {
   213  	if b.state == state {
   214  		return
   215  	}
   216  
   217  	previousState := b.state
   218  	b.state = state
   219  	b.incrementGeneration(now)
   220  	if b.OnStateChange != nil {
   221  		b.OnStateChange(ctx, previousState, b.state, b.generation)
   222  	}
   223  }
   224  
   225  func (b *Breaker) incrementGeneration(now time.Time) {
   226  	atomic.AddInt64(&b.generation, 1)
   227  	b.Counts = Counts{}
   228  
   229  	var zero time.Time
   230  	switch b.state {
   231  	case StateClosed:
   232  		if b.ClosedExpiryInterval == 0 {
   233  			b.stateExpiresAt = zero
   234  		} else {
   235  			b.stateExpiresAt = now.Add(b.ClosedExpiryInterval)
   236  		}
   237  	case StateOpen:
   238  		b.stateExpiresAt = now.Add(b.OpenExpiryInterval)
   239  	case StateHalfOpen:
   240  		b.stateExpiresAt = zero
   241  	default:
   242  		b.stateExpiresAt = zero
   243  	}
   244  }
   245  
   246  func (b *Breaker) shouldOpen(ctx context.Context) bool {
   247  	if b.ShouldOpenProvider != nil {
   248  		return b.ShouldOpenProvider(ctx, b.Counts)
   249  	}
   250  	return b.Counts.ConsecutiveFailures > b.OpenFailureThreshold
   251  }
   252  
   253  func (b *Breaker) now() time.Time {
   254  	if b.NowProvider != nil {
   255  		return b.NowProvider()
   256  	}
   257  	return time.Now()
   258  }