mirror of https://github.com/tidwall/tile38.git
197 lines
4.2 KiB
Go
197 lines
4.2 KiB
Go
|
package breaker
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"testing"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
var errSomeError = errors.New("errSomeError")
|
||
|
|
||
|
func alwaysPanics() error {
|
||
|
panic("foo")
|
||
|
}
|
||
|
|
||
|
func returnsError() error {
|
||
|
return errSomeError
|
||
|
}
|
||
|
|
||
|
func returnsSuccess() error {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func TestBreakerErrorExpiry(t *testing.T) {
|
||
|
breaker := New(2, 1, 1*time.Second)
|
||
|
|
||
|
for i := 0; i < 3; i++ {
|
||
|
if err := breaker.Run(returnsError); err != errSomeError {
|
||
|
t.Error(err)
|
||
|
}
|
||
|
time.Sleep(1 * time.Second)
|
||
|
}
|
||
|
|
||
|
for i := 0; i < 3; i++ {
|
||
|
if err := breaker.Go(returnsError); err != nil {
|
||
|
t.Error(err)
|
||
|
}
|
||
|
time.Sleep(1 * time.Second)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestBreakerPanicsCountAsErrors(t *testing.T) {
|
||
|
breaker := New(3, 2, 1*time.Second)
|
||
|
|
||
|
// three errors opens the breaker
|
||
|
for i := 0; i < 3; i++ {
|
||
|
func() {
|
||
|
defer func() {
|
||
|
val := recover()
|
||
|
if val.(string) != "foo" {
|
||
|
t.Error("incorrect panic")
|
||
|
}
|
||
|
}()
|
||
|
if err := breaker.Run(alwaysPanics); err != nil {
|
||
|
t.Error(err)
|
||
|
}
|
||
|
t.Error("shouldn't get here")
|
||
|
}()
|
||
|
}
|
||
|
|
||
|
// breaker is open
|
||
|
for i := 0; i < 5; i++ {
|
||
|
if err := breaker.Run(returnsError); err != ErrBreakerOpen {
|
||
|
t.Error(err)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestBreakerStateTransitions(t *testing.T) {
|
||
|
breaker := New(3, 2, 1*time.Second)
|
||
|
|
||
|
// three errors opens the breaker
|
||
|
for i := 0; i < 3; i++ {
|
||
|
if err := breaker.Run(returnsError); err != errSomeError {
|
||
|
t.Error(err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// breaker is open
|
||
|
for i := 0; i < 5; i++ {
|
||
|
if err := breaker.Run(returnsError); err != ErrBreakerOpen {
|
||
|
t.Error(err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// wait for it to half-close
|
||
|
time.Sleep(2 * time.Second)
|
||
|
// one success works, but is not enough to fully close
|
||
|
if err := breaker.Run(returnsSuccess); err != nil {
|
||
|
t.Error(err)
|
||
|
}
|
||
|
// error works, but re-opens immediately
|
||
|
if err := breaker.Run(returnsError); err != errSomeError {
|
||
|
t.Error(err)
|
||
|
}
|
||
|
// breaker is open
|
||
|
if err := breaker.Run(returnsError); err != ErrBreakerOpen {
|
||
|
t.Error(err)
|
||
|
}
|
||
|
|
||
|
// wait for it to half-close
|
||
|
time.Sleep(2 * time.Second)
|
||
|
// two successes is enough to close it for good
|
||
|
for i := 0; i < 2; i++ {
|
||
|
if err := breaker.Run(returnsSuccess); err != nil {
|
||
|
t.Error(err)
|
||
|
}
|
||
|
}
|
||
|
// error works
|
||
|
if err := breaker.Run(returnsError); err != errSomeError {
|
||
|
t.Error(err)
|
||
|
}
|
||
|
// breaker is still closed
|
||
|
if err := breaker.Run(returnsSuccess); err != nil {
|
||
|
t.Error(err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestBreakerAsyncStateTransitions(t *testing.T) {
|
||
|
breaker := New(3, 2, 1*time.Second)
|
||
|
|
||
|
// three errors opens the breaker
|
||
|
for i := 0; i < 3; i++ {
|
||
|
if err := breaker.Go(returnsError); err != nil {
|
||
|
t.Error(err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// just enough to yield the scheduler and let the goroutines work off
|
||
|
time.Sleep(1 * time.Millisecond)
|
||
|
|
||
|
// breaker is open
|
||
|
for i := 0; i < 5; i++ {
|
||
|
if err := breaker.Go(returnsError); err != ErrBreakerOpen {
|
||
|
t.Error(err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// wait for it to half-close
|
||
|
time.Sleep(2 * time.Second)
|
||
|
// one success works, but is not enough to fully close
|
||
|
if err := breaker.Go(returnsSuccess); err != nil {
|
||
|
t.Error(err)
|
||
|
}
|
||
|
// error works, but re-opens immediately
|
||
|
if err := breaker.Go(returnsError); err != nil {
|
||
|
t.Error(err)
|
||
|
}
|
||
|
// just enough to yield the scheduler and let the goroutines work off
|
||
|
time.Sleep(1 * time.Millisecond)
|
||
|
// breaker is open
|
||
|
if err := breaker.Go(returnsError); err != ErrBreakerOpen {
|
||
|
t.Error(err)
|
||
|
}
|
||
|
|
||
|
// wait for it to half-close
|
||
|
time.Sleep(2 * time.Second)
|
||
|
// two successes is enough to close it for good
|
||
|
for i := 0; i < 2; i++ {
|
||
|
if err := breaker.Go(returnsSuccess); err != nil {
|
||
|
t.Error(err)
|
||
|
}
|
||
|
}
|
||
|
// just enough to yield the scheduler and let the goroutines work off
|
||
|
time.Sleep(1 * time.Millisecond)
|
||
|
// error works
|
||
|
if err := breaker.Go(returnsError); err != nil {
|
||
|
t.Error(err)
|
||
|
}
|
||
|
// just enough to yield the scheduler and let the goroutines work off
|
||
|
time.Sleep(1 * time.Millisecond)
|
||
|
// breaker is still closed
|
||
|
if err := breaker.Go(returnsSuccess); err != nil {
|
||
|
t.Error(err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func ExampleBreaker() {
|
||
|
breaker := New(3, 1, 5*time.Second)
|
||
|
|
||
|
for {
|
||
|
result := breaker.Run(func() error {
|
||
|
// communicate with some external service and
|
||
|
// return an error if the communication failed
|
||
|
return nil
|
||
|
})
|
||
|
|
||
|
switch result {
|
||
|
case nil:
|
||
|
// success!
|
||
|
case ErrBreakerOpen:
|
||
|
// our function wasn't run because the breaker was open
|
||
|
default:
|
||
|
// some other error
|
||
|
}
|
||
|
}
|
||
|
}
|