go.fuchsia.dev/jiri@v0.0.0-20240502161911-b66513b29486/retry/retry.go (about)

     1  // Copyright 2015 The Vanadium Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package retry provides a facility for retrying function
     6  // invocations.
     7  package retry
     8  
     9  import (
    10  	"fmt"
    11  	"math"
    12  	"math/rand"
    13  	"time"
    14  
    15  	"go.fuchsia.dev/jiri"
    16  )
    17  
    18  type RetryOpt interface {
    19  	retryOpt()
    20  }
    21  
    22  type AttemptsOpt int
    23  
    24  func (a AttemptsOpt) retryOpt() {}
    25  
    26  type IntervalOpt time.Duration
    27  
    28  func (i IntervalOpt) retryOpt() {}
    29  
    30  const (
    31  	defaultAttempts = 3
    32  	defaultInterval = 5 * time.Second
    33  )
    34  
    35  type exponentialBackoff struct {
    36  	InitialInterval float64
    37  	MaxInterval     float64
    38  	Multiplier      float64
    39  	Iteration       int
    40  	Rand            *rand.Rand
    41  }
    42  
    43  func newExponentialBackoff(initialInterval float64, maxInterval float64, multiplier float64) *exponentialBackoff {
    44  	e := &exponentialBackoff{
    45  		InitialInterval: initialInterval,
    46  		MaxInterval:     maxInterval,
    47  		Multiplier:      multiplier,
    48  		Iteration:       0,
    49  		Rand:            rand.New(rand.NewSource(time.Now().UnixNano())),
    50  	}
    51  	return e
    52  }
    53  
    54  func (e *exponentialBackoff) nextBackoff() time.Duration {
    55  	next := e.InitialInterval*math.Pow(e.Multiplier, float64(e.Iteration)) + 10*e.Rand.Float64()
    56  	e.Iteration++
    57  	if next > e.MaxInterval {
    58  		next = e.MaxInterval
    59  	}
    60  	return time.Duration(float64(time.Second) * next)
    61  }
    62  
    63  // Function retries the given function for the given number of
    64  // attempts at the given interval.
    65  func Function(jirix *jiri.X, fn func() error, task string, opts ...RetryOpt) error {
    66  	attempts, interval := defaultAttempts, defaultInterval
    67  	for _, opt := range opts {
    68  		switch typedOpt := opt.(type) {
    69  		case AttemptsOpt:
    70  			attempts = int(typedOpt)
    71  		case IntervalOpt:
    72  			interval = time.Duration(typedOpt)
    73  		}
    74  	}
    75  
    76  	backoff := newExponentialBackoff(float64(interval), 64, 2)
    77  	var err error
    78  	for i := 1; i <= attempts; i++ {
    79  		if i > 1 {
    80  			jirix.Logger.Infof("Attempt %d/%d: %s\n\n", i, attempts, task)
    81  		}
    82  		if err = fn(); err == nil {
    83  			return nil
    84  		}
    85  		if i < attempts {
    86  			jirix.Logger.Errorf("%s\n\n", err)
    87  			backoffInterval := backoff.nextBackoff()
    88  			jirix.Logger.Infof("Wait for %s before next attempt...: %s\n\n", backoffInterval, task)
    89  			time.Sleep(backoffInterval)
    90  		}
    91  	}
    92  	if attempts > 1 {
    93  		return fmt.Errorf("%q failed %d times in a row, Last error: %s", task, attempts, err)
    94  	}
    95  	return err
    96  }