@@ -19,6 +19,7 @@ import (
1919 "reflect"
2020 "strconv"
2121 "strings"
22+ "time"
2223)
2324
2425func ValueToServiceComplete (output any ) ServiceCompleteEvent {
@@ -274,3 +275,79 @@ func cryptoSeed() (int64, error) {
274275 }
275276 return n .Int64 (), nil
276277}
278+
279+ // RetryWithBackoff runs action, retrying up to `retries` times when it returns a non-nil error.
280+ // Backoff grows exponentially from initialDelay, clamped to [minDelay, maxDelay], with ±20% jitter.
281+ // Returns nil on success, or the last error after exhausting retries.
282+ func RetryWithBackoff (
283+ action func () error ,
284+ retries int ,
285+ initialDelay , minDelay ,
286+ maxDelay time.Duration ,
287+ jitterFrac float64 ,
288+ ) error {
289+ if action == nil {
290+ return errors .New ("action is nil" )
291+ }
292+ if retries < 0 {
293+ return errors .New ("retries must be >= 0" )
294+ }
295+ // Normalize delays
296+ if minDelay < 0 {
297+ minDelay = 0
298+ }
299+ if maxDelay > 0 && minDelay > maxDelay {
300+ minDelay , maxDelay = maxDelay , minDelay
301+ }
302+ if maxDelay == 0 {
303+ maxDelay = time .Hour // a generous cap if none provided
304+ }
305+ if initialDelay <= 0 {
306+ initialDelay = minDelay
307+ }
308+ if initialDelay < minDelay {
309+ initialDelay = minDelay
310+ }
311+
312+ rng := mathrand .New (mathrand .NewSource (time .Now ().UnixNano ()))
313+
314+ var lastErr error
315+ for attempt := 0 ; attempt <= retries ; attempt ++ {
316+ if err := action (); err == nil {
317+ return nil
318+ } else {
319+ lastErr = err
320+ }
321+
322+ // No sleep after final attempt
323+ if attempt == retries {
324+ break
325+ }
326+
327+ // Exponential backoff: d = initial * 2^attempt
328+ delay := initialDelay << attempt
329+ if delay < minDelay {
330+ delay = minDelay
331+ }
332+ if delay > maxDelay {
333+ delay = maxDelay
334+ }
335+
336+ // Apply jitter: random in [delay*(1-j), delay*(1+j)]
337+ if delay > 0 && jitterFrac > 0 {
338+ df := float64 (delay )
339+ j := (rng .Float64 ()* 2 - 1 ) * (df * jitterFrac ) // [-df*j, +df*j]
340+ delay = time .Duration (df + j )
341+ // Re-clamp within [minDelay, maxDelay]
342+ if delay < minDelay {
343+ delay = minDelay
344+ }
345+ if delay > maxDelay {
346+ delay = maxDelay
347+ }
348+ }
349+
350+ time .Sleep (delay )
351+ }
352+ return lastErr
353+ }
0 commit comments