providers/lib/client/retry.go (84 lines of code) (raw):

// Copyright (c) Facebook, Inc. and its affiliates. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package client import ( "fmt" "io" "io/ioutil" "net/http" "strconv" "strings" "time" ) // backoff means that on each retry, we will multiply the delay time by this // maybe this could be configurable as well, but meh const backoff = 2 // FailedRetries is an error returned when all retries have been exhausted type FailedRetries int // Error is a part of the error interface func (fr FailedRetries) Error() string { return fmt.Sprintf("failed to fetch after %d retries", int(fr)) } // RetryPolicy is a function which returns true if HTTP status should be retried type RetryPolicy func(status int) bool func (rp *RetryPolicy) String() string { return "" } func (rp *RetryPolicy) Set(s string) error { switch s { case "": *rp = RetryNone case "all": *rp = RetryAll default: // treat it as a comma separated list of ints parts := strings.Split(s, ",") statuses := make([]int, len(parts)) for _, part := range parts { status, err := strconv.Atoi(part) if err != nil { return err } statuses = append(statuses, status) } *rp = Retry(statuses...) } return nil } // RetryNone is a RetryPolicy which doesn't retry anything func RetryNone(_ int) bool { return false } // RetryAll is a RetryPolicy which retries all http statuses func RetryAll(_ int) bool { return true } // Retry will only retry given statuses func Retry(statuses ...int) RetryPolicy { set := make(map[int]bool, len(statuses)) for _, s := range statuses { set[s] = true } return func(status int) bool { return set[status] } } // WithRetries will retry all given requests for the specified number of times // - if status is 200, returns // - if status is covered by the retry policy and hasn't been retried the total number of times, retry // - otherwise, fail the request func WithRetries(c Client, retries int, delay time.Duration, rp RetryPolicy) Client { if retries <= 0 { // if no retries, return the normal client return c } return &executorClient{c, &retryExecutor{retries, delay, rp}} } type retryExecutor struct { retries int delay time.Duration shouldRetry RetryPolicy } func (c *retryExecutor) execute(f func() (*http.Response, error)) (*http.Response, error) { delay := c.delay for retry := 0; retry <= c.retries; retry++ { resp, err := f() if err != nil { return resp, err } if resp.StatusCode == http.StatusOK { return resp, nil } if !c.shouldRetry(resp.StatusCode) { // unknown status, read the error and return it defer resp.Body.Close() body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1024*1024)) if err != nil { return nil, fmt.Errorf("cannot read http response: %v", err) } return nil, &Err{resp.StatusCode, resp.Status, string(body)} } // retry if have more retries left if retry != c.retries { time.Sleep(delay) } delay *= backoff } // no more retries left return nil, FailedRetries(c.retries) }