├── LICENSE ├── README.md ├── doc.go ├── example └── main.go ├── go.mod ├── go.sum ├── retrier.go └── retrier_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # retry 2 | 3 | An exponentially backing off retry package for Go. 4 | 5 | [![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://godoc.org/github.com/coder/retry) 6 | 7 | ``` 8 | go get github.com/coder/retry@latest 9 | ``` 10 | 11 | `retry` promotes control flow using `for`/`goto` instead of callbacks. 12 | 13 | ## Examples 14 | 15 | Wait for connectivity to google.com, checking at most once every 16 | second: 17 | 18 | ```go 19 | func pingGoogle(ctx context.Context) error { 20 | var err error 21 | 22 | r := retry.New(time.Second, time.Second*10); 23 | 24 | // Jitter is useful when the majority of clients to a service use 25 | // the same backoff policy. 26 | // 27 | // It is provided as a standard deviation. 28 | r.Jitter = 0.1 29 | 30 | retry: 31 | _, err = http.Get("https://google.com") 32 | if err != nil { 33 | if r.Wait(ctx) { 34 | goto retry 35 | } 36 | return err 37 | } 38 | 39 | return nil 40 | } 41 | ``` 42 | 43 | Wait for connectivity to google.com, checking at most 10 times: 44 | ```go 45 | func pingGoogle(ctx context.Context) error { 46 | var err error 47 | 48 | for r := retry.New(time.Second, time.Second*10); n := 0; r.Wait(ctx) && n < 10; n++ { 49 | _, err = http.Get("https://google.com") 50 | if err != nil { 51 | continue 52 | } 53 | break 54 | } 55 | return err 56 | } 57 | ``` -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package retry runs a fallible block of code until it succeeds. 2 | package retry 3 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/coder/retry" 9 | ) 10 | 11 | func main() { 12 | r := retry.New(time.Second, time.Second*10) 13 | 14 | ctx := context.Background() 15 | 16 | last := time.Now() 17 | for r.Wait(ctx) { 18 | // Do something that might fail 19 | fmt.Printf("%v: hi\n", time.Since(last).Round(time.Second)) 20 | last = time.Now() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/coder/retry 2 | 3 | go 1.20 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coder/retry/f5ccc4d2d45135bf65c7ccc5e78942dd7df19c84/go.sum -------------------------------------------------------------------------------- /retrier.go: -------------------------------------------------------------------------------- 1 | package retry 2 | 3 | import ( 4 | "context" 5 | "math" 6 | "math/rand" 7 | "time" 8 | ) 9 | 10 | // Retrier implements an exponentially backing off retry instance. 11 | // Use New instead of creating this object directly. 12 | type Retrier struct { 13 | // Delay is the current delay between attempts. 14 | Delay time.Duration 15 | 16 | // Floor and Ceil are the minimum and maximum delays. 17 | Floor, Ceil time.Duration 18 | 19 | // Rate is the rate at which the delay grows. 20 | // E.g. 2 means the delay doubles each time. 21 | Rate float64 22 | 23 | // Jitter determines the level of indeterminism in the delay. 24 | // 25 | // It is the standard deviation of the normal distribution of a random variable 26 | // multiplied by the delay. E.g. 0.1 means the delay is normally distributed 27 | // with a standard deviation of 10% of the delay. Floor and Ceil are still 28 | // respected, making outlandish values impossible. 29 | // 30 | // Jitter can help avoid thundering herds. 31 | Jitter float64 32 | } 33 | 34 | // New creates a retrier that exponentially backs off from floor to ceil pauses. 35 | func New(floor, ceil time.Duration) *Retrier { 36 | return &Retrier{ 37 | Delay: 0, 38 | Floor: floor, 39 | Ceil: ceil, 40 | // Phi scales more calmly than 2, but still has nice pleasing 41 | // properties. 42 | Rate: math.Phi, 43 | } 44 | } 45 | 46 | func applyJitter(d time.Duration, jitter float64) time.Duration { 47 | if jitter == 0 { 48 | return d 49 | } 50 | d *= time.Duration(1 + jitter*rand.NormFloat64()) 51 | if d < 0 { 52 | return 0 53 | } 54 | return d 55 | } 56 | 57 | // Wait returns after min(Delay*Growth, Ceil) or ctx is cancelled. 58 | // The first call to Wait will return immediately. 59 | func (r *Retrier) Wait(ctx context.Context) bool { 60 | r.Delay = time.Duration(float64(r.Delay) * r.Rate) 61 | 62 | r.Delay = applyJitter(r.Delay, r.Jitter) 63 | 64 | if r.Delay > r.Ceil { 65 | r.Delay = r.Ceil 66 | } 67 | 68 | select { 69 | case <-time.After(r.Delay): 70 | if r.Delay < r.Floor { 71 | r.Delay = r.Floor 72 | } 73 | return true 74 | case <-ctx.Done(): 75 | return false 76 | } 77 | } 78 | 79 | // Reset resets the retrier to its initial state. 80 | func (r *Retrier) Reset() { 81 | r.Delay = 0 82 | } 83 | -------------------------------------------------------------------------------- /retrier_test.go: -------------------------------------------------------------------------------- 1 | package retry 2 | 3 | import ( 4 | "context" 5 | "math" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestContextCancel(t *testing.T) { 11 | ctx, cancel := context.WithCancel(context.Background()) 12 | cancel() 13 | 14 | r := New(time.Hour, time.Hour) 15 | for r.Wait(ctx) { 16 | t.Fatalf("attempt allowed even though context cancelled") 17 | } 18 | } 19 | 20 | func TestFirstTryImmediately(t *testing.T) { 21 | ctx, cancel := context.WithCancel(context.Background()) 22 | defer cancel() 23 | 24 | r := New(time.Hour, time.Hour) 25 | tt := time.Now() 26 | if !r.Wait(ctx) { 27 | t.Fatalf("attempt not allowed") 28 | } 29 | if time.Since(tt) > time.Second { 30 | t.Fatalf("attempt took too long") 31 | } 32 | } 33 | 34 | func TestScalesExponentially(t *testing.T) { 35 | ctx, cancel := context.WithCancel(context.Background()) 36 | defer cancel() 37 | 38 | r := New(time.Second, time.Second*10) 39 | r.Rate = 2 40 | 41 | start := time.Now() 42 | 43 | for i := 0; i < 3; i++ { 44 | t.Logf("delay: %v", r.Delay) 45 | r.Wait(ctx) 46 | t.Logf("sinceStart: %v", time.Since(start).Round(time.Second)) 47 | } 48 | 49 | sinceStart := time.Since(start).Round(time.Second) 50 | if sinceStart != time.Second*6 { 51 | t.Fatalf("did not scale correctly: %v", sinceStart) 52 | } 53 | } 54 | 55 | func TestReset(t *testing.T) { 56 | r := New(time.Hour, time.Hour) 57 | // Should be immediate 58 | ctx := context.Background() 59 | r.Wait(ctx) 60 | r.Reset() 61 | r.Wait(ctx) 62 | } 63 | 64 | func TestJitter_Normal(t *testing.T) { 65 | t.Parallel() 66 | 67 | r := New(time.Millisecond, time.Millisecond) 68 | r.Jitter = 0.5 69 | 70 | var ( 71 | sum time.Duration 72 | waits []float64 73 | ctx = context.Background() 74 | ) 75 | for i := 0; i < 1000; i++ { 76 | start := time.Now() 77 | r.Wait(ctx) 78 | took := time.Since(start) 79 | waits = append(waits, (took.Seconds() * 1000)) 80 | sum += took 81 | } 82 | 83 | avg := float64(sum) / float64(len(waits)) 84 | std := stdDev(waits) 85 | if std > avg*0.1 { 86 | t.Fatalf("standard deviation too high: %v", std) 87 | } 88 | 89 | t.Logf("average: %v", time.Duration(avg)) 90 | t.Logf("std dev: %v", std) 91 | t.Logf("sample: %v", waits[len(waits)-10:]) 92 | } 93 | 94 | // stdDev returns the standard deviation of the sample. 95 | func stdDev(sample []float64) float64 { 96 | if len(sample) == 0 { 97 | return 0 98 | } 99 | mean := 0.0 100 | for _, v := range sample { 101 | mean += v 102 | } 103 | mean /= float64(len(sample)) 104 | 105 | variance := 0.0 106 | for _, v := range sample { 107 | variance += math.Pow(v-mean, 2) 108 | } 109 | variance /= float64(len(sample)) 110 | 111 | return math.Sqrt(variance) 112 | } 113 | --------------------------------------------------------------------------------