Skip to content

Commit 988e276

Browse files
committed
TMP Add ExponentialBackoff
I manually copied it from mongodb#1899.
1 parent b33dca9 commit 988e276

File tree

2 files changed

+173
-0
lines changed

2 files changed

+173
-0
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright 2008-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.mongodb.internal.time;
18+
19+
import com.mongodb.internal.VisibleForTesting;
20+
21+
import java.util.concurrent.ThreadLocalRandom;
22+
import java.util.function.DoubleSupplier;
23+
24+
import static com.mongodb.assertions.Assertions.assertTrue;
25+
import static com.mongodb.internal.VisibleForTesting.AccessModifier.PRIVATE;
26+
27+
/**
28+
* Provides exponential backoff calculations with jitter for retry scenarios.
29+
*/
30+
public final class ExponentialBackoff {
31+
32+
// Constants for transaction retry backoff
33+
@VisibleForTesting(otherwise = PRIVATE)
34+
static final double TRANSACTION_BASE_MS = 5.0;
35+
@VisibleForTesting(otherwise = PRIVATE)
36+
static final double TRANSACTION_MAX_MS = 500.0;
37+
@VisibleForTesting(otherwise = PRIVATE)
38+
static final double TRANSACTION_GROWTH = 1.5;
39+
40+
// TODO-JAVA-6079
41+
private static DoubleSupplier testJitterSupplier = null;
42+
43+
private ExponentialBackoff() {
44+
}
45+
46+
/**
47+
* Calculate the backoff in milliseconds for transaction retries.
48+
*
49+
* @param attemptNumber 0-based attempt number
50+
* @return The calculated backoff in milliseconds.
51+
*/
52+
public static long calculateTransactionBackoffMs(final int attemptNumber) {
53+
return calculateBackoffMs(TRANSACTION_BASE_MS, TRANSACTION_MAX_MS, TRANSACTION_GROWTH, attemptNumber);
54+
}
55+
56+
public static long calculateRetryBackoffMs(final int attemptNumber) {
57+
return calculateBackoffMs(100, 10000, 2, attemptNumber);
58+
}
59+
60+
private static long calculateBackoffMs(final double baseMs, final double maxMs, final double growth, final int attemptNumber) {
61+
assertTrue(attemptNumber > 0, "Attempt number must be greater than 0 in the context of transaction backoff calculation");
62+
double jitter = testJitterSupplier != null
63+
? testJitterSupplier.getAsDouble()
64+
: ThreadLocalRandom.current().nextDouble();
65+
return Math.round(jitter * Math.min(
66+
baseMs * Math.pow(growth, attemptNumber - 1),
67+
maxMs));
68+
}
69+
70+
/**
71+
* Set a custom jitter supplier for testing purposes.
72+
*
73+
* @param supplier A DoubleSupplier that returns values in [0, 1) range.
74+
*/
75+
@VisibleForTesting(otherwise = PRIVATE)
76+
public static void setTestJitterSupplier(final DoubleSupplier supplier) {
77+
testJitterSupplier = supplier;
78+
}
79+
80+
/**
81+
* Clear the test jitter supplier, reverting to default ThreadLocalRandom behavior.
82+
*/
83+
@VisibleForTesting(otherwise = PRIVATE)
84+
public static void clearTestJitterSupplier() {
85+
testJitterSupplier = null;
86+
}
87+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2008-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.mongodb.internal.time;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import java.util.function.DoubleSupplier;
22+
23+
import static org.junit.jupiter.api.Assertions.assertEquals;
24+
import static org.junit.jupiter.api.Assertions.assertTrue;
25+
26+
class ExponentialBackoffTest {
27+
/**
28+
* Expected {@linkplain ExponentialBackoff#calculateTransactionBackoffMs(int) backoffs} with 1.0 as
29+
* {@link ExponentialBackoff#setTestJitterSupplier(DoubleSupplier) jiter}.
30+
*/
31+
private static final double[] EXPECTED_BACKOFFS_MAX_VALUES = {5.0, 7.5, 11.25, 16.875, 25.3125, 37.96875, 56.953125, 85.4296875, 128.14453125,
32+
192.21679688, 288.32519531, 432.48779297, 500.0};
33+
34+
@Test
35+
void testCalculateTransactionBackoffMs() {
36+
// Test that the backoff sequence follows the expected pattern with growth factor ExponentialBackoff.TRANSACTION_GROWTH
37+
// Expected sequence (without jitter): 5, 7.5, 11.25, ...
38+
// With jitter, actual values will be between 0 and these maxima
39+
40+
for (int attemptNumber = 1; attemptNumber <= EXPECTED_BACKOFFS_MAX_VALUES.length; attemptNumber++) {
41+
long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
42+
long expectedBackoff = Math.round(EXPECTED_BACKOFFS_MAX_VALUES[attemptNumber - 1]);
43+
assertTrue(backoff >= 0 && backoff <= expectedBackoff,
44+
String.format("Attempt %d: backoff should be between 0 ms and %d ms, got: %d", attemptNumber,
45+
expectedBackoff, backoff));
46+
}
47+
}
48+
49+
@Test
50+
void testCalculateTransactionBackoffMsRespectsMaximum() {
51+
// Even at high attempt numbers, backoff should never exceed ExponentialBackoff.TRANSACTION_MAX_MS
52+
for (int attemptNumber = 1; attemptNumber < EXPECTED_BACKOFFS_MAX_VALUES.length * 2; attemptNumber++) {
53+
long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
54+
assertTrue(backoff >= 0 && backoff <= ExponentialBackoff.TRANSACTION_MAX_MS,
55+
String.format("Attempt %d: backoff should be capped at %f ms, got: %d ms",
56+
attemptNumber, ExponentialBackoff.TRANSACTION_MAX_MS, backoff));
57+
}
58+
}
59+
60+
@Test
61+
void testCustomJitter() {
62+
// Test with jitter = 1.0
63+
ExponentialBackoff.setTestJitterSupplier(() -> 1.0);
64+
try {
65+
for (int attemptNumber = 1; attemptNumber <= EXPECTED_BACKOFFS_MAX_VALUES.length; attemptNumber++) {
66+
long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
67+
long expected = Math.round(EXPECTED_BACKOFFS_MAX_VALUES[attemptNumber - 1]);
68+
assertEquals(expected, backoff,
69+
String.format("Attempt %d: with jitter=1.0, backoff should be %d ms", attemptNumber, expected));
70+
}
71+
} finally {
72+
ExponentialBackoff.clearTestJitterSupplier();
73+
}
74+
75+
// Test with jitter = 0, all backoffs should be 0
76+
ExponentialBackoff.setTestJitterSupplier(() -> 0.0);
77+
try {
78+
for (int attemptNumber = 1; attemptNumber < EXPECTED_BACKOFFS_MAX_VALUES.length; attemptNumber++) {
79+
long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
80+
assertEquals(0, backoff, "With jitter=0, backoff should always be 0 ms");
81+
}
82+
} finally {
83+
ExponentialBackoff.clearTestJitterSupplier();
84+
}
85+
}
86+
}

0 commit comments

Comments
 (0)