Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions try.go
Original file line number Diff line number Diff line change
Expand Up @@ -396,13 +396,18 @@ func calculateNextDelay(cfg *Config, attempt int, err error) time.Duration {
// 4. Compute the jitter window — distinct from the backoff cap.
// MaxJitter, if set, caps only the random spread while leaving the
// deterministic base delay intact (Option A semantics):
// FullJitter: rand[0, jitterWindow) — base = 0
// EqualJitter: cap/2 + rand[0, jitterWindow) — base = cap/2
var jitterWindow time.Duration
if cfg.MaxJitter > 0 && cfg.MaxJitter < cap {
// FullJitter: rand[0, jitterWindow) — base = 0, window <= cap
// EqualJitter: cap/2 + rand[0, jitterWindow) — base = cap/2, window <= cap - cap/2
//
// For EqualJitter the default window is the remaining half of the cap, so
// the delay stays within [cap/2, cap) as documented instead of reaching
// up to 1.5x the exponential cap.
jitterWindow := cap
if cfg.Jitter == EqualJitter {
jitterWindow = cap - cap/2
}
if cfg.MaxJitter > 0 && cfg.MaxJitter < jitterWindow {
jitterWindow = cfg.MaxJitter
} else {
jitterWindow = cap // default: full backoff cap is the jitter window
}

// 5. Enforce the 1ms floor on the jitter window *before* passing it to
Expand Down Expand Up @@ -435,8 +440,8 @@ func calculateNextDelay(cfg *Config, attempt int, err error) time.Duration {
}

// 7. Enforce MaxDelay as the hard ceiling on the final delay.
// EqualJitter's base + jitter can slightly exceed MaxDelay when cap is
// close to MaxDelay. Cap here rather than constraining the components.
// The 1ms floors above can push a tiny delay past a sub-millisecond
// MaxDelay. Cap here rather than constraining the components.
if cfg.MaxDelay > 0 && d > cfg.MaxDelay {
d = cfg.MaxDelay
}
Expand Down
34 changes: 34 additions & 0 deletions try_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1268,3 +1268,37 @@ func TestAppendErrHistory_RingEviction(t *testing.T) {
t.Error("e1 should have been evicted")
}
}

func TestDo_EqualJitter_StaysWithinCap(t *testing.T) {
// EqualJitter is documented as cap/2 + rand[0, cap/2): every delay must be
// at least half the exponential cap and strictly below the cap itself,
// even while the cap is still below MaxDelay.
const initial = 100 * time.Millisecond
const attempts = 4

for run := 0; run < 200; run++ {
clk := &testClock{afterChan: make(chan time.Time, attempts)}
for i := 0; i < attempts; i++ {
clk.afterChan <- time.Now()
}

var infos []RetryInfo
_, _ = Do(context.Background(), func(ctx context.Context) (int, error) {
return 0, errors.New("fail")
},
WithAttempts(attempts),
WithInitialDelay(initial),
WithMaxDelay(time.Hour),
WithJitter(EqualJitter),
WithClock(clk),
WithOnRetry(func(info RetryInfo) { infos = append(infos, info) }),
)

for _, info := range infos {
capDelay := initial << (info.Attempt - 1)
if info.Delay < capDelay/2 || info.Delay >= capDelay {
t.Fatalf("attempt %d: delay %v outside [%v, %v)", info.Attempt, info.Delay, capDelay/2, capDelay)
}
}
}
}
Loading