blob: b7739809844388f246eb0c452fa7a09d803e243e [file] [log] [blame]
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package breaker
import (
"sync"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
func TestBucketReset(t *testing.T) {
bu := bucket{12, 8}
bu.reset()
if bu.successes != 0 {
t.Errorf("got successes = %d, want %d", bu.successes, 0)
}
if bu.failures != 0 {
t.Errorf("got failures = %d, want %d", bu.failures, 0)
}
}
func TestResetCounts(t *testing.T) {
b := newTestBreaker(Config{})
for i := 0; i < len(b.buckets); i++ {
b.buckets[i].successes = 10
b.buckets[i].failures = 15
}
b.resetCounts()
testBuckets(t, b.buckets[:], 0, 0)
testCounts(t, b, 0, 0, 0)
}
func TestNewBreaker(t *testing.T) {
timeNow = func() time.Time {
return time.Date(2020, time.May, 26, 18, 0, 0, 0, time.UTC)
}
got, err := New(Config{
FailsToRed: 10,
FailureThreshold: 0.65,
GreenInterval: 20 * time.Second,
MinTimeout: 30 * time.Second,
MaxTimeout: 16 * time.Minute,
SuccsToGreen: 15,
})
if err != nil {
t.Fatalf("New() returned %e, want nil", err)
}
want := &Breaker{
config: Config{
FailsToRed: 10,
FailureThreshold: 0.65,
GreenInterval: 20 * time.Second,
MinTimeout: 30 * time.Second,
MaxTimeout: 16 * time.Minute,
SuccsToGreen: 15,
},
buckets: [numBuckets]bucket{},
granularity: 2500 * time.Millisecond,
state: Green,
cur: 0,
consecutiveSuccs: 0,
timeout: 30 * time.Second,
lastEvent: time.Date(2020, time.May, 26, 18, 0, 0, 0, time.UTC),
}
diff := cmp.Diff(want, got, cmpopts.IgnoreUnexported(sync.Mutex{}), cmp.AllowUnexported(Breaker{}, bucket{}))
if diff != "" {
t.Fatalf("mismatch (-want +got):\n%s", diff)
}
}
func TestIllegalBreaker(t *testing.T) {
for _, test := range []struct {
name string
config Config
}{
{
name: "FailsToRed cannot be 0",
config: Config{
FailsToRed: 0,
FailureThreshold: 0.65,
GreenInterval: 20 * time.Second,
MinTimeout: 30 * time.Second,
MaxTimeout: 16 * time.Minute,
SuccsToGreen: 15,
},
},
{
name: "FailsToRed cannot be negative",
config: Config{
FailsToRed: -5,
FailureThreshold: 0.65,
GreenInterval: 20 * time.Second,
MinTimeout: 30 * time.Second,
MaxTimeout: 16 * time.Minute,
SuccsToGreen: 15,
},
},
{
name: "FailureThreshold cannot be 0",
config: Config{
FailsToRed: 8,
FailureThreshold: 0,
GreenInterval: 20 * time.Second,
MinTimeout: 30 * time.Second,
MaxTimeout: 16 * time.Minute,
SuccsToGreen: 15,
},
},
{
name: "FailureThreshold cannot be negative",
config: Config{
FailsToRed: 8,
FailureThreshold: -0.8,
GreenInterval: 20 * time.Second,
MinTimeout: 30 * time.Second,
MaxTimeout: 16 * time.Minute,
SuccsToGreen: 15,
},
},
{
name: "FailureThreshold cannot exceed 1",
config: Config{
FailsToRed: 8,
FailureThreshold: 1.2,
GreenInterval: 20 * time.Second,
MinTimeout: 30 * time.Second,
MaxTimeout: 16 * time.Minute,
SuccsToGreen: 15,
},
},
{
name: "GreenInterval cannot be 0",
config: Config{
FailsToRed: 8,
FailureThreshold: 0.65,
GreenInterval: 0,
MinTimeout: 30 * time.Second,
MaxTimeout: 16 * time.Minute,
SuccsToGreen: 15,
},
},
{
name: "GreenInterval cannot be negative",
config: Config{
FailsToRed: 8,
FailureThreshold: 0.65,
GreenInterval: -4 * time.Second,
MinTimeout: 30 * time.Second,
MaxTimeout: 16 * time.Minute,
SuccsToGreen: 15,
},
},
{
name: "MinTimeout cannot be 0",
config: Config{
FailsToRed: 8,
FailureThreshold: 0.65,
GreenInterval: 20 * time.Second,
MinTimeout: 0,
MaxTimeout: 16 * time.Minute,
SuccsToGreen: 15,
},
},
{
name: "MinTimeout cannot be negative",
config: Config{
FailsToRed: 8,
FailureThreshold: 0.65,
GreenInterval: 20 * time.Second,
MinTimeout: -2 * time.Second,
MaxTimeout: 16 * time.Minute,
SuccsToGreen: 15,
},
},
{
name: "MaxTimeout cannot be 0",
config: Config{
FailsToRed: 8,
FailureThreshold: 0.65,
GreenInterval: 20 * time.Second,
MinTimeout: 30 * time.Second,
MaxTimeout: 0,
SuccsToGreen: 15,
},
},
{
name: "MaxTimeout cannot be negative",
config: Config{
FailsToRed: 8,
FailureThreshold: 0.65,
GreenInterval: 20 * time.Second,
MinTimeout: 30 * time.Second,
MaxTimeout: -12 * time.Minute,
SuccsToGreen: 15,
},
},
{
name: "SuccsToGreen cannot be 0",
config: Config{
FailsToRed: 8,
FailureThreshold: 0.65,
GreenInterval: 20 * time.Second,
MinTimeout: 30 * time.Second,
MaxTimeout: 16 * time.Minute,
SuccsToGreen: 0,
},
},
{
name: "SuccsToGreen cannot be negative",
config: Config{
FailsToRed: 8,
FailureThreshold: 0.65,
GreenInterval: 20 * time.Second,
MinTimeout: 30 * time.Second,
MaxTimeout: 16 * time.Minute,
SuccsToGreen: -7,
},
},
{
name: "multiple illegal values return error",
config: Config{
FailsToRed: 0,
FailureThreshold: 1.4,
GreenInterval: 20 * time.Second,
MinTimeout: -30 * time.Second,
MaxTimeout: 16 * time.Minute,
SuccsToGreen: 100,
},
},
} {
t.Run(test.name, func(t *testing.T) {
b, err := New(test.config)
if err == nil {
t.Fatalf("New() returned nil error")
}
if b != nil {
t.Fatalf("New() returned %+v, want nil", b)
}
})
}
}
func TestBreakerGranularity(t *testing.T) {
for _, test := range []struct {
config Config
want time.Duration
}{
{
config: Config{},
want: 1250 * time.Millisecond,
},
{
config: Config{GreenInterval: 1 * time.Second},
want: 125 * time.Millisecond,
},
{
config: Config{GreenInterval: 3 * time.Second},
want: 375 * time.Millisecond,
},
{
config: Config{GreenInterval: 1 * time.Minute},
want: 7500 * time.Millisecond,
},
{
config: Config{GreenInterval: 1 * time.Hour},
want: 450 * time.Second,
},
} {
b := newTestBreaker(test.config)
if b.granularity != test.want {
t.Errorf("b.granularity = %d, want %d", b.granularity, test.want)
}
}
}
func TestState(t *testing.T) {
for _, want := range []State{
Green,
Yellow,
Red,
} {
b := newTestBreaker(Config{})
b.state = want
if got := b.checkState(); got != want {
t.Errorf("b.checkState() = %s, got %s", got, want)
}
if got := b.State(); got != want {
t.Errorf("b.State() = %s, want %s", got, want)
}
}
}
func TestAllow(t *testing.T) {
for _, test := range []struct {
state State
shouldAllow bool
}{
{
state: Green,
shouldAllow: true,
},
{
state: Yellow,
shouldAllow: true,
},
{
state: Red,
shouldAllow: false,
},
} {
b := newTestBreaker(Config{})
b.state = test.state
allowed := b.Allow()
if allowed != test.shouldAllow {
t.Errorf("b.Allow() = %t in %s, want %t", allowed, test.state, test.shouldAllow)
}
}
}
func TestSuccesses(t *testing.T) {
b := newTestBreaker(Config{})
b.succeeded()
testCounts(t, b, 1, 1, 0)
b.succeeded()
testCounts(t, b, 2, 2, 0)
b.succeeded()
testCounts(t, b, 3, 3, 0)
}
func TestFailures(t *testing.T) {
b := newTestBreaker(Config{})
b.failed()
testCounts(t, b, 0, 0, 1)
b.failed()
testCounts(t, b, 0, 0, 2)
b.failed()
testCounts(t, b, 0, 0, 3)
}
func TestSucceededAndFailed(t *testing.T) {
b := newTestBreaker(Config{})
b.succeeded()
testCounts(t, b, 1, 1, 0)
b.failed()
testCounts(t, b, 0, 1, 1)
b.failed()
testCounts(t, b, 0, 1, 2)
b.succeeded()
testCounts(t, b, 1, 2, 2)
b.succeeded()
testCounts(t, b, 2, 3, 2)
b.succeeded()
testCounts(t, b, 3, 4, 2)
b.failed()
testCounts(t, b, 0, 4, 3)
}
func TestUpdate(t *testing.T) {
now := time.Now()
b := newTestBreaker(Config{})
b.lastEvent = now
b.granularity = 1 * time.Second
for i := 0; i < len(b.buckets); i++ {
b.buckets[i].successes = 4
b.buckets[i].failures = 9
}
// Update 0 buckets.
b.update(now.Add(-1 * time.Second))
if b.cur != 0 {
t.Errorf("cur: got %d, want %d", b.cur, 0)
}
testBuckets(t, b.buckets[:], 4, 9)
testCounts(t, b, 0, 4*len(b.buckets), 9*len(b.buckets))
// Update next 3 buckets.
b.update(now.Add(3 * time.Second))
if b.cur != 3 {
t.Errorf("cur: got %d, want %d", b.cur, 3)
}
testBuckets(t, b.buckets[:1], 4, 9)
testBuckets(t, b.buckets[1:4], 0, 0)
testBuckets(t, b.buckets[4:], 4, 9)
testCounts(t, b, 0, 4*len(b.buckets)-12, 9*len(b.buckets)-27)
// Update all buckets.
b.update(now.Add(1003 * time.Second))
expectedCur := 0
if b.cur != expectedCur {
t.Errorf("cur: got %d, want %d", b.cur, expectedCur)
}
testBuckets(t, b.buckets[:], 0, 0)
testCounts(t, b, 0, 0, 0)
}
func TestStateChanges(t *testing.T) {
for _, test := range []struct {
name string
config Config
preSuccesses int
preFailures int
fromState State
allow bool
success bool
sleep time.Duration
toState State
}{
{
name: "breaker state remains green when FailsToRed is not exceeded",
config: Config{FailsToRed: 8, FailureThreshold: 0.5},
preSuccesses: 6,
preFailures: 7,
fromState: Green,
allow: true,
success: false,
toState: Green,
},
{
name: "breaker state remains green when FailureThreshold is not exceeded",
config: Config{FailsToRed: 2, FailureThreshold: 0.8},
preSuccesses: 3,
preFailures: 6,
fromState: Green,
allow: true,
success: false,
toState: Green,
},
{
name: "breaker state remains green when failure ratio = FailureThreshold",
config: Config{FailsToRed: 10, FailureThreshold: 0.5},
preSuccesses: 20,
preFailures: 19,
fromState: Green,
allow: true,
success: false,
toState: Green,
},
{
name: "breaker state remains green when failures = FailsToRed",
config: Config{FailsToRed: 10, FailureThreshold: 0.3},
preSuccesses: 10,
preFailures: 9,
fromState: Green,
allow: true,
success: false,
toState: Green,
},
{
name: "breaker state changes to red when FailureThreshold is exceeded and after FailsToRed has been exceeded",
config: Config{FailsToRed: 10, FailureThreshold: 0.5},
preSuccesses: 20,
preFailures: 20,
fromState: Green,
allow: true,
success: false,
toState: Red,
},
{
name: "breaker state changes to red when FailsToRed is exceeded and after FailureThreshold has been exceeded",
config: Config{FailsToRed: 20, FailureThreshold: 0.3},
preSuccesses: 20,
preFailures: 20,
fromState: Green,
allow: true,
success: false,
toState: Red,
},
{
name: "breaker state changes from green to red",
config: Config{FailsToRed: 4, FailureThreshold: 0.5},
preSuccesses: 4,
preFailures: 4,
fromState: Green,
allow: true,
success: false,
toState: Red,
},
{
name: "failure in yellow state changes breaker to red state",
config: Config{},
preSuccesses: 0,
preFailures: 0,
fromState: Yellow,
allow: true,
success: false,
toState: Red,
},
{
name: "breaker state changes from yellow to green",
config: Config{SuccsToGreen: 1},
preSuccesses: 0,
preFailures: 0,
fromState: Yellow,
allow: true,
success: true,
toState: Green,
},
{
name: "breaker state changes from red to yellow",
config: Config{MinTimeout: 1 * time.Second},
preSuccesses: 0,
preFailures: 0,
fromState: Red,
sleep: 1*time.Second + 1*time.Nanosecond,
toState: Yellow,
},
} {
t.Run(test.name, func(t *testing.T) {
now := time.Time{}
timeNow = func() time.Time { return now }
b := newTestBreaker(test.config)
b.state = test.fromState
b.buckets[0].successes = test.preSuccesses
b.buckets[0].failures = test.preFailures
allowed := b.Allow()
if allowed != test.allow {
t.Fatalf("b.Allow() = %t in %s, want %t", allowed, test.fromState, test.allow)
}
if test.allow {
b.Record(test.success)
}
// Pseudo sleep.
now = now.Add(test.sleep)
if state := b.State(); state != test.toState {
t.Errorf("b.State() = %s, want %s", state, test.toState)
}
})
}
}
func TestRunningBreaker(t *testing.T) {
now := time.Time{}
timeNow = func() time.Time { return now }
b := newTestBreaker(Config{
GreenInterval: 5 * time.Second,
})
// The following tests happen sequentially. The tests' states depend on previous tests.
for _, test := range []struct {
name string
firstSleep time.Duration
allow bool
secondSleep time.Duration
success bool
wantConsecutiveSuccs int
wantSuccesses int
wantFailures int
}{
{
name: "successFunc called after a long time updates counts",
firstSleep: 20 * time.Second,
allow: true,
secondSleep: 20 * time.Second,
success: true,
wantConsecutiveSuccs: 1,
wantSuccesses: 1,
wantFailures: 0,
},
{
name: "success within GreenInterval updates counts correctly",
firstSleep: 1 * time.Second,
allow: true,
secondSleep: 3 * time.Second,
success: true,
wantConsecutiveSuccs: 2,
wantSuccesses: 2,
wantFailures: 0,
},
{
name: "success after a long time updates counts correctly",
firstSleep: 30 * time.Second,
allow: true,
secondSleep: 80 * time.Second,
success: true,
wantConsecutiveSuccs: 3,
wantSuccesses: 1,
wantFailures: 0,
},
{
name: "failure within GreenInterval updates counts correctly",
firstSleep: 1 * time.Second,
allow: true,
secondSleep: 3 * time.Second,
success: false,
wantConsecutiveSuccs: 0,
wantSuccesses: 1,
wantFailures: 1,
},
{
name: "second failure within GreenInterval updates counts correctly",
firstSleep: 1 * time.Millisecond,
allow: true,
secondSleep: 3 * time.Millisecond,
success: false,
wantConsecutiveSuccs: 0,
wantSuccesses: 1,
wantFailures: 2,
},
{
name: "failure after a long time updates counts correctly",
firstSleep: 10 * time.Second,
allow: true,
secondSleep: 4 * time.Minute,
success: false,
wantConsecutiveSuccs: 0,
wantSuccesses: 0,
wantFailures: 1,
},
} {
t.Run(test.name, func(t *testing.T) {
now = now.Add(test.firstSleep)
allowed := b.Allow()
if allowed != test.allow {
t.Fatalf("breaker.Allow() = %t, want %t", allowed, test.allow)
}
now = now.Add(test.secondSleep)
if test.allow {
b.Record(test.success)
}
testCounts(t, b, test.wantConsecutiveSuccs, test.wantSuccesses, test.wantFailures)
})
}
}
func TestIncreaseTimeout(t *testing.T) {
b := newTestBreaker(Config{
MinTimeout: 1 * time.Second,
MaxTimeout: 12 * time.Second,
})
b.timeout = 3 * time.Second
b.increaseTimeout()
testTimeouts(t, b, 6*time.Second, 1*time.Second, 12*time.Second)
b.increaseTimeout()
testTimeouts(t, b, 12*time.Second, 1*time.Second, 12*time.Second)
b.increaseTimeout()
testTimeouts(t, b, 12*time.Second, 1*time.Second, 12*time.Second)
b.config.MaxTimeout = 14 * time.Second
testTimeouts(t, b, 12*time.Second, 1*time.Second, 14*time.Second)
b.increaseTimeout()
testTimeouts(t, b, 14*time.Second, 1*time.Second, 14*time.Second)
b.increaseTimeout()
testTimeouts(t, b, 14*time.Second, 1*time.Second, 14*time.Second)
}
// newTestBreaker is like New, but with default values for easier testing.
func newTestBreaker(config Config) *Breaker {
if config.FailsToRed <= 0 {
config.FailsToRed = 10
}
if config.FailureThreshold <= 0 {
config.FailureThreshold = 0.5
}
if config.GreenInterval <= 0 {
config.GreenInterval = 10 * time.Second
}
if config.MinTimeout <= 0 {
config.MinTimeout = 30 * time.Second
}
if config.MaxTimeout <= 0 {
config.MaxTimeout = 4 * time.Minute
}
if config.SuccsToGreen <= 0 {
config.SuccsToGreen = 20
}
b, _ := New(config)
return b
}
func testCounts(t *testing.T, b *Breaker, consecutiveSuccs, wantSuccesses, wantFailures int) {
if b.consecutiveSuccs != consecutiveSuccs {
t.Errorf("b.consecutiveSuccs = %d, want %d", b.consecutiveSuccs, consecutiveSuccs)
}
successes, failures := b.counts()
if successes != wantSuccesses {
t.Errorf("successes = %d, want %d", successes, wantSuccesses)
}
if failures != wantFailures {
t.Errorf("failures = %d, want %d", failures, wantFailures)
}
}
func testBuckets(t *testing.T, buckets []bucket, successes, failures int) {
for i, bu := range buckets {
if bu.successes != successes {
t.Errorf("slice bucket %d successes: got %d, want %d", i, bu.successes, successes)
}
if bu.failures != failures {
t.Errorf("slice bucket %d failures: got %d, want %d", i, bu.failures, failures)
}
}
}
func testTimeouts(t *testing.T, b *Breaker, timeout, minTimeout, maxTimeout time.Duration) {
if b.timeout != timeout {
t.Errorf("b.timeout = %s, want %s", b.timeout, timeout)
}
if b.config.MinTimeout != minTimeout {
t.Errorf("b.config.MinTimeout = %s, want %s", b.config.MinTimeout, minTimeout)
}
if b.config.MaxTimeout != maxTimeout {
t.Errorf("b.config.MaxTimeout = %s, want %s", b.config.MaxTimeout, maxTimeout)
}
}