mirror of
https://github.com/crazybber/go-pattern-examples.git
synced 2024-11-26 21:56:02 +03:00
371 lines
11 KiB
Go
371 lines
11 KiB
Go
package gobreaker
|
|
|
|
import (
|
|
"fmt"
|
|
"runtime"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
var defaultCB *CircuitBreaker
|
|
var customCB *CircuitBreaker
|
|
var negativeDurationCB *CircuitBreaker
|
|
|
|
type StateChange struct {
|
|
name string
|
|
from State
|
|
to State
|
|
}
|
|
|
|
var stateChange StateChange
|
|
|
|
func pseudoSleep(cb *CircuitBreaker, period time.Duration) {
|
|
if !cb.expiry.IsZero() {
|
|
cb.expiry = cb.expiry.Add(-period)
|
|
}
|
|
}
|
|
|
|
func succeed(cb *CircuitBreaker) error {
|
|
_, err := cb.Execute(func() (interface{}, error) { return nil, nil })
|
|
return err
|
|
}
|
|
|
|
func succeedLater(cb *CircuitBreaker, delay time.Duration) <-chan error {
|
|
ch := make(chan error)
|
|
go func() {
|
|
_, err := cb.Execute(func() (interface{}, error) {
|
|
time.Sleep(delay)
|
|
return nil, nil
|
|
})
|
|
ch <- err
|
|
}()
|
|
return ch
|
|
}
|
|
|
|
func succeed2Step(cb *TwoStepCircuitBreaker) error {
|
|
done, err := cb.Allow()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
done(true)
|
|
return nil
|
|
}
|
|
|
|
func fail(cb *CircuitBreaker) error {
|
|
msg := "fail"
|
|
_, err := cb.Execute(func() (interface{}, error) { return nil, fmt.Errorf(msg) })
|
|
if err.Error() == msg {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
func fail2Step(cb *TwoStepCircuitBreaker) error {
|
|
done, err := cb.Allow()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
done(false)
|
|
return nil
|
|
}
|
|
|
|
func causePanic(cb *CircuitBreaker) error {
|
|
_, err := cb.Execute(func() (interface{}, error) { panic("oops"); return nil, nil })
|
|
return err
|
|
}
|
|
|
|
func newCustom() *CircuitBreaker {
|
|
var customSt Settings
|
|
customSt.Name = "cb"
|
|
customSt.MaxRequests = 3
|
|
customSt.Interval = time.Duration(30) * time.Second
|
|
customSt.Timeout = time.Duration(90) * time.Second
|
|
customSt.ReadyToTrip = func(counts Counts) bool {
|
|
numReqs := counts.Requests
|
|
failureRatio := float64(counts.TotalFailures) / float64(numReqs)
|
|
|
|
counts.clear() // no effect on customCB.counts
|
|
|
|
return numReqs >= 3 && failureRatio >= 0.6
|
|
}
|
|
customSt.OnStateChange = func(name string, from State, to State) {
|
|
stateChange = StateChange{name, from, to}
|
|
}
|
|
|
|
return NewCircuitBreaker(customSt)
|
|
}
|
|
|
|
func newNegativeDurationCB() *CircuitBreaker {
|
|
var negativeSt Settings
|
|
negativeSt.Name = "ncb"
|
|
negativeSt.Interval = time.Duration(-30) * time.Second
|
|
negativeSt.Timeout = time.Duration(-90) * time.Second
|
|
|
|
return NewCircuitBreaker(negativeSt)
|
|
}
|
|
|
|
func init() {
|
|
defaultCB = NewCircuitBreaker(Settings{})
|
|
customCB = newCustom()
|
|
negativeDurationCB = newNegativeDurationCB()
|
|
}
|
|
|
|
func TestStateConstants(t *testing.T) {
|
|
assert.Equal(t, State(0), StateClosed)
|
|
assert.Equal(t, State(1), StateHalfOpen)
|
|
assert.Equal(t, State(2), StateOpen)
|
|
|
|
assert.Equal(t, StateClosed.String(), "closed")
|
|
assert.Equal(t, StateHalfOpen.String(), "half-open")
|
|
assert.Equal(t, StateOpen.String(), "open")
|
|
assert.Equal(t, State(100).String(), "unknown state: 100")
|
|
}
|
|
|
|
func TestNewCircuitBreaker(t *testing.T) {
|
|
defaultCB := NewCircuitBreaker(Settings{})
|
|
assert.Equal(t, "", defaultCB.name)
|
|
assert.Equal(t, uint32(1), defaultCB.maxRequests)
|
|
assert.Equal(t, time.Duration(0), defaultCB.interval)
|
|
assert.Equal(t, time.Duration(60)*time.Second, defaultCB.timeout)
|
|
assert.NotNil(t, defaultCB.readyToTrip)
|
|
assert.Nil(t, defaultCB.onStateChange)
|
|
assert.Equal(t, StateClosed, defaultCB.state)
|
|
assert.Equal(t, Counts{0, 0, 0, 0, 0}, defaultCB.counts)
|
|
assert.True(t, defaultCB.expiry.IsZero())
|
|
|
|
customCB := newCustom()
|
|
assert.Equal(t, "cb", customCB.name)
|
|
assert.Equal(t, uint32(3), customCB.maxRequests)
|
|
assert.Equal(t, time.Duration(30)*time.Second, customCB.interval)
|
|
assert.Equal(t, time.Duration(90)*time.Second, customCB.timeout)
|
|
assert.NotNil(t, customCB.readyToTrip)
|
|
assert.NotNil(t, customCB.onStateChange)
|
|
assert.Equal(t, StateClosed, customCB.state)
|
|
assert.Equal(t, Counts{0, 0, 0, 0, 0}, customCB.counts)
|
|
assert.False(t, customCB.expiry.IsZero())
|
|
|
|
negativeDurationCB := newNegativeDurationCB()
|
|
assert.Equal(t, "ncb", negativeDurationCB.name)
|
|
assert.Equal(t, uint32(1), negativeDurationCB.maxRequests)
|
|
assert.Equal(t, time.Duration(0)*time.Second, negativeDurationCB.interval)
|
|
assert.Equal(t, time.Duration(60)*time.Second, negativeDurationCB.timeout)
|
|
assert.NotNil(t, negativeDurationCB.readyToTrip)
|
|
assert.Nil(t, negativeDurationCB.onStateChange)
|
|
assert.Equal(t, StateClosed, negativeDurationCB.state)
|
|
assert.Equal(t, Counts{0, 0, 0, 0, 0}, negativeDurationCB.counts)
|
|
assert.True(t, negativeDurationCB.expiry.IsZero())
|
|
}
|
|
|
|
func TestDefaultCircuitBreaker(t *testing.T) {
|
|
assert.Equal(t, "", defaultCB.Name())
|
|
|
|
for i := 0; i < 5; i++ {
|
|
assert.Nil(t, fail(defaultCB))
|
|
}
|
|
assert.Equal(t, StateClosed, defaultCB.State())
|
|
assert.Equal(t, Counts{5, 0, 5, 0, 5}, defaultCB.counts)
|
|
|
|
assert.Nil(t, succeed(defaultCB))
|
|
assert.Equal(t, StateClosed, defaultCB.State())
|
|
assert.Equal(t, Counts{6, 1, 5, 1, 0}, defaultCB.counts)
|
|
|
|
assert.Nil(t, fail(defaultCB))
|
|
assert.Equal(t, StateClosed, defaultCB.State())
|
|
assert.Equal(t, Counts{7, 1, 6, 0, 1}, defaultCB.counts)
|
|
|
|
// StateClosed to StateOpen
|
|
for i := 0; i < 5; i++ {
|
|
assert.Nil(t, fail(defaultCB)) // 6 consecutive failures
|
|
}
|
|
assert.Equal(t, StateOpen, defaultCB.State())
|
|
assert.Equal(t, Counts{0, 0, 0, 0, 0}, defaultCB.counts)
|
|
assert.False(t, defaultCB.expiry.IsZero())
|
|
|
|
assert.Error(t, succeed(defaultCB))
|
|
assert.Error(t, fail(defaultCB))
|
|
assert.Equal(t, Counts{0, 0, 0, 0, 0}, defaultCB.counts)
|
|
|
|
pseudoSleep(defaultCB, time.Duration(59)*time.Second)
|
|
assert.Equal(t, StateOpen, defaultCB.State())
|
|
|
|
// StateOpen to StateHalfOpen
|
|
pseudoSleep(defaultCB, time.Duration(1)*time.Second) // over Timeout
|
|
assert.Equal(t, StateHalfOpen, defaultCB.State())
|
|
assert.True(t, defaultCB.expiry.IsZero())
|
|
|
|
// StateHalfOpen to StateOpen
|
|
assert.Nil(t, fail(defaultCB))
|
|
assert.Equal(t, StateOpen, defaultCB.State())
|
|
assert.Equal(t, Counts{0, 0, 0, 0, 0}, defaultCB.counts)
|
|
assert.False(t, defaultCB.expiry.IsZero())
|
|
|
|
// StateOpen to StateHalfOpen
|
|
pseudoSleep(defaultCB, time.Duration(60)*time.Second)
|
|
assert.Equal(t, StateHalfOpen, defaultCB.State())
|
|
assert.True(t, defaultCB.expiry.IsZero())
|
|
|
|
// StateHalfOpen to StateClosed
|
|
assert.Nil(t, succeed(defaultCB))
|
|
assert.Equal(t, StateClosed, defaultCB.State())
|
|
assert.Equal(t, Counts{0, 0, 0, 0, 0}, defaultCB.counts)
|
|
assert.True(t, defaultCB.expiry.IsZero())
|
|
}
|
|
|
|
func TestCustomCircuitBreaker(t *testing.T) {
|
|
assert.Equal(t, "cb", customCB.Name())
|
|
|
|
for i := 0; i < 5; i++ {
|
|
assert.Nil(t, succeed(customCB))
|
|
assert.Nil(t, fail(customCB))
|
|
}
|
|
assert.Equal(t, StateClosed, customCB.State())
|
|
assert.Equal(t, Counts{10, 5, 5, 0, 1}, customCB.counts)
|
|
|
|
pseudoSleep(customCB, time.Duration(29)*time.Second)
|
|
assert.Nil(t, succeed(customCB))
|
|
assert.Equal(t, StateClosed, customCB.State())
|
|
assert.Equal(t, Counts{11, 6, 5, 1, 0}, customCB.counts)
|
|
|
|
pseudoSleep(customCB, time.Duration(1)*time.Second) // over Interval
|
|
assert.Nil(t, fail(customCB))
|
|
assert.Equal(t, StateClosed, customCB.State())
|
|
assert.Equal(t, Counts{1, 0, 1, 0, 1}, customCB.counts)
|
|
|
|
// StateClosed to StateOpen
|
|
assert.Nil(t, succeed(customCB))
|
|
assert.Nil(t, fail(customCB)) // failure ratio: 2/3 >= 0.6
|
|
assert.Equal(t, StateOpen, customCB.State())
|
|
assert.Equal(t, Counts{0, 0, 0, 0, 0}, customCB.counts)
|
|
assert.False(t, customCB.expiry.IsZero())
|
|
assert.Equal(t, StateChange{"cb", StateClosed, StateOpen}, stateChange)
|
|
|
|
// StateOpen to StateHalfOpen
|
|
pseudoSleep(customCB, time.Duration(90)*time.Second)
|
|
assert.Equal(t, StateHalfOpen, customCB.State())
|
|
assert.True(t, defaultCB.expiry.IsZero())
|
|
assert.Equal(t, StateChange{"cb", StateOpen, StateHalfOpen}, stateChange)
|
|
|
|
assert.Nil(t, succeed(customCB))
|
|
assert.Nil(t, succeed(customCB))
|
|
assert.Equal(t, StateHalfOpen, customCB.State())
|
|
assert.Equal(t, Counts{2, 2, 0, 2, 0}, customCB.counts)
|
|
|
|
// StateHalfOpen to StateClosed
|
|
ch := succeedLater(customCB, time.Duration(100)*time.Millisecond) // 3 consecutive successes
|
|
time.Sleep(time.Duration(50) * time.Millisecond)
|
|
assert.Equal(t, Counts{3, 2, 0, 2, 0}, customCB.counts)
|
|
assert.Error(t, succeed(customCB)) // over MaxRequests
|
|
assert.Nil(t, <-ch)
|
|
assert.Equal(t, StateClosed, customCB.State())
|
|
assert.Equal(t, Counts{0, 0, 0, 0, 0}, customCB.counts)
|
|
assert.False(t, customCB.expiry.IsZero())
|
|
assert.Equal(t, StateChange{"cb", StateHalfOpen, StateClosed}, stateChange)
|
|
}
|
|
|
|
func TestTwoStepCircuitBreaker(t *testing.T) {
|
|
tscb := NewTwoStepCircuitBreaker(Settings{Name: "tscb"})
|
|
assert.Equal(t, "tscb", tscb.Name())
|
|
|
|
for i := 0; i < 5; i++ {
|
|
assert.Nil(t, fail2Step(tscb))
|
|
}
|
|
|
|
assert.Equal(t, StateClosed, tscb.State())
|
|
assert.Equal(t, Counts{5, 0, 5, 0, 5}, tscb.cb.counts)
|
|
|
|
assert.Nil(t, succeed2Step(tscb))
|
|
assert.Equal(t, StateClosed, tscb.State())
|
|
assert.Equal(t, Counts{6, 1, 5, 1, 0}, tscb.cb.counts)
|
|
|
|
assert.Nil(t, fail2Step(tscb))
|
|
assert.Equal(t, StateClosed, tscb.State())
|
|
assert.Equal(t, Counts{7, 1, 6, 0, 1}, tscb.cb.counts)
|
|
|
|
// StateClosed to StateOpen
|
|
for i := 0; i < 5; i++ {
|
|
assert.Nil(t, fail2Step(tscb)) // 6 consecutive failures
|
|
}
|
|
assert.Equal(t, StateOpen, tscb.State())
|
|
assert.Equal(t, Counts{0, 0, 0, 0, 0}, tscb.cb.counts)
|
|
assert.False(t, tscb.cb.expiry.IsZero())
|
|
|
|
assert.Error(t, succeed2Step(tscb))
|
|
assert.Error(t, fail2Step(tscb))
|
|
assert.Equal(t, Counts{0, 0, 0, 0, 0}, tscb.cb.counts)
|
|
|
|
pseudoSleep(tscb.cb, time.Duration(59)*time.Second)
|
|
assert.Equal(t, StateOpen, tscb.State())
|
|
|
|
// StateOpen to StateHalfOpen
|
|
pseudoSleep(tscb.cb, time.Duration(1)*time.Second) // over Timeout
|
|
assert.Equal(t, StateHalfOpen, tscb.State())
|
|
assert.True(t, tscb.cb.expiry.IsZero())
|
|
|
|
// StateHalfOpen to StateOpen
|
|
assert.Nil(t, fail2Step(tscb))
|
|
assert.Equal(t, StateOpen, tscb.State())
|
|
assert.Equal(t, Counts{0, 0, 0, 0, 0}, tscb.cb.counts)
|
|
assert.False(t, tscb.cb.expiry.IsZero())
|
|
|
|
// StateOpen to StateHalfOpen
|
|
pseudoSleep(tscb.cb, time.Duration(60)*time.Second)
|
|
assert.Equal(t, StateHalfOpen, tscb.State())
|
|
assert.True(t, tscb.cb.expiry.IsZero())
|
|
|
|
// StateHalfOpen to StateClosed
|
|
assert.Nil(t, succeed2Step(tscb))
|
|
assert.Equal(t, StateClosed, tscb.State())
|
|
assert.Equal(t, Counts{0, 0, 0, 0, 0}, tscb.cb.counts)
|
|
assert.True(t, tscb.cb.expiry.IsZero())
|
|
}
|
|
|
|
func TestPanicInRequest(t *testing.T) {
|
|
assert.Panics(t, func() { causePanic(defaultCB) })
|
|
assert.Equal(t, Counts{1, 0, 1, 0, 1}, defaultCB.counts)
|
|
}
|
|
|
|
func TestGeneration(t *testing.T) {
|
|
pseudoSleep(customCB, time.Duration(29)*time.Second)
|
|
assert.Nil(t, succeed(customCB))
|
|
ch := succeedLater(customCB, time.Duration(1500)*time.Millisecond)
|
|
time.Sleep(time.Duration(500) * time.Millisecond)
|
|
assert.Equal(t, Counts{2, 1, 0, 1, 0}, customCB.counts)
|
|
|
|
time.Sleep(time.Duration(500) * time.Millisecond) // over Interval
|
|
assert.Equal(t, StateClosed, customCB.State())
|
|
assert.Equal(t, Counts{0, 0, 0, 0, 0}, customCB.counts)
|
|
|
|
// the request from the previous generation has no effect on customCB.counts
|
|
assert.Nil(t, <-ch)
|
|
assert.Equal(t, Counts{0, 0, 0, 0, 0}, customCB.counts)
|
|
}
|
|
|
|
func TestCircuitBreakerInParallel(t *testing.T) {
|
|
runtime.GOMAXPROCS(runtime.NumCPU())
|
|
|
|
ch := make(chan error)
|
|
|
|
const numReqs = 10000
|
|
routine := func() {
|
|
for i := 0; i < numReqs; i++ {
|
|
ch <- succeed(customCB)
|
|
}
|
|
}
|
|
|
|
const numRoutines = 10
|
|
for i := 0; i < numRoutines; i++ {
|
|
go routine()
|
|
}
|
|
|
|
total := uint32(numReqs * numRoutines)
|
|
for i := uint32(0); i < total; i++ {
|
|
err := <-ch
|
|
assert.Nil(t, err)
|
|
}
|
|
assert.Equal(t, Counts{total, total, 0, total, 0}, customCB.counts)
|
|
}
|