diff --git a/pkg/scheduler/scheduler.go b/pkg/scheduler/scheduler.go index 1d6573d78..ac9ee726c 100644 --- a/pkg/scheduler/scheduler.go +++ b/pkg/scheduler/scheduler.go @@ -977,6 +977,16 @@ func calculateBackoffDelay(retryCount int) time.Duration { baseDelay := 1 * time.Second maxDelay := 5 * time.Second + + // Clamp the exponent before computing the power. Without this, large + // retry counts (which can reach ~120) overflow time.Duration's int64 + // when converted from math.Pow, wrapping around to 0 or negative values + // that silently slip past the maxDelay clamp below. + const maxExponent = 32 + if retryCount > maxExponent { + return maxDelay + } + delay := time.Duration(math.Pow(2, float64(retryCount))) * baseDelay if delay > maxDelay { delay = maxDelay diff --git a/pkg/scheduler/scheduler_test.go b/pkg/scheduler/scheduler_test.go index cdee78920..9d7ddf312 100644 --- a/pkg/scheduler/scheduler_test.go +++ b/pkg/scheduler/scheduler_test.go @@ -2423,3 +2423,30 @@ func TestConcurrencyLimit(t *testing.T) { }) } } + +func TestCalculateBackoffDelay(t *testing.T) { + maxDelay := 5 * time.Second + + tests := []struct { + retryCount int + want time.Duration + }{ + {retryCount: 0, want: 0}, + {retryCount: 1, want: 2 * time.Second}, + {retryCount: 2, want: 4 * time.Second}, + {retryCount: 3, want: maxDelay}, + {retryCount: 30, want: maxDelay}, + // High retry counts (reachable up to maxScheduleRetryCount-1) must + // stay clamped at maxDelay rather than overflowing to 0 / negative. + {retryCount: 62, want: maxDelay}, + {retryCount: 63, want: maxDelay}, + {retryCount: 119, want: maxDelay}, + } + + for _, tt := range tests { + got := calculateBackoffDelay(tt.retryCount) + assert.GreaterOrEqual(t, got, time.Duration(0), "retryCount=%d produced a negative delay", tt.retryCount) + assert.LessOrEqual(t, got, maxDelay, "retryCount=%d exceeded maxDelay", tt.retryCount) + assert.Equal(t, tt.want, got, "retryCount=%d", tt.retryCount) + } +}