github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/utils/retryutils/jitter.go (about) 1 // Copyright 2022 Gravitational, Inc 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 retryutils 16 17 import ( 18 "math/rand" 19 "sync" 20 "sync/atomic" 21 "time" 22 23 "github.com/gravitational/trace" 24 ) 25 26 // Jitter is a function which applies random jitter to a 27 // duration. Used to randomize backoff values. Must be 28 // safe for concurrent usage. 29 type Jitter func(time.Duration) time.Duration 30 31 // NewJitter builds a new default jitter (currently jitters on 32 // the range [d/2,d), but this is subject to change). 33 func NewJitter() Jitter { 34 return NewHalfJitter() 35 } 36 37 // NewFullJitter builds a new jitter on the range [0,d). Most use-cases 38 // are better served by a jitter with a meaningful minimum value, but if 39 // the *only* purpose of the jitter is to spread out retries to the greatest 40 // extent possible (e.g. when retrying a CompareAndSwap operation), a full jitter 41 // may be appropriate. 42 func NewFullJitter() Jitter { 43 jitter, _ := newJitter(1, newDefaultRng()) 44 return jitter 45 } 46 47 // NewShardedFullJitter is equivalent to NewFullJitter except that it 48 // performs better under high concurrency at the cost of having a larger 49 // footprint in memory. 50 func NewShardedFullJitter() Jitter { 51 jitter, _ := newShardedJitter(1, newDefaultRng) 52 return jitter 53 } 54 55 // NewHalfJitter returns a new jitter on the range [d/2,d). This is 56 // a large range and most suitable for jittering things like backoff 57 // operations where breaking cycles quickly is a priority. 58 func NewHalfJitter() Jitter { 59 jitter, _ := newJitter(2, newDefaultRng()) 60 return jitter 61 } 62 63 // NewShardedHalfJitter is equivalent to NewHalfJitter except that it 64 // performs better under high concurrency at the cost of having a larger 65 // footprint in memory. 66 func NewShardedHalfJitter() Jitter { 67 jitter, _ := newShardedJitter(2, newDefaultRng) 68 return jitter 69 } 70 71 // NewSeventhJitter builds a new jitter on the range [6d/7,d). Prefer smaller 72 // jitters such as this when jittering periodic operations (e.g. cert rotation 73 // checks) since large jitters result in significantly increased load. 74 func NewSeventhJitter() Jitter { 75 jitter, _ := newJitter(7, newDefaultRng()) 76 return jitter 77 } 78 79 // NewShardedSeventhJitter is equivalent to NewSeventhJitter except that it 80 // performs better under high concurrency at the cost of having a larger 81 // footprint in memory. 82 func NewShardedSeventhJitter() Jitter { 83 jitter, _ := newShardedJitter(7, newDefaultRng) 84 return jitter 85 } 86 87 func newDefaultRng() rng { 88 return rand.New(rand.NewSource(time.Now().UnixNano())) 89 } 90 91 // rng is an interface implemented by math/rand.Rand. This interface 92 // is used in testting. 93 type rng interface { 94 // Int63n returns, as an int64, a non-negative pseudo-random number 95 // in the half-open interval [0,n). It panics if n <= 0. 96 Int63n(n int64) int64 97 } 98 99 // newJitter builds a new jitter on the range [d*(n-1)/n,d) 100 // newJitter only returns an error if n < 1. 101 func newJitter(n time.Duration, rng rng) (Jitter, error) { 102 if n < 1 { 103 return nil, trace.BadParameter("newJitter expects n>=1, but got %v", n) 104 } 105 var mu sync.Mutex 106 return func(d time.Duration) time.Duration { 107 // values less than 1 cause rng to panic, and some logic 108 // relies on treating zero duration as non-blocking case. 109 if d < 1 { 110 return 0 111 } 112 mu.Lock() 113 defer mu.Unlock() 114 return d*(n-1)/n + time.Duration(rng.Int63n(int64(d))/int64(n)) 115 }, nil 116 } 117 118 // newShardedJitter constructs a new sharded jitter instance on the range [d*(n-1)/n,d) 119 // newShardedJitter only returns an error if n < 1. 120 func newShardedJitter(n time.Duration, mkrng func() rng) (Jitter, error) { 121 // the shard count here is pretty arbitrary. it was selected based on 122 // fiddling with some benchmarks. seems to be a good balance between 123 // limiting size and maximing perf under 100k concurrent calls 124 const shards = 64 125 126 if n < 1 { 127 return nil, trace.BadParameter("newShardedJitter expects n>=1, but got %v", n) 128 } 129 130 var rngs [shards]rng 131 var mus [shards]sync.Mutex 132 var ctr atomic.Uint64 133 var initOnce sync.Once 134 135 return func(d time.Duration) time.Duration { 136 // rng's allocate >4kb each during init, which is a bit annoying if the jitter 137 // isn't actually being used (e.g. when importing a package that has a global jitter). 138 // best to allocate lazily (this has no measurable impact on benchmarks). 139 initOnce.Do(func() { 140 for i := range rngs { 141 rngs[i] = mkrng() 142 } 143 }) 144 // values less than 1 cause rng to panic, and some logic 145 // relies on treating zero duration as non-blocking case. 146 if d < 1 { 147 return 0 148 } 149 idx := ctr.Add(1) % shards 150 mus[idx].Lock() 151 r := d*(n-1)/n + time.Duration(rngs[idx].Int63n(int64(d))/int64(n)) 152 mus[idx].Unlock() 153 return r 154 }, nil 155 }