github.com/xmidt-org/webpa-common@v1.11.9/xhttp/retry.go (about)

     1  package xhttp
     2  
     3  import (
     4  	"net/http"
     5  	"time"
     6  
     7  	"github.com/go-kit/kit/log"
     8  	"github.com/go-kit/kit/log/level"
     9  	"github.com/go-kit/kit/metrics"
    10  	"github.com/go-kit/kit/metrics/discard"
    11  	"github.com/xmidt-org/webpa-common/logging"
    12  )
    13  
    14  const DefaultRetryInterval = time.Second
    15  
    16  // temporaryError is the expected interface for a (possibly) temporary error.
    17  // Several of the error types in the net package implicitely implement this interface,
    18  // for example net.DNSError.
    19  type temporaryError interface {
    20  	Temporary() bool
    21  }
    22  
    23  // ShouldRetryFunc is a predicate for determining if the error returned from an HTTP transaction
    24  // should be retried.
    25  type ShouldRetryFunc func(error) bool
    26  
    27  // ShouldRetryStatusFunc is a predicate for determining if the status coded returned from an HTTP transaction
    28  // should be retried.
    29  type ShouldRetryStatusFunc func(int) bool
    30  
    31  // DefaultShouldRetry is the default retry predicate.  It returns true if and only if err exposes a Temporary() bool
    32  // method and that method returns true.  That means, for example, that for a net.DNSError with the temporary flag set to true
    33  // this predicate also returns true.
    34  func DefaultShouldRetry(err error) bool {
    35  	if temp, ok := err.(temporaryError); ok {
    36  		return temp.Temporary()
    37  	}
    38  
    39  	return false
    40  }
    41  
    42  // DefaultShouldRetryStatus is the default retry predicate. It returns false on all status codes
    43  // aka. it will never retry
    44  func DefaultShouldRetryStatus(status int) bool {
    45  	return false
    46  }
    47  
    48  // RetryOptions are the configuration options for a retry transactor
    49  type RetryOptions struct {
    50  	// Logger is the go-kit logger to use.  Defaults to logging.DefaultLogger() if unset.
    51  	Logger log.Logger
    52  
    53  	// Retries is the count of retries.  If not positive, then no transactor decoration is performed.
    54  	Retries int
    55  
    56  	// Interval is the time between retries.  If not set, DefaultRetryInterval is used.
    57  	Interval time.Duration
    58  
    59  	// Sleep is function used to wait out a duration.  If unset, time.Sleep is used.
    60  	Sleep func(time.Duration)
    61  
    62  	// ShouldRetry is the retry predicate.  Defaults to DefaultShouldRetry if unset.
    63  	ShouldRetry ShouldRetryFunc
    64  
    65  	// ShouldRetryStatus is the retry predicate.  Defaults to DefaultShouldRetry if unset.
    66  	ShouldRetryStatus ShouldRetryStatusFunc
    67  
    68  	// Counter is the counter for total retries.  If unset, no metrics are collected on retries.
    69  	Counter metrics.Counter
    70  
    71  	// UpdateRequest provides the ability to update the request before it is sent. default is noop
    72  	UpdateRequest func(*http.Request)
    73  }
    74  
    75  // RetryTransactor returns an HTTP transactor function, of the same signature as http.Client.Do, that
    76  // retries a certain number of times.  Note that net/http.RoundTripper.RoundTrip also is of this signature,
    77  // so this decorator can be used with a RoundTripper or an http.Client equally well.
    78  //
    79  // If o.Retries is nonpositive, next is returned undecorated.
    80  func RetryTransactor(o RetryOptions, next func(*http.Request) (*http.Response, error)) func(*http.Request) (*http.Response, error) {
    81  	if o.Retries < 1 {
    82  		return next
    83  	}
    84  
    85  	if o.Logger == nil {
    86  		o.Logger = logging.DefaultLogger()
    87  	}
    88  
    89  	if o.Counter == nil {
    90  		o.Counter = discard.NewCounter()
    91  	}
    92  
    93  	if o.ShouldRetry == nil {
    94  		o.ShouldRetry = DefaultShouldRetry
    95  	}
    96  
    97  	if o.ShouldRetryStatus == nil {
    98  		o.ShouldRetryStatus = DefaultShouldRetryStatus
    99  	}
   100  
   101  	if o.UpdateRequest == nil {
   102  		//noop
   103  		o.UpdateRequest = func(*http.Request) {}
   104  	}
   105  
   106  	if o.Interval < 1 {
   107  		o.Interval = DefaultRetryInterval
   108  	}
   109  
   110  	if o.Sleep == nil {
   111  		o.Sleep = time.Sleep
   112  	}
   113  
   114  	return func(request *http.Request) (*http.Response, error) {
   115  		if err := EnsureRewindable(request); err != nil {
   116  			return nil, err
   117  		}
   118  		var statusCode int
   119  
   120  		// initial attempt:
   121  		response, err := next(request)
   122  		if response != nil {
   123  			statusCode = response.StatusCode
   124  		}
   125  
   126  		for r := 0; r < o.Retries && ((err != nil && o.ShouldRetry(err)) || o.ShouldRetryStatus(statusCode)); r++ {
   127  			o.Counter.Add(1.0)
   128  			o.Sleep(o.Interval)
   129  			o.Logger.Log(level.Key(), level.DebugValue(), logging.MessageKey(), "retrying HTTP transaction", "url", request.URL.String(), logging.ErrorKey(), err, "retry", r+1, "statusCode", statusCode)
   130  
   131  			if err := Rewind(request); err != nil {
   132  				return nil, err
   133  			}
   134  
   135  			o.UpdateRequest(request)
   136  			response, err = next(request)
   137  			if response != nil {
   138  				statusCode = response.StatusCode
   139  			}
   140  		}
   141  
   142  		if err != nil {
   143  			o.Logger.Log(level.Key(), level.DebugValue(), logging.MessageKey(), "All HTTP transaction retries failed", "url", request.URL.String(), logging.ErrorKey(), err, "retries", o.Retries)
   144  		}
   145  
   146  		return response, err
   147  	}
   148  }