From cdeeef78edf3258367e29628a12ad1c9c756d271 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Wed, 9 Sep 2026 12:29:43 +0200 Subject: [PATCH 1/5] fix(retry): make StreamRetryPolicy.exponential() actually exponential Delays accumulated as step * n(n+1)/2 instead of doubling. Compute the delay from the retry index as step * 2^(n-1), guarding the shift against overflow at high retry counts. --- .../core/api/model/retry/StreamRetryPolicy.kt | 22 ++++++++++---- .../api/model/retry/StreamRetryPolicyTest.kt | 30 +++++++++++++++---- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/stream-android-core/src/main/java/io/getstream/android/core/api/model/retry/StreamRetryPolicy.kt b/stream-android-core/src/main/java/io/getstream/android/core/api/model/retry/StreamRetryPolicy.kt index 84018bc8..e88faf98 100644 --- a/stream-android-core/src/main/java/io/getstream/android/core/api/model/retry/StreamRetryPolicy.kt +++ b/stream-android-core/src/main/java/io/getstream/android/core/api/model/retry/StreamRetryPolicy.kt @@ -62,9 +62,9 @@ private constructor( /** * Creates an **exponential back-off** policy. * - * Delay grows according to: + * Delay doubles on every retry: * ```text - * nextDelay = prevDelay + retryIndex × backoffStepMillis + * nextDelay = backoffStepMillis × 2^(retryIndex - 1) * ``` * * then clamped to `[backoffStepMillis, maxBackoffMillis]`. @@ -75,6 +75,7 @@ private constructor( * retry 1 → 250 ms * retry 2 → 500 ms * retry 3 → 1 000 ms + * retry 4 → 2 000 ms * … * ``` * @@ -100,10 +101,19 @@ private constructor( maxBackoffMills = maxBackoffMillis, initialDelayMillis = initialDelayMillis, giveUpFunction = giveUp, - nextBackOffDelayFunction = { retry, prev -> - (prev + retry * backoffStepMillis) - .coerceAtMost(maxBackoffMillis) - .coerceIn(backoffStepMillis, maxBackoffMillis) + nextBackOffDelayFunction = { retry, _ -> + val shift = (retry - 1).coerceIn(0, Long.SIZE_BITS - 1) + // Largest step that still fits under the cap once shifted; comparing + // against it keeps `shl` from overflowing on high retry counts. + val largestShiftableStep = maxBackoffMillis shr shift + if (backoffStepMillis > largestShiftableStep) { + maxBackoffMillis + } else { + (backoffStepMillis shl shift).coerceIn( + backoffStepMillis, + maxBackoffMillis, + ) + } }, ) .also { it.requireValid() } diff --git a/stream-android-core/src/test/java/io/getstream/android/core/api/model/retry/StreamRetryPolicyTest.kt b/stream-android-core/src/test/java/io/getstream/android/core/api/model/retry/StreamRetryPolicyTest.kt index bf98ecb3..08330646 100644 --- a/stream-android-core/src/test/java/io/getstream/android/core/api/model/retry/StreamRetryPolicyTest.kt +++ b/stream-android-core/src/test/java/io/getstream/android/core/api/model/retry/StreamRetryPolicyTest.kt @@ -59,17 +59,18 @@ class StreamRetryPolicyTest { fun `exponential backoff delay calculation increases exponentially`() { val policy = StreamRetryPolicy.exponential(backoffStepMillis = 100, initialDelayMillis = 0) - // First retry: prev=0 + retry=1 * 100 = 100 + // Retry n waits 100 * 2^(n-1) val delay1 = policy.nextBackOffDelayFunction(1, 0) assertEquals(100, delay1) - // Second retry: prev=100 + retry=2 * 100 = 300 val delay2 = policy.nextBackOffDelayFunction(2, delay1) - assertEquals(300, delay2) + assertEquals(200, delay2) - // Third retry: prev=300 + retry=3 * 100 = 600 val delay3 = policy.nextBackOffDelayFunction(3, delay2) - assertEquals(600, delay3) + assertEquals(400, delay3) + + val delay4 = policy.nextBackOffDelayFunction(4, delay3) + assertEquals(800, delay4) } @Test @@ -77,11 +78,28 @@ class StreamRetryPolicyTest { val policy = StreamRetryPolicy.exponential(backoffStepMillis = 1000, maxBackoffMillis = 3000) - // Should exceed max: 0 + 10 * 1000 = 10000, but capped at 3000 + // Should exceed max: 1000 * 2^9 = 512_000, but capped at 3000 val delay = policy.nextBackOffDelayFunction(10, 0) assertEquals(3000, delay) } + @Test + fun `exponential backoff delay does not overflow on very high retry counts`() { + val policy = + StreamRetryPolicy.exponential(backoffStepMillis = 1000, maxBackoffMillis = 30_000) + + assertEquals(30_000, policy.nextBackOffDelayFunction(64, 0)) + assertEquals(30_000, policy.nextBackOffDelayFunction(Int.MAX_VALUE, 0)) + } + + @Test + fun `exponential backoff delay ignores the previous delay`() { + val policy = StreamRetryPolicy.exponential(backoffStepMillis = 100) + + assertEquals(400, policy.nextBackOffDelayFunction(3, 0)) + assertEquals(400, policy.nextBackOffDelayFunction(3, 12_345)) + } + @Test fun `exponential backoff delay is clamped to minimum`() { val policy = StreamRetryPolicy.exponential(backoffStepMillis = 100) From 29349d70170f6fdb2027af0721969784b3602b2e Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Wed, 9 Sep 2026 13:42:44 +0200 Subject: [PATCH 2/5] feat(retry): add StreamRetryPolicy.quadratic() back-off Preserves the triangular curve that exponential() produced before it was corrected, so callers that want step * n(n+1)/2 growth have a named policy for it. --- .../core/api/model/retry/StreamRetryPolicy.kt | 51 +++++++ .../api/model/retry/StreamRetryPolicyTest.kt | 131 ++++++++++++++++++ 2 files changed, 182 insertions(+) diff --git a/stream-android-core/src/main/java/io/getstream/android/core/api/model/retry/StreamRetryPolicy.kt b/stream-android-core/src/main/java/io/getstream/android/core/api/model/retry/StreamRetryPolicy.kt index e88faf98..83eb50ec 100644 --- a/stream-android-core/src/main/java/io/getstream/android/core/api/model/retry/StreamRetryPolicy.kt +++ b/stream-android-core/src/main/java/io/getstream/android/core/api/model/retry/StreamRetryPolicy.kt @@ -118,6 +118,57 @@ private constructor( ) .also { it.requireValid() } + /** + * Creates a **quadratic back-off** policy. + * + * Each retry adds a growing increment to the previous delay: + * ```text + * nextDelay = prevDelay + retryIndex × backoffStepMillis + * ``` + * + * then clamped to `[backoffStepMillis, maxBackoffMillis]`. Driven from an + * [initialDelayMillis] of `0`, the delays are the triangular numbers scaled by the step — + * `backoffStepMillis × n(n + 1) / 2` — so growth sits between [linear] and [exponential]. + * + * Example with defaults: + * ``` + * attempt 1 → 0 ms + * retry 1 → 250 ms + * retry 2 → 750 ms + * retry 3 → 1 500 ms + * retry 4 → 2 500 ms + * … + * ``` + * + * Unlike [exponential], this reads the previous delay, so the curve depends on the retry + * loop feeding each delay back in — which `StreamRetryProcessor` does. + * + * Parameter semantics match [exponential]. + */ + public fun quadratic( + @IntRange(from = 1) minRetries: Int = 1, + @IntRange(from = 1) maxRetries: Int = 5, + @IntRange(from = 0) backoffStepMillis: Long = 250, + @IntRange(from = 0) maxBackoffMillis: Long = 15_000, + @IntRange(from = 0) initialDelayMillis: Long = 0, + giveUp: (Int, Throwable) -> Boolean = { retry, _ -> retry > maxRetries }, + ): StreamRetryPolicy = + StreamRetryPolicy( + minRetries = minRetries, + maxRetries = maxRetries, + minBackoffMills = backoffStepMillis, + maxBackoffMills = maxBackoffMillis, + initialDelayMillis = initialDelayMillis, + giveUpFunction = giveUp, + nextBackOffDelayFunction = { retry, prev -> + (prev + retry * backoffStepMillis).coerceIn( + backoffStepMillis, + maxBackoffMillis, + ) + }, + ) + .also { it.requireValid() } + /** * Creates a **linear back-off** policy. * diff --git a/stream-android-core/src/test/java/io/getstream/android/core/api/model/retry/StreamRetryPolicyTest.kt b/stream-android-core/src/test/java/io/getstream/android/core/api/model/retry/StreamRetryPolicyTest.kt index 08330646..db209cb8 100644 --- a/stream-android-core/src/test/java/io/getstream/android/core/api/model/retry/StreamRetryPolicyTest.kt +++ b/stream-android-core/src/test/java/io/getstream/android/core/api/model/retry/StreamRetryPolicyTest.kt @@ -117,6 +117,112 @@ class StreamRetryPolicyTest { assertTrue(policy.giveUpFunction(4, error)) // retry 4 > maxRetries 3 } + // ======================================== + // Quadratic Factory Function + // ======================================== + + @Test + fun `quadratic creates policy with correct defaults`() { + val policy = StreamRetryPolicy.quadratic() + + assertEquals(1, policy.minRetries) + assertEquals(5, policy.maxRetries) + assertEquals(250, policy.minBackoffMills) + assertEquals(15_000, policy.maxBackoffMills) + assertEquals(0, policy.initialDelayMillis) + } + + @Test + fun `quadratic creates policy with custom parameters`() { + val policy = + StreamRetryPolicy.quadratic( + minRetries = 2, + maxRetries = 10, + backoffStepMillis = 500, + maxBackoffMillis = 30_000, + initialDelayMillis = 100, + ) + + assertEquals(2, policy.minRetries) + assertEquals(10, policy.maxRetries) + assertEquals(500, policy.minBackoffMills) + assertEquals(30_000, policy.maxBackoffMills) + assertEquals(100, policy.initialDelayMillis) + } + + @Test + fun `quadratic backoff delay follows the triangular numbers`() { + val policy = StreamRetryPolicy.quadratic(backoffStepMillis = 100, initialDelayMillis = 0) + + // Fed back through the retry loop, retry n waits 100 * n(n+1)/2 + val delay1 = policy.nextBackOffDelayFunction(1, 0) + assertEquals(100, delay1) + + val delay2 = policy.nextBackOffDelayFunction(2, delay1) + assertEquals(300, delay2) + + val delay3 = policy.nextBackOffDelayFunction(3, delay2) + assertEquals(600, delay3) + + val delay4 = policy.nextBackOffDelayFunction(4, delay3) + assertEquals(1000, delay4) + } + + @Test + fun `quadratic backoff grows slower than exponential and faster than linear`() { + val step = 100L + val linear = StreamRetryPolicy.linear(backoffStepMillis = step) + val quadratic = StreamRetryPolicy.quadratic(backoffStepMillis = step) + val exponential = StreamRetryPolicy.exponential(backoffStepMillis = step) + + var linearDelay = 0L + var quadraticDelay = 0L + var exponentialDelay = 0L + for (retry in 1..5) { + linearDelay = linear.nextBackOffDelayFunction(retry, linearDelay) + quadraticDelay = quadratic.nextBackOffDelayFunction(retry, quadraticDelay) + exponentialDelay = exponential.nextBackOffDelayFunction(retry, exponentialDelay) + } + + assertEquals(500, linearDelay) + assertEquals(1500, quadraticDelay) + assertEquals(1600, exponentialDelay) + } + + @Test + fun `quadratic backoff delay is capped at maxBackoffMillis`() { + val policy = StreamRetryPolicy.quadratic(backoffStepMillis = 1000, maxBackoffMillis = 3000) + + // Should exceed max: 0 + 10 * 1000 = 10_000, but capped at 3000 + val delay = policy.nextBackOffDelayFunction(10, 0) + assertEquals(3000, delay) + } + + @Test + fun `quadratic backoff delay saturates at the cap on high retry counts`() { + val policy = + StreamRetryPolicy.quadratic(backoffStepMillis = 1000, maxBackoffMillis = 30_000) + + assertEquals(30_000, policy.nextBackOffDelayFunction(1000, 30_000)) + } + + @Test + fun `quadratic backoff delay is clamped to minimum`() { + val policy = StreamRetryPolicy.quadratic(backoffStepMillis = 100) + + // Delay should never be less than backoffStepMillis + val delay = policy.nextBackOffDelayFunction(1, 0) + assertTrue(delay >= 100) + } + + @Test + fun `quadratic giveUp function respects maxRetries`() { + val policy = StreamRetryPolicy.quadratic(maxRetries = 3) + + val error = RuntimeException("test") + assertTrue(policy.giveUpFunction(4, error)) // retry 4 > maxRetries 3 + } + // ======================================== // Linear Factory Function // ======================================== @@ -270,6 +376,31 @@ class StreamRetryPolicyTest { StreamRetryPolicy.exponential(initialDelayMillis = -100) } + @Test(expected = IllegalArgumentException::class) + fun `quadratic throws when minRetries is zero`() { + StreamRetryPolicy.quadratic(minRetries = 0) + } + + @Test(expected = IllegalArgumentException::class) + fun `quadratic throws when maxRetries is less than minRetries`() { + StreamRetryPolicy.quadratic(minRetries = 10, maxRetries = 5) + } + + @Test(expected = IllegalArgumentException::class) + fun `quadratic throws when minBackoffMills is negative`() { + StreamRetryPolicy.quadratic(backoffStepMillis = -50) + } + + @Test(expected = IllegalArgumentException::class) + fun `quadratic throws when maxBackoffMillis is less than minBackoffMills`() { + StreamRetryPolicy.quadratic(backoffStepMillis = 2000, maxBackoffMillis = 1000) + } + + @Test(expected = IllegalArgumentException::class) + fun `quadratic throws when initialDelayMillis is negative`() { + StreamRetryPolicy.quadratic(initialDelayMillis = -500) + } + @Test(expected = IllegalArgumentException::class) fun `linear throws when minRetries is zero`() { StreamRetryPolicy.linear(minRetries = 0) From 551c164e83e846e91b457163736df857c0b47144 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Mon, 14 Sep 2026 09:13:55 +0200 Subject: [PATCH 3/5] refactor(retry): drop unreachable clamp and cover the processor loop The branch guard already bounds the shifted value, so the coerceIn could never fire. Add a processor-level test that pins elapsed scheduler time across five attempts, where the accumulation bug actually lived. --- .../core/api/model/retry/StreamRetryPolicy.kt | 5 +--- .../StreamRetryProcessorImplTest.kt | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/stream-android-core/src/main/java/io/getstream/android/core/api/model/retry/StreamRetryPolicy.kt b/stream-android-core/src/main/java/io/getstream/android/core/api/model/retry/StreamRetryPolicy.kt index e88faf98..3745e6dd 100644 --- a/stream-android-core/src/main/java/io/getstream/android/core/api/model/retry/StreamRetryPolicy.kt +++ b/stream-android-core/src/main/java/io/getstream/android/core/api/model/retry/StreamRetryPolicy.kt @@ -109,10 +109,7 @@ private constructor( if (backoffStepMillis > largestShiftableStep) { maxBackoffMillis } else { - (backoffStepMillis shl shift).coerceIn( - backoffStepMillis, - maxBackoffMillis, - ) + backoffStepMillis shl shift } }, ) diff --git a/stream-android-core/src/test/java/io/getstream/android/core/internal/processing/StreamRetryProcessorImplTest.kt b/stream-android-core/src/test/java/io/getstream/android/core/internal/processing/StreamRetryProcessorImplTest.kt index 4af4cdfb..35e25360 100644 --- a/stream-android-core/src/test/java/io/getstream/android/core/internal/processing/StreamRetryProcessorImplTest.kt +++ b/stream-android-core/src/test/java/io/getstream/android/core/internal/processing/StreamRetryProcessorImplTest.kt @@ -82,6 +82,32 @@ class StreamRetryProcessorImplTest { assertEquals(100, dispatcher.scheduler.currentTime) } + @Test + fun `exponential policy doubles the elapsed delay between attempts`() = runTest { + val dispatcher = StandardTestDispatcher(testScheduler) + val scope = TestScope(dispatcher) + + val policy = + StreamRetryPolicy.exponential( + minRetries = 1, + maxRetries = 5, + backoffStepMillis = 250, + initialDelayMillis = 0, + ) + + val counter = AtomicInteger() + val job = + scope.async { retry.retry(policy) { counter.incrementAndGet().also { error("Boom") } } } + + dispatcher.scheduler.advanceUntilIdle() + + assertTrue(job.await().isFailure) + assertEquals(5, counter.get()) + // Four waits between five attempts: 250 + 500 + 1000 + 2000. + // The pre-fix policy accumulated instead, giving 250 + 750 + 1500 + 2500 = 5000. + assertEquals(3750, dispatcher.scheduler.currentTime) + } + @Test fun `returns failure after exhausting maxRetries`() = runTest { val policy = StreamRetryPolicy.linear(maxRetries = 2, minRetries = 1) From 5c803892910353192c98a5a58f94f07d381066c9 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Mon, 14 Sep 2026 09:16:02 +0200 Subject: [PATCH 4/5] refactor(retry): build quadratic policy through the custom factory Removes the duplicated constructor block flagged by SonarCloud (14 lines, 7.7% of new code). custom() already clamps the lambda result to the same bounds, so behaviour is unchanged. --- .../core/api/model/retry/StreamRetryPolicy.kt | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/stream-android-core/src/main/java/io/getstream/android/core/api/model/retry/StreamRetryPolicy.kt b/stream-android-core/src/main/java/io/getstream/android/core/api/model/retry/StreamRetryPolicy.kt index 749e01dc..a60545d7 100644 --- a/stream-android-core/src/main/java/io/getstream/android/core/api/model/retry/StreamRetryPolicy.kt +++ b/stream-android-core/src/main/java/io/getstream/android/core/api/model/retry/StreamRetryPolicy.kt @@ -150,21 +150,16 @@ private constructor( @IntRange(from = 0) initialDelayMillis: Long = 0, giveUp: (Int, Throwable) -> Boolean = { retry, _ -> retry > maxRetries }, ): StreamRetryPolicy = - StreamRetryPolicy( - minRetries = minRetries, - maxRetries = maxRetries, - minBackoffMills = backoffStepMillis, - maxBackoffMills = maxBackoffMillis, - initialDelayMillis = initialDelayMillis, - giveUpFunction = giveUp, - nextBackOffDelayFunction = { retry, prev -> - (prev + retry * backoffStepMillis).coerceIn( - backoffStepMillis, - maxBackoffMillis, - ) - }, - ) - .also { it.requireValid() } + custom( + minRetries = minRetries, + maxRetries = maxRetries, + minBackoffMills = backoffStepMillis, + maxBackoffMills = maxBackoffMillis, + initialDelayMillis = initialDelayMillis, + giveUp = giveUp, + ) { retry, prev -> + prev + retry * backoffStepMillis + } /** * Creates a **linear back-off** policy. From c1f38de17a1569960295aab942651e23e2e278e3 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Mon, 14 Sep 2026 10:07:06 +0200 Subject: [PATCH 5/5] docs(retry): correct the quadratic-vs-exponential comparison Quadratic is the steeper curve over retries 1-4 and only loses to doubling at retry 5, which the default maxRetries never reaches. Reword the KDoc, rename the comparison test and assert the crossover, and strengthen the cap test to grow into the cap instead of restating a fixed point. --- .../core/api/model/retry/StreamRetryPolicy.kt | 8 ++++- .../api/model/retry/StreamRetryPolicyTest.kt | 36 +++++++++++++------ 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/stream-android-core/src/main/java/io/getstream/android/core/api/model/retry/StreamRetryPolicy.kt b/stream-android-core/src/main/java/io/getstream/android/core/api/model/retry/StreamRetryPolicy.kt index a60545d7..1fe8395d 100644 --- a/stream-android-core/src/main/java/io/getstream/android/core/api/model/retry/StreamRetryPolicy.kt +++ b/stream-android-core/src/main/java/io/getstream/android/core/api/model/retry/StreamRetryPolicy.kt @@ -125,7 +125,13 @@ private constructor( * * then clamped to `[backoffStepMillis, maxBackoffMillis]`. Driven from an * [initialDelayMillis] of `0`, the delays are the triangular numbers scaled by the step — - * `backoffStepMillis × n(n + 1) / 2` — so growth sits between [linear] and [exponential]. + * `backoffStepMillis × n(n + 1) / 2`. + * + * Asymptotically this is the gentler curve, but over the range a retry loop actually covers + * it is the **steeper** of the two. At a 100 ms step it runs `100, 300, 600, 1000` against + * [exponential]'s `100, 200, 400, 800`; doubling only overtakes it at retry 5, and with the + * default `maxRetries = 5` the processor never computes that retry. Expect longer waits + * than [exponential], not shorter. * * Example with defaults: * ``` diff --git a/stream-android-core/src/test/java/io/getstream/android/core/api/model/retry/StreamRetryPolicyTest.kt b/stream-android-core/src/test/java/io/getstream/android/core/api/model/retry/StreamRetryPolicyTest.kt index db209cb8..ad8ffbff 100644 --- a/stream-android-core/src/test/java/io/getstream/android/core/api/model/retry/StreamRetryPolicyTest.kt +++ b/stream-android-core/src/test/java/io/getstream/android/core/api/model/retry/StreamRetryPolicyTest.kt @@ -169,24 +169,35 @@ class StreamRetryPolicyTest { } @Test - fun `quadratic backoff grows slower than exponential and faster than linear`() { + fun `quadratic backoff stays above exponential until they cross at retry 5`() { val step = 100L val linear = StreamRetryPolicy.linear(backoffStepMillis = step) val quadratic = StreamRetryPolicy.quadratic(backoffStepMillis = step) val exponential = StreamRetryPolicy.exponential(backoffStepMillis = step) - var linearDelay = 0L - var quadraticDelay = 0L - var exponentialDelay = 0L + val linearDelays = mutableListOf() + val quadraticDelays = mutableListOf() + val exponentialDelays = mutableListOf() for (retry in 1..5) { - linearDelay = linear.nextBackOffDelayFunction(retry, linearDelay) - quadraticDelay = quadratic.nextBackOffDelayFunction(retry, quadraticDelay) - exponentialDelay = exponential.nextBackOffDelayFunction(retry, exponentialDelay) + linearDelays += linear.nextBackOffDelayFunction(retry, linearDelays.lastOrNull() ?: 0) + quadraticDelays += + quadratic.nextBackOffDelayFunction(retry, quadraticDelays.lastOrNull() ?: 0) + exponentialDelays += + exponential.nextBackOffDelayFunction(retry, exponentialDelays.lastOrNull() ?: 0) } - assertEquals(500, linearDelay) - assertEquals(1500, quadraticDelay) - assertEquals(1600, exponentialDelay) + assertEquals(listOf(100L, 200L, 300L, 400L, 500L), linearDelays) + assertEquals(listOf(100L, 300L, 600L, 1000L, 1500L), quadraticDelays) + assertEquals(listOf(100L, 200L, 400L, 800L, 1600L), exponentialDelays) + + // Quadratic is the steeper curve over retries 2..4 and only loses to doubling at retry 5. + // The default maxRetries = 5 stops the processor before it computes retry 5, so callers + // on defaults always see the quadratic delays as the longer ones. + for (retry in 2..4) { + assertTrue(quadraticDelays[retry - 1] > exponentialDelays[retry - 1]) + } + assertTrue(exponentialDelays[4] > quadraticDelays[4]) + assertTrue(quadraticDelays.drop(1).zip(linearDelays.drop(1)).all { it.first > it.second }) } @Test @@ -203,7 +214,10 @@ class StreamRetryPolicyTest { val policy = StreamRetryPolicy.quadratic(backoffStepMillis = 1000, maxBackoffMillis = 30_000) - assertEquals(30_000, policy.nextBackOffDelayFunction(1000, 30_000)) + // From a zero previous delay, so this pins growth into the cap at an extreme retry index + // rather than restating that the cap is a fixed point. + assertEquals(30_000, policy.nextBackOffDelayFunction(1000, 0)) + assertEquals(30_000, policy.nextBackOffDelayFunction(Int.MAX_VALUE, 0)) } @Test