oras.land/oras-go/v2@v2.5.1-0.20240520045656-aef90e4d04c4/registry/remote/retry/policy.go (about) 1 /* 2 Copyright The ORAS Authors. 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14 */ 15 16 package retry 17 18 import ( 19 "hash/maphash" 20 "math" 21 "math/rand" 22 "net" 23 "net/http" 24 "strconv" 25 "time" 26 ) 27 28 // headerRetryAfter is the header key for Retry-After. 29 const headerRetryAfter = "Retry-After" 30 31 // DefaultPolicy is a policy with fine-tuned retry parameters. 32 // It uses an exponential backoff with jitter. 33 var DefaultPolicy Policy = &GenericPolicy{ 34 Retryable: DefaultPredicate, 35 Backoff: DefaultBackoff, 36 MinWait: 200 * time.Millisecond, 37 MaxWait: 3 * time.Second, 38 MaxRetry: 5, 39 } 40 41 // DefaultPredicate is a predicate that retries on 5xx errors, 429 Too Many 42 // Requests, 408 Request Timeout and on network dial timeout. 43 var DefaultPredicate Predicate = func(resp *http.Response, err error) (bool, error) { 44 if err != nil { 45 // retry on Dial timeout 46 if err, ok := err.(net.Error); ok && err.Timeout() { 47 return true, nil 48 } 49 return false, err 50 } 51 52 if resp.StatusCode == http.StatusRequestTimeout || resp.StatusCode == http.StatusTooManyRequests { 53 return true, nil 54 } 55 56 if resp.StatusCode == 0 || resp.StatusCode >= 500 { 57 return true, nil 58 } 59 60 return false, nil 61 } 62 63 // DefaultBackoff is a backoff that uses an exponential backoff with jitter. 64 // It uses a base of 250ms, a factor of 2 and a jitter of 10%. 65 var DefaultBackoff Backoff = ExponentialBackoff(250*time.Millisecond, 2, 0.1) 66 67 // Policy is a retry policy. 68 type Policy interface { 69 // Retry returns the duration to wait before retrying the request. 70 // It returns a negative value if the request should not be retried. 71 // The attempt is used to: 72 // - calculate the backoff duration, the default backoff is an exponential backoff. 73 // - determine if the request should be retried. 74 // The attempt starts at 0 and should be less than MaxRetry for the request to 75 // be retried. 76 Retry(attempt int, resp *http.Response, err error) (time.Duration, error) 77 } 78 79 // Predicate is a function that returns true if the request should be retried. 80 type Predicate func(resp *http.Response, err error) (bool, error) 81 82 // Backoff is a function that returns the duration to wait before retrying the 83 // request. The attempt, is the next attempt number. The response is the 84 // response from the previous request. 85 type Backoff func(attempt int, resp *http.Response) time.Duration 86 87 // ExponentialBackoff returns a Backoff that uses an exponential backoff with 88 // jitter. The backoff is calculated as: 89 // 90 // temp = backoff * factor ^ attempt 91 // interval = temp * (1 - jitter) + rand.Int63n(2 * jitter * temp) 92 // 93 // The HTTP response is checked for a Retry-After header. If it is present, the 94 // value is used as the backoff duration. 95 func ExponentialBackoff(backoff time.Duration, factor, jitter float64) Backoff { 96 return func(attempt int, resp *http.Response) time.Duration { 97 var h maphash.Hash 98 h.SetSeed(maphash.MakeSeed()) 99 rand := rand.New(rand.NewSource(int64(h.Sum64()))) 100 101 // check Retry-After 102 if resp != nil && resp.StatusCode == http.StatusTooManyRequests { 103 if v := resp.Header.Get(headerRetryAfter); v != "" { 104 if retryAfter, _ := strconv.ParseInt(v, 10, 64); retryAfter > 0 { 105 return time.Duration(retryAfter) * time.Second 106 } 107 } 108 } 109 110 // do exponential backoff with jitter 111 temp := float64(backoff) * math.Pow(factor, float64(attempt)) 112 return time.Duration(temp*(1-jitter)) + time.Duration(rand.Int63n(int64(2*jitter*temp))) 113 } 114 } 115 116 // GenericPolicy is a generic retry policy. 117 type GenericPolicy struct { 118 // Retryable is a predicate that returns true if the request should be 119 // retried. 120 Retryable Predicate 121 122 // Backoff is a function that returns the duration to wait before retrying. 123 Backoff Backoff 124 125 // MinWait is the minimum duration to wait before retrying. 126 MinWait time.Duration 127 128 // MaxWait is the maximum duration to wait before retrying. 129 MaxWait time.Duration 130 131 // MaxRetry is the maximum number of retries. 132 MaxRetry int 133 } 134 135 // Retry returns the duration to wait before retrying the request. 136 // It returns -1 if the request should not be retried. 137 func (p *GenericPolicy) Retry(attempt int, resp *http.Response, err error) (time.Duration, error) { 138 if attempt >= p.MaxRetry { 139 return -1, nil 140 } 141 if ok, err := p.Retryable(resp, err); err != nil { 142 return -1, err 143 } else if !ok { 144 return -1, nil 145 } 146 backoff := p.Backoff(attempt, resp) 147 if backoff < p.MinWait { 148 backoff = p.MinWait 149 } 150 if backoff > p.MaxWait { 151 backoff = p.MaxWait 152 } 153 return backoff, nil 154 }