mirror of https://github.com/hashicorp/packer
parent
aa3cb5be63
commit
d72040f4fa
@ -0,0 +1,81 @@
|
||||
package retry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Config represents a retry config
|
||||
type Config struct {
|
||||
// The operation will be retried until StartTimeout has elapsed. 0 means
|
||||
// forever.
|
||||
StartTimeout time.Duration
|
||||
|
||||
// RetryDelay gives the time elapsed after a failure and before we try
|
||||
// again. Returns 2s by default.
|
||||
RetryDelay func() time.Duration
|
||||
|
||||
// Max number of retries, 0 means infinite
|
||||
Tries int
|
||||
|
||||
// ShouldRetry tells wether error should be retried. Nil defaults to always
|
||||
// true.
|
||||
ShouldRetry func(error) bool
|
||||
}
|
||||
|
||||
// Run fn until context is cancelled up until StartTimeout time has passed.
|
||||
func (cfg Config) Run(ctx context.Context, fn func(context.Context) error) error {
|
||||
retryDelay := func() time.Duration { return 2 * time.Second }
|
||||
if cfg.RetryDelay != nil {
|
||||
retryDelay = cfg.RetryDelay
|
||||
}
|
||||
shouldRetry := func(error) bool { return true }
|
||||
if cfg.ShouldRetry != nil {
|
||||
shouldRetry = cfg.ShouldRetry
|
||||
}
|
||||
var startTimeout <-chan time.Time // nil chans never unlock !
|
||||
if cfg.StartTimeout != 0 {
|
||||
startTimeout = time.After(cfg.StartTimeout)
|
||||
}
|
||||
|
||||
for try := 0; ; try++ {
|
||||
var err error
|
||||
if cfg.Tries != 0 && try == cfg.Tries {
|
||||
return err
|
||||
}
|
||||
if err = fn(ctx); err == nil {
|
||||
return nil
|
||||
}
|
||||
if !shouldRetry(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Print(fmt.Errorf("Retryable error: %s", err))
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return err
|
||||
case <-startTimeout:
|
||||
return err
|
||||
default:
|
||||
time.Sleep(retryDelay())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Backoff struct {
|
||||
InitialBackoff time.Duration
|
||||
MaxBackoff time.Duration
|
||||
Multiplier float64
|
||||
}
|
||||
|
||||
func (lb *Backoff) Linear() time.Duration {
|
||||
wait := lb.InitialBackoff
|
||||
lb.InitialBackoff = time.Duration(lb.Multiplier * float64(lb.InitialBackoff))
|
||||
if lb.MaxBackoff != 0 && lb.InitialBackoff > lb.MaxBackoff {
|
||||
lb.InitialBackoff = lb.MaxBackoff
|
||||
}
|
||||
return wait
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
package retry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func success(context.Context) error { return nil }
|
||||
|
||||
func wait(ctx context.Context) error {
|
||||
<-ctx.Done()
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
var failErr = errors.New("woops !")
|
||||
|
||||
func fail(context.Context) error { return failErr }
|
||||
|
||||
func TestConfig_Run(t *testing.T) {
|
||||
cancelledCtx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
type fields struct {
|
||||
StartTimeout time.Duration
|
||||
RetryDelay func() time.Duration
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
fn func(context.Context) error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr error
|
||||
}{
|
||||
{"success",
|
||||
fields{StartTimeout: time.Second, RetryDelay: nil},
|
||||
args{context.Background(), success},
|
||||
nil},
|
||||
{"context cancelled",
|
||||
fields{StartTimeout: time.Second, RetryDelay: nil},
|
||||
args{cancelledCtx, wait},
|
||||
context.Canceled},
|
||||
{"timeout",
|
||||
fields{StartTimeout: 20 * time.Millisecond, RetryDelay: func() time.Duration { return 10 * time.Millisecond }},
|
||||
args{cancelledCtx, fail},
|
||||
failErr},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := Config{
|
||||
StartTimeout: tt.fields.StartTimeout,
|
||||
RetryDelay: tt.fields.RetryDelay,
|
||||
}
|
||||
if err := cfg.Run(tt.args.ctx, tt.args.fn); err != tt.wantErr {
|
||||
t.Fatalf("Config.Run() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackoff_Linear(t *testing.T) {
|
||||
b := Backoff{
|
||||
InitialBackoff: 2 * time.Minute,
|
||||
Multiplier: 2,
|
||||
}
|
||||
|
||||
linear := (&b).Linear
|
||||
|
||||
if linear() != 2*time.Minute {
|
||||
t.Fatal("first backoff should be 2 minutes")
|
||||
}
|
||||
|
||||
if linear() != 4*time.Minute {
|
||||
t.Fatal("second backoff should be 4 minutes")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue