1 // Copyright 2025 Google LLC
2 //
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 package retry
16 17 import (
18 "context"
19 "io"
20 "math/rand"
21 "net/http"
22 "time"
23 )
24 25 const (
26 maxRetryAttempts = 5
27 )
28 29 var (
30 syscallRetryable = func(error) bool { return false }
31 )
32 33 // defaultBackoff is basically equivalent to gax.Backoff without the need for
34 // the dependency.
35 type defaultBackoff struct {
36 max time.Duration
37 mul float64
38 cur time.Duration
39 }
40 41 func (b *defaultBackoff) Pause() time.Duration {
42 d := time.Duration(1 + rand.Int63n(int64(b.cur)))
43 b.cur = time.Duration(float64(b.cur) * b.mul)
44 if b.cur > b.max {
45 b.cur = b.max
46 }
47 return d
48 }
49 50 // Sleep is the equivalent of gax.Sleep without the need for the dependency.
51 func Sleep(ctx context.Context, d time.Duration) error {
52 t := time.NewTimer(d)
53 select {
54 case <-ctx.Done():
55 t.Stop()
56 return ctx.Err()
57 case <-t.C:
58 return nil
59 }
60 }
61 62 // New returns a new Retryer with the default backoff strategy.
63 func New() *Retryer {
64 return &Retryer{bo: &defaultBackoff{
65 cur: 100 * time.Millisecond,
66 max: 30 * time.Second,
67 mul: 2,
68 }}
69 }
70 71 type backoff interface {
72 Pause() time.Duration
73 }
74 75 // Retryer is a retryer for HTTP requests.
76 type Retryer struct {
77 bo backoff
78 attempts int
79 }
80 81 // Retry determines if a request should be retried.
82 func (r *Retryer) Retry(status int, err error) (time.Duration, bool) {
83 if status == http.StatusOK {
84 return 0, false
85 }
86 retryOk := shouldRetry(status, err)
87 if !retryOk {
88 return 0, false
89 }
90 if r.attempts == maxRetryAttempts {
91 return 0, false
92 }
93 r.attempts++
94 return r.bo.Pause(), true
95 }
96 97 func shouldRetry(status int, err error) bool {
98 if 500 <= status && status <= 599 {
99 return true
100 }
101 if err == io.ErrUnexpectedEOF {
102 return true
103 }
104 // Transient network errors should be retried.
105 if syscallRetryable(err) {
106 return true
107 }
108 if err, ok := err.(interface{ Temporary() bool }); ok {
109 if err.Temporary() {
110 return true
111 }
112 }
113 if err, ok := err.(interface{ Unwrap() error }); ok {
114 return shouldRetry(status, err.Unwrap())
115 }
116 return false
117 }
118