fix(time): prevent overflow in ExponentialDelayWithJitter backoff#4852
fix(time): prevent overflow in ExponentialDelayWithJitter backoff#4852anxkhn wants to merge 1 commit into
Conversation
ExponentialDelayWithJitter computed `baseDelay * (1 << attempt)` before applying `min(delay, maxDelay)`, so a large attempt overflowed the int64 time.Duration ahead of the cap. The wrapped negative or zero value passed the cap unchanged, making `delay > 0` false and skipping the sleep. In the scheduler this `attempt` is a host's ConcurrentRegisterCount, not a retry counter. With the production constants (baseDelay 30ms, maxDelay 1s) the backoff breaks once a host has >= 39 concurrent registrations, which is exactly the thundering herd the delay exists to dampen. Compute the shift only when it cannot overflow time.Duration (using the leading-zero count of baseDelay); otherwise the result would exceed maxDelay anyway, so clamp to maxDelay directly. Also guard a non-positive baseDelay, matching the existing guard in RandomDelayWithJitter. Behavior is unchanged for the existing small-attempt cases. Add regression cases covering the overflow regime. Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4852 +/- ##
=======================================
Coverage 28.04% 28.05%
=======================================
Files 232 232
Lines 23148 23151 +3
=======================================
+ Hits 6491 6494 +3
Misses 16218 16218
Partials 439 439
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
|
nice, the old code would wrap negative on large attempt values before the min() could cap it. using LeadingZeros to check if the shift is safe is clever avoids the overflow without needing big.Int or float math. test coverage for attempt=39, 62, 100 is thorough. the only edge case i can think of is baseDelay=0 which your guard handles already |
|
Thanks for taking a look, @pmady. Yes, that is the crux: the multiply/shift happened before On the Good call on |
Description
ExponentialDelayWithJittercomputedbaseDelay * (1 << attempt)and only then appliedmin(delay, maxDelay). A largeattemptoverflows the int64-nanosecondtime.Durationbefore the cap is applied, so the wrapped negative or zero value passes the cap unchanged,delay > 0is false, and the sleep is skipped.The shift is now applied only when it cannot overflow
time.Duration(gated on the leading-zero count ofbaseDelay); otherwise the product would exceedmaxDelayanyway, so the cap is used directly. A non-positivebaseDelayis also guarded, matching the existing guard inRandomDelayWithJitter. Behavior is unchanged for the existing small-attempt cases. Three regression cases cover the overflow regime.Related Issue
Closes #4851
Motivation and Context
The scheduler passes
attempt = host.ConcurrentRegisterCount, which is the number of concurrent in-flight registrations on a host rather than a retry counter (scheduler/service/service_v2.go:1325, base 30ms, max 1s). With those constants the overflow makes the register-peer backoff break once a host has roughly 39 or more concurrent registrations, disabling the thundering-herd mitigation in exactly the scenario it is meant to guard against.Screenshots (if appropriate)
Types of changes
Checklist