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 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) }