blob: 991b9199e84b289e42dbca231ad8b037e13e40b7 [file] [log] [blame]
// Copyright 2018 The Fuchsia 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 retry
import (
"context"
"time"
)
// Retry the operation using the provided back-off policy until it succeeds, or
// the context is cancelled. Any intermediate errors (but not the final error)
// returned by the function will be sent on the given channel, if the channel is
// non-nil.
func Retry(ctx context.Context, b Backoff, f func() error, c chan<- error) error {
b.Reset()
var timer *time.Timer
for {
err := f()
if err == nil {
return nil
}
next := b.Next()
if next == Stop {
return err
}
if timer == nil {
timer = time.NewTimer(next)
defer timer.Stop()
} else {
// Reset() is only safe to call after the timer has fired and its
// channel has been drained. At this point in the loop we know that
// the timer's channel was drained on the last iteration (otherwise
// we would have exited the loop), so it's safe to call Reset().
timer.Reset(next)
}
// If the context has already been canceled, exit immediately rather
// than entering the select statement to ensure determinism when the
// backoff is zero. If multiple channels in a select statement are ready
// when entering the statement, Go will randomly choose one, which is
// not the desired behavior here - we always want to stop once the
// context gets canceled.
if ctx.Err() != nil {
return err
}
select {
case <-ctx.Done():
return err
case <-timer.C:
if c != nil {
c <- err
}
}
}
}