Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

util: Refactor Backoff to return the next step rather than sleeping #71088

Merged
merged 1 commit into from
Dec 4, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
62 changes: 48 additions & 14 deletions staging/src/k8s.io/apimachinery/pkg/util/wait/wait.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,49 @@ type ConditionFunc func() (done bool, err error)

// Backoff holds parameters applied to a Backoff function.
type Backoff struct {
Duration time.Duration // the base duration
Factor float64 // Duration is multiplied by factor each iteration
Jitter float64 // The amount of jitter applied each iteration
Steps int // Exit with error after this many steps
// The initial duration.
Duration time.Duration
// Duration is multiplied by factor each iteration. Must be greater
// than or equal to zero.
Factor float64
// The amount of jitter applied each iteration. Jitter is applied after
// cap.
Jitter float64
// The number of steps before duration stops changing. If zero, initial
// duration is always used. Used for exponential backoff in combination
// with Factor.
Steps int
// The returned duration will never be greater than cap *before* jitter
// is applied. The actual maximum cap is `cap * (1.0 + jitter)`.
Cap time.Duration
}

// Step returns the next interval in the exponential backoff. This method
// will mutate the provided backoff.
func (b *Backoff) Step() time.Duration {
if b.Steps < 1 {
if b.Jitter > 0 {
return Jitter(b.Duration, b.Jitter)
}
return b.Duration
}
b.Steps--

duration := b.Duration

// calculate the next step
if b.Factor != 0 {
b.Duration = time.Duration(float64(b.Duration) * b.Factor)
liggitt marked this conversation as resolved.
Show resolved Hide resolved
if b.Cap > 0 && b.Duration > b.Cap {
b.Duration = b.Cap
b.Steps = 0
}
}

if b.Jitter > 0 {
duration = Jitter(duration, b.Jitter)
}
return duration
}

// ExponentialBackoff repeats a condition check with exponential backoff.
Expand All @@ -190,19 +229,14 @@ type Backoff struct {
// If the condition never returns true, ErrWaitTimeout is returned. All other
// errors terminate immediately.
func ExponentialBackoff(backoff Backoff, condition ConditionFunc) error {
duration := backoff.Duration
for i := 0; i < backoff.Steps; i++ {
if i != 0 {
adjusted := duration
if backoff.Jitter > 0.0 {
adjusted = Jitter(duration, backoff.Jitter)
}
time.Sleep(adjusted)
duration = time.Duration(float64(duration) * backoff.Factor)
}
for backoff.Steps > 0 {
if ok, err := condition(); err != nil || ok {
return err
}
if backoff.Steps == 1 {
break
}
time.Sleep(backoff.Step())
liggitt marked this conversation as resolved.
Show resolved Hide resolved
liggitt marked this conversation as resolved.
Show resolved Hide resolved
}
return ErrWaitTimeout
}
Expand Down
45 changes: 45 additions & 0 deletions staging/src/k8s.io/apimachinery/pkg/util/wait/wait_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package wait
import (
"errors"
"fmt"
"math/rand"
"sync"
"sync/atomic"
"testing"
Expand Down Expand Up @@ -499,3 +500,47 @@ func TestPollUntil(t *testing.T) {
// make sure we finished the poll
<-pollDone
}

func TestBackoff_Step(t *testing.T) {
tests := []struct {
initial *Backoff
want []time.Duration
}{
{initial: &Backoff{Duration: time.Second, Steps: 0}, want: []time.Duration{time.Second, time.Second, time.Second}},
{initial: &Backoff{Duration: time.Second, Steps: 1}, want: []time.Duration{time.Second, time.Second, time.Second}},
{initial: &Backoff{Duration: time.Second, Factor: 1.0, Steps: 1}, want: []time.Duration{time.Second, time.Second, time.Second}},
{initial: &Backoff{Duration: time.Second, Factor: 2, Steps: 3}, want: []time.Duration{1 * time.Second, 2 * time.Second, 4 * time.Second}},
{initial: &Backoff{Duration: time.Second, Factor: 2, Steps: 3, Cap: 3 * time.Second}, want: []time.Duration{1 * time.Second, 2 * time.Second, 3 * time.Second}},
{initial: &Backoff{Duration: time.Second, Factor: 2, Steps: 2, Cap: 3 * time.Second, Jitter: 0.5}, want: []time.Duration{2 * time.Second, 3 * time.Second, 3 * time.Second}},
liggitt marked this conversation as resolved.
Show resolved Hide resolved
{initial: &Backoff{Duration: time.Second, Factor: 2, Steps: 6, Jitter: 4}, want: []time.Duration{1 * time.Second, 2 * time.Second, 4 * time.Second, 8 * time.Second, 16 * time.Second, 32 * time.Second}},
}
for seed := int64(0); seed < 5; seed++ {
for _, tt := range tests {
initial := *tt.initial
t.Run(fmt.Sprintf("%#v seed=%d", initial, seed), func(t *testing.T) {
rand.Seed(seed)
for i := 0; i < len(tt.want); i++ {
got := initial.Step()
t.Logf("[%d]=%s", i, got)
if initial.Jitter > 0 {
if got == tt.want[i] {
// this is statistically unlikely to happen by chance
t.Errorf("Backoff.Step(%d) = %v, no jitter", i, got)
continue
}
diff := float64(tt.want[i]-got) / float64(tt.want[i])
if diff > initial.Jitter {
t.Errorf("Backoff.Step(%d) = %v, want %v, outside range", i, got, tt.want)
continue
}
} else {
if got != tt.want[i] {
t.Errorf("Backoff.Step(%d) = %v, want %v", i, got, tt.want)
continue
}
}
}
})
}
}
}