internal/requesthelper/retry.go (88 lines of code) (raw):

package requesthelper import ( "fmt" "math" "net/http" "time" "github.com/go-kit/kit/log" ) // SleepFunc pauses the execution for at least duration d. type SleepFunc func(d time.Duration) var ( // ActualSleep uses actual time to pause the execution. ActualSleep SleepFunc = time.Sleep ) const ( // time to sleep between retries is an exponential backoff formula: // t(n) = k * m^n expRetryN = 7 // how many times we retry the Download expRetryK = time.Second * 3 expRetryM = 2 ) // WithRetries retrieves a response body using the specified downloader. Any // error returned from d will be retried (and retrieved response bodies will be // closed on failures). If the retries do not succeed, the last error is returned. // // It sleeps in exponentially increasing durations between retries. func WithRetries(ctx *log.Context, rm *RequestManager, sf SleepFunc, eTag string) (*http.Response, error) { var lastErr error for n := 0; n < expRetryN; n++ { resp, err := rm.MakeRequest(ctx, eTag) // If there was no error, return the response if err == nil { return resp, nil } lastErr = err ctx.Log("warning", fmt.Sprintf("error on attempt %v: %v", n+1, err)) status := -1 if resp != nil { if resp.Body != nil { // we are not going to read this response body resp.Body.Close() } status = resp.StatusCode } // status == -1 means that there wasn't any http request if status == -1 { te, haste := lastErr.(interface { Temporary() bool }) to, hasto := lastErr.(interface { Timeout() bool }) if haste || hasto { if haste && te.Temporary() { ctx.Log("message", fmt.Sprintf("temporary error occurred. Retrying: %v", lastErr)) } else if hasto && to.Timeout() { ctx.Log("message", fmt.Sprintf("timeout error occurred. Retrying: %v", lastErr)) } else { ctx.Log("message", fmt.Sprintf("non-timeout, non-temporary error occurred, skipping retries: %v", lastErr)) break } } else { ctx.Log("message", "no response returned and unexpected error, skipping retries.") break } } else if responseNotModified(status) { return resp, nil } else if noImmediateGoalStatesToProcess(status) { return resp, nil } else if !isTransientHTTPStatusCode(status) { ctx.Log("message", fmt.Sprintf("RequestManager returned %v, skipping retries", status)) break } if n < expRetryN-1 { // have more retries to go, sleep before retrying slp := expRetryK * time.Duration(int(math.Pow(float64(expRetryM), float64(n)))) sf(slp) } } return nil, lastErr } func isTransientHTTPStatusCode(statusCode int) bool { switch statusCode { case http.StatusRequestTimeout, // 408 http.StatusTooManyRequests, // 429 http.StatusInternalServerError, // 500 http.StatusBadGateway, // 502 http.StatusServiceUnavailable, // 503 http.StatusGatewayTimeout: // 504 return true // timeout and too many requests default: return false } } func responseNotModified(statusCode int) bool { return statusCode == http.StatusNotModified } // Ignore the perfectly valid case of Resource Not Found. // There simply is no immediate goal state to process and the most likely reason is that one was never sent to the VM. func noImmediateGoalStatesToProcess(statusCode int) bool { return statusCode == http.StatusNotFound }