diff --git a/README.md b/README.md index bbc2376..ceda7e5 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ gobreaker ========= [![GoDoc](https://godoc.org/github.com/sony/gobreaker?status.svg)](http://godoc.org/github.com/sony/gobreaker) +[![Build Status](https://travis-ci.org/sony/gobreaker.svg?branch=master)](https://travis-ci.org/sony/gobreaker) +[![Coverage Status](https://coveralls.io/repos/sony/gobreaker/badge.svg?branch=master&service=github)](https://coveralls.io/github/sony/gobreaker?branch=master) [gobreaker][repo-url] implements the [Circuit Breaker pattern](https://msdn.microsoft.com/en-us/library/dn589784.aspx) in Go. @@ -9,7 +11,7 @@ Installation ------------ ``` -go get github.com/sony/gobreaker +go get github.com/rocyou/gobreaker ``` Usage @@ -27,20 +29,21 @@ You can configure `CircuitBreaker` by the struct `Settings`: ```go type Settings struct { Name string - MaxRequests uint32 + ReadyToClose func(counts Counts) (bool, bool) Interval time.Duration Timeout time.Duration ReadyToTrip func(counts Counts) bool OnStateChange func(name string, from State, to State) - IsSuccessful func(err error) bool } ``` - `Name` is the name of the `CircuitBreaker`. -- `MaxRequests` is the maximum number of requests allowed to pass through - when the `CircuitBreaker` is half-open. - If `MaxRequests` is 0, `CircuitBreaker` allows only 1 request. +- `ReadyToClose` is called with a copy of `Counts` for each request in the half-open state. + If `ReadyToClose` returns true, the `CircuitBreaker` will be placed into the close state. + If `ReadyToClose` returns false, the `CircuitBreaker` will be placed into the open state if second returned value is true. + If `ReadyToClose` is nil, default `ReadyToClose` is used. + Default `ReadyToClose` returns true when the number of consecutive successes is more than 1. - `Interval` is the cyclic period of the closed state for `CircuitBreaker` to clear the internal `Counts`, described later in this section. @@ -57,11 +60,6 @@ type Settings struct { - `OnStateChange` is called whenever the state of `CircuitBreaker` changes. -- `IsSuccessful` is called with the error returned from a request. - If `IsSuccessful` returns true, the error is counted as a success. - Otherwise the error is counted as a failure. - If `IsSuccessful` is nil, default `IsSuccessful` is used, which returns false for all non-nil errors. - The struct `Counts` holds the numbers of requests and their successes/failures: ```go diff --git a/example/http_breaker.go b/example/http_breaker.go index 7c0936f..8f9e639 100644 --- a/example/http_breaker.go +++ b/example/http_breaker.go @@ -5,8 +5,9 @@ import ( "io/ioutil" "log" "net/http" + "time" - "github.com/sony/gobreaker" + "github.com/rocyou/gobreaker" ) var cb *gobreaker.CircuitBreaker @@ -15,10 +16,24 @@ func init() { var st gobreaker.Settings st.Name = "HTTP GET" st.ReadyToTrip = func(counts gobreaker.Counts) bool { - failureRatio := float64(counts.TotalFailures) / float64(counts.Requests) - return counts.Requests >= 3 && failureRatio >= 0.6 + return counts.ConsecutiveFailures >= 3 + } + st.Interval = time.Duration(0) * time.Second + st.Timeout = time.Duration(3) * time.Second + st.ReadyToClose = func(counts gobreaker.Counts) (bool, bool) { + if counts.TotalSuccesses >= 2 { + return true, true + } + var nowOpen bool + if counts.ConsecutiveFailures >= 3 { + nowOpen = true + } + return false, nowOpen + } + st.OnStateChange = func(name string, from gobreaker.State, to gobreaker.State) { + fmt.Printf("change state from:%+v to:%+v\n", from, to) + //implement your action here } - cb = gobreaker.NewCircuitBreaker(st) } @@ -29,19 +44,16 @@ func Get(url string) ([]byte, error) { if err != nil { return nil, err } - defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } - return body, nil }) if err != nil { return nil, err } - return body.([]byte), nil } diff --git a/go.mod b/go.mod index 4367bed..466a3b0 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ -module github.com/sony/gobreaker +module github.com/rocyou/gobreaker -go 1.12 +go 1.16 -require github.com/stretchr/testify v1.3.0 +require github.com/stretchr/testify v1.7.0 diff --git a/go.sum b/go.sum index 4347755..acb88a4 100644 --- a/go.sum +++ b/go.sum @@ -3,5 +3,9 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/gobreaker.go b/gobreaker.go index 7503a27..b296680 100644 --- a/gobreaker.go +++ b/gobreaker.go @@ -20,8 +20,6 @@ const ( ) var ( - // ErrTooManyRequests is returned when the CB state is half open and the requests count is over the cb maxRequests - ErrTooManyRequests = errors.New("too many requests") // ErrOpenState is returned when the CB state is open ErrOpenState = errors.New("circuit breaker is open") ) @@ -80,9 +78,11 @@ func (c *Counts) clear() { // // Name is the name of the CircuitBreaker. // -// MaxRequests is the maximum number of requests allowed to pass through -// when the CircuitBreaker is half-open. -// If MaxRequests is 0, the CircuitBreaker allows only 1 request. +// ReadyToClose is called with a copy of Counts for each request in the half-open state. +// If ReadyToClose returns true, the CircuitBreaker will be placed into the close state. +// If ReadyToClose returns false, the CircuitBreaker will be placed into the open state if second returned value is true. +// If ReadyToClose is nil, default ReadyToClose is used. +// Default ReadyToClose returns true when the number of consecutive successes is more than 1. // // Interval is the cyclic period of the closed state // for the CircuitBreaker to clear the internal Counts. @@ -98,29 +98,22 @@ func (c *Counts) clear() { // Default ReadyToTrip returns true when the number of consecutive failures is more than 5. // // OnStateChange is called whenever the state of the CircuitBreaker changes. -// -// IsSuccessful is called with the error returned from a request. -// If IsSuccessful returns true, the error is counted as a success. -// Otherwise the error is counted as a failure. -// If IsSuccessful is nil, default IsSuccessful is used, which returns false for all non-nil errors. type Settings struct { Name string - MaxRequests uint32 + ReadyToClose func(counts Counts) (bool, bool) Interval time.Duration Timeout time.Duration ReadyToTrip func(counts Counts) bool OnStateChange func(name string, from State, to State) - IsSuccessful func(err error) bool } // CircuitBreaker is a state machine to prevent sending requests that are likely to fail. type CircuitBreaker struct { name string - maxRequests uint32 + readyToClose func(counts Counts) (bool, bool) interval time.Duration timeout time.Duration readyToTrip func(counts Counts) bool - isSuccessful func(err error) bool onStateChange func(name string, from State, to State) mutex sync.Mutex @@ -144,10 +137,10 @@ func NewCircuitBreaker(st Settings) *CircuitBreaker { cb.name = st.Name cb.onStateChange = st.OnStateChange - if st.MaxRequests == 0 { - cb.maxRequests = 1 + if st.ReadyToClose == nil { + cb.readyToClose = defaultReadyToClose } else { - cb.maxRequests = st.MaxRequests + cb.readyToClose = st.ReadyToClose } if st.Interval <= 0 { @@ -168,12 +161,6 @@ func NewCircuitBreaker(st Settings) *CircuitBreaker { cb.readyToTrip = st.ReadyToTrip } - if st.IsSuccessful == nil { - cb.isSuccessful = defaultIsSuccessful - } else { - cb.isSuccessful = st.IsSuccessful - } - cb.toNewGeneration(time.Now()) return cb @@ -193,8 +180,8 @@ func defaultReadyToTrip(counts Counts) bool { return counts.ConsecutiveFailures > 5 } -func defaultIsSuccessful(err error) bool { - return err == nil +func defaultReadyToClose(counts Counts) (bool, bool) { + return counts.ConsecutiveSuccesses >= 1, true } // Name returns the name of the CircuitBreaker. @@ -212,14 +199,6 @@ func (cb *CircuitBreaker) State() State { return state } -// Counts returns internal counters -func (cb *CircuitBreaker) Counts() Counts { - cb.mutex.Lock() - defer cb.mutex.Unlock() - - return cb.counts -} - // Execute runs the given request if the CircuitBreaker accepts it. // Execute returns an error instantly if the CircuitBreaker rejects the request. // Otherwise, Execute returns the result of the request. @@ -240,7 +219,7 @@ func (cb *CircuitBreaker) Execute(req func() (interface{}, error)) (interface{}, }() result, err := req() - cb.afterRequest(generation, cb.isSuccessful(err)) + cb.afterRequest(generation, err == nil) return result, err } @@ -254,11 +233,6 @@ func (tscb *TwoStepCircuitBreaker) State() State { return tscb.cb.State() } -// Counts returns internal counters -func (tscb *TwoStepCircuitBreaker) Counts() Counts { - return tscb.cb.Counts() -} - // Allow checks if a new request can proceed. It returns a callback that should be used to // register the success or failure in a separate step. If the circuit breaker doesn't allow // requests, it returns an error. @@ -282,8 +256,6 @@ func (cb *CircuitBreaker) beforeRequest() (uint64, error) { if state == StateOpen { return generation, ErrOpenState - } else if state == StateHalfOpen && cb.counts.Requests >= cb.maxRequests { - return generation, ErrTooManyRequests } cb.counts.onRequest() @@ -313,7 +285,7 @@ func (cb *CircuitBreaker) onSuccess(state State, now time.Time) { cb.counts.onSuccess() case StateHalfOpen: cb.counts.onSuccess() - if cb.counts.ConsecutiveSuccesses >= cb.maxRequests { + if ok, _ := cb.readyToClose(cb.counts); ok { cb.setState(StateClosed, now) } } @@ -326,8 +298,13 @@ func (cb *CircuitBreaker) onFailure(state State, now time.Time) { if cb.readyToTrip(cb.counts) { cb.setState(StateOpen, now) } + case StateHalfOpen: - cb.setState(StateOpen, now) + cb.counts.onFailure() + ok, nowOpen := cb.readyToClose(cb.counts) + if !ok && nowOpen { + cb.setState(StateOpen, now) + } } } @@ -337,11 +314,13 @@ func (cb *CircuitBreaker) currentState(now time.Time) (State, uint64) { if !cb.expiry.IsZero() && cb.expiry.Before(now) { cb.toNewGeneration(now) } + case StateOpen: if cb.expiry.Before(now) { cb.setState(StateHalfOpen, now) } } + return cb.state, cb.generation } @@ -372,8 +351,10 @@ func (cb *CircuitBreaker) toNewGeneration(now time.Time) { } else { cb.expiry = now.Add(cb.interval) } + case StateOpen: cb.expiry = now.Add(cb.timeout) + default: // StateHalfOpen cb.expiry = zero } diff --git a/gobreaker_test.go b/gobreaker_test.go index 1f5b3d4..7017ba2 100644 --- a/gobreaker_test.go +++ b/gobreaker_test.go @@ -81,7 +81,21 @@ func causePanic(cb *CircuitBreaker) error { func newCustom() *CircuitBreaker { var customSt Settings customSt.Name = "cb" - customSt.MaxRequests = 3 + customSt.ReadyToClose = func(counts Counts) (bool, bool) { + if counts.ConsecutiveSuccesses >= 3 { + return true, true + } + + numReqs := counts.Requests + failureRatio := float64(counts.TotalFailures) / float64(numReqs) + + var nowOpen bool + if numReqs >= 3 && failureRatio >= 0.6 { + nowOpen = true + } + + return false, nowOpen + } customSt.Interval = time.Duration(30) * time.Second customSt.Timeout = time.Duration(90) * time.Second customSt.ReadyToTrip = func(counts Counts) bool { @@ -128,7 +142,7 @@ func TestStateConstants(t *testing.T) { func TestNewCircuitBreaker(t *testing.T) { defaultCB := NewCircuitBreaker(Settings{}) assert.Equal(t, "", defaultCB.name) - assert.Equal(t, uint32(1), defaultCB.maxRequests) + assert.NotNil(t, defaultCB.readyToClose) assert.Equal(t, time.Duration(0), defaultCB.interval) assert.Equal(t, time.Duration(60)*time.Second, defaultCB.timeout) assert.NotNil(t, defaultCB.readyToTrip) @@ -139,7 +153,7 @@ func TestNewCircuitBreaker(t *testing.T) { customCB := newCustom() assert.Equal(t, "cb", customCB.name) - assert.Equal(t, uint32(3), customCB.maxRequests) + assert.NotNil(t, customCB.readyToClose) 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) @@ -150,7 +164,7 @@ func TestNewCircuitBreaker(t *testing.T) { negativeDurationCB := newNegativeDurationCB() assert.Equal(t, "ncb", negativeDurationCB.name) - assert.Equal(t, uint32(1), negativeDurationCB.maxRequests) + assert.NotNil(t, negativeDurationCB.readyToClose) 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) @@ -258,7 +272,7 @@ func TestCustomCircuitBreaker(t *testing.T) { 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, succeed(customCB)) assert.Nil(t, <-ch) assert.Equal(t, StateClosed, customCB.State()) assert.Equal(t, Counts{0, 0, 0, 0, 0}, customCB.counts) @@ -344,30 +358,6 @@ func TestGeneration(t *testing.T) { assert.Equal(t, Counts{0, 0, 0, 0, 0}, customCB.counts) } -func TestCustomIsSuccessful(t *testing.T) { - isSuccessful := func(error) bool { - return true - } - cb := NewCircuitBreaker(Settings{IsSuccessful: isSuccessful}) - - for i := 0; i < 5; i++ { - assert.Nil(t, fail(cb)) - } - assert.Equal(t, StateClosed, cb.State()) - assert.Equal(t, Counts{5, 5, 0, 5, 0}, cb.counts) - - cb.counts.clear() - - cb.isSuccessful = func(err error) bool { - return err == nil - } - for i := 0; i < 6; i++ { - assert.Nil(t, fail(cb)) - } - assert.Equal(t, StateOpen, cb.State()) - -} - func TestCircuitBreakerInParallel(t *testing.T) { runtime.GOMAXPROCS(runtime.NumCPU())