From b200c8e660a28fe10458eed58ec3cfe2c9701a16 Mon Sep 17 00:00:00 2001 From: Brendan Burns <5751682+brendandburns@users.noreply.github.com> Date: Wed, 4 Nov 2026 00:40:51 +0000 Subject: [PATCH 1/2] Initial plan From 4567e307042b316d21cedff1aba92a2c4642c317 Mon Sep 17 00:00:00 2001 From: Brendan Burns <5751682+brendandburns@users.noreply.github.com> Date: Wed, 4 Nov 2026 00:47:49 +0000 Subject: [PATCH 2/2] Fix exponential workqueue backoff saturation (closes #4985) --- .../ItemExponentialFailureRateLimiter.java | 23 ++++++++++-- ...ItemExponentialFailureRateLimiterTest.java | 36 +++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/extended/src/main/java/io/kubernetes/client/extended/workqueue/ratelimiter/ItemExponentialFailureRateLimiter.java b/extended/src/main/java/io/kubernetes/client/extended/workqueue/ratelimiter/ItemExponentialFailureRateLimiter.java index 871a5504bb..71344c6b79 100644 --- a/extended/src/main/java/io/kubernetes/client/extended/workqueue/ratelimiter/ItemExponentialFailureRateLimiter.java +++ b/extended/src/main/java/io/kubernetes/client/extended/workqueue/ratelimiter/ItemExponentialFailureRateLimiter.java @@ -36,8 +36,27 @@ public ItemExponentialFailureRateLimiter(Duration baseDelay, Duration maxDelay) @Override public Duration when(T item) { long exp = failures.computeIfAbsent(item, k -> new AtomicLong()).getAndIncrement(); - long d = maxDelay.toMillis() >> exp; - return d > baseDelay.toMillis() ? baseDelay.multipliedBy(1L << exp) : maxDelay; + if (baseDelay.isNegative() || maxDelay.isNegative()) { + // Preserve the existing behavior for negative duration configurations. + long d = maxDelay.toMillis() >> exp; + return d > baseDelay.toMillis() ? baseDelay.multipliedBy(1L << exp) : maxDelay; + } + if (baseDelay.compareTo(maxDelay) >= 0) { + return maxDelay; + } + if (baseDelay.isZero()) { + return baseDelay; + } + Duration delay = baseDelay; + Duration halfMaxDelay = maxDelay.dividedBy(2); + for (long i = 0; i < exp; i++) { + // Check before doubling to avoid overflow; positive durations reach the cap in at most 94 steps. + if (delay.compareTo(halfMaxDelay) > 0) { + return maxDelay; + } + delay = delay.multipliedBy(2); + } + return delay; } @Override diff --git a/extended/src/test/java/io/kubernetes/client/extended/workqueue/ratelimiter/ItemExponentialFailureRateLimiterTest.java b/extended/src/test/java/io/kubernetes/client/extended/workqueue/ratelimiter/ItemExponentialFailureRateLimiterTest.java index 8aa6f6a769..e8ee595dfd 100644 --- a/extended/src/test/java/io/kubernetes/client/extended/workqueue/ratelimiter/ItemExponentialFailureRateLimiterTest.java +++ b/extended/src/test/java/io/kubernetes/client/extended/workqueue/ratelimiter/ItemExponentialFailureRateLimiterTest.java @@ -15,10 +15,46 @@ import static org.assertj.core.api.Assertions.assertThat; import java.time.Duration; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; class ItemExponentialFailureRateLimiterTest { + static Stream positiveDelays() { + return Stream.of( + Arguments.of(Duration.ofMillis(5), Duration.ofSeconds(1000)), + Arguments.of(Duration.ofMillis(1), Duration.ofMillis(Long.MAX_VALUE)), + Arguments.of(Duration.ofSeconds(Long.MAX_VALUE / 4), Duration.ofSeconds(Long.MAX_VALUE)), + Arguments.of(Duration.ofNanos(1), Duration.ofSeconds(Long.MAX_VALUE, 999_999_999)), + Arguments.of(Duration.ofNanos(3), Duration.ofNanos(10)), + Arguments.of(Duration.ofSeconds(2), Duration.ofSeconds(1)), + Arguments.of(Duration.ofMillis(1), Duration.ZERO), + Arguments.of(Duration.ZERO, Duration.ofSeconds(1))); + } + + @ParameterizedTest + @MethodSource("positiveDelays") + void repeatedFailuresStayCapped(Duration baseDelay, Duration maxDelay) { + RateLimiter rateLimiter = new ItemExponentialFailureRateLimiter<>(baseDelay, maxDelay); + Duration expected = baseDelay.compareTo(maxDelay) < 0 ? baseDelay : maxDelay; + + for (int call = 1; call <= 1024; call++) { + assertThat(rateLimiter.when("item")).as("call %s", call).isEqualTo(expected); + assertThat(rateLimiter.numRequeues("item")).isEqualTo(call); + expected = + expected.compareTo(maxDelay.minus(expected)) >= 0 ? maxDelay : expected.plus(expected); + } + + rateLimiter.forget("item"); + assertThat(rateLimiter.numRequeues("item")).isZero(); + assertThat(rateLimiter.when("item")) + .isEqualTo(baseDelay.compareTo(maxDelay) < 0 ? baseDelay : maxDelay); + assertThat(rateLimiter.numRequeues("item")).isEqualTo(1); + } + @Test void itemExponentialFailureRateLimiter() { RateLimiter rateLimiter =