Package wait provides waiting strategies. A Waiter returns a channel that fires when the caller may proceed.
- Channel-native.
Waitreturns a channel; compose withselectandcontext.Context. - Two built-in strategies.
Decayfor supervisors,Jitterfor retries. - Zero dependencies. Standard library only.
package main
import (
"context"
"fmt"
"time"
"lesiw.io/wait"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
var (
start = time.Now()
since = start
tries = 0
)
fn := func() error {
tries++
now := time.Now()
fmt.Printf("attempt=%d elapsed=%v delay=%v\n",
tries,
now.Sub(start).Round(time.Millisecond),
now.Sub(since).Round(time.Millisecond),
)
since = now
if tries < 5 {
return fmt.Errorf("bang!")
}
return nil
}
wtr := wait.Jitter(2 * time.Second)
for fn() != nil {
select {
case <-wtr.Wait():
case <-ctx.Done():
return
}
}
fmt.Println("succeeded after", tries, "tries")
}| Waiter | Behavior |
|---|---|
Decay(limit) |
Delay increases under rapid calls and decays with idle time. Good for supervisor restart loops. |
Jitter(limit) |
Random [0, upper); upper doubles from limit/1024 to limit. Good for retry pacing. |
Most waiting strategies are simple to understand but subtle to implement. They're also usually a part of the error path, which tends to suffer in terms of test-coverage and real-world observation.
So for that reason I think it makes sense to group them together as implementations to a simple Waiter() interface.
Tunables are deliberately kept to a minimum to avoid having to double-check math at every callsite. Typically the only interesting question is "What's the maximum delay you will tolerate?" and so the Waiters in this package are built around that question.
- Suture v4 Spec
— the failure-decay counter
Decayis inspired by. - Exponential Backoff And Jitter
— Marc Brooker's 2015 AWS engineering post that defines and
analyzes the Full Jitter algorithm
Jitteruses.