github.com/mailgun/holster/v4@v4.20.0/retry/retry.go (about) 1 package retry 2 3 import ( 4 "context" 5 "fmt" 6 "sync" 7 "time" 8 9 "github.com/mailgun/holster/v4/errors" 10 "github.com/mailgun/holster/v4/syncutil" 11 ) 12 13 const ( 14 Cancelled = cancelReason("context cancelled") 15 Stopped = cancelReason("retry stopped") 16 AttemptsExhausted = cancelReason("attempts exhausted") 17 ) 18 19 type Func func(context.Context, int) error 20 21 type cancelReason string 22 23 type stopErr struct { 24 err error 25 } 26 27 func (e *stopErr) Error() string { 28 return fmt.Sprintf("stop err: %s", e.err.Error()) 29 } 30 31 type Err struct { 32 Err error 33 Reason cancelReason 34 Attempts int 35 } 36 37 func (e *Err) Cause() error { return e.Err } 38 func (e *Err) Error() string { 39 return fmt.Sprintf("on attempt '%d'; %s: %s", e.Attempts, e.Reason, e.Err.Error()) 40 } 41 42 func (e *Err) Is(target error) bool { 43 _, ok := target.(*Err) 44 return ok 45 } 46 47 // Stop forces the retry to cancel with the provided error 48 // and retry.Err.Reason == retry.Stopped 49 func Stop(err error) error { 50 return &stopErr{err: err} 51 } 52 53 // Until will retry the provided `retry.Func` until it returns nil or 54 // the context is cancelled. Optionally users may use `retry.Stop()` to force 55 // the retry to terminate with an error. Returns a `retry.Err` with 56 // the included Reason and Attempts 57 func Until(ctx context.Context, backOff BackOff, f Func) error { 58 var attempt int 59 for { 60 attempt++ 61 if err := f(ctx, attempt); err != nil { 62 var stop *stopErr 63 if errors.As(err, &stop) { 64 return &Err{Attempts: attempt, Reason: Stopped, Err: stop.err} 65 } 66 interval, retry := backOff.Next() 67 if !retry { 68 return &Err{Attempts: attempt, Reason: AttemptsExhausted, Err: err} 69 } 70 timer := time.NewTimer(interval) 71 select { 72 case <-timer.C: 73 timer.Stop() 74 continue 75 case <-ctx.Done(): 76 if !timer.Stop() { 77 <-timer.C 78 } 79 return &Err{Attempts: attempt, Reason: Cancelled, Err: err} 80 } 81 } 82 return nil 83 } 84 } 85 86 type AsyncItem struct { 87 Retrying bool 88 Attempts int 89 Err error 90 } 91 92 func (s *AsyncItem) Error() string { 93 return s.Err.Error() 94 } 95 96 type Async struct { 97 asyncs map[interface{}]AsyncItem 98 mutex *sync.Mutex 99 ctx context.Context 100 wg syncutil.WaitGroup 101 } 102 103 // Given a function that takes a context, run the provided function; if it fails, retry the function asynchronously 104 // and return Async{}. Subsequent calls to with the same 'key' will return Async{} if the function is still 105 // retrying, this continues until the retry period has exhausted or the context expires or is cancelled. 106 // Then the final error returned by f() is returned to the caller on the final call with the same 'key' 107 // 108 // The code assumes the caller will continue to call `Async()` until either the retries have exhausted or 109 // an Async{Retrying: false} is returned. 110 func NewRetryAsync() *Async { 111 return &Async{ 112 mutex: &sync.Mutex{}, 113 asyncs: make(map[interface{}]AsyncItem), 114 } 115 } 116 117 // Return the number of active async retries 118 func (s *Async) Len() int { 119 s.mutex.Lock() 120 defer s.mutex.Unlock() 121 return len(s.asyncs) 122 } 123 124 // Stop forces stop of all running async retries 125 func (s *Async) Stop() { 126 s.wg.Stop() 127 } 128 129 // Wait waits for all running async retries to complete 130 func (s *Async) Wait() { 131 s.wg.Wait() 132 } 133 134 func (s *Async) Async(key interface{}, ctx context.Context, bo BackOff, 135 f func(context.Context, int) error) *AsyncItem { 136 137 // does this key have an existing retry running? 138 s.mutex.Lock() 139 if async, ok := s.asyncs[key]; ok { 140 // Remove entries that are no longer re-trying 141 if !async.Retrying { 142 delete(s.asyncs, key) 143 } 144 s.mutex.Unlock() 145 return &async 146 } 147 s.mutex.Unlock() 148 149 // Attempt to run the function, if successful return nil 150 err := f(s.ctx, 0) 151 if err == nil { 152 return nil 153 } 154 155 async := AsyncItem{ 156 Retrying: true, 157 Err: err, 158 } 159 160 s.mutex.Lock() 161 s.asyncs[key] = async 162 s.mutex.Unlock() 163 164 // Create an go routine to run the retry 165 s.wg.Until(func(done chan struct{}) bool { 166 async := AsyncItem{Retrying: true} 167 168 for { 169 // Retry the function 170 async.Attempts++ 171 async.Err = f(ctx, async.Attempts) 172 173 // If success, then indicate we are no longer retrying 174 if async.Err == nil { 175 async.Retrying = false 176 177 s.mutex.Lock() 178 s.asyncs[key] = async 179 s.mutex.Unlock() 180 return false 181 } 182 183 // Record the error and attempts 184 s.mutex.Lock() 185 s.asyncs[key] = async 186 s.mutex.Unlock() 187 188 interval, retry := bo.Next() 189 if !retry { 190 async.Retrying = false 191 s.mutex.Lock() 192 s.asyncs[key] = async 193 s.mutex.Unlock() 194 return false 195 } 196 197 timer := time.NewTimer(interval) 198 select { 199 case <-timer.C: 200 timer.Stop() 201 case <-ctx.Done(): 202 async.Retrying = false 203 204 s.mutex.Lock() 205 s.asyncs[key] = async 206 s.mutex.Unlock() 207 timer.Stop() 208 return false 209 case <-done: 210 // immediate abort, abandon all work 211 if !timer.Stop() { 212 <-timer.C 213 } 214 return false 215 } 216 } 217 }) 218 return &async 219 } 220 221 // Return errors from failed asyncs and clean up the internal async map 222 func (s *Async) Errs() map[interface{}]AsyncItem { 223 results := make(map[interface{}]AsyncItem) 224 s.mutex.Lock() 225 226 for key, async := range s.asyncs { 227 // Remove entries that are no longer re-trying 228 if !async.Retrying { 229 delete(s.asyncs, key) 230 231 // Only include async's that had an error 232 if async.Err != nil { 233 results[key] = async 234 } 235 } 236 } 237 s.mutex.Unlock() 238 return results 239 }