Skip to content

Commit 1c401bf

Browse files
committed
1 parent d7edabd commit 1c401bf

File tree

2 files changed

+81
-8
lines changed

2 files changed

+81
-8
lines changed

go/runtime.go

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/gin-gonic/gin"
99
"log"
1010
"runtime/debug"
11+
"time"
1112
)
1213

1314
type haltType struct {
@@ -188,14 +189,9 @@ func (c ClientRuntime) Start() error {
188189
Routes: LoadRoutes(c.httpHandler),
189190
}
190191

191-
for {
192-
err = c.client.StartApp(req)
193-
if err == nil {
194-
break
195-
}
196-
}
197-
198-
return nil
192+
return RetryWithBackoff(func() error {
193+
return c.client.StartApp(req)
194+
}, 5, 0, 100*time.Millisecond, 5*time.Second, 0.2)
199195
}
200196

201197
func (c ClientRuntime) RunService(ctx context.Context, event ServiceStartEvent) (evt ServiceCompleteEvent) {

go/util.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"reflect"
2020
"strconv"
2121
"strings"
22+
"time"
2223
)
2324

2425
func 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

Comments
 (0)