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 }