go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/breaker/breaker_test.go (about)

     1  package breaker
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"testing"
     7  	"time"
     8  
     9  	"go.charczuk.com/sdk/assert"
    10  	"go.charczuk.com/sdk/errutil"
    11  )
    12  
    13  func Test_New(t *testing.T) {
    14  	b := New(new(actionShim[any, any]))
    15  	assert.ItsEqual(t, DefaultHalfOpenMaxActions, b.Config.HalfOpenMaxActionsOrDefault())
    16  	assert.ItsEqual(t, DefaultOpenExpiryInterval, b.Config.OpenExpiryIntervalOrDefault())
    17  }
    18  
    19  func Test_New_Options(t *testing.T) {
    20  	b := New(
    21  		new(actionShim[any, any]),
    22  		OptConfig[any, any](Config{
    23  			HalfOpenMaxActions: 5,
    24  			OpenExpiryInterval: 10 * time.Second,
    25  		}),
    26  	)
    27  	assert.ItsEqual(t, 5, b.Config.HalfOpenMaxActions)
    28  	assert.ItsEqual(t, 10*time.Second, b.Config.OpenExpiryInterval)
    29  }
    30  
    31  func Test_Breaker_basic(t *testing.T) {
    32  	ctx := context.Background()
    33  
    34  	b, c := createTestBreaker[any, any]()
    35  
    36  	failureThreshold := b.Config.FailureThresholdOrDefault()
    37  	for i := 0; i < int(failureThreshold)-1; i++ {
    38  		assert.ItsNil(t, fail(b))
    39  	}
    40  	assert.ItsEqual(t, StateClosed, b.EvaluateState(ctx))
    41  	assert.ItsEqual(t, Counts{4, 0, 4, 0, 4}, b.Counts())
    42  
    43  	assert.ItsNil(t, succeed(b))
    44  	assert.ItsEqual(t, StateClosed, b.EvaluateState(ctx))
    45  	assert.ItsEqual(t, Counts{5, 1, 4, 1, 0}, b.Counts())
    46  
    47  	assert.ItsNil(t, fail(b))
    48  	assert.ItsEqual(t, StateClosed, b.EvaluateState(ctx))
    49  	assert.ItsEqual(t, Counts{6, 1, 5, 0, 1}, b.Counts())
    50  
    51  	// StateClosed to StateOpen
    52  	for i := 0; i < int(failureThreshold)-1; i++ {
    53  		assert.ItsNil(t, fail(b)) // 5 more consecutive failures
    54  	}
    55  	assert.ItsEqual(t, StateOpen, b.EvaluateState(ctx))
    56  	assert.ItsEqual(t, Counts{0, 0, 0, 0, 0}, b.Counts())
    57  	assert.ItsEqual(t, false, b.openStateExpiresAt.IsZero())
    58  
    59  	err := succeed(b)
    60  	assert.ItsEqual(t, true, ErrIsOpen(err)) // this shouldn't have called the action, should yield closed
    61  	err = fail(b)
    62  	assert.ItsEqual(t, true, ErrIsOpen(err)) // this shouldn't have called the action either
    63  	assert.ItsEqual(t, Counts{0, 0, 0, 0, 0}, b.Counts())
    64  
    65  	c.Sleep(b.Config.OpenExpiryIntervalOrDefault() - time.Second) // push forward time by almost expiry interval
    66  	assert.ItsEqual(t, StateOpen, b.EvaluateState(ctx))
    67  
    68  	// StateOpen to StateHalfOpen
    69  	c.Sleep(2 * time.Second) // to just past the open expiry timeout
    70  	assert.ItsEqual(t, StateHalfOpen, b.EvaluateState(ctx), fmt.Sprintf("time until expiry: %v", b.openStateExpiresAt.Sub(b.now())))
    71  	assert.ItsEqual(t, true, b.openStateExpiresAt.IsZero())
    72  
    73  	// StateHalfOpen to StateOpen
    74  	// there are like, (3) calls queued here
    75  	assert.ItsNil(t, fail(b))
    76  	assert.ItsEqual(t, StateOpen, b.EvaluateState(ctx))
    77  	assert.ItsEqual(t, Counts{0, 0, 0, 0, 0}, b.Counts())
    78  	assert.ItsEqual(t, false, b.openStateExpiresAt.IsZero())
    79  
    80  	// StateOpen to StateHalfOpen
    81  	c.Sleep(b.Config.OpenExpiryIntervalOrDefault() + time.Second)
    82  
    83  	assert.ItsEqual(t, StateHalfOpen, b.EvaluateState(ctx))
    84  	assert.ItsEqual(t, true, b.openStateExpiresAt.IsZero())
    85  
    86  	// StateHalfOpen to StateClosed
    87  	for x := 0; x < int(b.Config.HalfOpenMaxActionsOrDefault()); x++ {
    88  		assert.ItsNil(t, succeed(b))
    89  	}
    90  	assert.ItsEqual(t, StateClosed, b.EvaluateState(ctx))
    91  	assert.ItsEqual(t, Counts{0, 0, 0, 0, 0}, b.Counts())
    92  	assert.ItsEqual(t, true, b.openStateExpiresAt.IsZero(), "we do not expect an expiry interval")
    93  
    94  	c.Sleep(time.Second)
    95  	assert.ItsEqual(t, StateClosed, b.EvaluateState(ctx))
    96  }
    97  
    98  func Test_Breaker_closedFailureExpiry(t *testing.T) {
    99  	b, c := createTestBreaker[any, any]()
   100  
   101  	var err error
   102  	err = succeed(b)
   103  	assert.ItsNil(t, err)
   104  
   105  	err = fail(b)
   106  	assert.ItsNil(t, err)
   107  
   108  	err = succeed(b)
   109  	assert.ItsNil(t, err)
   110  
   111  	assert.ItsEqual(t, 0, b.Counts().ConsecutiveFailures)
   112  	assert.ItsEqual(t, 1, b.Counts().ConsecutiveSuccesses)
   113  	assert.ItsEqual(t, 1, b.Counts().TotalFailures)
   114  	assert.ItsEqual(t, 2, b.Counts().TotalSuccesses)
   115  
   116  	c.Sleep(b.Config.ClosedFailureExpiryIntervalOrDefault() + time.Second)
   117  
   118  	err = succeed(b)
   119  	assert.ItsNil(t, err)
   120  
   121  	assert.ItsEqual(t, 0, b.Counts().ConsecutiveFailures)
   122  	assert.ItsEqual(t, 1, b.Counts().ConsecutiveSuccesses)
   123  	assert.ItsEqual(t, 0, b.Counts().TotalFailures)
   124  	assert.ItsEqual(t, 1, b.Counts().TotalSuccesses)
   125  
   126  	err = succeed(b)
   127  	assert.ItsNil(t, err)
   128  
   129  	assert.ItsEqual(t, 0, b.Counts().ConsecutiveFailures)
   130  	assert.ItsEqual(t, 2, b.Counts().ConsecutiveSuccesses)
   131  	assert.ItsEqual(t, 0, b.Counts().TotalFailures)
   132  	assert.ItsEqual(t, 2, b.Counts().TotalSuccesses)
   133  
   134  	err = fail(b)
   135  	assert.ItsNil(t, err)
   136  
   137  	assert.ItsEqual(t, 1, b.Counts().ConsecutiveFailures)
   138  	assert.ItsEqual(t, 0, b.Counts().ConsecutiveSuccesses)
   139  	assert.ItsEqual(t, 1, b.Counts().TotalFailures)
   140  	assert.ItsEqual(t, 2, b.Counts().TotalSuccesses)
   141  }
   142  
   143  func Test_Breaker_ErrStateOpen(t *testing.T) {
   144  	b := New(new(actionShim[any, any]))
   145  	b.state = StateOpen
   146  	b.openStateExpiresAt = time.Now().Add(time.Hour)
   147  	_, err := b.Call(context.TODO(), nil)
   148  	assert.ItsEqual(t, true, errutil.Is(err, ErrOpenState), fmt.Sprintf("%v", err))
   149  }
   150  
   151  func Test_Breaker_ErrTooManyRequests(t *testing.T) {
   152  	b := New(new(actionShim[any, any]))
   153  
   154  	b.state = StateHalfOpen
   155  	b.counts.Requests = 10
   156  	b.Config.HalfOpenMaxActions = 5
   157  
   158  	_, err := b.Call(context.Background(), nil)
   159  	assert.ItsEqual(t, true, errutil.Is(err, ErrTooManyRequests))
   160  	assert.ItsEqual(t, false, b.Action.(*actionShim[any, any]).didCall)
   161  }
   162  
   163  func Test_Breaker_failWithPanic(t *testing.T) {
   164  	b, _ := createTestBreaker[any, any]()
   165  
   166  	var didCallOnStateChange bool
   167  	b.OnStateChange = func(ctx context.Context, from, to State, generation int64) {
   168  		didCallOnStateChange = true
   169  		assert.ItsEqual(t, 1, generation)
   170  		assert.ItsEqual(t, StateClosed, from)
   171  		assert.ItsEqual(t, StateOpen, to)
   172  	}
   173  
   174  	var err error
   175  	err = succeed(b)
   176  	assert.ItsNil(t, err)
   177  
   178  	for x := 0; x < int(b.Config.FailureThresholdOrDefault())-1; x++ {
   179  		err = failWithPanic(b)
   180  		assert.ItsNil(t, err)
   181  	}
   182  
   183  	assert.ItsEqual(t, 4, b.Counts().ConsecutiveFailures)
   184  	assert.ItsEqual(t, 0, b.Counts().ConsecutiveSuccesses)
   185  	assert.ItsEqual(t, 4, b.Counts().TotalFailures)
   186  	assert.ItsEqual(t, 1, b.Counts().TotalSuccesses)
   187  
   188  	err = failWithPanic(b)
   189  	assert.ItsNil(t, err, "the `fail` handler will nil out the expected error")
   190  	assert.ItsEqual(t, true, didCallOnStateChange)
   191  
   192  	assert.ItsEqual(t, 0, b.Counts().ConsecutiveFailures)
   193  	assert.ItsEqual(t, 0, b.Counts().ConsecutiveSuccesses)
   194  	assert.ItsEqual(t, 0, b.Counts().TotalFailures)
   195  	assert.ItsEqual(t, 0, b.Counts().TotalSuccesses)
   196  }
   197  
   198  func Test_Breaker_ShouldOpen(t *testing.T) {
   199  	b, _ := createTestBreaker[string, string]()
   200  
   201  	b.Action.(*actionShim[string, string]).res = "ok!"
   202  	var didCallShouldOpen bool
   203  	b.ShouldOpen = func(ctx context.Context, counts Counts, args string) bool {
   204  		didCallShouldOpen = true
   205  		return args == "foo" // we also have to have a failure!
   206  	}
   207  	res, err := b.Call(context.Background(), "not-foo")
   208  	assert.ItsNil(t, err)
   209  	assert.ItsEqual(t, "ok!", res)
   210  
   211  	b.Action.(*actionShim[string, string]).err = fmt.Errorf("not-ok")
   212  
   213  	res, err = b.Call(context.Background(), "foo")
   214  	assert.ItsEqual(t, true, didCallShouldOpen)
   215  	assert.ItsNotNil(t, err)
   216  	assert.ItsEqual(t, "ok!", res)
   217  }
   218  
   219  func Test_Breaker_OnStateChange(t *testing.T) {
   220  	b, _ := createTestBreaker[any, any]()
   221  
   222  	var didCallOnStateChange bool
   223  	b.OnStateChange = func(ctx context.Context, from, to State, generation int64) {
   224  		didCallOnStateChange = true
   225  		assert.ItsEqual(t, 1, generation)
   226  		assert.ItsEqual(t, StateClosed, from)
   227  		assert.ItsEqual(t, StateOpen, to)
   228  	}
   229  
   230  	var err error
   231  	err = succeed(b)
   232  	assert.ItsNil(t, err)
   233  
   234  	for x := 0; x < int(b.Config.FailureThresholdOrDefault())-1; x++ {
   235  		err = fail(b)
   236  		assert.ItsNil(t, err)
   237  	}
   238  
   239  	assert.ItsEqual(t, 4, b.Counts().ConsecutiveFailures)
   240  	assert.ItsEqual(t, 0, b.Counts().ConsecutiveSuccesses)
   241  	assert.ItsEqual(t, 4, b.Counts().TotalFailures)
   242  	assert.ItsEqual(t, 1, b.Counts().TotalSuccesses)
   243  
   244  	err = fail(b)
   245  	assert.ItsNil(t, err, "the `fail` handler will nil out the expected error")
   246  	assert.ItsEqual(t, true, didCallOnStateChange)
   247  
   248  	assert.ItsEqual(t, 0, b.Counts().ConsecutiveFailures)
   249  	assert.ItsEqual(t, 0, b.Counts().ConsecutiveSuccesses)
   250  	assert.ItsEqual(t, 0, b.Counts().TotalFailures)
   251  	assert.ItsEqual(t, 0, b.Counts().TotalSuccesses)
   252  }
   253  
   254  func Test_Breaker_OnOpen(t *testing.T) {
   255  	var didCallOpen bool
   256  	b := New(new(actionShim[any, any]))
   257  	b.OpenAction = ActionFunc[any, any](func(_ context.Context, _ any) (any, error) {
   258  		didCallOpen = true
   259  		return "on open", nil
   260  	})
   261  	b.state = StateOpen
   262  	b.openStateExpiresAt = time.Now().Add(b.Config.OpenExpiryIntervalOrDefault() << 1)
   263  	res, err := b.Call(context.Background(), nil)
   264  	assert.ItsNil(t, err)
   265  	assert.ItsEqual(t, true, didCallOpen)
   266  	assert.ItsEqual(t, false, b.Action.(*actionShim[any, any]).didCall)
   267  	assert.ItsEqual(t, "on open", res)
   268  }