Skip to content

Commit 7cbf5e7

Browse files
nhachichastIncMale
authored andcommitted
JAVA-5950 Update Transactions Convenient API with exponential backoff on retries (#1899)
1 parent 974a9a5 commit 7cbf5e7

16 files changed

Lines changed: 484 additions & 124 deletions

File tree

driver-benchmarks/src/main/com/mongodb/benchmark/benchmarks/RawBsonArrayEncodingBenchmark.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717

1818
package com.mongodb.benchmark.benchmarks;
1919

20-
import org.bson.BsonArray;import org.bson.BsonDocument;
20+
import org.bson.BsonArray;
21+
import org.bson.BsonDocument;
2122
import org.bson.RawBsonDocument;
2223
import org.bson.codecs.BsonDocumentCodec;
2324

@@ -52,4 +53,4 @@ public void setUp() throws IOException {
5253
public int getBytesPerRun() {
5354
return documentBytes.length * NUM_INTERNAL_ITERATIONS;
5455
}
55-
}
56+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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;
18+
19+
import com.mongodb.lang.Nullable;
20+
21+
/**
22+
* An exception indicating that the convenient transactions API ({@code withTransaction})
23+
* exceeded its overall timeout while retrying the user-supplied callback or the commit loop.
24+
* The last encountered error (if any) is attached as the
25+
* {@linkplain Throwable#getCause() cause}.
26+
*
27+
* @since 5.7
28+
* @mongodb.driver.manual core/transactions-in-applications/#callback-api withTransaction
29+
*/
30+
public final class WithTransactionTimeoutException extends MongoClientException {
31+
32+
private static final long serialVersionUID = 1L;
33+
34+
/**
35+
* Construct a new instance
36+
* @param message the message
37+
* @param cause the cause
38+
* @since 5.7
39+
*/
40+
public WithTransactionTimeoutException(final String message, @Nullable final Throwable cause) {
41+
super(message, cause);
42+
}
43+
}

driver-core/src/main/com/mongodb/internal/TimeoutContext.java

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,6 @@ public TimeoutContext(final TimeoutSettings timeoutSettings) {
113113
this(false, timeoutSettings, startTimeout(timeoutSettings.getTimeoutMS()));
114114
}
115115

116-
private TimeoutContext(final TimeoutSettings timeoutSettings, @Nullable final Timeout timeout) {
117-
this(false, timeoutSettings, timeout);
118-
}
119-
120116
private TimeoutContext(final boolean isMaintenanceContext,
121117
final TimeoutSettings timeoutSettings,
122118
@Nullable final Timeout timeout) {
@@ -181,6 +177,7 @@ public Timeout timeoutIncludingRoundTrip() {
181177
* @param alternativeTimeoutMS the alternative timeout.
182178
* @return timeout to use.
183179
*/
180+
@VisibleForTesting(otherwise = PRIVATE)
184181
public long timeoutOrAlternative(final long alternativeTimeoutMS) {
185182
if (timeout == null) {
186183
return alternativeTimeoutMS;
@@ -389,11 +386,6 @@ public TimeoutContext withAdditionalReadTimeout(final int additionalReadTimeout)
389386
return new TimeoutContext(timeoutSettings.withReadTimeoutMS(newReadTimeout > 0 ? newReadTimeout : Long.MAX_VALUE));
390387
}
391388

392-
// Creates a copy of the timeout context that can be reset without resetting the original.
393-
public TimeoutContext copyTimeoutContext() {
394-
return new TimeoutContext(getTimeoutSettings(), getTimeout());
395-
}
396-
397389
@Override
398390
public String toString() {
399391
return "TimeoutContext{"

driver-core/src/main/com/mongodb/internal/connection/Time.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@
2020
* To enable unit testing of classes that rely on System.nanoTime
2121
*
2222
* <p>This class is not part of the public API and may be removed or changed at any time</p>
23+
*
24+
* @deprecated Use {@link com.mongodb.internal.time.SystemNanoTime} in production code,
25+
* and {@code Mockito.mockStatic} in test code to tamper with it.
2326
*/
27+
@Deprecated
2428
public final class Time {
2529
static final long CONSTANT_TIME = 42;
2630

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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+
private static final double TRANSACTION_BASE_MS = 5.0;
33+
@VisibleForTesting(otherwise = PRIVATE)
34+
static final double TRANSACTION_MAX_MS = 500.0;
35+
private static final double TRANSACTION_GROWTH = 1.5;
36+
37+
// TODO-JAVA-6079
38+
private static DoubleSupplier testJitterSupplier = null;
39+
40+
private ExponentialBackoff() {
41+
}
42+
43+
/**
44+
* Calculate the backoff in milliseconds for transaction retries.
45+
*
46+
* @param attemptNumber attempt number > 0
47+
* @return The calculated backoff in milliseconds.
48+
*/
49+
public static long calculateTransactionBackoffMs(final int attemptNumber) {
50+
assertTrue(attemptNumber > 0, "Attempt number must be at least 1 (1-based) in the context of transaction backoff calculation");
51+
double jitter = testJitterSupplier != null
52+
? testJitterSupplier.getAsDouble()
53+
: ThreadLocalRandom.current().nextDouble();
54+
return Math.round(jitter * Math.min(
55+
TRANSACTION_BASE_MS * Math.pow(TRANSACTION_GROWTH, attemptNumber - 1),
56+
TRANSACTION_MAX_MS));
57+
}
58+
59+
/**
60+
* Set a custom jitter supplier for testing purposes.
61+
*
62+
* @param supplier A DoubleSupplier that returns values in [0, 1] range.
63+
*/
64+
@VisibleForTesting(otherwise = PRIVATE)
65+
public static void setTestJitterSupplier(final DoubleSupplier supplier) {
66+
testJitterSupplier = supplier;
67+
}
68+
69+
/**
70+
* Clear the test jitter supplier, reverting to default ThreadLocalRandom behavior.
71+
*/
72+
@VisibleForTesting(otherwise = PRIVATE)
73+
public static void clearTestJitterSupplier() {
74+
testJitterSupplier = null;
75+
}
76+
}

driver-core/src/main/com/mongodb/internal/time/StartTime.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,6 @@ public interface StartTime {
5959
* @return a StartPoint, as of now
6060
*/
6161
static StartTime now() {
62-
return TimePoint.at(System.nanoTime());
62+
return TimePoint.at(SystemNanoTime.get());
6363
}
6464
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
package com.mongodb.internal.time;
17+
18+
/**
19+
* Avoid using this class directly and prefer using other program elements from {@link com.mongodb.internal.time}, if possible.
20+
* <p>
21+
* We do not use {@link System#nanoTime()} directly in the rest of the {@link com.mongodb.internal.time} package,
22+
* and use {@link SystemNanoTime#get()} instead because we need to tamper with it via {@code Mockito.mockStatic},
23+
* and mocking methods of {@link System} class is both impossible and unwise.
24+
*/
25+
public final class SystemNanoTime {
26+
private SystemNanoTime() {
27+
}
28+
29+
public static long get() {
30+
return System.nanoTime();
31+
}
32+
}

driver-core/src/main/com/mongodb/internal/time/TimePoint.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,14 @@ static TimePoint at(@Nullable final Long nanos) {
6262

6363
@VisibleForTesting(otherwise = PRIVATE)
6464
long currentNanos() {
65-
return System.nanoTime();
65+
return SystemNanoTime.get();
6666
}
6767

6868
/**
6969
* Returns the current {@link TimePoint}.
7070
*/
7171
static TimePoint now() {
72-
return at(System.nanoTime());
72+
return at(SystemNanoTime.get());
7373
}
7474

7575
/**
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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) jitter}.
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+
for (int attemptNumber = 1; attemptNumber <= EXPECTED_BACKOFFS_MAX_VALUES.length; attemptNumber++) {
37+
long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
38+
long expectedBackoff = Math.round(EXPECTED_BACKOFFS_MAX_VALUES[attemptNumber - 1]);
39+
assertTrue(backoff >= 0 && backoff <= expectedBackoff,
40+
String.format("Attempt %d: backoff should be between 0 ms and %d ms, got: %d", attemptNumber,
41+
expectedBackoff, backoff));
42+
}
43+
}
44+
45+
@Test
46+
void testCalculateTransactionBackoffMsRespectsMaximum() {
47+
for (int attemptNumber = 1; attemptNumber < EXPECTED_BACKOFFS_MAX_VALUES.length * 2; attemptNumber++) {
48+
long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
49+
assertTrue(backoff >= 0 && backoff <= ExponentialBackoff.TRANSACTION_MAX_MS,
50+
String.format("Attempt %d: backoff should be capped at %f ms, got: %d ms",
51+
attemptNumber, ExponentialBackoff.TRANSACTION_MAX_MS, backoff));
52+
}
53+
}
54+
55+
@Test
56+
void testCalculateTransactionBackoffMsWithJitterOne() {
57+
ExponentialBackoff.setTestJitterSupplier(() -> 1.0);
58+
try {
59+
for (int attemptNumber = 1; attemptNumber <= EXPECTED_BACKOFFS_MAX_VALUES.length; attemptNumber++) {
60+
long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
61+
long expected = Math.round(EXPECTED_BACKOFFS_MAX_VALUES[attemptNumber - 1]);
62+
assertEquals(expected, backoff,
63+
String.format("Attempt %d: with jitter=1.0, backoff should be %d ms", attemptNumber, expected));
64+
}
65+
} finally {
66+
ExponentialBackoff.clearTestJitterSupplier();
67+
}
68+
}
69+
70+
@Test
71+
void testCalculateTransactionBackoffMsWithJitterZero() {
72+
ExponentialBackoff.setTestJitterSupplier(() -> 0.0);
73+
try {
74+
for (int attemptNumber = 1; attemptNumber <= EXPECTED_BACKOFFS_MAX_VALUES.length; attemptNumber++) {
75+
long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
76+
assertEquals(0, backoff, String.format("Attempt %d: with jitter=0, backoff should always be 0 ms", attemptNumber));
77+
}
78+
} finally {
79+
ExponentialBackoff.clearTestJitterSupplier();
80+
}
81+
}
82+
}

driver-scala/src/main/scala/org/mongodb/scala/package.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,14 @@ package object scala extends ClientSessionImplicits with ObservableImplicits wit
407407
*/
408408
type MongoOperationTimeoutException = com.mongodb.MongoOperationTimeoutException
409409

410+
/**
411+
* An exception indicating that the convenient transactions API (`withTransaction`) exceeded its overall timeout
412+
* while retrying the user-supplied callback or the commit loop.
413+
*
414+
* @since 5.7
415+
*/
416+
type WithTransactionTimeoutException = com.mongodb.WithTransactionTimeoutException
417+
410418
/**
411419
* An exception indicating a failure to apply the write concern to the requested write operation
412420
*

0 commit comments

Comments
 (0)