github.com/blend/go-sdk@v1.20220411.3/breaker/breaker_test.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  	"fmt"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/blend/go-sdk/assert"
    17  	"github.com/blend/go-sdk/ex"
    18  )
    19  
    20  func TestNew(t *testing.T) {
    21  	its := assert.New(t)
    22  
    23  	b := New()
    24  	its.Equal(DefaultHalfOpenMaxActions, b.HalfOpenMaxActions)
    25  	its.Equal(DefaultOpenExpiryInterval, b.OpenExpiryInterval)
    26  	its.Equal(DefaultClosedExpiryInterval, b.ClosedExpiryInterval)
    27  }
    28  
    29  func TestNewOptions(t *testing.T) {
    30  	its := assert.New(t)
    31  
    32  	b := New(
    33  		OptHalfOpenMaxActions(5),
    34  		OptOpenExpiryInterval(10*time.Second),
    35  		OptClosedExpiryInterval(20*time.Second),
    36  	)
    37  	its.Equal(5, b.HalfOpenMaxActions)
    38  	its.Equal(10*time.Second, b.OpenExpiryInterval)
    39  	its.Equal(20*time.Second, b.ClosedExpiryInterval)
    40  }
    41  
    42  func createTestBreaker() *Breaker {
    43  	return New(OptClosedExpiryInterval(0))
    44  }
    45  
    46  func succeed(b *Breaker) error {
    47  	_, err := b.Intercept(ActionerFunc(func(_ context.Context, _ interface{}) (interface{}, error) {
    48  		return nil, nil
    49  	})).Action(context.Background(), nil)
    50  	return err
    51  }
    52  
    53  func fail(b *Breaker) error {
    54  	msg := "fail"
    55  	_, err := b.Intercept(ActionerFunc(func(_ context.Context, _ interface{}) (interface{}, error) {
    56  		return nil, fmt.Errorf(msg)
    57  	})).Action(context.Background(), nil)
    58  	if err != nil && err.Error() == msg {
    59  		return nil
    60  	}
    61  	return err
    62  }
    63  
    64  func pseudoSleep(b *Breaker, period time.Duration) {
    65  	if !b.stateExpiresAt.IsZero() {
    66  		b.stateExpiresAt = b.stateExpiresAt.Add(-period)
    67  	}
    68  }
    69  
    70  func Test_Breaker(t *testing.T) {
    71  	its := assert.New(t)
    72  	ctx := context.Background()
    73  
    74  	b := createTestBreaker()
    75  
    76  	for i := 0; i < 5; i++ {
    77  		its.Nil(fail(b))
    78  	}
    79  	its.Equal(StateClosed, b.EvaluateState(ctx))
    80  	its.Equal(Counts{5, 0, 5, 0, 5}, b.Counts)
    81  
    82  	its.Nil(succeed(b))
    83  	its.Equal(StateClosed, b.EvaluateState(ctx))
    84  	its.Equal(Counts{6, 1, 5, 1, 0}, b.Counts)
    85  
    86  	its.Nil(fail(b))
    87  	its.Equal(StateClosed, b.EvaluateState(ctx))
    88  	its.Equal(Counts{7, 1, 6, 0, 1}, b.Counts)
    89  
    90  	// StateClosed to StateOpen
    91  	for i := 0; i < 5; i++ {
    92  		its.Nil(fail(b)) // 5 more consecutive failures
    93  	}
    94  	its.Equal(StateOpen, b.EvaluateState(ctx))
    95  	its.Equal(Counts{0, 0, 0, 0, 0}, b.Counts)
    96  	its.False(b.stateExpiresAt.IsZero())
    97  
    98  	err := succeed(b)
    99  	its.True(ErrIsOpen(err)) // this shouldn't have called the action, should yield closed
   100  	err = fail(b)
   101  	its.True(ErrIsOpen(err)) // this shouldn't have called the action either
   102  	its.Equal(Counts{0, 0, 0, 0, 0}, b.Counts)
   103  
   104  	pseudoSleep(b, 59*time.Second) // push forward time by 59s
   105  	its.Equal(StateOpen, b.EvaluateState(ctx))
   106  
   107  	// StateOpen to StateHalfOpen
   108  	pseudoSleep(b, 2*time.Second) // over Timeout
   109  	its.Equal(StateHalfOpen, b.EvaluateState(ctx))
   110  	its.True(b.stateExpiresAt.IsZero())
   111  
   112  	// StateHalfOpen to StateOpen
   113  	// there are like, (3) calls queued here
   114  	its.Nil(fail(b))
   115  	its.Equal(StateOpen, b.EvaluateState(ctx))
   116  	its.Equal(Counts{0, 0, 0, 0, 0}, b.Counts)
   117  	its.False(b.stateExpiresAt.IsZero())
   118  
   119  	// StateOpen to StateHalfOpen
   120  	pseudoSleep(b, time.Duration(60)*time.Second)
   121  	its.Equal(StateHalfOpen, b.EvaluateState(ctx))
   122  	its.True(b.stateExpiresAt.IsZero())
   123  
   124  	// StateHalfOpen to StateClosed
   125  	its.Nil(succeed(b))
   126  	its.Equal(StateClosed, b.EvaluateState(ctx))
   127  	its.Equal(Counts{0, 0, 0, 0, 0}, b.Counts)
   128  	its.True(b.stateExpiresAt.IsZero())
   129  }
   130  
   131  func Test_Breaker_ErrStateOpen(t *testing.T) {
   132  	its := assert.New(t)
   133  
   134  	var didCall bool
   135  	b := New()
   136  	b.state = StateOpen
   137  	b.stateExpiresAt = time.Now().Add(time.Hour)
   138  	_, err := b.Intercept(ActionerFunc(func(_ context.Context, _ interface{}) (interface{}, error) {
   139  		didCall = true
   140  		return nil, nil
   141  	})).Action(context.Background(), nil)
   142  	its.True(ex.Is(err, ErrOpenState), fmt.Sprintf("%v", err))
   143  	its.False(didCall)
   144  }
   145  
   146  func Test_Breaker_ErrTooManyRequests(t *testing.T) {
   147  	its := assert.New(t)
   148  
   149  	var didCall bool
   150  	b := New()
   151  
   152  	b.state = StateHalfOpen
   153  	b.Counts.Requests = 10
   154  	b.HalfOpenMaxActions = 5
   155  
   156  	_, err := b.Intercept(ActionerFunc(func(_ context.Context, _ interface{}) (interface{}, error) {
   157  		didCall = true
   158  		return nil, nil
   159  	})).Action(context.Background(), nil)
   160  	its.True(ex.Is(err, ErrTooManyRequests))
   161  	its.False(didCall)
   162  }
   163  
   164  func Test_Breaker_callsOnOpenHandler(t *testing.T) {
   165  	its := assert.New(t)
   166  
   167  	var didCall, didCallOpen bool
   168  	b := New(
   169  		OptOpenAction(ActionerFunc(func(_ context.Context, _ interface{}) (interface{}, error) {
   170  			didCallOpen = true
   171  			return "on open", nil
   172  		})),
   173  	)
   174  
   175  	b.state = StateOpen
   176  	b.stateExpiresAt = time.Now().Add(time.Hour)
   177  
   178  	res, err := b.Intercept(ActionerFunc(func(_ context.Context, _ interface{}) (interface{}, error) {
   179  		didCall = true
   180  		return nil, nil
   181  	})).Action(context.Background(), nil)
   182  	its.Nil(err)
   183  	its.False(didCall)
   184  	its.True(didCallOpen)
   185  	its.Equal("on open", res)
   186  }