From 7b279402aff6abe0eddd38153f81f45e6731122e Mon Sep 17 00:00:00 2001
From: Almas Abdrazak <20584185+strogiyotec@users.noreply.github.com>
Date: Fri, 16 Jan 2026 19:27:20 +0000
Subject: [PATCH 01/72] Version: bump 5.7.0-beta0
---
gradle.properties | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gradle.properties b/gradle.properties
index 00024442054..3c6c3297aaa 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -14,7 +14,7 @@
# limitations under the License.
#
-version=5.7.0-SNAPSHOT
+version=5.7.0-beta0
org.gradle.daemon=true
org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Duser.country=US -Duser.language=en
From 109410230f7d255aa160b36335358adc8662e3fe Mon Sep 17 00:00:00 2001
From: Almas Abdrazak <20584185+strogiyotec@users.noreply.github.com>
Date: Fri, 16 Jan 2026 19:27:20 +0000
Subject: [PATCH 02/72] Version: bump 5.7.0-SNAPSHOT
---
gradle.properties | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gradle.properties b/gradle.properties
index 3c6c3297aaa..00024442054 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -14,7 +14,7 @@
# limitations under the License.
#
-version=5.7.0-beta0
+version=5.7.0-SNAPSHOT
org.gradle.daemon=true
org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Duser.country=US -Duser.language=en
From 024e420631ec1f7ff514c1a93af01bd9a906bc34 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Wed, 3 Dec 2025 17:39:43 +0000
Subject: [PATCH 03/72] JAVA-5950 - Update Transactions Convenient API with
exponential backoff on retries
---
.../mongodb/internal/ExponentialBackoff.java | 174 +++++++++++++++
.../internal/ExponentialBackoffTest.java | 205 ++++++++++++++++++
.../client/internal/ClientSessionImpl.java | 38 +++-
.../client/WithTransactionProseTest.java | 39 ++++
4 files changed, 454 insertions(+), 2 deletions(-)
create mode 100644 driver-core/src/main/com/mongodb/internal/ExponentialBackoff.java
create mode 100644 driver-core/src/test/unit/com/mongodb/internal/ExponentialBackoffTest.java
diff --git a/driver-core/src/main/com/mongodb/internal/ExponentialBackoff.java b/driver-core/src/main/com/mongodb/internal/ExponentialBackoff.java
new file mode 100644
index 00000000000..518286319ad
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/internal/ExponentialBackoff.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2008-present MongoDB, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.mongodb.internal;
+
+import com.mongodb.annotations.NotThreadSafe;
+
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * Implements exponential backoff with jitter for retry scenarios.
+ * Formula: delayMS = jitter * min(maxBackoffMs, baseBackoffMs * growthFactor^retryCount)
+ * where jitter is random value [0, 1).
+ *
+ * This class provides factory methods for common use cases:
+ *
+ * - {@link #forTransactionRetry()} - For withTransaction retries (5ms base, 500ms max, 1.5 growth)
+ * - {@link #forCommandRetry()} - For command retries with overload (100ms base, 10000ms max, 2.0 growth)
+ *
+ */
+@NotThreadSafe
+public final class ExponentialBackoff {
+ // Transaction retry constants (per spec)
+ private static final double TRANSACTION_BASE_BACKOFF_MS = 5.0;
+ private static final double TRANSACTION_MAX_BACKOFF_MS = 500.0;
+ private static final double TRANSACTION_BACKOFF_GROWTH = 1.5;
+
+ // Command retry constants (per spec)
+ private static final double COMMAND_BASE_BACKOFF_MS = 100.0;
+ private static final double COMMAND_MAX_BACKOFF_MS = 10000.0;
+ private static final double COMMAND_BACKOFF_GROWTH = 2.0;
+
+ private final double baseBackoffMs;
+ private final double maxBackoffMs;
+ private final double growthFactor;
+ private int retryCount = 0;
+
+ /**
+ * Creates an exponential backoff instance with specified parameters.
+ *
+ * @param baseBackoffMs Initial backoff in milliseconds
+ * @param maxBackoffMs Maximum backoff cap in milliseconds
+ * @param growthFactor Exponential growth factor (e.g., 1.5 or 2.0)
+ */
+ public ExponentialBackoff(final double baseBackoffMs, final double maxBackoffMs, final double growthFactor) {
+ this.baseBackoffMs = baseBackoffMs;
+ this.maxBackoffMs = maxBackoffMs;
+ this.growthFactor = growthFactor;
+ }
+
+ /**
+ * Creates a backoff instance configured for withTransaction retries.
+ * Uses: 5ms base, 500ms max, 1.5 growth factor.
+ *
+ * @return ExponentialBackoff configured for transaction retries
+ */
+ public static ExponentialBackoff forTransactionRetry() {
+ return new ExponentialBackoff(
+ TRANSACTION_BASE_BACKOFF_MS,
+ TRANSACTION_MAX_BACKOFF_MS,
+ TRANSACTION_BACKOFF_GROWTH
+ );
+ }
+
+ /**
+ * Creates a backoff instance configured for command retries during overload.
+ * Uses: 100ms base, 10000ms max, 2.0 growth factor.
+ *
+ * @return ExponentialBackoff configured for command retries
+ */
+ public static ExponentialBackoff forCommandRetry() {
+ return new ExponentialBackoff(
+ COMMAND_BASE_BACKOFF_MS,
+ COMMAND_MAX_BACKOFF_MS,
+ COMMAND_BACKOFF_GROWTH
+ );
+ }
+
+ /**
+ * Calculate next backoff delay with jitter.
+ *
+ * @return delay in milliseconds
+ */
+ public long calculateDelayMs() {
+ double jitter = ThreadLocalRandom.current().nextDouble();
+ double exponentialBackoff = baseBackoffMs * Math.pow(growthFactor, retryCount);
+ double cappedBackoff = Math.min(exponentialBackoff, maxBackoffMs);
+ retryCount++;
+ return Math.round(jitter * cappedBackoff);
+ }
+
+ /**
+ * Apply backoff delay by sleeping current thread.
+ *
+ * @throws InterruptedException if thread is interrupted during sleep
+ */
+ public void applyBackoff() throws InterruptedException {
+ long delayMs = calculateDelayMs();
+ if (delayMs > 0) {
+ Thread.sleep(delayMs);
+ }
+ }
+
+ /**
+ * Check if applying backoff would exceed the retry time limit.
+ * @param startTimeMs start time of retry attempts
+ * @param maxRetryTimeMs maximum retry time allowed
+ * @return true if backoff would exceed limit, false otherwise
+ */
+// public boolean wouldExceedTimeLimit(final long startTimeMs, final long maxRetryTimeMs) {
+// long elapsedMs = ClientSessionClock.INSTANCE.now() - startTimeMs;
+// // Peek at next delay without incrementing counter
+// double exponentialBackoff = baseBackoffMs * Math.pow(growthFactor, retryCount);
+// double cappedBackoff = Math.min(exponentialBackoff, maxBackoffMs);
+// long maxPossibleDelay = Math.round(cappedBackoff); // worst case with jitter=1
+// return elapsedMs + maxPossibleDelay > maxRetryTimeMs;
+// }
+
+ /**
+ * Reset retry counter for new sequence of retries.
+ */
+ public void reset() {
+ retryCount = 0;
+ }
+
+ /**
+ * Get current retry count for testing.
+ *
+ * @return current retry count
+ */
+ public int getRetryCount() {
+ return retryCount;
+ }
+
+ /**
+ * Get the base backoff in milliseconds.
+ *
+ * @return base backoff
+ */
+ public double getBaseBackoffMs() {
+ return baseBackoffMs;
+ }
+
+ /**
+ * Get the maximum backoff in milliseconds.
+ *
+ * @return maximum backoff
+ */
+ public double getMaxBackoffMs() {
+ return maxBackoffMs;
+ }
+
+ /**
+ * Get the growth factor.
+ *
+ * @return growth factor
+ */
+ public double getGrowthFactor() {
+ return growthFactor;
+ }
+}
diff --git a/driver-core/src/test/unit/com/mongodb/internal/ExponentialBackoffTest.java b/driver-core/src/test/unit/com/mongodb/internal/ExponentialBackoffTest.java
new file mode 100644
index 00000000000..bfee96e67fb
--- /dev/null
+++ b/driver-core/src/test/unit/com/mongodb/internal/ExponentialBackoffTest.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2008-present MongoDB, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.mongodb.internal;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ExponentialBackoffTest {
+
+ @Test
+ void testTransactionRetryBackoff() {
+ ExponentialBackoff backoff = ExponentialBackoff.forTransactionRetry();
+
+ // Verify configuration
+ assertEquals(5.0, backoff.getBaseBackoffMs());
+ assertEquals(500.0, backoff.getMaxBackoffMs());
+ assertEquals(1.5, backoff.getGrowthFactor());
+
+ // First retry (i=0): delay = jitter * min(5 * 1.5^0, 500) = jitter * 5
+ // Since jitter is random [0,1), the delay should be between 0 and 5ms
+ long delay1 = backoff.calculateDelayMs();
+ assertTrue(delay1 >= 0 && delay1 <= 5, "First delay should be 0-5ms, got: " + delay1);
+
+ // Second retry (i=1): delay = jitter * min(5 * 1.5^1, 500) = jitter * 7.5
+ long delay2 = backoff.calculateDelayMs();
+ assertTrue(delay2 >= 0 && delay2 <= 8, "Second delay should be 0-8ms, got: " + delay2);
+
+ // Third retry (i=2): delay = jitter * min(5 * 1.5^2, 500) = jitter * 11.25
+ long delay3 = backoff.calculateDelayMs();
+ assertTrue(delay3 >= 0 && delay3 <= 12, "Third delay should be 0-12ms, got: " + delay3);
+
+ // Verify the retry count is incrementing properly
+ assertEquals(3, backoff.getRetryCount());
+ }
+
+ @Test
+ void testTransactionRetryBackoffRespectsMaximum() {
+ ExponentialBackoff backoff = ExponentialBackoff.forTransactionRetry();
+
+ // Advance to a high retry count where backoff would exceed 500ms without capping
+ for (int i = 0; i < 20; i++) {
+ backoff.calculateDelayMs();
+ }
+
+ // Even at high retry counts, delay should never exceed 500ms
+ for (int i = 0; i < 5; i++) {
+ long delay = backoff.calculateDelayMs();
+ assertTrue(delay >= 0 && delay <= 500, "Delay should be capped at 500ms, got: " + delay);
+ }
+ }
+
+ @Test
+ void testTransactionRetryBackoffSequenceWithExpectedValues() {
+ // Test that the backoff sequence follows the expected pattern with growth factor 1.5
+ // Expected sequence (without jitter): 5, 7.5, 11.25, 16.875, 25.3125, 37.96875, 56.953125, ...
+ // With jitter, actual values will be between 0 and these maxima
+
+ ExponentialBackoff backoff = ExponentialBackoff.forTransactionRetry();
+
+ double[] expectedMaxValues = {5.0, 7.5, 11.25, 16.875, 25.3125, 37.96875, 56.953125, 85.4296875,
+ 128.14453125, 192.21679688, 288.32519531, 432.48779297, 500.0};
+
+ for (int i = 0; i < expectedMaxValues.length; i++) {
+ long delay = backoff.calculateDelayMs();
+ assertTrue(delay >= 0 && delay <= Math.round(expectedMaxValues[i]),
+ String.format("Retry %d: delay should be 0-%d ms, got: %d", i, Math.round(expectedMaxValues[i]), delay));
+ }
+ }
+
+ @Test
+ void testCommandRetryBackoff() {
+ ExponentialBackoff backoff = ExponentialBackoff.forCommandRetry();
+
+ // Verify configuration
+ assertEquals(100.0, backoff.getBaseBackoffMs());
+ assertEquals(10000.0, backoff.getMaxBackoffMs());
+ assertEquals(2.0, backoff.getGrowthFactor());
+
+ // Test sequence with growth factor 2.0
+ // Expected max delays: 100, 200, 400, 800, 1600, 3200, 6400, 10000 (capped)
+ long delay1 = backoff.calculateDelayMs();
+ assertTrue(delay1 >= 0 && delay1 <= 100, "First delay should be 0-100ms, got: " + delay1);
+
+ long delay2 = backoff.calculateDelayMs();
+ assertTrue(delay2 >= 0 && delay2 <= 200, "Second delay should be 0-200ms, got: " + delay2);
+
+ long delay3 = backoff.calculateDelayMs();
+ assertTrue(delay3 >= 0 && delay3 <= 400, "Third delay should be 0-400ms, got: " + delay3);
+
+ long delay4 = backoff.calculateDelayMs();
+ assertTrue(delay4 >= 0 && delay4 <= 800, "Fourth delay should be 0-800ms, got: " + delay4);
+
+ long delay5 = backoff.calculateDelayMs();
+ assertTrue(delay5 >= 0 && delay5 <= 1600, "Fifth delay should be 0-1600ms, got: " + delay5);
+ }
+
+ @Test
+ void testCommandRetryBackoffRespectsMaximum() {
+ ExponentialBackoff backoff = ExponentialBackoff.forCommandRetry();
+
+ // Advance to where exponential would exceed 10000ms
+ for (int i = 0; i < 10; i++) {
+ backoff.calculateDelayMs();
+ }
+
+ // Even at high retry counts, delay should never exceed 10000ms
+ for (int i = 0; i < 5; i++) {
+ long delay = backoff.calculateDelayMs();
+ assertTrue(delay >= 0 && delay <= 10000, "Delay should be capped at 10000ms, got: " + delay);
+ }
+ }
+
+ @Test
+ void testCustomBackoff() {
+ // Test with custom parameters
+ ExponentialBackoff backoff = new ExponentialBackoff(50.0, 2000.0, 1.8);
+
+ assertEquals(50.0, backoff.getBaseBackoffMs());
+ assertEquals(2000.0, backoff.getMaxBackoffMs());
+ assertEquals(1.8, backoff.getGrowthFactor());
+
+ // First delay: 0-50ms
+ long delay1 = backoff.calculateDelayMs();
+ assertTrue(delay1 >= 0 && delay1 <= 50, "First delay should be 0-50ms, got: " + delay1);
+
+ // Second delay: 0-90ms (50 * 1.8)
+ long delay2 = backoff.calculateDelayMs();
+ assertTrue(delay2 >= 0 && delay2 <= 90, "Second delay should be 0-90ms, got: " + delay2);
+ }
+
+ @Test
+ void testReset() {
+ ExponentialBackoff backoff = ExponentialBackoff.forTransactionRetry();
+
+ // Perform some retries
+ backoff.calculateDelayMs();
+ backoff.calculateDelayMs();
+ assertEquals(2, backoff.getRetryCount());
+
+ // Reset and verify counter is back to 0
+ backoff.reset();
+ assertEquals(0, backoff.getRetryCount());
+
+ // First delay after reset should be in the initial range again
+ long delay = backoff.calculateDelayMs();
+ assertTrue(delay >= 0 && delay <= 5, "First delay after reset should be 0-5ms, got: " + delay);
+ }
+
+// @Test
+// void testWouldExceedTimeLimitTransactionRetry() {
+// ExponentialBackoff backoff = ExponentialBackoff.forTransactionRetry();
+// long startTime = ClientSessionClock.INSTANCE.now();
+//
+// // Initially, should not exceed time limit
+// assertFalse(backoff.wouldExceedTimeLimit(startTime, 120000));
+//
+// // With very little time remaining (4ms), first backoff (up to 5ms) would exceed
+// long nearLimitTime = startTime - 119996; // 4ms remaining
+// assertTrue(backoff.wouldExceedTimeLimit(nearLimitTime, 120000));
+// }
+
+// @Test
+// void testWouldExceedTimeLimitCommandRetry() {
+// ExponentialBackoff backoff = ExponentialBackoff.forCommandRetry();
+// long startTime = ClientSessionClock.INSTANCE.now();
+//
+// // Initially, should not exceed time limit
+// assertFalse(backoff.wouldExceedTimeLimit(startTime, 10000));
+//
+// // With 99ms remaining, first backoff (up to 100ms) would exceed
+// long nearLimitTime = startTime - 9901; // 99ms remaining
+// assertTrue(backoff.wouldExceedTimeLimit(nearLimitTime, 10000));
+// }
+
+ @Test
+ void testCommandRetrySequenceMatchesSpec() {
+ // Test that command retry follows spec: 100ms * 2^i capped at 10000ms
+ ExponentialBackoff backoff = ExponentialBackoff.forCommandRetry();
+
+ double[] expectedMaxValues = {100.0, 200.0, 400.0, 800.0, 1600.0, 3200.0, 6400.0, 10000.0, 10000.0};
+
+ for (int i = 0; i < expectedMaxValues.length; i++) {
+ long delay = backoff.calculateDelayMs();
+ double expectedMax = expectedMaxValues[i];
+ assertTrue(delay >= 0 && delay <= Math.round(expectedMax),
+ String.format("Retry %d: delay should be 0-%d ms, got: %d", i, Math.round(expectedMax), delay));
+ }
+ }
+}
diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
index aa1414dce5d..63e1d68165b 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
@@ -22,12 +22,14 @@
import com.mongodb.MongoExecutionTimeoutException;
import com.mongodb.MongoInternalException;
import com.mongodb.MongoOperationTimeoutException;
+import com.mongodb.MongoTimeoutException;
import com.mongodb.ReadConcern;
import com.mongodb.TransactionOptions;
import com.mongodb.WriteConcern;
import com.mongodb.client.ClientSession;
import com.mongodb.client.TransactionBody;
import com.mongodb.internal.TimeoutContext;
+import com.mongodb.internal.ExponentialBackoff;
import com.mongodb.internal.operation.AbortTransactionOperation;
import com.mongodb.internal.operation.CommitTransactionOperation;
import com.mongodb.internal.operation.OperationHelper;
@@ -251,10 +253,35 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
notNull("transactionBody", transactionBody);
long startTime = ClientSessionClock.INSTANCE.now();
TimeoutContext withTransactionTimeoutContext = createTimeoutContext(options);
+ // Use CSOT timeout if set, otherwise default to MAX_RETRY_TIME_LIMIT_MS
+ Long timeoutMS = withTransactionTimeoutContext.getTimeoutSettings().getTimeoutMS();
+ long maxRetryTimeMS = timeoutMS != null ? timeoutMS : MAX_RETRY_TIME_LIMIT_MS;
+ ExponentialBackoff transactionBackoff = null;
+ boolean isRetry = false;
try {
outer:
while (true) {
+ // Apply exponential backoff before retrying transaction
+ if (isRetry) {
+ // Check if we've exceeded the retry time limit BEFORE applying backoff
+ if (ClientSessionClock.INSTANCE.now() - startTime >= maxRetryTimeMS) {
+ throw withTransactionTimeoutContext.hasTimeoutMS()
+ ? new MongoOperationTimeoutException("Transaction retry exceeded the timeout limit")
+ : new MongoTimeoutException("Transaction retry time limit exceeded");
+
+ }
+ if (transactionBackoff == null) {
+ transactionBackoff = ExponentialBackoff.forTransactionRetry();
+ }
+ try {
+ transactionBackoff.applyBackoff();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new MongoClientException("Transaction retry interrupted", e);
+ }
+ }
+ isRetry = true;
T retVal;
try {
startTransaction(options, withTransactionTimeoutContext.copyTimeoutContext());
@@ -269,7 +296,7 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
if (e instanceof MongoException && !(e instanceof MongoOperationTimeoutException)) {
MongoException exceptionToHandle = OperationHelper.unwrap((MongoException) e);
if (exceptionToHandle.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)
- && ClientSessionClock.INSTANCE.now() - startTime < MAX_RETRY_TIME_LIMIT_MS) {
+ && ClientSessionClock.INSTANCE.now() - startTime < maxRetryTimeMS) {
if (transactionSpan != null) {
transactionSpan.spanFinalizing(false);
}
@@ -280,13 +307,20 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
}
if (transactionState == TransactionState.IN) {
while (true) {
+ // Check if we've exceeded the retry time limit
+ if (ClientSessionClock.INSTANCE.now() - startTime >= maxRetryTimeMS) {
+ throw hasTimeoutMS(withTransactionTimeoutContext)
+ ? new MongoOperationTimeoutException("Transaction commit retry time limit exceeded")
+ : new MongoTimeoutException("Transaction commit retry time limit exceeded");
+ }
+
try {
commitTransaction(false);
break;
} catch (MongoException e) {
clearTransactionContextOnError(e);
if (!(e instanceof MongoOperationTimeoutException)
- && ClientSessionClock.INSTANCE.now() - startTime < MAX_RETRY_TIME_LIMIT_MS) {
+ && ClientSessionClock.INSTANCE.now() - startTime < maxRetryTimeMS) {
applyMajorityWriteConcernToTransactionOptions();
if (!(e instanceof MongoExecutionTimeoutException)
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index 1afbf61565e..406cee6c9a6 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -27,6 +27,7 @@
import org.junit.jupiter.api.Test;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
import static com.mongodb.ClusterFixture.TIMEOUT;
import static com.mongodb.ClusterFixture.isDiscoverableReplicaSet;
@@ -203,6 +204,44 @@ public void testTimeoutMSAndLegacySettings() {
}
}
+ //
+ // Test that exponential backoff is applied when retrying transactions
+ // Backoff uses growth factor of 1.5 as per spec
+ //
+ @Test
+ public void testExponentialBackoffOnTransientError() {
+ // Configure failpoint to simulate transient errors
+ MongoDatabase failPointAdminDb = client.getDatabase("admin");
+ failPointAdminDb.runCommand(
+ Document.parse("{'configureFailPoint': 'failCommand', 'mode': {'times': 3}, "
+ + "'data': {'failCommands': ['insert'], 'errorCode': 112, "
+ + "'errorLabels': ['TransientTransactionError']}}"));
+
+ try (ClientSession session = client.startSession()) {
+ long startTime = System.currentTimeMillis();
+
+ // Track retry count
+ AtomicInteger retryCount = new AtomicInteger(0);
+
+ session.withTransaction(() -> {
+ retryCount.incrementAndGet(); // Count the attempt before the operation that might fail
+ collection.insertOne(session, Document.parse("{ _id : 'backoff-test' }"));
+ return retryCount;
+ });
+
+ long elapsedTime = System.currentTimeMillis() - startTime;
+
+ // With backoff (growth factor 1.5), we expect at least some delay between retries
+ // Expected delays (without jitter): 5ms, 7.5ms, 11.25ms
+ // With jitter, actual delays will be between 0 and these values
+ // 3 retries with backoff should take at least a few milliseconds
+ assertTrue(elapsedTime > 5, "Expected backoff delays to be applied");
+ assertEquals(4, retryCount.get(), "Expected 1 initial attempt + 3 retries");
+ } finally {
+ failPointAdminDb.runCommand(Document.parse("{'configureFailPoint': 'failCommand', 'mode': 'off'}"));
+ }
+ }
+
private boolean canRunTests() {
return isSharded() || isDiscoverableReplicaSet();
}
From 8071de642b327af8689573c535e515ee245befc0 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Tue, 9 Dec 2025 13:27:38 +0000
Subject: [PATCH 04/72] Simplifying test, clean up.
---
.../com/mongodb/client/internal/ClientSessionImpl.java | 1 -
.../com/mongodb/client/WithTransactionProseTest.java | 7 -------
2 files changed, 8 deletions(-)
diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
index 63e1d68165b..855c21e275e 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
@@ -269,7 +269,6 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
throw withTransactionTimeoutContext.hasTimeoutMS()
? new MongoOperationTimeoutException("Transaction retry exceeded the timeout limit")
: new MongoTimeoutException("Transaction retry time limit exceeded");
-
}
if (transactionBackoff == null) {
transactionBackoff = ExponentialBackoff.forTransactionRetry();
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index 406cee6c9a6..01768a43671 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -229,13 +229,6 @@ public void testExponentialBackoffOnTransientError() {
return retryCount;
});
- long elapsedTime = System.currentTimeMillis() - startTime;
-
- // With backoff (growth factor 1.5), we expect at least some delay between retries
- // Expected delays (without jitter): 5ms, 7.5ms, 11.25ms
- // With jitter, actual delays will be between 0 and these values
- // 3 retries with backoff should take at least a few milliseconds
- assertTrue(elapsedTime > 5, "Expected backoff delays to be applied");
assertEquals(4, retryCount.get(), "Expected 1 initial attempt + 3 retries");
} finally {
failPointAdminDb.runCommand(Document.parse("{'configureFailPoint': 'failCommand', 'mode': 'off'}"));
From 0f6e118da4a756a854638c4bed207353f5e28ac2 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Tue, 9 Dec 2025 14:22:57 +0000
Subject: [PATCH 05/72] Fixing test
---
.../com/mongodb/client/internal/ClientSessionImpl.java | 7 -------
.../com/mongodb/client/WithTransactionProseTest.java | 2 +-
2 files changed, 1 insertion(+), 8 deletions(-)
diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
index 855c21e275e..b38b1f9027d 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
@@ -306,13 +306,6 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
}
if (transactionState == TransactionState.IN) {
while (true) {
- // Check if we've exceeded the retry time limit
- if (ClientSessionClock.INSTANCE.now() - startTime >= maxRetryTimeMS) {
- throw hasTimeoutMS(withTransactionTimeoutContext)
- ? new MongoOperationTimeoutException("Transaction commit retry time limit exceeded")
- : new MongoTimeoutException("Transaction commit retry time limit exceeded");
- }
-
try {
commitTransaction(false);
break;
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index 01768a43671..c8a27cce9cc 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -147,7 +147,7 @@ public void testRetryTimeoutEnforcedUnknownTransactionCommit() {
public void testRetryTimeoutEnforcedTransientTransactionErrorOnCommit() {
MongoDatabase failPointAdminDb = client.getDatabase("admin");
failPointAdminDb.runCommand(
- Document.parse("{'configureFailPoint': 'failCommand', 'mode': {'times': 2}, "
+ Document.parse("{'configureFailPoint': 'failCommand', 'mode': {'times': 1}, "
+ "'data': {'failCommands': ['commitTransaction'], 'errorCode': 251, 'codeName': 'NoSuchTransaction', "
+ "'errmsg': 'Transaction 0 has been aborted', 'closeConnection': false}}"));
From 19f296faebe75550060776b0d9b6ae78057a2a0f Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Tue, 9 Dec 2025 15:32:31 +0000
Subject: [PATCH 06/72] Update
driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
.../main/com/mongodb/client/internal/ClientSessionImpl.java | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
index b38b1f9027d..a65d3340886 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
@@ -267,8 +267,8 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
// Check if we've exceeded the retry time limit BEFORE applying backoff
if (ClientSessionClock.INSTANCE.now() - startTime >= maxRetryTimeMS) {
throw withTransactionTimeoutContext.hasTimeoutMS()
- ? new MongoOperationTimeoutException("Transaction retry exceeded the timeout limit")
- : new MongoTimeoutException("Transaction retry time limit exceeded");
+ ? new MongoOperationTimeoutException("Transaction retry time limit of " + maxRetryTimeMS + "ms exceeded")
+ : new MongoTimeoutException("Transaction retry time limit of " + maxRetryTimeMS + "ms exceeded");
}
if (transactionBackoff == null) {
transactionBackoff = ExponentialBackoff.forTransactionRetry();
From 455f3526d12952d55c3cbf577546c13ede3bdbc4 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Tue, 9 Dec 2025 15:43:23 +0000
Subject: [PATCH 07/72] retrigger checks
From 85ec5ef3a5f35830b136f886731113af0ec91b84 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Tue, 9 Dec 2025 17:01:23 +0000
Subject: [PATCH 08/72] retrigger checks
From 5f2157a05a872c20f15f566250f058880baadeef Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Tue, 9 Dec 2025 17:16:33 +0000
Subject: [PATCH 09/72] retrigger checks
From f94d9c78d9e582878e6fb2b8c2bfaebffe20ec2b Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Tue, 9 Dec 2025 17:20:32 +0000
Subject: [PATCH 10/72] retrigger checks
From 8c322c609fd0cd676de18f3c437ca8a6386473fb Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Tue, 9 Dec 2025 17:25:53 +0000
Subject: [PATCH 11/72] test cleanup
---
.../functional/com/mongodb/client/WithTransactionProseTest.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index c8a27cce9cc..78d884ca14c 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -124,7 +124,7 @@ public void testRetryTimeoutEnforcedUnknownTransactionCommit() {
try (ClientSession session = client.startSession()) {
ClientSessionClock.INSTANCE.setTime(START_TIME_MS);
- session.withTransaction((TransactionBody) () -> {
+ session.withTransaction(() -> {
ClientSessionClock.INSTANCE.setTime(ERROR_GENERATING_INTERVAL);
collection.insertOne(session, new Document("_id", 2));
return null;
From 14f339de3bf16556266e3e9b664b588f3112d4c7 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Tue, 9 Dec 2025 22:53:30 +0000
Subject: [PATCH 12/72] retrigger checks
From f593950182db339100f04b4202bd87691d150da2 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Tue, 9 Dec 2025 23:16:34 +0000
Subject: [PATCH 13/72] Test cleanup
---
.../com/mongodb/client/WithTransactionProseTest.java | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index 78d884ca14c..e06f3c82908 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -119,7 +119,7 @@ public void testRetryTimeoutEnforcedTransientTransactionError() {
public void testRetryTimeoutEnforcedUnknownTransactionCommit() {
MongoDatabase failPointAdminDb = client.getDatabase("admin");
failPointAdminDb.runCommand(
- Document.parse("{'configureFailPoint': 'failCommand', 'mode': {'times': 2}, "
+ Document.parse("{'configureFailPoint': 'failCommand', 'mode': {'times': 1}, "
+ "'data': {'failCommands': ['commitTransaction'], 'errorCode': 91, 'closeConnection': false}}"));
try (ClientSession session = client.startSession()) {
@@ -153,7 +153,7 @@ public void testRetryTimeoutEnforcedTransientTransactionErrorOnCommit() {
try (ClientSession session = client.startSession()) {
ClientSessionClock.INSTANCE.setTime(START_TIME_MS);
- session.withTransaction((TransactionBody) () -> {
+ session.withTransaction(() -> {
ClientSessionClock.INSTANCE.setTime(ERROR_GENERATING_INTERVAL);
collection.insertOne(session, Document.parse("{ _id : 1 }"));
return null;
From d12e1f3d53c33833932bef0060997bace626fd5e Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Wed, 10 Dec 2025 00:06:08 +0000
Subject: [PATCH 14/72] retrigger checks
From 26da297dc17ae47dc3c2f94c183893dc29221c1a Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Wed, 10 Dec 2025 14:31:11 +0000
Subject: [PATCH 15/72] Update the implementation according to the spec
---
.../client/internal/ClientSessionImpl.java | 21 ++++++++++++-------
.../client/WithTransactionProseTest.java | 10 ++++-----
2 files changed, 17 insertions(+), 14 deletions(-)
diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
index a65d3340886..59ef120a08b 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
@@ -22,7 +22,6 @@
import com.mongodb.MongoExecutionTimeoutException;
import com.mongodb.MongoInternalException;
import com.mongodb.MongoOperationTimeoutException;
-import com.mongodb.MongoTimeoutException;
import com.mongodb.ReadConcern;
import com.mongodb.TransactionOptions;
import com.mongodb.WriteConcern;
@@ -258,23 +257,27 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
long maxRetryTimeMS = timeoutMS != null ? timeoutMS : MAX_RETRY_TIME_LIMIT_MS;
ExponentialBackoff transactionBackoff = null;
boolean isRetry = false;
+ MongoException lastError = null;
try {
outer:
while (true) {
// Apply exponential backoff before retrying transaction
if (isRetry) {
- // Check if we've exceeded the retry time limit BEFORE applying backoff
- if (ClientSessionClock.INSTANCE.now() - startTime >= maxRetryTimeMS) {
- throw withTransactionTimeoutContext.hasTimeoutMS()
- ? new MongoOperationTimeoutException("Transaction retry time limit of " + maxRetryTimeMS + "ms exceeded")
- : new MongoTimeoutException("Transaction retry time limit of " + maxRetryTimeMS + "ms exceeded");
- }
if (transactionBackoff == null) {
transactionBackoff = ExponentialBackoff.forTransactionRetry();
}
+ // Calculate backoff delay and check if it would exceed timeout
+ long backoffMs = transactionBackoff.calculateDelayMs();
+ if (ClientSessionClock.INSTANCE.now() - startTime + backoffMs >= maxRetryTimeMS) {
+ // Throw the last error as per spec
+ // lastError is always set here since we only retry on MongoException
+ throw lastError;
+ }
try {
- transactionBackoff.applyBackoff();
+ if (backoffMs > 0) {
+ Thread.sleep(backoffMs);
+ }
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new MongoClientException("Transaction retry interrupted", e);
@@ -296,6 +299,7 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
MongoException exceptionToHandle = OperationHelper.unwrap((MongoException) e);
if (exceptionToHandle.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)
&& ClientSessionClock.INSTANCE.now() - startTime < maxRetryTimeMS) {
+ lastError = exceptionToHandle; // Track the last error for timeout scenarios
if (transactionSpan != null) {
transactionSpan.spanFinalizing(false);
}
@@ -310,6 +314,7 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
commitTransaction(false);
break;
} catch (MongoException e) {
+ lastError = e; // Track the last error for timeout scenarios
clearTransactionContextOnError(e);
if (!(e instanceof MongoOperationTimeoutException)
&& ClientSessionClock.INSTANCE.now() - startTime < maxRetryTimeMS) {
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index e06f3c82908..45f28e586fe 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -119,12 +119,12 @@ public void testRetryTimeoutEnforcedTransientTransactionError() {
public void testRetryTimeoutEnforcedUnknownTransactionCommit() {
MongoDatabase failPointAdminDb = client.getDatabase("admin");
failPointAdminDb.runCommand(
- Document.parse("{'configureFailPoint': 'failCommand', 'mode': {'times': 1}, "
+ Document.parse("{'configureFailPoint': 'failCommand', 'mode': {'times': 2}, "
+ "'data': {'failCommands': ['commitTransaction'], 'errorCode': 91, 'closeConnection': false}}"));
try (ClientSession session = client.startSession()) {
ClientSessionClock.INSTANCE.setTime(START_TIME_MS);
- session.withTransaction(() -> {
+ session.withTransaction((TransactionBody) () -> {
ClientSessionClock.INSTANCE.setTime(ERROR_GENERATING_INTERVAL);
collection.insertOne(session, new Document("_id", 2));
return null;
@@ -147,13 +147,13 @@ public void testRetryTimeoutEnforcedUnknownTransactionCommit() {
public void testRetryTimeoutEnforcedTransientTransactionErrorOnCommit() {
MongoDatabase failPointAdminDb = client.getDatabase("admin");
failPointAdminDb.runCommand(
- Document.parse("{'configureFailPoint': 'failCommand', 'mode': {'times': 1}, "
+ Document.parse("{'configureFailPoint': 'failCommand', 'mode': {'times': 2}, "
+ "'data': {'failCommands': ['commitTransaction'], 'errorCode': 251, 'codeName': 'NoSuchTransaction', "
+ "'errmsg': 'Transaction 0 has been aborted', 'closeConnection': false}}"));
try (ClientSession session = client.startSession()) {
ClientSessionClock.INSTANCE.setTime(START_TIME_MS);
- session.withTransaction(() -> {
+ session.withTransaction((TransactionBody) () -> {
ClientSessionClock.INSTANCE.setTime(ERROR_GENERATING_INTERVAL);
collection.insertOne(session, Document.parse("{ _id : 1 }"));
return null;
@@ -218,8 +218,6 @@ public void testExponentialBackoffOnTransientError() {
+ "'errorLabels': ['TransientTransactionError']}}"));
try (ClientSession session = client.startSession()) {
- long startTime = System.currentTimeMillis();
-
// Track retry count
AtomicInteger retryCount = new AtomicInteger(0);
From a1d5bcaa8ca8cc4820b95817ae0ea2c266e1e232 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Wed, 10 Dec 2025 16:28:47 +0000
Subject: [PATCH 16/72] Added prose test
---
.../mongodb/internal/ExponentialBackoff.java | 41 ++---
.../internal/ExponentialBackoffTest.java | 152 ++++++++++++++----
.../client/WithTransactionProseTest.java | 55 +++++++
3 files changed, 196 insertions(+), 52 deletions(-)
diff --git a/driver-core/src/main/com/mongodb/internal/ExponentialBackoff.java b/driver-core/src/main/com/mongodb/internal/ExponentialBackoff.java
index 518286319ad..0a01d38e271 100644
--- a/driver-core/src/main/com/mongodb/internal/ExponentialBackoff.java
+++ b/driver-core/src/main/com/mongodb/internal/ExponentialBackoff.java
@@ -19,6 +19,9 @@
import com.mongodb.annotations.NotThreadSafe;
import java.util.concurrent.ThreadLocalRandom;
+import java.util.function.DoubleSupplier;
+
+import static com.mongodb.internal.VisibleForTesting.AccessModifier.PRIVATE;
/**
* Implements exponential backoff with jitter for retry scenarios.
@@ -48,6 +51,9 @@ public final class ExponentialBackoff {
private final double growthFactor;
private int retryCount = 0;
+ // Test-only jitter supplier - when set, overrides ThreadLocalRandom
+ private static volatile DoubleSupplier testJitterSupplier = null;
+
/**
* Creates an exponential backoff instance with specified parameters.
*
@@ -95,7 +101,10 @@ public static ExponentialBackoff forCommandRetry() {
* @return delay in milliseconds
*/
public long calculateDelayMs() {
- double jitter = ThreadLocalRandom.current().nextDouble();
+ // Use test jitter supplier if set, otherwise use ThreadLocalRandom
+ double jitter = testJitterSupplier != null
+ ? testJitterSupplier.getAsDouble()
+ : ThreadLocalRandom.current().nextDouble();
double exponentialBackoff = baseBackoffMs * Math.pow(growthFactor, retryCount);
double cappedBackoff = Math.min(exponentialBackoff, maxBackoffMs);
retryCount++;
@@ -103,31 +112,23 @@ public long calculateDelayMs() {
}
/**
- * Apply backoff delay by sleeping current thread.
+ * Set a custom jitter supplier for testing purposes.
+ * This overrides the default ThreadLocalRandom jitter generation.
*
- * @throws InterruptedException if thread is interrupted during sleep
+ * @param supplier A DoubleSupplier that returns values in [0, 1) range, or null to use default
*/
- public void applyBackoff() throws InterruptedException {
- long delayMs = calculateDelayMs();
- if (delayMs > 0) {
- Thread.sleep(delayMs);
- }
+ @VisibleForTesting(otherwise = PRIVATE)
+ public static void setTestJitterSupplier(final DoubleSupplier supplier) {
+ testJitterSupplier = supplier;
}
/**
- * Check if applying backoff would exceed the retry time limit.
- * @param startTimeMs start time of retry attempts
- * @param maxRetryTimeMs maximum retry time allowed
- * @return true if backoff would exceed limit, false otherwise
+ * Clear the test jitter supplier, reverting to default ThreadLocalRandom behavior.
*/
-// public boolean wouldExceedTimeLimit(final long startTimeMs, final long maxRetryTimeMs) {
-// long elapsedMs = ClientSessionClock.INSTANCE.now() - startTimeMs;
-// // Peek at next delay without incrementing counter
-// double exponentialBackoff = baseBackoffMs * Math.pow(growthFactor, retryCount);
-// double cappedBackoff = Math.min(exponentialBackoff, maxBackoffMs);
-// long maxPossibleDelay = Math.round(cappedBackoff); // worst case with jitter=1
-// return elapsedMs + maxPossibleDelay > maxRetryTimeMs;
-// }
+ @VisibleForTesting(otherwise = PRIVATE)
+ public static void clearTestJitterSupplier() {
+ testJitterSupplier = null;
+ }
/**
* Reset retry counter for new sequence of retries.
diff --git a/driver-core/src/test/unit/com/mongodb/internal/ExponentialBackoffTest.java b/driver-core/src/test/unit/com/mongodb/internal/ExponentialBackoffTest.java
index bfee96e67fb..84ab56a0e47 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/ExponentialBackoffTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/ExponentialBackoffTest.java
@@ -16,6 +16,7 @@
package com.mongodb.internal;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -23,6 +24,12 @@
public class ExponentialBackoffTest {
+ @AfterEach
+ void cleanup() {
+ // Always clear the test jitter supplier after each test to avoid test pollution
+ ExponentialBackoff.clearTestJitterSupplier();
+ }
+
@Test
void testTransactionRetryBackoff() {
ExponentialBackoff backoff = ExponentialBackoff.forTransactionRetry();
@@ -73,13 +80,11 @@ void testTransactionRetryBackoffSequenceWithExpectedValues() {
ExponentialBackoff backoff = ExponentialBackoff.forTransactionRetry();
- double[] expectedMaxValues = {5.0, 7.5, 11.25, 16.875, 25.3125, 37.96875, 56.953125, 85.4296875,
- 128.14453125, 192.21679688, 288.32519531, 432.48779297, 500.0};
+ double[] expectedMaxValues = {5.0, 7.5, 11.25, 16.875, 25.3125, 37.96875, 56.953125, 85.4296875, 128.14453125, 192.21679688, 288.32519531, 432.48779297, 500.0};
for (int i = 0; i < expectedMaxValues.length; i++) {
long delay = backoff.calculateDelayMs();
- assertTrue(delay >= 0 && delay <= Math.round(expectedMaxValues[i]),
- String.format("Retry %d: delay should be 0-%d ms, got: %d", i, Math.round(expectedMaxValues[i]), delay));
+ assertTrue(delay >= 0 && delay <= Math.round(expectedMaxValues[i]), String.format("Retry %d: delay should be 0-%d ms, got: %d", i, Math.round(expectedMaxValues[i]), delay));
}
}
@@ -162,32 +167,6 @@ void testReset() {
assertTrue(delay >= 0 && delay <= 5, "First delay after reset should be 0-5ms, got: " + delay);
}
-// @Test
-// void testWouldExceedTimeLimitTransactionRetry() {
-// ExponentialBackoff backoff = ExponentialBackoff.forTransactionRetry();
-// long startTime = ClientSessionClock.INSTANCE.now();
-//
-// // Initially, should not exceed time limit
-// assertFalse(backoff.wouldExceedTimeLimit(startTime, 120000));
-//
-// // With very little time remaining (4ms), first backoff (up to 5ms) would exceed
-// long nearLimitTime = startTime - 119996; // 4ms remaining
-// assertTrue(backoff.wouldExceedTimeLimit(nearLimitTime, 120000));
-// }
-
-// @Test
-// void testWouldExceedTimeLimitCommandRetry() {
-// ExponentialBackoff backoff = ExponentialBackoff.forCommandRetry();
-// long startTime = ClientSessionClock.INSTANCE.now();
-//
-// // Initially, should not exceed time limit
-// assertFalse(backoff.wouldExceedTimeLimit(startTime, 10000));
-//
-// // With 99ms remaining, first backoff (up to 100ms) would exceed
-// long nearLimitTime = startTime - 9901; // 99ms remaining
-// assertTrue(backoff.wouldExceedTimeLimit(nearLimitTime, 10000));
-// }
-
@Test
void testCommandRetrySequenceMatchesSpec() {
// Test that command retry follows spec: 100ms * 2^i capped at 10000ms
@@ -198,8 +177,117 @@ void testCommandRetrySequenceMatchesSpec() {
for (int i = 0; i < expectedMaxValues.length; i++) {
long delay = backoff.calculateDelayMs();
double expectedMax = expectedMaxValues[i];
- assertTrue(delay >= 0 && delay <= Math.round(expectedMax),
- String.format("Retry %d: delay should be 0-%d ms, got: %d", i, Math.round(expectedMax), delay));
+ assertTrue(delay >= 0 && delay <= Math.round(expectedMax), String.format("Retry %d: delay should be 0-%d ms, got: %d", i, Math.round(expectedMax), delay));
+ }
+ }
+
+ // Tests for the test jitter supplier functionality
+
+ @Test
+ void testJitterSupplierWithZeroJitter() {
+ // Set jitter to always return 0 (no backoff)
+ ExponentialBackoff.setTestJitterSupplier(() -> 0.0);
+
+ ExponentialBackoff backoff = ExponentialBackoff.forTransactionRetry();
+
+ // With jitter = 0, all delays should be 0
+ for (int i = 0; i < 10; i++) {
+ long delay = backoff.calculateDelayMs();
+ assertEquals(0, delay, "With jitter=0, delay should always be 0ms");
+ }
+ }
+
+ @Test
+ void testJitterSupplierWithFullJitter() {
+ // Set jitter to always return 1.0 (full backoff)
+ ExponentialBackoff.setTestJitterSupplier(() -> 1.0);
+
+ ExponentialBackoff backoff = ExponentialBackoff.forTransactionRetry();
+
+ // Expected delays with jitter=1.0 and growth factor 1.5
+ double[] expectedDelays = {5.0, 7.5, 11.25, 16.875, 25.3125, 37.96875, 56.953125, 85.4296875, 128.14453125, 192.21679688, 288.32519531, 432.48779297, 500.0};
+
+ for (int i = 0; i < expectedDelays.length; i++) {
+ long delay = backoff.calculateDelayMs();
+ long expected = Math.round(expectedDelays[i]);
+ assertEquals(expected, delay, String.format("Retry %d: with jitter=1.0, delay should be %dms", i, expected));
+ }
+ }
+
+ @Test
+ void testJitterSupplierWithHalfJitter() {
+ // Set jitter to always return 0.5 (half backoff)
+ ExponentialBackoff.setTestJitterSupplier(() -> 0.5);
+
+ ExponentialBackoff backoff = ExponentialBackoff.forTransactionRetry();
+
+ // Expected delays with jitter=0.5 and growth factor 1.5
+ double[] expectedMaxDelays = {5.0, 7.5, 11.25, 16.875, 25.3125, 37.96875, 56.953125, 85.4296875, 128.14453125, 192.21679688, 288.32519531, 432.48779297, 500.0};
+
+ for (int i = 0; i < expectedMaxDelays.length; i++) {
+ long delay = backoff.calculateDelayMs();
+ long expected = Math.round(0.5 * expectedMaxDelays[i]);
+ assertEquals(expected, delay, String.format("Retry %d: with jitter=0.5, delay should be %dms", i, expected));
+ }
+ }
+
+ @Test
+ void testJitterSupplierForCommandRetry() {
+ // Test that custom jitter also works with command retry configuration
+ ExponentialBackoff.setTestJitterSupplier(() -> 1.0);
+
+ ExponentialBackoff backoff = ExponentialBackoff.forCommandRetry();
+
+ // Expected first few delays with jitter=1.0 and growth factor 2.0
+ long[] expectedDelays = {100, 200, 400, 800, 1600, 3200, 6400, 10000};
+
+ for (int i = 0; i < expectedDelays.length; i++) {
+ long delay = backoff.calculateDelayMs();
+ assertEquals(expectedDelays[i], delay, String.format("Command retry %d: with jitter=1.0, delay should be %dms", i, expectedDelays[i]));
+ }
+ }
+
+ @Test
+ void testClearingJitterSupplierReturnsToRandom() {
+ // First set a fixed jitter
+ ExponentialBackoff.setTestJitterSupplier(() -> 0.0);
+
+ ExponentialBackoff backoff1 = ExponentialBackoff.forTransactionRetry();
+ long delay1 = backoff1.calculateDelayMs();
+ assertEquals(0, delay1, "With jitter=0, delay should be 0ms");
+
+ // Clear the test jitter supplier
+ ExponentialBackoff.clearTestJitterSupplier();
+
+ // Now delays should be random again
+ ExponentialBackoff backoff2 = ExponentialBackoff.forTransactionRetry();
+
+ // Run multiple times to verify randomness (statistically very unlikely to get all zeros)
+ boolean foundNonZero = false;
+ for (int i = 0; i < 20; i++) {
+ long delay = backoff2.calculateDelayMs();
+ assertTrue(delay >= 0 && delay <= Math.round(5.0 * Math.pow(1.5, i)), "Delay should be within expected range");
+ if (delay > 0) {
+ foundNonZero = true;
+ }
}
+ assertTrue(foundNonZero, "After clearing test jitter, should get some non-zero delays (random behavior)");
+ }
+
+ @Test
+ void testJitterSupplierWithCustomBackoff() {
+ // Test that custom jitter works with custom backoff parameters
+ ExponentialBackoff.setTestJitterSupplier(() -> 0.75);
+
+ ExponentialBackoff backoff = new ExponentialBackoff(100.0, 1000.0, 2.5);
+
+ // First delay: 0.75 * 100 = 75
+ assertEquals(75, backoff.calculateDelayMs());
+
+ // Second delay: 0.75 * 100 * 2.5 = 0.75 * 250 = 188 (rounded)
+ assertEquals(188, backoff.calculateDelayMs());
+
+ // Third delay: 0.75 * 100 * 2.5^2 = 0.75 * 625 = 469 (rounded)
+ assertEquals(469, backoff.calculateDelayMs());
}
}
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index 45f28e586fe..43292321ead 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -22,6 +22,7 @@
import com.mongodb.TransactionOptions;
import com.mongodb.client.internal.ClientSessionClock;
import com.mongodb.client.model.Sorts;
+import com.mongodb.internal.ExponentialBackoff;
import org.bson.Document;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -233,6 +234,60 @@ public void testExponentialBackoffOnTransientError() {
}
}
+ //
+ // Test that retries within withTransaction do not occur immediately
+ // This test verifies that exponential backoff is enforced during commit retries
+ // See: https://github.com/mongodb/specifications/blob/master/source/transactions-convenient-api/tests/README.md#retry-backoff-is-enforced
+ //
+ @Test
+ public void testRetryBackoffIsEnforced() {
+ MongoDatabase failPointAdminDb = client.getDatabase("admin");
+
+ // Test 1: Run with jitter = 0 (no backoff)
+ ExponentialBackoff.setTestJitterSupplier(() -> 0.0);
+
+ failPointAdminDb.runCommand(Document.parse("{'configureFailPoint': 'failCommand', 'mode': {'times': 13}, " + "'data': {'failCommands': ['commitTransaction'], 'errorCode': 251}}"));
+
+ long noBackoffTime;
+ try (ClientSession session = client.startSession()) {
+ long startNanos = System.nanoTime();
+ session.withTransaction(() -> {
+ collection.insertOne(session, Document.parse("{ _id : 'backoff-test-no-jitter' }"));
+ return null;
+ });
+ long endNanos = System.nanoTime();
+ noBackoffTime = TimeUnit.NANOSECONDS.toMillis(endNanos - startNanos);
+ } finally {
+ // Clear the test jitter supplier to avoid affecting other tests
+ ExponentialBackoff.clearTestJitterSupplier();
+ failPointAdminDb.runCommand(Document.parse("{'configureFailPoint': 'failCommand', 'mode': 'off'}"));
+ }
+
+ // Test 2: Run with jitter = 1 (full backoff)
+ ExponentialBackoff.setTestJitterSupplier(() -> 1.0);
+
+ failPointAdminDb.runCommand(Document.parse("{'configureFailPoint': 'failCommand', 'mode': {'times': 13}, " + "'data': {'failCommands': ['commitTransaction'], 'errorCode': 251}}"));
+
+ long withBackoffTime;
+ try (ClientSession session = client.startSession()) {
+ long startNanos = System.nanoTime();
+ session.withTransaction(() -> {
+ collection.insertOne(session, Document.parse("{ _id : 'backoff-test-full-jitter' }"));
+ return null;
+ });
+ long endNanos = System.nanoTime();
+ withBackoffTime = TimeUnit.NANOSECONDS.toMillis(endNanos - startNanos);
+ } finally {
+ ExponentialBackoff.clearTestJitterSupplier();
+ failPointAdminDb.runCommand(Document.parse("{'configureFailPoint': 'failCommand', 'mode': 'off'}"));
+ }
+
+ long expectedWithBackoffTime = noBackoffTime + 2200; // 2.2 seconds as per spec
+ long actualDifference = Math.abs(withBackoffTime - expectedWithBackoffTime);
+
+ assertTrue(actualDifference < 1000, String.format("Expected withBackoffTime to be ~%dms (noBackoffTime %dms + 2200ms), " + "but got %dms. Difference: %dms (tolerance: 1000ms per spec)", expectedWithBackoffTime, noBackoffTime, withBackoffTime, actualDifference));
+ }
+
private boolean canRunTests() {
return isSharded() || isDiscoverableReplicaSet();
}
From e8857e009af7d70bd218c854b81dc31c34da67ea Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Wed, 10 Dec 2025 18:16:26 +0000
Subject: [PATCH 17/72] Flaky test
---
.../client/AbstractClientSideOperationsTimeoutProseTest.java | 3 +++
.../com/mongodb/client/WithTransactionProseTest.java | 2 ++
2 files changed, 5 insertions(+)
diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
index 7828ecde684..b037f97e964 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
@@ -674,6 +674,9 @@ public void test10CustomTestWithTransactionUsesASingleTimeout() {
}
@DisplayName("10. Convenient Transactions - Custom Test: with transaction uses a single timeout - lock")
+ // The timing of when the timeout check occurred relative to the retry attempts and backoff delays could vary based on system load and jitter values, sometimes allowing
+ // the LockTimeout error to surface before the timeout was detected.
+ @FlakyTest(maxAttempts = 3)
@Test
public void test10CustomTestWithTransactionUsesASingleTimeoutWithLock() {
assumeTrue(serverVersionAtLeast(4, 4));
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index 43292321ead..bcd52025ac2 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -25,6 +25,7 @@
import com.mongodb.internal.ExponentialBackoff;
import org.bson.Document;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.concurrent.TimeUnit;
@@ -239,6 +240,7 @@ public void testExponentialBackoffOnTransientError() {
// This test verifies that exponential backoff is enforced during commit retries
// See: https://github.com/mongodb/specifications/blob/master/source/transactions-convenient-api/tests/README.md#retry-backoff-is-enforced
//
+ @DisplayName("Retry Backoff is Enforced")
@Test
public void testRetryBackoffIsEnforced() {
MongoDatabase failPointAdminDb = client.getDatabase("admin");
From b8d7abfc9051a8d4899f4673e064cc416885b281 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Thu, 11 Dec 2025 11:51:59 +0000
Subject: [PATCH 18/72] Remove extra Test annotation
---
.../client/AbstractClientSideOperationsTimeoutProseTest.java | 1 -
1 file changed, 1 deletion(-)
diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
index b037f97e964..240ca1490c7 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
@@ -677,7 +677,6 @@ public void test10CustomTestWithTransactionUsesASingleTimeout() {
// The timing of when the timeout check occurred relative to the retry attempts and backoff delays could vary based on system load and jitter values, sometimes allowing
// the LockTimeout error to surface before the timeout was detected.
@FlakyTest(maxAttempts = 3)
- @Test
public void test10CustomTestWithTransactionUsesASingleTimeoutWithLock() {
assumeTrue(serverVersionAtLeast(4, 4));
assumeFalse(isStandalone());
From c2a91bf9527d73a1307fc6682f599432fb984704 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Sun, 14 Dec 2025 16:06:12 +0000
Subject: [PATCH 19/72] Throwing correct exception when CSOT is used
---
.../mongodb/client/internal/ClientSessionImpl.java | 12 +++++++++---
...AbstractClientSideOperationsTimeoutProseTest.java | 4 +---
2 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
index 59ef120a08b..5798f8afd80 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
@@ -270,9 +270,15 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
// Calculate backoff delay and check if it would exceed timeout
long backoffMs = transactionBackoff.calculateDelayMs();
if (ClientSessionClock.INSTANCE.now() - startTime + backoffMs >= maxRetryTimeMS) {
- // Throw the last error as per spec
- // lastError is always set here since we only retry on MongoException
- throw lastError;
+ // If CSOT is enabled (timeoutMS is set), throw MongoOperationTimeoutException
+ // Otherwise, throw the last error directly for backward compatibility
+ if (timeoutMS != null) {
+ throw new MongoOperationTimeoutException(
+ "Transaction retry timeout exceeded after " + (ClientSessionClock.INSTANCE.now() - startTime) + "ms",
+ lastError);
+ } else {
+ throw lastError;
+ }
}
try {
if (backoffMs > 0) {
diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
index 240ca1490c7..7828ecde684 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
@@ -674,9 +674,7 @@ public void test10CustomTestWithTransactionUsesASingleTimeout() {
}
@DisplayName("10. Convenient Transactions - Custom Test: with transaction uses a single timeout - lock")
- // The timing of when the timeout check occurred relative to the retry attempts and backoff delays could vary based on system load and jitter values, sometimes allowing
- // the LockTimeout error to surface before the timeout was detected.
- @FlakyTest(maxAttempts = 3)
+ @Test
public void test10CustomTestWithTransactionUsesASingleTimeoutWithLock() {
assumeTrue(serverVersionAtLeast(4, 4));
assumeFalse(isStandalone());
From 895788364e36e44c3a5f8cd059018625814141d1 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Sun, 14 Dec 2025 17:51:47 +0000
Subject: [PATCH 20/72] Simplifying implementation by relying on CSOT to throw
when timeout is exceeded (ex
operationContext.getTimeoutContext().getReadTimeoutMS())
---
.../client/internal/ClientSessionImpl.java | 28 ++++---------------
1 file changed, 5 insertions(+), 23 deletions(-)
diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
index 5798f8afd80..a68ec9bc69e 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
@@ -27,8 +27,10 @@
import com.mongodb.WriteConcern;
import com.mongodb.client.ClientSession;
import com.mongodb.client.TransactionBody;
-import com.mongodb.internal.TimeoutContext;
import com.mongodb.internal.ExponentialBackoff;
+import com.mongodb.internal.TimeoutContext;
+import com.mongodb.internal.observability.micrometer.TracingManager;
+import com.mongodb.internal.observability.micrometer.TransactionSpan;
import com.mongodb.internal.operation.AbortTransactionOperation;
import com.mongodb.internal.operation.CommitTransactionOperation;
import com.mongodb.internal.operation.OperationHelper;
@@ -37,8 +39,6 @@
import com.mongodb.internal.operation.WriteOperation;
import com.mongodb.internal.session.BaseClientSessionImpl;
import com.mongodb.internal.session.ServerSessionPool;
-import com.mongodb.internal.observability.micrometer.TracingManager;
-import com.mongodb.internal.observability.micrometer.TransactionSpan;
import com.mongodb.lang.Nullable;
import static com.mongodb.MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL;
@@ -252,12 +252,8 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
notNull("transactionBody", transactionBody);
long startTime = ClientSessionClock.INSTANCE.now();
TimeoutContext withTransactionTimeoutContext = createTimeoutContext(options);
- // Use CSOT timeout if set, otherwise default to MAX_RETRY_TIME_LIMIT_MS
- Long timeoutMS = withTransactionTimeoutContext.getTimeoutSettings().getTimeoutMS();
- long maxRetryTimeMS = timeoutMS != null ? timeoutMS : MAX_RETRY_TIME_LIMIT_MS;
ExponentialBackoff transactionBackoff = null;
boolean isRetry = false;
- MongoException lastError = null;
try {
outer:
@@ -267,19 +263,7 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
if (transactionBackoff == null) {
transactionBackoff = ExponentialBackoff.forTransactionRetry();
}
- // Calculate backoff delay and check if it would exceed timeout
long backoffMs = transactionBackoff.calculateDelayMs();
- if (ClientSessionClock.INSTANCE.now() - startTime + backoffMs >= maxRetryTimeMS) {
- // If CSOT is enabled (timeoutMS is set), throw MongoOperationTimeoutException
- // Otherwise, throw the last error directly for backward compatibility
- if (timeoutMS != null) {
- throw new MongoOperationTimeoutException(
- "Transaction retry timeout exceeded after " + (ClientSessionClock.INSTANCE.now() - startTime) + "ms",
- lastError);
- } else {
- throw lastError;
- }
- }
try {
if (backoffMs > 0) {
Thread.sleep(backoffMs);
@@ -304,8 +288,7 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
if (e instanceof MongoException && !(e instanceof MongoOperationTimeoutException)) {
MongoException exceptionToHandle = OperationHelper.unwrap((MongoException) e);
if (exceptionToHandle.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)
- && ClientSessionClock.INSTANCE.now() - startTime < maxRetryTimeMS) {
- lastError = exceptionToHandle; // Track the last error for timeout scenarios
+ && ClientSessionClock.INSTANCE.now() - startTime < MAX_RETRY_TIME_LIMIT_MS) {
if (transactionSpan != null) {
transactionSpan.spanFinalizing(false);
}
@@ -320,10 +303,9 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
commitTransaction(false);
break;
} catch (MongoException e) {
- lastError = e; // Track the last error for timeout scenarios
clearTransactionContextOnError(e);
if (!(e instanceof MongoOperationTimeoutException)
- && ClientSessionClock.INSTANCE.now() - startTime < maxRetryTimeMS) {
+ && ClientSessionClock.INSTANCE.now() - startTime < MAX_RETRY_TIME_LIMIT_MS) {
applyMajorityWriteConcernToTransactionOptions();
if (!(e instanceof MongoExecutionTimeoutException)
From e008a432234eea92d2ee914bda53cf5f3c82cc94 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Tue, 13 Jan 2026 15:52:49 +0000
Subject: [PATCH 21/72] Fixing implementation according to spec changes in
JAVA-6046 and https://github.com/mongodb/specifications/pull/1868
---
.../mongodb/internal/ExponentialBackoff.java | 16 ++++++++
.../client/internal/ClientSessionImpl.java | 39 ++++++++++++-------
.../client/WithTransactionProseTest.java | 6 ++-
3 files changed, 44 insertions(+), 17 deletions(-)
diff --git a/driver-core/src/main/com/mongodb/internal/ExponentialBackoff.java b/driver-core/src/main/com/mongodb/internal/ExponentialBackoff.java
index 0a01d38e271..42a9ff1d385 100644
--- a/driver-core/src/main/com/mongodb/internal/ExponentialBackoff.java
+++ b/driver-core/src/main/com/mongodb/internal/ExponentialBackoff.java
@@ -111,6 +111,22 @@ public long calculateDelayMs() {
return Math.round(jitter * cappedBackoff);
}
+ /**
+ * Calculate backoff delay with jitter for a specific retry count.
+ * This method does not modify the internal retry counter.
+ *
+ * @param retryCount the retry count to calculate delay for
+ * @return delay in milliseconds
+ */
+ public long calculateDelayMs(final int retryCount) {
+ double jitter = testJitterSupplier != null
+ ? testJitterSupplier.getAsDouble()
+ : ThreadLocalRandom.current().nextDouble();
+ double exponentialBackoff = baseBackoffMs * Math.pow(growthFactor, retryCount);
+ double cappedBackoff = Math.min(exponentialBackoff, maxBackoffMs);
+ return Math.round(jitter * cappedBackoff);
+ }
+
/**
* Set a custom jitter supplier for testing purposes.
* This overrides the default ThreadLocalRandom jitter generation.
diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
index a68ec9bc69e..ddbc0ecf205 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
@@ -252,18 +252,22 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
notNull("transactionBody", transactionBody);
long startTime = ClientSessionClock.INSTANCE.now();
TimeoutContext withTransactionTimeoutContext = createTimeoutContext(options);
- ExponentialBackoff transactionBackoff = null;
- boolean isRetry = false;
+ ExponentialBackoff transactionBackoff = ExponentialBackoff.forTransactionRetry();
+ int transactionAttempt = 0;
+ MongoException lastError = null;
try {
outer:
while (true) {
- // Apply exponential backoff before retrying transaction
- if (isRetry) {
- if (transactionBackoff == null) {
- transactionBackoff = ExponentialBackoff.forTransactionRetry();
+ if (transactionAttempt > 0) {
+ long backoffMs = transactionBackoff.calculateDelayMs(transactionAttempt - 1);
+ // Check if backoff would exceed timeout
+ if (ClientSessionClock.INSTANCE.now() + backoffMs - startTime >= MAX_RETRY_TIME_LIMIT_MS) {
+ if (lastError != null) {
+ throw lastError;
+ }
+ throw new MongoClientException("Transaction retry timeout exceeded");
}
- long backoffMs = transactionBackoff.calculateDelayMs();
try {
if (backoffMs > 0) {
Thread.sleep(backoffMs);
@@ -273,10 +277,11 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
throw new MongoClientException("Transaction retry interrupted", e);
}
}
- isRetry = true;
T retVal;
try {
startTransaction(options, withTransactionTimeoutContext.copyTimeoutContext());
+ transactionAttempt++;
+
if (transactionSpan != null) {
transactionSpan.setIsConvenientTransaction();
}
@@ -285,14 +290,17 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
if (transactionState == TransactionState.IN) {
abortTransaction();
}
- if (e instanceof MongoException && !(e instanceof MongoOperationTimeoutException)) {
- MongoException exceptionToHandle = OperationHelper.unwrap((MongoException) e);
- if (exceptionToHandle.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)
- && ClientSessionClock.INSTANCE.now() - startTime < MAX_RETRY_TIME_LIMIT_MS) {
- if (transactionSpan != null) {
- transactionSpan.spanFinalizing(false);
+ if (e instanceof MongoException) {
+ lastError = (MongoException) e; // Store last error
+ if (!(e instanceof MongoOperationTimeoutException)) {
+ MongoException exceptionToHandle = OperationHelper.unwrap((MongoException) e);
+ if (exceptionToHandle.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)
+ && ClientSessionClock.INSTANCE.now() - startTime < MAX_RETRY_TIME_LIMIT_MS) {
+ if (transactionSpan != null) {
+ transactionSpan.spanFinalizing(false);
+ }
+ continue;
}
- continue;
}
}
throw e;
@@ -315,6 +323,7 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
if (transactionSpan != null) {
transactionSpan.spanFinalizing(true);
}
+ lastError = e;
continue outer;
}
}
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index bcd52025ac2..5d3b3b176df 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -284,10 +284,12 @@ public void testRetryBackoffIsEnforced() {
failPointAdminDb.runCommand(Document.parse("{'configureFailPoint': 'failCommand', 'mode': 'off'}"));
}
- long expectedWithBackoffTime = noBackoffTime + 2200; // 2.2 seconds as per spec
+ long expectedWithBackoffTime = noBackoffTime + 1800; // 1.8 seconds as per spec
long actualDifference = Math.abs(withBackoffTime - expectedWithBackoffTime);
- assertTrue(actualDifference < 1000, String.format("Expected withBackoffTime to be ~%dms (noBackoffTime %dms + 2200ms), " + "but got %dms. Difference: %dms (tolerance: 1000ms per spec)", expectedWithBackoffTime, noBackoffTime, withBackoffTime, actualDifference));
+ assertTrue(actualDifference < 1000, String.format("Expected withBackoffTime to be ~%dms (noBackoffTime %dms + 1800ms), but"
+ + " got %dms. Difference: %dms (tolerance: 1000ms per spec)", expectedWithBackoffTime, noBackoffTime, withBackoffTime,
+ actualDifference));
}
private boolean canRunTests() {
From a54d95aeaca2f976d43854c260043a68c3b4c5b9 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Tue, 13 Jan 2026 23:47:24 +0000
Subject: [PATCH 22/72] Update
driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
Co-authored-by: Valentin Kovalenko
---
.../com/mongodb/client/WithTransactionProseTest.java | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index 5d3b3b176df..50c1a69f90a 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -225,8 +225,7 @@ public void testExponentialBackoffOnTransientError() {
session.withTransaction(() -> {
retryCount.incrementAndGet(); // Count the attempt before the operation that might fail
- collection.insertOne(session, Document.parse("{ _id : 'backoff-test' }"));
- return retryCount;
+ return collection.insertOne(session, Document.parse("{ _id : 'backoff-test' }"));
});
assertEquals(4, retryCount.get(), "Expected 1 initial attempt + 3 retries");
From 553d6ba5c71b7fdaf9ce98b241fae22247539326 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Wed, 14 Jan 2026 00:00:25 +0000
Subject: [PATCH 23/72] Update
driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
Co-authored-by: Valentin Kovalenko
---
.../functional/com/mongodb/client/WithTransactionProseTest.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index 50c1a69f90a..9453ff09b2c 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -211,7 +211,7 @@ public void testTimeoutMSAndLegacySettings() {
// Backoff uses growth factor of 1.5 as per spec
//
@Test
- public void testExponentialBackoffOnTransientError() {
+ void testExponentialBackoffOnTransientError() {
// Configure failpoint to simulate transient errors
MongoDatabase failPointAdminDb = client.getDatabase("admin");
failPointAdminDb.runCommand(
From c0136e124257c358dc69aa487446283a7a54282d Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Wed, 14 Jan 2026 00:01:09 +0000
Subject: [PATCH 24/72] Update
driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
Co-authored-by: Valentin Kovalenko
---
.../com/mongodb/client/WithTransactionProseTest.java | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index 9453ff09b2c..9c917ac7771 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -276,8 +276,7 @@ public void testRetryBackoffIsEnforced() {
collection.insertOne(session, Document.parse("{ _id : 'backoff-test-full-jitter' }"));
return null;
});
- long endNanos = System.nanoTime();
- withBackoffTime = TimeUnit.NANOSECONDS.toMillis(endNanos - startNanos);
+ withBackoffTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
} finally {
ExponentialBackoff.clearTestJitterSupplier();
failPointAdminDb.runCommand(Document.parse("{'configureFailPoint': 'failCommand', 'mode': 'off'}"));
From 09a129108794807b18349f16fb9aa182a024e02f Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Wed, 14 Jan 2026 00:01:52 +0000
Subject: [PATCH 25/72] Update
driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
Co-authored-by: Valentin Kovalenko
---
.../functional/com/mongodb/client/WithTransactionProseTest.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index 9c917ac7771..e38d99fd014 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -241,7 +241,7 @@ void testExponentialBackoffOnTransientError() {
//
@DisplayName("Retry Backoff is Enforced")
@Test
- public void testRetryBackoffIsEnforced() {
+ void testRetryBackoffIsEnforced() {
MongoDatabase failPointAdminDb = client.getDatabase("admin");
// Test 1: Run with jitter = 0 (no backoff)
From 520feadcbf7d4c18ddf0c05ff940604b5a8c8ab9 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Wed, 14 Jan 2026 00:03:12 +0000
Subject: [PATCH 26/72] Update
driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
Co-authored-by: Valentin Kovalenko
---
.../com/mongodb/client/WithTransactionProseTest.java | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index e38d99fd014..2441f13a741 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -256,8 +256,7 @@ void testRetryBackoffIsEnforced() {
collection.insertOne(session, Document.parse("{ _id : 'backoff-test-no-jitter' }"));
return null;
});
- long endNanos = System.nanoTime();
- noBackoffTime = TimeUnit.NANOSECONDS.toMillis(endNanos - startNanos);
+ noBackoffTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);```
} finally {
// Clear the test jitter supplier to avoid affecting other tests
ExponentialBackoff.clearTestJitterSupplier();
From 478e2428fa1a4d992def330031857bd3a674879e Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Wed, 14 Jan 2026 00:05:21 +0000
Subject: [PATCH 27/72] Update
driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
Co-authored-by: Valentin Kovalenko
---
.../com/mongodb/client/WithTransactionProseTest.java | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index 2441f13a741..0bcb5a417fa 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -271,10 +271,7 @@ void testRetryBackoffIsEnforced() {
long withBackoffTime;
try (ClientSession session = client.startSession()) {
long startNanos = System.nanoTime();
- session.withTransaction(() -> {
- collection.insertOne(session, Document.parse("{ _id : 'backoff-test-full-jitter' }"));
- return null;
- });
+ session.withTransaction(() -> collection.insertOne(session, Document.parse("{ _id : 'backoff-test-full-jitter' }")));
withBackoffTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
} finally {
ExponentialBackoff.clearTestJitterSupplier();
From 365f05450afb8e0442dc67c00381a461c0865a9a Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Wed, 14 Jan 2026 00:06:46 +0000
Subject: [PATCH 28/72] Update
driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
Co-authored-by: Valentin Kovalenko
---
.../com/mongodb/client/WithTransactionProseTest.java | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index 0bcb5a417fa..92ad8792c33 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -252,10 +252,7 @@ void testRetryBackoffIsEnforced() {
long noBackoffTime;
try (ClientSession session = client.startSession()) {
long startNanos = System.nanoTime();
- session.withTransaction(() -> {
- collection.insertOne(session, Document.parse("{ _id : 'backoff-test-no-jitter' }"));
- return null;
- });
+ session.withTransaction(() -> collection.insertOne(session, Document.parse("{ _id : 'backoff-test-no-jitter' }")));
noBackoffTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);```
} finally {
// Clear the test jitter supplier to avoid affecting other tests
From 098b4b40d25102959caf1eb53c6663700e74aaae Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Wed, 14 Jan 2026 01:02:28 +0000
Subject: [PATCH 29/72] Update
driver-core/src/main/com/mongodb/internal/ExponentialBackoff.java
Co-authored-by: Valentin Kovalenko
---
.../src/main/com/mongodb/internal/ExponentialBackoff.java | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/driver-core/src/main/com/mongodb/internal/ExponentialBackoff.java b/driver-core/src/main/com/mongodb/internal/ExponentialBackoff.java
index 42a9ff1d385..0ac1e12ee4e 100644
--- a/driver-core/src/main/com/mongodb/internal/ExponentialBackoff.java
+++ b/driver-core/src/main/com/mongodb/internal/ExponentialBackoff.java
@@ -69,7 +69,8 @@ public ExponentialBackoff(final double baseBackoffMs, final double maxBackoffMs,
/**
* Creates a backoff instance configured for withTransaction retries.
- * Uses: 5ms base, 500ms max, 1.5 growth factor.
+ * Uses: {@value TRANSACTION_BASE_BACKOFF_MS} ms base, {@value TRANSACTION_MAX_BACKOFF_MS} ms max,
+ * {@value TRANSACTION_BACKOFF_GROWTH} growth factor.
*
* @return ExponentialBackoff configured for transaction retries
*/
From 4df0f942a9a2c816cd68e0afc4adb65f9587f965 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Thu, 15 Jan 2026 23:25:48 +0000
Subject: [PATCH 30/72] PR feedback
---
.../mongodb/internal/ExponentialBackoff.java | 192 -------------
.../internal/time/ExponentialBackoff.java | 88 ++++++
.../internal/ExponentialBackoffTest.java | 268 ++----------------
.../client/internal/ClientSessionImpl.java | 40 +--
.../client/WithTransactionProseTest.java | 97 +++----
5 files changed, 180 insertions(+), 505 deletions(-)
delete mode 100644 driver-core/src/main/com/mongodb/internal/ExponentialBackoff.java
create mode 100644 driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java
diff --git a/driver-core/src/main/com/mongodb/internal/ExponentialBackoff.java b/driver-core/src/main/com/mongodb/internal/ExponentialBackoff.java
deleted file mode 100644
index 0ac1e12ee4e..00000000000
--- a/driver-core/src/main/com/mongodb/internal/ExponentialBackoff.java
+++ /dev/null
@@ -1,192 +0,0 @@
-/*
- * Copyright 2008-present MongoDB, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.mongodb.internal;
-
-import com.mongodb.annotations.NotThreadSafe;
-
-import java.util.concurrent.ThreadLocalRandom;
-import java.util.function.DoubleSupplier;
-
-import static com.mongodb.internal.VisibleForTesting.AccessModifier.PRIVATE;
-
-/**
- * Implements exponential backoff with jitter for retry scenarios.
- * Formula: delayMS = jitter * min(maxBackoffMs, baseBackoffMs * growthFactor^retryCount)
- * where jitter is random value [0, 1).
- *
- * This class provides factory methods for common use cases:
- *
- * - {@link #forTransactionRetry()} - For withTransaction retries (5ms base, 500ms max, 1.5 growth)
- * - {@link #forCommandRetry()} - For command retries with overload (100ms base, 10000ms max, 2.0 growth)
- *
- */
-@NotThreadSafe
-public final class ExponentialBackoff {
- // Transaction retry constants (per spec)
- private static final double TRANSACTION_BASE_BACKOFF_MS = 5.0;
- private static final double TRANSACTION_MAX_BACKOFF_MS = 500.0;
- private static final double TRANSACTION_BACKOFF_GROWTH = 1.5;
-
- // Command retry constants (per spec)
- private static final double COMMAND_BASE_BACKOFF_MS = 100.0;
- private static final double COMMAND_MAX_BACKOFF_MS = 10000.0;
- private static final double COMMAND_BACKOFF_GROWTH = 2.0;
-
- private final double baseBackoffMs;
- private final double maxBackoffMs;
- private final double growthFactor;
- private int retryCount = 0;
-
- // Test-only jitter supplier - when set, overrides ThreadLocalRandom
- private static volatile DoubleSupplier testJitterSupplier = null;
-
- /**
- * Creates an exponential backoff instance with specified parameters.
- *
- * @param baseBackoffMs Initial backoff in milliseconds
- * @param maxBackoffMs Maximum backoff cap in milliseconds
- * @param growthFactor Exponential growth factor (e.g., 1.5 or 2.0)
- */
- public ExponentialBackoff(final double baseBackoffMs, final double maxBackoffMs, final double growthFactor) {
- this.baseBackoffMs = baseBackoffMs;
- this.maxBackoffMs = maxBackoffMs;
- this.growthFactor = growthFactor;
- }
-
- /**
- * Creates a backoff instance configured for withTransaction retries.
- * Uses: {@value TRANSACTION_BASE_BACKOFF_MS} ms base, {@value TRANSACTION_MAX_BACKOFF_MS} ms max,
- * {@value TRANSACTION_BACKOFF_GROWTH} growth factor.
- *
- * @return ExponentialBackoff configured for transaction retries
- */
- public static ExponentialBackoff forTransactionRetry() {
- return new ExponentialBackoff(
- TRANSACTION_BASE_BACKOFF_MS,
- TRANSACTION_MAX_BACKOFF_MS,
- TRANSACTION_BACKOFF_GROWTH
- );
- }
-
- /**
- * Creates a backoff instance configured for command retries during overload.
- * Uses: 100ms base, 10000ms max, 2.0 growth factor.
- *
- * @return ExponentialBackoff configured for command retries
- */
- public static ExponentialBackoff forCommandRetry() {
- return new ExponentialBackoff(
- COMMAND_BASE_BACKOFF_MS,
- COMMAND_MAX_BACKOFF_MS,
- COMMAND_BACKOFF_GROWTH
- );
- }
-
- /**
- * Calculate next backoff delay with jitter.
- *
- * @return delay in milliseconds
- */
- public long calculateDelayMs() {
- // Use test jitter supplier if set, otherwise use ThreadLocalRandom
- double jitter = testJitterSupplier != null
- ? testJitterSupplier.getAsDouble()
- : ThreadLocalRandom.current().nextDouble();
- double exponentialBackoff = baseBackoffMs * Math.pow(growthFactor, retryCount);
- double cappedBackoff = Math.min(exponentialBackoff, maxBackoffMs);
- retryCount++;
- return Math.round(jitter * cappedBackoff);
- }
-
- /**
- * Calculate backoff delay with jitter for a specific retry count.
- * This method does not modify the internal retry counter.
- *
- * @param retryCount the retry count to calculate delay for
- * @return delay in milliseconds
- */
- public long calculateDelayMs(final int retryCount) {
- double jitter = testJitterSupplier != null
- ? testJitterSupplier.getAsDouble()
- : ThreadLocalRandom.current().nextDouble();
- double exponentialBackoff = baseBackoffMs * Math.pow(growthFactor, retryCount);
- double cappedBackoff = Math.min(exponentialBackoff, maxBackoffMs);
- return Math.round(jitter * cappedBackoff);
- }
-
- /**
- * Set a custom jitter supplier for testing purposes.
- * This overrides the default ThreadLocalRandom jitter generation.
- *
- * @param supplier A DoubleSupplier that returns values in [0, 1) range, or null to use default
- */
- @VisibleForTesting(otherwise = PRIVATE)
- public static void setTestJitterSupplier(final DoubleSupplier supplier) {
- testJitterSupplier = supplier;
- }
-
- /**
- * Clear the test jitter supplier, reverting to default ThreadLocalRandom behavior.
- */
- @VisibleForTesting(otherwise = PRIVATE)
- public static void clearTestJitterSupplier() {
- testJitterSupplier = null;
- }
-
- /**
- * Reset retry counter for new sequence of retries.
- */
- public void reset() {
- retryCount = 0;
- }
-
- /**
- * Get current retry count for testing.
- *
- * @return current retry count
- */
- public int getRetryCount() {
- return retryCount;
- }
-
- /**
- * Get the base backoff in milliseconds.
- *
- * @return base backoff
- */
- public double getBaseBackoffMs() {
- return baseBackoffMs;
- }
-
- /**
- * Get the maximum backoff in milliseconds.
- *
- * @return maximum backoff
- */
- public double getMaxBackoffMs() {
- return maxBackoffMs;
- }
-
- /**
- * Get the growth factor.
- *
- * @return growth factor
- */
- public double getGrowthFactor() {
- return growthFactor;
- }
-}
diff --git a/driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java b/driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java
new file mode 100644
index 00000000000..2a69e8ba9e2
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2008-present MongoDB, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.mongodb.internal.time;
+
+import com.mongodb.annotations.NotThreadSafe;
+import com.mongodb.internal.VisibleForTesting;
+
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.function.DoubleSupplier;
+
+import static com.mongodb.internal.VisibleForTesting.AccessModifier.PRIVATE;
+
+/**
+ * Implements exponential backoff with jitter for retry scenarios.
+ */
+@NotThreadSafe
+public enum ExponentialBackoff {
+ TRANSACTION(5.0, 500.0, 1.5);
+
+ private final double baseMs, maxMs, growth;
+
+ // TODO remove this global state once https://jira.mongodb.org/browse/JAVA-6060 is done
+ private static DoubleSupplier testJitterSupplier = null;
+
+ ExponentialBackoff(final double baseMs, final double maxMs, final double growth) {
+ this.baseMs = baseMs;
+ this.maxMs = maxMs;
+ this.growth = growth;
+ }
+
+ /**
+ * Calculate the next delay in milliseconds based on the retry count.
+ *
+ * @param retryCount The number of retries that have occurred.
+ * @return The calculated delay in milliseconds.
+ */
+ public long calculateDelayBeforeNextRetryMs(final int retryCount) {
+ double jitter = testJitterSupplier != null
+ ? testJitterSupplier.getAsDouble()
+ : ThreadLocalRandom.current().nextDouble();
+ double backoff = Math.min(baseMs * Math.pow(growth, retryCount), maxMs);
+ return Math.round(jitter * backoff);
+ }
+
+ /**
+ * Calculate the next delay in milliseconds based on the retry count and a provided jitter.
+ *
+ * @param retryCount The number of retries that have occurred.
+ * @param jitter A double in the range [0, 1) to apply as jitter.
+ * @return The calculated delay in milliseconds.
+ */
+ public long calculateDelayBeforeNextRetryMs(final int retryCount, final double jitter) {
+ double backoff = Math.min(baseMs * Math.pow(growth, retryCount), maxMs);
+ return Math.round(jitter * backoff);
+ }
+
+ /**
+ * Set a custom jitter supplier for testing purposes.
+ *
+ * @param supplier A DoubleSupplier that returns values in [0, 1) range.
+ */
+ @VisibleForTesting(otherwise = PRIVATE)
+ public static void setTestJitterSupplier(final DoubleSupplier supplier) {
+ testJitterSupplier = supplier;
+ }
+
+ /**
+ * Clear the test jitter supplier, reverting to default ThreadLocalRandom behavior.
+ */
+ @VisibleForTesting(otherwise = PRIVATE)
+ public static void clearTestJitterSupplier() {
+ testJitterSupplier = null;
+ }
+}
diff --git a/driver-core/src/test/unit/com/mongodb/internal/ExponentialBackoffTest.java b/driver-core/src/test/unit/com/mongodb/internal/ExponentialBackoffTest.java
index 84ab56a0e47..67238532488 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/ExponentialBackoffTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/ExponentialBackoffTest.java
@@ -16,7 +16,7 @@
package com.mongodb.internal;
-import org.junit.jupiter.api.AfterEach;
+import com.mongodb.internal.time.ExponentialBackoff;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -24,270 +24,50 @@
public class ExponentialBackoffTest {
- @AfterEach
- void cleanup() {
- // Always clear the test jitter supplier after each test to avoid test pollution
- ExponentialBackoff.clearTestJitterSupplier();
- }
-
@Test
void testTransactionRetryBackoff() {
- ExponentialBackoff backoff = ExponentialBackoff.forTransactionRetry();
-
- // Verify configuration
- assertEquals(5.0, backoff.getBaseBackoffMs());
- assertEquals(500.0, backoff.getMaxBackoffMs());
- assertEquals(1.5, backoff.getGrowthFactor());
-
- // First retry (i=0): delay = jitter * min(5 * 1.5^0, 500) = jitter * 5
- // Since jitter is random [0,1), the delay should be between 0 and 5ms
- long delay1 = backoff.calculateDelayMs();
- assertTrue(delay1 >= 0 && delay1 <= 5, "First delay should be 0-5ms, got: " + delay1);
-
- // Second retry (i=1): delay = jitter * min(5 * 1.5^1, 500) = jitter * 7.5
- long delay2 = backoff.calculateDelayMs();
- assertTrue(delay2 >= 0 && delay2 <= 8, "Second delay should be 0-8ms, got: " + delay2);
-
- // Third retry (i=2): delay = jitter * min(5 * 1.5^2, 500) = jitter * 11.25
- long delay3 = backoff.calculateDelayMs();
- assertTrue(delay3 >= 0 && delay3 <= 12, "Third delay should be 0-12ms, got: " + delay3);
-
- // Verify the retry count is incrementing properly
- assertEquals(3, backoff.getRetryCount());
- }
-
- @Test
- void testTransactionRetryBackoffRespectsMaximum() {
- ExponentialBackoff backoff = ExponentialBackoff.forTransactionRetry();
-
- // Advance to a high retry count where backoff would exceed 500ms without capping
- for (int i = 0; i < 20; i++) {
- backoff.calculateDelayMs();
- }
-
- // Even at high retry counts, delay should never exceed 500ms
- for (int i = 0; i < 5; i++) {
- long delay = backoff.calculateDelayMs();
- assertTrue(delay >= 0 && delay <= 500, "Delay should be capped at 500ms, got: " + delay);
- }
- }
-
- @Test
- void testTransactionRetryBackoffSequenceWithExpectedValues() {
// Test that the backoff sequence follows the expected pattern with growth factor 1.5
- // Expected sequence (without jitter): 5, 7.5, 11.25, 16.875, 25.3125, 37.96875, 56.953125, ...
+ // Expected sequence (without jitter): 5, 7.5, 11.25, ...
// With jitter, actual values will be between 0 and these maxima
-
- ExponentialBackoff backoff = ExponentialBackoff.forTransactionRetry();
-
double[] expectedMaxValues = {5.0, 7.5, 11.25, 16.875, 25.3125, 37.96875, 56.953125, 85.4296875, 128.14453125, 192.21679688, 288.32519531, 432.48779297, 500.0};
- for (int i = 0; i < expectedMaxValues.length; i++) {
- long delay = backoff.calculateDelayMs();
- assertTrue(delay >= 0 && delay <= Math.round(expectedMaxValues[i]), String.format("Retry %d: delay should be 0-%d ms, got: %d", i, Math.round(expectedMaxValues[i]), delay));
+ ExponentialBackoff backoff = ExponentialBackoff.TRANSACTION;
+ for (int retry = 0; retry < expectedMaxValues.length; retry++) {
+ long delay = backoff.calculateDelayBeforeNextRetryMs(retry);
+ assertTrue(delay >= 0 && delay <= Math.round(expectedMaxValues[retry]), String.format("Retry %d: delay should be 0-%d ms, got: %d", retry, Math.round(expectedMaxValues[retry]), delay));
}
}
@Test
- void testCommandRetryBackoff() {
- ExponentialBackoff backoff = ExponentialBackoff.forCommandRetry();
-
- // Verify configuration
- assertEquals(100.0, backoff.getBaseBackoffMs());
- assertEquals(10000.0, backoff.getMaxBackoffMs());
- assertEquals(2.0, backoff.getGrowthFactor());
-
- // Test sequence with growth factor 2.0
- // Expected max delays: 100, 200, 400, 800, 1600, 3200, 6400, 10000 (capped)
- long delay1 = backoff.calculateDelayMs();
- assertTrue(delay1 >= 0 && delay1 <= 100, "First delay should be 0-100ms, got: " + delay1);
-
- long delay2 = backoff.calculateDelayMs();
- assertTrue(delay2 >= 0 && delay2 <= 200, "Second delay should be 0-200ms, got: " + delay2);
-
- long delay3 = backoff.calculateDelayMs();
- assertTrue(delay3 >= 0 && delay3 <= 400, "Third delay should be 0-400ms, got: " + delay3);
-
- long delay4 = backoff.calculateDelayMs();
- assertTrue(delay4 >= 0 && delay4 <= 800, "Fourth delay should be 0-800ms, got: " + delay4);
-
- long delay5 = backoff.calculateDelayMs();
- assertTrue(delay5 >= 0 && delay5 <= 1600, "Fifth delay should be 0-1600ms, got: " + delay5);
- }
-
- @Test
- void testCommandRetryBackoffRespectsMaximum() {
- ExponentialBackoff backoff = ExponentialBackoff.forCommandRetry();
-
- // Advance to where exponential would exceed 10000ms
- for (int i = 0; i < 10; i++) {
- backoff.calculateDelayMs();
- }
-
- // Even at high retry counts, delay should never exceed 10000ms
- for (int i = 0; i < 5; i++) {
- long delay = backoff.calculateDelayMs();
- assertTrue(delay >= 0 && delay <= 10000, "Delay should be capped at 10000ms, got: " + delay);
- }
- }
-
- @Test
- void testCustomBackoff() {
- // Test with custom parameters
- ExponentialBackoff backoff = new ExponentialBackoff(50.0, 2000.0, 1.8);
-
- assertEquals(50.0, backoff.getBaseBackoffMs());
- assertEquals(2000.0, backoff.getMaxBackoffMs());
- assertEquals(1.8, backoff.getGrowthFactor());
-
- // First delay: 0-50ms
- long delay1 = backoff.calculateDelayMs();
- assertTrue(delay1 >= 0 && delay1 <= 50, "First delay should be 0-50ms, got: " + delay1);
-
- // Second delay: 0-90ms (50 * 1.8)
- long delay2 = backoff.calculateDelayMs();
- assertTrue(delay2 >= 0 && delay2 <= 90, "Second delay should be 0-90ms, got: " + delay2);
- }
-
- @Test
- void testReset() {
- ExponentialBackoff backoff = ExponentialBackoff.forTransactionRetry();
-
- // Perform some retries
- backoff.calculateDelayMs();
- backoff.calculateDelayMs();
- assertEquals(2, backoff.getRetryCount());
-
- // Reset and verify counter is back to 0
- backoff.reset();
- assertEquals(0, backoff.getRetryCount());
-
- // First delay after reset should be in the initial range again
- long delay = backoff.calculateDelayMs();
- assertTrue(delay >= 0 && delay <= 5, "First delay after reset should be 0-5ms, got: " + delay);
- }
-
- @Test
- void testCommandRetrySequenceMatchesSpec() {
- // Test that command retry follows spec: 100ms * 2^i capped at 10000ms
- ExponentialBackoff backoff = ExponentialBackoff.forCommandRetry();
-
- double[] expectedMaxValues = {100.0, 200.0, 400.0, 800.0, 1600.0, 3200.0, 6400.0, 10000.0, 10000.0};
-
- for (int i = 0; i < expectedMaxValues.length; i++) {
- long delay = backoff.calculateDelayMs();
- double expectedMax = expectedMaxValues[i];
- assertTrue(delay >= 0 && delay <= Math.round(expectedMax), String.format("Retry %d: delay should be 0-%d ms, got: %d", i, Math.round(expectedMax), delay));
- }
- }
-
- // Tests for the test jitter supplier functionality
-
- @Test
- void testJitterSupplierWithZeroJitter() {
- // Set jitter to always return 0 (no backoff)
- ExponentialBackoff.setTestJitterSupplier(() -> 0.0);
-
- ExponentialBackoff backoff = ExponentialBackoff.forTransactionRetry();
+ void testTransactionRetryBackoffRespectsMaximum() {
+ ExponentialBackoff backoff = ExponentialBackoff.TRANSACTION;
- // With jitter = 0, all delays should be 0
- for (int i = 0; i < 10; i++) {
- long delay = backoff.calculateDelayMs();
- assertEquals(0, delay, "With jitter=0, delay should always be 0ms");
+ // Even at high retry counts, delay should never exceed 500ms
+ for (int retry = 0; retry < 25; retry++) {
+ long delay = backoff.calculateDelayBeforeNextRetryMs(retry);
+ assertTrue(delay >= 0 && delay <= 500, String.format("Retry %d: delay should be capped at 500 ms, got: %d ms", retry, delay));
}
}
@Test
- void testJitterSupplierWithFullJitter() {
- // Set jitter to always return 1.0 (full backoff)
- ExponentialBackoff.setTestJitterSupplier(() -> 1.0);
-
- ExponentialBackoff backoff = ExponentialBackoff.forTransactionRetry();
+ void testCustomJitter() {
+ ExponentialBackoff backoff = ExponentialBackoff.TRANSACTION;
// Expected delays with jitter=1.0 and growth factor 1.5
double[] expectedDelays = {5.0, 7.5, 11.25, 16.875, 25.3125, 37.96875, 56.953125, 85.4296875, 128.14453125, 192.21679688, 288.32519531, 432.48779297, 500.0};
+ double jitter = 1.0;
- for (int i = 0; i < expectedDelays.length; i++) {
- long delay = backoff.calculateDelayMs();
- long expected = Math.round(expectedDelays[i]);
- assertEquals(expected, delay, String.format("Retry %d: with jitter=1.0, delay should be %dms", i, expected));
- }
- }
-
- @Test
- void testJitterSupplierWithHalfJitter() {
- // Set jitter to always return 0.5 (half backoff)
- ExponentialBackoff.setTestJitterSupplier(() -> 0.5);
-
- ExponentialBackoff backoff = ExponentialBackoff.forTransactionRetry();
-
- // Expected delays with jitter=0.5 and growth factor 1.5
- double[] expectedMaxDelays = {5.0, 7.5, 11.25, 16.875, 25.3125, 37.96875, 56.953125, 85.4296875, 128.14453125, 192.21679688, 288.32519531, 432.48779297, 500.0};
-
- for (int i = 0; i < expectedMaxDelays.length; i++) {
- long delay = backoff.calculateDelayMs();
- long expected = Math.round(0.5 * expectedMaxDelays[i]);
- assertEquals(expected, delay, String.format("Retry %d: with jitter=0.5, delay should be %dms", i, expected));
+ for (int retry = 0; retry < expectedDelays.length; retry++) {
+ long delay = backoff.calculateDelayBeforeNextRetryMs(retry, jitter);
+ long expected = Math.round(expectedDelays[retry]);
+ assertEquals(expected, delay, String.format("Retry %d: with jitter=1.0, delay should be %d ms", retry, expected));
}
- }
-
- @Test
- void testJitterSupplierForCommandRetry() {
- // Test that custom jitter also works with command retry configuration
- ExponentialBackoff.setTestJitterSupplier(() -> 1.0);
-
- ExponentialBackoff backoff = ExponentialBackoff.forCommandRetry();
-
- // Expected first few delays with jitter=1.0 and growth factor 2.0
- long[] expectedDelays = {100, 200, 400, 800, 1600, 3200, 6400, 10000};
-
- for (int i = 0; i < expectedDelays.length; i++) {
- long delay = backoff.calculateDelayMs();
- assertEquals(expectedDelays[i], delay, String.format("Command retry %d: with jitter=1.0, delay should be %dms", i, expectedDelays[i]));
- }
- }
-
- @Test
- void testClearingJitterSupplierReturnsToRandom() {
- // First set a fixed jitter
- ExponentialBackoff.setTestJitterSupplier(() -> 0.0);
- ExponentialBackoff backoff1 = ExponentialBackoff.forTransactionRetry();
- long delay1 = backoff1.calculateDelayMs();
- assertEquals(0, delay1, "With jitter=0, delay should be 0ms");
-
- // Clear the test jitter supplier
- ExponentialBackoff.clearTestJitterSupplier();
-
- // Now delays should be random again
- ExponentialBackoff backoff2 = ExponentialBackoff.forTransactionRetry();
-
- // Run multiple times to verify randomness (statistically very unlikely to get all zeros)
- boolean foundNonZero = false;
- for (int i = 0; i < 20; i++) {
- long delay = backoff2.calculateDelayMs();
- assertTrue(delay >= 0 && delay <= Math.round(5.0 * Math.pow(1.5, i)), "Delay should be within expected range");
- if (delay > 0) {
- foundNonZero = true;
- }
+ // With jitter = 0, all delays should be 0
+ jitter = 0;
+ for (int retry = 0; retry < 10; retry++) {
+ long delay = backoff.calculateDelayBeforeNextRetryMs(retry, jitter);
+ assertEquals(0, delay, "With jitter=0, delay should always be 0 ms");
}
- assertTrue(foundNonZero, "After clearing test jitter, should get some non-zero delays (random behavior)");
- }
-
- @Test
- void testJitterSupplierWithCustomBackoff() {
- // Test that custom jitter works with custom backoff parameters
- ExponentialBackoff.setTestJitterSupplier(() -> 0.75);
-
- ExponentialBackoff backoff = new ExponentialBackoff(100.0, 1000.0, 2.5);
-
- // First delay: 0.75 * 100 = 75
- assertEquals(75, backoff.calculateDelayMs());
-
- // Second delay: 0.75 * 100 * 2.5 = 0.75 * 250 = 188 (rounded)
- assertEquals(188, backoff.calculateDelayMs());
-
- // Third delay: 0.75 * 100 * 2.5^2 = 0.75 * 625 = 469 (rounded)
- assertEquals(469, backoff.calculateDelayMs());
}
}
diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
index ddbc0ecf205..fcaea52aaa1 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
@@ -27,7 +27,6 @@
import com.mongodb.WriteConcern;
import com.mongodb.client.ClientSession;
import com.mongodb.client.TransactionBody;
-import com.mongodb.internal.ExponentialBackoff;
import com.mongodb.internal.TimeoutContext;
import com.mongodb.internal.observability.micrometer.TracingManager;
import com.mongodb.internal.observability.micrometer.TransactionSpan;
@@ -39,6 +38,7 @@
import com.mongodb.internal.operation.WriteOperation;
import com.mongodb.internal.session.BaseClientSessionImpl;
import com.mongodb.internal.session.ServerSessionPool;
+import com.mongodb.internal.time.ExponentialBackoff;
import com.mongodb.lang.Nullable;
import static com.mongodb.MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL;
@@ -47,6 +47,7 @@
import static com.mongodb.assertions.Assertions.assertTrue;
import static com.mongodb.assertions.Assertions.isTrue;
import static com.mongodb.assertions.Assertions.notNull;
+import static com.mongodb.internal.thread.InterruptionUtil.interruptAndCreateMongoInterruptedException;
final class ClientSessionImpl extends BaseClientSessionImpl implements ClientSession {
@@ -252,7 +253,7 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
notNull("transactionBody", transactionBody);
long startTime = ClientSessionClock.INSTANCE.now();
TimeoutContext withTransactionTimeoutContext = createTimeoutContext(options);
- ExponentialBackoff transactionBackoff = ExponentialBackoff.forTransactionRetry();
+ ExponentialBackoff transactionBackoff = ExponentialBackoff.TRANSACTION;
int transactionAttempt = 0;
MongoException lastError = null;
@@ -260,22 +261,7 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
outer:
while (true) {
if (transactionAttempt > 0) {
- long backoffMs = transactionBackoff.calculateDelayMs(transactionAttempt - 1);
- // Check if backoff would exceed timeout
- if (ClientSessionClock.INSTANCE.now() + backoffMs - startTime >= MAX_RETRY_TIME_LIMIT_MS) {
- if (lastError != null) {
- throw lastError;
- }
- throw new MongoClientException("Transaction retry timeout exceeded");
- }
- try {
- if (backoffMs > 0) {
- Thread.sleep(backoffMs);
- }
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new MongoClientException("Transaction retry interrupted", e);
- }
+ backoff(transactionBackoff, transactionAttempt, startTime, lastError);
}
T retVal;
try {
@@ -387,4 +373,22 @@ private TimeoutContext createTimeoutContext(final TransactionOptions transaction
TransactionOptions.merge(transactionOptions, getOptions().getDefaultTransactionOptions()),
operationExecutor.getTimeoutSettings()));
}
+
+ private static void backoff(final ExponentialBackoff exponentialBackoff, final int transactionAttempt, final long startTime,
+ final MongoException lastError) {
+ long backoffMs = exponentialBackoff.calculateDelayBeforeNextRetryMs(transactionAttempt - 1);
+ if (ClientSessionClock.INSTANCE.now() + backoffMs - startTime >= MAX_RETRY_TIME_LIMIT_MS) {
+ if (lastError != null) {
+ throw lastError;
+ }
+ throw new MongoClientException("Transaction retry timeout exceeded");
+ }
+ try {
+ if (backoffMs > 0) {
+ Thread.sleep(backoffMs);
+ }
+ } catch (InterruptedException e) {
+ throw interruptAndCreateMongoInterruptedException("Transaction retry interrupted", e);
+ }
+ }
}
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index 92ad8792c33..e2dce11583f 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -22,7 +22,8 @@
import com.mongodb.TransactionOptions;
import com.mongodb.client.internal.ClientSessionClock;
import com.mongodb.client.model.Sorts;
-import com.mongodb.internal.ExponentialBackoff;
+import com.mongodb.internal.time.ExponentialBackoff;
+import org.bson.BsonDocument;
import org.bson.Document;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
@@ -34,6 +35,7 @@
import static com.mongodb.ClusterFixture.TIMEOUT;
import static com.mongodb.ClusterFixture.isDiscoverableReplicaSet;
import static com.mongodb.ClusterFixture.isSharded;
+import static com.mongodb.client.Fixture.getPrimary;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -206,83 +208,76 @@ public void testTimeoutMSAndLegacySettings() {
}
}
- //
- // Test that exponential backoff is applied when retrying transactions
- // Backoff uses growth factor of 1.5 as per spec
- //
- @Test
- void testExponentialBackoffOnTransientError() {
- // Configure failpoint to simulate transient errors
- MongoDatabase failPointAdminDb = client.getDatabase("admin");
- failPointAdminDb.runCommand(
- Document.parse("{'configureFailPoint': 'failCommand', 'mode': {'times': 3}, "
- + "'data': {'failCommands': ['insert'], 'errorCode': 112, "
- + "'errorLabels': ['TransientTransactionError']}}"));
-
- try (ClientSession session = client.startSession()) {
- // Track retry count
- AtomicInteger retryCount = new AtomicInteger(0);
-
- session.withTransaction(() -> {
- retryCount.incrementAndGet(); // Count the attempt before the operation that might fail
- return collection.insertOne(session, Document.parse("{ _id : 'backoff-test' }"));
- });
-
- assertEquals(4, retryCount.get(), "Expected 1 initial attempt + 3 retries");
- } finally {
- failPointAdminDb.runCommand(Document.parse("{'configureFailPoint': 'failCommand', 'mode': 'off'}"));
- }
- }
-
- //
- // Test that retries within withTransaction do not occur immediately
- // This test verifies that exponential backoff is enforced during commit retries
- // See: https://github.com/mongodb/specifications/blob/master/source/transactions-convenient-api/tests/README.md#retry-backoff-is-enforced
- //
+ /**
+ * See
+ * Convenient API Prose Tests.
+ */
@DisplayName("Retry Backoff is Enforced")
@Test
- void testRetryBackoffIsEnforced() {
- MongoDatabase failPointAdminDb = client.getDatabase("admin");
-
- // Test 1: Run with jitter = 0 (no backoff)
+ public void testRetryBackoffIsEnforced() throws InterruptedException {
+ // Run with jitter = 0 (no backoff)
ExponentialBackoff.setTestJitterSupplier(() -> 0.0);
- failPointAdminDb.runCommand(Document.parse("{'configureFailPoint': 'failCommand', 'mode': {'times': 13}, " + "'data': {'failCommands': ['commitTransaction'], 'errorCode': 251}}"));
+ BsonDocument failPointDocument = BsonDocument.parse("{'configureFailPoint': 'failCommand', 'mode': {'times': 13}, "
+ + "'data': {'failCommands': ['commitTransaction'], 'errorCode': 251}}");
long noBackoffTime;
- try (ClientSession session = client.startSession()) {
+ try (ClientSession session = client.startSession();
+ FailPoint ignored = FailPoint.enable(failPointDocument, getPrimary())) {
long startNanos = System.nanoTime();
- session.withTransaction(() -> collection.insertOne(session, Document.parse("{ _id : 'backoff-test-no-jitter' }")));
- noBackoffTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);```
+ session.withTransaction(() -> collection.insertOne(session, Document.parse("{}")));
+ noBackoffTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
} finally {
// Clear the test jitter supplier to avoid affecting other tests
ExponentialBackoff.clearTestJitterSupplier();
- failPointAdminDb.runCommand(Document.parse("{'configureFailPoint': 'failCommand', 'mode': 'off'}"));
}
- // Test 2: Run with jitter = 1 (full backoff)
+ // Run with jitter = 1 (full backoff)
ExponentialBackoff.setTestJitterSupplier(() -> 1.0);
- failPointAdminDb.runCommand(Document.parse("{'configureFailPoint': 'failCommand', 'mode': {'times': 13}, " + "'data': {'failCommands': ['commitTransaction'], 'errorCode': 251}}"));
+ failPointDocument = BsonDocument.parse("{'configureFailPoint': 'failCommand', 'mode': {'times': 13}, "
+ + "'data': {'failCommands': ['commitTransaction'], 'errorCode': 251}}");
long withBackoffTime;
- try (ClientSession session = client.startSession()) {
+ try (ClientSession session = client.startSession();
+ FailPoint ignored = FailPoint.enable(failPointDocument, getPrimary())) {
long startNanos = System.nanoTime();
- session.withTransaction(() -> collection.insertOne(session, Document.parse("{ _id : 'backoff-test-full-jitter' }")));
+ session.withTransaction(() -> collection.insertOne(session, Document.parse("{}")));
withBackoffTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
} finally {
ExponentialBackoff.clearTestJitterSupplier();
- failPointAdminDb.runCommand(Document.parse("{'configureFailPoint': 'failCommand', 'mode': 'off'}"));
}
- long expectedWithBackoffTime = noBackoffTime + 1800; // 1.8 seconds as per spec
+ long expectedWithBackoffTime = noBackoffTime + 1800;
long actualDifference = Math.abs(withBackoffTime - expectedWithBackoffTime);
- assertTrue(actualDifference < 1000, String.format("Expected withBackoffTime to be ~%dms (noBackoffTime %dms + 1800ms), but"
- + " got %dms. Difference: %dms (tolerance: 1000ms per spec)", expectedWithBackoffTime, noBackoffTime, withBackoffTime,
+ assertTrue(actualDifference < 1000, String.format("Expected withBackoffTime to be ~% dms (noBackoffTime %d ms + 1800 ms), but"
+ + " got %d ms. Difference: %d ms (tolerance: 1000 ms per spec)", expectedWithBackoffTime, noBackoffTime, withBackoffTime,
actualDifference));
}
+ /**
+ * This test is not from the specification.
+ */
+ @Test
+ public void testExponentialBackoffOnTransientError() throws InterruptedException {
+ BsonDocument failPointDocument = BsonDocument.parse("{'configureFailPoint': 'failCommand', 'mode': {'times': 3}, "
+ + "'data': {'failCommands': ['insert'], 'errorCode': 112, "
+ + "'errorLabels': ['TransientTransactionError']}}");
+
+ try (ClientSession session = client.startSession();
+ FailPoint ignored = FailPoint.enable(failPointDocument, getPrimary())) {
+ AtomicInteger attemptsCount = new AtomicInteger(0);
+
+ session.withTransaction(() -> {
+ attemptsCount.incrementAndGet(); // Count the attempt before the operation that might fail
+ return collection.insertOne(session, Document.parse("{}"));
+ });
+
+ assertEquals(4, attemptsCount.get(), "Expected 1 initial attempt + 3 retries");
+ }
+ }
+
private boolean canRunTests() {
return isSharded() || isDiscoverableReplicaSet();
}
From 9f34468423d4348aefff8d2ae135ecd2d443b2bd Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Fri, 16 Jan 2026 00:24:22 +0000
Subject: [PATCH 31/72] remove annotation
---
.../src/main/com/mongodb/internal/time/ExponentialBackoff.java | 2 --
.../src/main/com/mongodb/client/internal/ClientSessionImpl.java | 2 +-
2 files changed, 1 insertion(+), 3 deletions(-)
diff --git a/driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java b/driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java
index 2a69e8ba9e2..ed9bba51d7f 100644
--- a/driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java
+++ b/driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java
@@ -16,7 +16,6 @@
package com.mongodb.internal.time;
-import com.mongodb.annotations.NotThreadSafe;
import com.mongodb.internal.VisibleForTesting;
import java.util.concurrent.ThreadLocalRandom;
@@ -27,7 +26,6 @@
/**
* Implements exponential backoff with jitter for retry scenarios.
*/
-@NotThreadSafe
public enum ExponentialBackoff {
TRANSACTION(5.0, 500.0, 1.5);
diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
index fcaea52aaa1..03ef3248c36 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
@@ -277,7 +277,7 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
abortTransaction();
}
if (e instanceof MongoException) {
- lastError = (MongoException) e; // Store last error
+ lastError = (MongoException) e;
if (!(e instanceof MongoOperationTimeoutException)) {
MongoException exceptionToHandle = OperationHelper.unwrap((MongoException) e);
if (exceptionToHandle.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)
From 88071e03710aba8a5ad1015668eb332bc69880ca Mon Sep 17 00:00:00 2001
From: Valentin Kovalenko
Date: Mon, 26 Jan 2026 04:36:28 -0700
Subject: [PATCH 32/72] Fix/improve ClientSessionImpl, remove
ClientSessionClock
---
.../com/mongodb/internal/TimeoutContext.java | 10 +--
.../com/mongodb/internal/connection/Time.java | 4 ++
.../com/mongodb/internal/time/StartTime.java | 2 +-
.../mongodb/internal/time/SystemNanoTime.java | 32 +++++++++
.../com/mongodb/internal/time/TimePoint.java | 4 +-
.../client/internal/ClientSessionClock.java | 41 -----------
.../client/internal/ClientSessionImpl.java | 37 +++++-----
.../ClientSideOperationTimeoutTest.java | 1 -
.../client/WithTransactionProseTest.java | 71 +++++++++++--------
9 files changed, 107 insertions(+), 95 deletions(-)
create mode 100644 driver-core/src/main/com/mongodb/internal/time/SystemNanoTime.java
delete mode 100644 driver-sync/src/main/com/mongodb/client/internal/ClientSessionClock.java
diff --git a/driver-core/src/main/com/mongodb/internal/TimeoutContext.java b/driver-core/src/main/com/mongodb/internal/TimeoutContext.java
index 838c5208807..ee61eacf7d4 100644
--- a/driver-core/src/main/com/mongodb/internal/TimeoutContext.java
+++ b/driver-core/src/main/com/mongodb/internal/TimeoutContext.java
@@ -168,6 +168,10 @@ public Timeout timeoutIncludingRoundTrip() {
return timeout == null ? null : timeout.shortenBy(minRoundTripTimeMS, MILLISECONDS);
}
+ public Timeout timeoutOrAlternative(final Timeout alternative) {
+ return timeout == null ? alternative : timeout;
+ }
+
/**
* Returns the remaining {@code timeoutMS} if set or the {@code alternativeTimeoutMS}.
*
@@ -176,6 +180,7 @@ public Timeout timeoutIncludingRoundTrip() {
* @param alternativeTimeoutMS the alternative timeout.
* @return timeout to use.
*/
+ @VisibleForTesting(otherwise = PRIVATE)
public long timeoutOrAlternative(final long alternativeTimeoutMS) {
if (timeout == null) {
return alternativeTimeoutMS;
@@ -380,11 +385,6 @@ public TimeoutContext withAdditionalReadTimeout(final int additionalReadTimeout)
return new TimeoutContext(timeoutSettings.withReadTimeoutMS(newReadTimeout > 0 ? newReadTimeout : Long.MAX_VALUE));
}
- // Creates a copy of the timeout context that can be reset without resetting the original.
- public TimeoutContext copyTimeoutContext() {
- return new TimeoutContext(getTimeoutSettings(), getTimeout());
- }
-
@Override
public String toString() {
return "TimeoutContext{"
diff --git a/driver-core/src/main/com/mongodb/internal/connection/Time.java b/driver-core/src/main/com/mongodb/internal/connection/Time.java
index e3940adf1de..9b7f935e631 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/Time.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/Time.java
@@ -20,7 +20,11 @@
* To enable unit testing of classes that rely on System.nanoTime
*
* This class is not part of the public API and may be removed or changed at any time
+ *
+ * @deprecated Use {@link com.mongodb.internal.time.SystemNanoTime} in production code,
+ * and {@code Mockito.mockStatic} in test code to tamper with it.
*/
+@Deprecated
public final class Time {
static final long CONSTANT_TIME = 42;
diff --git a/driver-core/src/main/com/mongodb/internal/time/StartTime.java b/driver-core/src/main/com/mongodb/internal/time/StartTime.java
index 1d8f186ab67..650f9a0ebb9 100644
--- a/driver-core/src/main/com/mongodb/internal/time/StartTime.java
+++ b/driver-core/src/main/com/mongodb/internal/time/StartTime.java
@@ -59,6 +59,6 @@ public interface StartTime {
* @return a StartPoint, as of now
*/
static StartTime now() {
- return TimePoint.at(System.nanoTime());
+ return TimePoint.at(SystemNanoTime.get());
}
}
diff --git a/driver-core/src/main/com/mongodb/internal/time/SystemNanoTime.java b/driver-core/src/main/com/mongodb/internal/time/SystemNanoTime.java
new file mode 100644
index 00000000000..f047108d509
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/internal/time/SystemNanoTime.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2008-present MongoDB, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.mongodb.internal.time;
+
+/**
+ * Avoid using this class directly and prefer using other program elements from {@link com.mongodb.internal.time}, if possible.
+ *
+ * We do not use {@link System#nanoTime()} directly in the rest of the {@link com.mongodb.internal.time} package,
+ * and use {@link SystemNanoTime#get()} instead because we need to tamper with it via {@code Mockito.mockStatic},
+ * and mocking methods of {@link System} class is both impossible and unwise.
+ */
+public final class SystemNanoTime {
+ private SystemNanoTime() {
+ }
+
+ public static long get() {
+ return System.nanoTime();
+ }
+}
diff --git a/driver-core/src/main/com/mongodb/internal/time/TimePoint.java b/driver-core/src/main/com/mongodb/internal/time/TimePoint.java
index 811065d13a6..c3b130e584d 100644
--- a/driver-core/src/main/com/mongodb/internal/time/TimePoint.java
+++ b/driver-core/src/main/com/mongodb/internal/time/TimePoint.java
@@ -61,14 +61,14 @@ static TimePoint at(@Nullable final Long nanos) {
@VisibleForTesting(otherwise = PRIVATE)
long currentNanos() {
- return System.nanoTime();
+ return SystemNanoTime.get();
}
/**
* Returns the current {@link TimePoint}.
*/
static TimePoint now() {
- return at(System.nanoTime());
+ return at(SystemNanoTime.get());
}
/**
diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionClock.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionClock.java
deleted file mode 100644
index a5ba63e3cd6..00000000000
--- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionClock.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright 2008-present MongoDB, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.mongodb.client.internal;
-
-/**
- *
This class is not part of the public API and may be removed or changed at any time
- */
-public final class ClientSessionClock {
- public static final ClientSessionClock INSTANCE = new ClientSessionClock(0L);
-
- private long currentTime;
-
- private ClientSessionClock(final long millis) {
- currentTime = millis;
- }
-
- public long now() {
- if (currentTime == 0L) {
- return System.currentTimeMillis();
- }
- return currentTime;
- }
-
- public void setTime(final long millis) {
- currentTime = millis;
- }
-}
diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
index 03ef3248c36..693df7dc7fa 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
@@ -39,8 +39,12 @@
import com.mongodb.internal.session.BaseClientSessionImpl;
import com.mongodb.internal.session.ServerSessionPool;
import com.mongodb.internal.time.ExponentialBackoff;
+import com.mongodb.internal.time.Timeout;
import com.mongodb.lang.Nullable;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BooleanSupplier;
+
import static com.mongodb.MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL;
import static com.mongodb.MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL;
import static com.mongodb.assertions.Assertions.assertNotNull;
@@ -51,7 +55,7 @@
final class ClientSessionImpl extends BaseClientSessionImpl implements ClientSession {
- private static final int MAX_RETRY_TIME_LIMIT_MS = 120000;
+ private static final long MAX_RETRY_TIME_LIMIT_MS = 120000;
private final OperationExecutor operationExecutor;
private TransactionState transactionState = TransactionState.NONE;
@@ -251,9 +255,11 @@ public T withTransaction(final TransactionBody transactionBody) {
@Override
public T withTransaction(final TransactionBody transactionBody, final TransactionOptions options) {
notNull("transactionBody", transactionBody);
- long startTime = ClientSessionClock.INSTANCE.now();
TimeoutContext withTransactionTimeoutContext = createTimeoutContext(options);
- ExponentialBackoff transactionBackoff = ExponentialBackoff.TRANSACTION;
+ Timeout withTransactionTimeout = withTransactionTimeoutContext.timeoutOrAlternative(
+ assertNotNull(TimeoutContext.startTimeout(MAX_RETRY_TIME_LIMIT_MS)));
+ BooleanSupplier withTransactionTimeoutExpired = () -> withTransactionTimeout.call(TimeUnit.MILLISECONDS,
+ () -> false, ms -> false, () -> true);
int transactionAttempt = 0;
MongoException lastError = null;
@@ -261,11 +267,11 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
outer:
while (true) {
if (transactionAttempt > 0) {
- backoff(transactionBackoff, transactionAttempt, startTime, lastError);
+ backoff(transactionAttempt, withTransactionTimeout, assertNotNull(lastError));
}
T retVal;
try {
- startTransaction(options, withTransactionTimeoutContext.copyTimeoutContext());
+ startTransaction(options, withTransactionTimeoutContext);
transactionAttempt++;
if (transactionSpan != null) {
@@ -281,7 +287,7 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
if (!(e instanceof MongoOperationTimeoutException)) {
MongoException exceptionToHandle = OperationHelper.unwrap((MongoException) e);
if (exceptionToHandle.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)
- && ClientSessionClock.INSTANCE.now() - startTime < MAX_RETRY_TIME_LIMIT_MS) {
+ && !withTransactionTimeoutExpired.getAsBoolean()) {
if (transactionSpan != null) {
transactionSpan.spanFinalizing(false);
}
@@ -297,9 +303,10 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
commitTransaction(false);
break;
} catch (MongoException e) {
+ lastError = e;
clearTransactionContextOnError(e);
if (!(e instanceof MongoOperationTimeoutException)
- && ClientSessionClock.INSTANCE.now() - startTime < MAX_RETRY_TIME_LIMIT_MS) {
+ && !withTransactionTimeoutExpired.getAsBoolean()) {
applyMajorityWriteConcernToTransactionOptions();
if (!(e instanceof MongoExecutionTimeoutException)
@@ -309,7 +316,6 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
if (transactionSpan != null) {
transactionSpan.spanFinalizing(true);
}
- lastError = e;
continue outer;
}
}
@@ -374,15 +380,12 @@ private TimeoutContext createTimeoutContext(final TransactionOptions transaction
operationExecutor.getTimeoutSettings()));
}
- private static void backoff(final ExponentialBackoff exponentialBackoff, final int transactionAttempt, final long startTime,
- final MongoException lastError) {
- long backoffMs = exponentialBackoff.calculateDelayBeforeNextRetryMs(transactionAttempt - 1);
- if (ClientSessionClock.INSTANCE.now() + backoffMs - startTime >= MAX_RETRY_TIME_LIMIT_MS) {
- if (lastError != null) {
- throw lastError;
- }
- throw new MongoClientException("Transaction retry timeout exceeded");
- }
+ private static void backoff(final int transactionAttempt,
+ final Timeout withTransactionTimeout, final MongoException lastError) {
+ long backoffMs = ExponentialBackoff.TRANSACTION.calculateDelayBeforeNextRetryMs(transactionAttempt - 1);
+ withTransactionTimeout.shortenBy(backoffMs, TimeUnit.MILLISECONDS).onExpired(() -> {
+ throw lastError;
+ });
try {
if (backoffMs > 0) {
Thread.sleep(backoffMs);
diff --git a/driver-sync/src/test/functional/com/mongodb/client/ClientSideOperationTimeoutTest.java b/driver-sync/src/test/functional/com/mongodb/client/ClientSideOperationTimeoutTest.java
index cb62545f4e4..9fc2f0e6acc 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/ClientSideOperationTimeoutTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/ClientSideOperationTimeoutTest.java
@@ -23,7 +23,6 @@
import static org.junit.jupiter.api.Assumptions.assumeFalse;
-
// See https://github.com/mongodb/specifications/tree/master/source/client-side-operation-timeout/tests
public class ClientSideOperationTimeoutTest extends UnifiedSyncTest {
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index e2dce11583f..514dbc4a2e6 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -20,17 +20,22 @@
import com.mongodb.MongoClientException;
import com.mongodb.MongoException;
import com.mongodb.TransactionOptions;
-import com.mongodb.client.internal.ClientSessionClock;
import com.mongodb.client.model.Sorts;
import com.mongodb.internal.time.ExponentialBackoff;
+import com.mongodb.internal.time.StartTime;
+import com.mongodb.internal.time.SystemNanoTime;
import org.bson.BsonDocument;
import org.bson.Document;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
import static com.mongodb.ClusterFixture.TIMEOUT;
import static com.mongodb.ClusterFixture.isDiscoverableReplicaSet;
@@ -44,8 +49,7 @@
// See https://github.com/mongodb/specifications/blob/master/source/transactions-convenient-api/tests/README.md#prose-tests
public class WithTransactionProseTest extends DatabaseTestCase {
- private static final long START_TIME_MS = 1L;
- private static final long ERROR_GENERATING_INTERVAL = 121000L;
+ private static final Duration ERROR_GENERATING_INTERVAL = Duration.ofSeconds(120);
@BeforeEach
@Override
@@ -66,7 +70,7 @@ public void setUp() {
public void testCallbackRaisesCustomError() {
final String exceptionMessage = "NotTransientOrUnknownError";
try (ClientSession session = client.startSession()) {
- session.withTransaction((TransactionBody) () -> {
+ session.withTransaction(() -> {
throw new MongoException(exceptionMessage);
});
// should not get here
@@ -101,13 +105,13 @@ public void testRetryTimeoutEnforcedTransientTransactionError() {
final String errorMessage = "transient transaction error";
try (ClientSession session = client.startSession()) {
- ClientSessionClock.INSTANCE.setTime(START_TIME_MS);
- session.withTransaction((TransactionBody) () -> {
- ClientSessionClock.INSTANCE.setTime(ERROR_GENERATING_INTERVAL);
- MongoException e = new MongoException(112, errorMessage);
- e.addLabel(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL);
- throw e;
- });
+ doWithSystemNanoTimeHandle(systemNanoTimeHandle ->
+ session.withTransaction(() -> {
+ systemNanoTimeHandle.setRelativeToStart(ERROR_GENERATING_INTERVAL);
+ MongoException e = new MongoException(112, errorMessage);
+ e.addLabel(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL);
+ throw e;
+ }));
fail("Test should have thrown an exception.");
} catch (Exception e) {
assertEquals(errorMessage, e.getMessage());
@@ -127,12 +131,12 @@ public void testRetryTimeoutEnforcedUnknownTransactionCommit() {
+ "'data': {'failCommands': ['commitTransaction'], 'errorCode': 91, 'closeConnection': false}}"));
try (ClientSession session = client.startSession()) {
- ClientSessionClock.INSTANCE.setTime(START_TIME_MS);
- session.withTransaction((TransactionBody) () -> {
- ClientSessionClock.INSTANCE.setTime(ERROR_GENERATING_INTERVAL);
- collection.insertOne(session, new Document("_id", 2));
- return null;
- });
+ doWithSystemNanoTimeHandle(systemNanoTimeHandle ->
+ session.withTransaction(() -> {
+ systemNanoTimeHandle.setRelativeToStart(ERROR_GENERATING_INTERVAL);
+ collection.insertOne(session, new Document("_id", 2));
+ return null;
+ }));
fail("Test should have thrown an exception.");
} catch (Exception e) {
assertEquals(91, ((MongoException) e).getCode());
@@ -156,12 +160,12 @@ public void testRetryTimeoutEnforcedTransientTransactionErrorOnCommit() {
+ "'errmsg': 'Transaction 0 has been aborted', 'closeConnection': false}}"));
try (ClientSession session = client.startSession()) {
- ClientSessionClock.INSTANCE.setTime(START_TIME_MS);
- session.withTransaction((TransactionBody) () -> {
- ClientSessionClock.INSTANCE.setTime(ERROR_GENERATING_INTERVAL);
- collection.insertOne(session, Document.parse("{ _id : 1 }"));
- return null;
- });
+ doWithSystemNanoTimeHandle(systemNanoTimeHandle ->
+ session.withTransaction(() -> {
+ systemNanoTimeHandle.setRelativeToStart(ERROR_GENERATING_INTERVAL);
+ collection.insertOne(session, Document.parse("{ _id : 1 }"));
+ return null;
+ }));
fail("Test should have thrown an exception.");
} catch (Exception e) {
assertEquals(251, ((MongoException) e).getCode());
@@ -224,9 +228,9 @@ public void testRetryBackoffIsEnforced() throws InterruptedException {
long noBackoffTime;
try (ClientSession session = client.startSession();
FailPoint ignored = FailPoint.enable(failPointDocument, getPrimary())) {
- long startNanos = System.nanoTime();
+ StartTime startTime = StartTime.now();
session.withTransaction(() -> collection.insertOne(session, Document.parse("{}")));
- noBackoffTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
+ noBackoffTime = startTime.elapsed().toMillis();
} finally {
// Clear the test jitter supplier to avoid affecting other tests
ExponentialBackoff.clearTestJitterSupplier();
@@ -241,9 +245,9 @@ public void testRetryBackoffIsEnforced() throws InterruptedException {
long withBackoffTime;
try (ClientSession session = client.startSession();
FailPoint ignored = FailPoint.enable(failPointDocument, getPrimary())) {
- long startNanos = System.nanoTime();
+ StartTime startTime = StartTime.now();
session.withTransaction(() -> collection.insertOne(session, Document.parse("{}")));
- withBackoffTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
+ withBackoffTime = startTime.elapsed().toMillis();
} finally {
ExponentialBackoff.clearTestJitterSupplier();
}
@@ -278,7 +282,18 @@ public void testExponentialBackoffOnTransientError() throws InterruptedException
}
}
- private boolean canRunTests() {
+ private static boolean canRunTests() {
return isSharded() || isDiscoverableReplicaSet();
}
+
+ private static void doWithSystemNanoTimeHandle(final Consumer action) {
+ long startNanos = SystemNanoTime.get();
+ try (MockedStatic mockedStaticSystem = Mockito.mockStatic(SystemNanoTime.class)) {
+ action.accept(change -> mockedStaticSystem.when(SystemNanoTime::get).thenReturn(startNanos + change.toNanos()));
+ }
+ }
+
+ private interface SystemNanoTimeHandle {
+ void setRelativeToStart(Duration change);
+ }
}
From e59c4cb3265ef1632976411843281b0da483003a Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Thu, 29 Jan 2026 00:03:33 +0000
Subject: [PATCH 33/72] PR feedback
---
.../internal/time/ExponentialBackoff.java | 42 +++++---------
.../internal/ExponentialBackoffTest.java | 55 +++++++++++--------
.../client/internal/ClientSessionImpl.java | 2 +-
.../client/WithTransactionProseTest.java | 5 +-
4 files changed, 50 insertions(+), 54 deletions(-)
diff --git a/driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java b/driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java
index ed9bba51d7f..4f0d7749caa 100644
--- a/driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java
+++ b/driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java
@@ -24,46 +24,34 @@
import static com.mongodb.internal.VisibleForTesting.AccessModifier.PRIVATE;
/**
- * Implements exponential backoff with jitter for retry scenarios.
+ * Provides exponential backoff calculations with jitter for retry scenarios.
*/
-public enum ExponentialBackoff {
- TRANSACTION(5.0, 500.0, 1.5);
+public final class ExponentialBackoff {
- private final double baseMs, maxMs, growth;
+ // Constants for transaction retry backoff
+ private static final double TRANSACTION_BASE_MS = 5.0;
+ private static final double TRANSACTION_MAX_MS = 500.0;
+ private static final double TRANSACTION_GROWTH = 1.5;
- // TODO remove this global state once https://jira.mongodb.org/browse/JAVA-6060 is done
+ // TODO-JAVA-6079
private static DoubleSupplier testJitterSupplier = null;
- ExponentialBackoff(final double baseMs, final double maxMs, final double growth) {
- this.baseMs = baseMs;
- this.maxMs = maxMs;
- this.growth = growth;
+ private ExponentialBackoff() {
}
/**
- * Calculate the next delay in milliseconds based on the retry count.
+ * Calculate the backoff in milliseconds for transaction retries.
*
- * @param retryCount The number of retries that have occurred.
- * @return The calculated delay in milliseconds.
+ * @param attemptNumber The 0-based attempt number
+ * @return The calculated backoff in milliseconds.
*/
- public long calculateDelayBeforeNextRetryMs(final int retryCount) {
+ public static long calculateTransactionBackoffMs(final int attemptNumber) {
double jitter = testJitterSupplier != null
? testJitterSupplier.getAsDouble()
: ThreadLocalRandom.current().nextDouble();
- double backoff = Math.min(baseMs * Math.pow(growth, retryCount), maxMs);
- return Math.round(jitter * backoff);
- }
-
- /**
- * Calculate the next delay in milliseconds based on the retry count and a provided jitter.
- *
- * @param retryCount The number of retries that have occurred.
- * @param jitter A double in the range [0, 1) to apply as jitter.
- * @return The calculated delay in milliseconds.
- */
- public long calculateDelayBeforeNextRetryMs(final int retryCount, final double jitter) {
- double backoff = Math.min(baseMs * Math.pow(growth, retryCount), maxMs);
- return Math.round(jitter * backoff);
+ return Math.round(jitter * Math.min(
+ TRANSACTION_BASE_MS * Math.pow(TRANSACTION_GROWTH, attemptNumber),
+ TRANSACTION_MAX_MS));
}
/**
diff --git a/driver-core/src/test/unit/com/mongodb/internal/ExponentialBackoffTest.java b/driver-core/src/test/unit/com/mongodb/internal/ExponentialBackoffTest.java
index 67238532488..0dbc5e48995 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/ExponentialBackoffTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/ExponentialBackoffTest.java
@@ -31,43 +31,50 @@ void testTransactionRetryBackoff() {
// With jitter, actual values will be between 0 and these maxima
double[] expectedMaxValues = {5.0, 7.5, 11.25, 16.875, 25.3125, 37.96875, 56.953125, 85.4296875, 128.14453125, 192.21679688, 288.32519531, 432.48779297, 500.0};
- ExponentialBackoff backoff = ExponentialBackoff.TRANSACTION;
- for (int retry = 0; retry < expectedMaxValues.length; retry++) {
- long delay = backoff.calculateDelayBeforeNextRetryMs(retry);
- assertTrue(delay >= 0 && delay <= Math.round(expectedMaxValues[retry]), String.format("Retry %d: delay should be 0-%d ms, got: %d", retry, Math.round(expectedMaxValues[retry]), delay));
+ for (int attemptNumber = 0; attemptNumber < expectedMaxValues.length; attemptNumber++) {
+ long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
+ assertTrue(backoff >= 0 && backoff <= Math.round(expectedMaxValues[attemptNumber]),
+ String.format("Attempt %d: backoff should be 0-%d ms, got: %d", attemptNumber, Math.round(expectedMaxValues[attemptNumber]), backoff));
}
}
@Test
void testTransactionRetryBackoffRespectsMaximum() {
- ExponentialBackoff backoff = ExponentialBackoff.TRANSACTION;
-
- // Even at high retry counts, delay should never exceed 500ms
- for (int retry = 0; retry < 25; retry++) {
- long delay = backoff.calculateDelayBeforeNextRetryMs(retry);
- assertTrue(delay >= 0 && delay <= 500, String.format("Retry %d: delay should be capped at 500 ms, got: %d ms", retry, delay));
+ // Even at high attempt numbers, backoff should never exceed 500ms
+ for (int attemptNumber = 0; attemptNumber < 25; attemptNumber++) {
+ long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
+ assertTrue(backoff >= 0 && backoff <= 500,
+ String.format("Attempt %d: backoff should be capped at 500 ms, got: %d ms", attemptNumber, backoff));
}
}
@Test
void testCustomJitter() {
- ExponentialBackoff backoff = ExponentialBackoff.TRANSACTION;
-
- // Expected delays with jitter=1.0 and growth factor 1.5
- double[] expectedDelays = {5.0, 7.5, 11.25, 16.875, 25.3125, 37.96875, 56.953125, 85.4296875, 128.14453125, 192.21679688, 288.32519531, 432.48779297, 500.0};
- double jitter = 1.0;
+ // Expected backoffs with jitter=1.0 and growth factor 1.5
+ double[] expectedBackoffs = {5.0, 7.5, 11.25, 16.875, 25.3125, 37.96875, 56.953125, 85.4296875, 128.14453125, 192.21679688, 288.32519531, 432.48779297, 500.0};
- for (int retry = 0; retry < expectedDelays.length; retry++) {
- long delay = backoff.calculateDelayBeforeNextRetryMs(retry, jitter);
- long expected = Math.round(expectedDelays[retry]);
- assertEquals(expected, delay, String.format("Retry %d: with jitter=1.0, delay should be %d ms", retry, expected));
+ // Test with jitter = 1.0
+ ExponentialBackoff.setTestJitterSupplier(() -> 1.0);
+ try {
+ for (int attemptNumber = 0; attemptNumber < expectedBackoffs.length; attemptNumber++) {
+ long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
+ long expected = Math.round(expectedBackoffs[attemptNumber]);
+ assertEquals(expected, backoff,
+ String.format("Attempt %d: with jitter=1.0, backoff should be %d ms", attemptNumber, expected));
+ }
+ } finally {
+ ExponentialBackoff.clearTestJitterSupplier();
}
- // With jitter = 0, all delays should be 0
- jitter = 0;
- for (int retry = 0; retry < 10; retry++) {
- long delay = backoff.calculateDelayBeforeNextRetryMs(retry, jitter);
- assertEquals(0, delay, "With jitter=0, delay should always be 0 ms");
+ // Test with jitter = 0, all backoffs should be 0
+ ExponentialBackoff.setTestJitterSupplier(() -> 0.0);
+ try {
+ for (int attemptNumber = 0; attemptNumber < 10; attemptNumber++) {
+ long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
+ assertEquals(0, backoff, "With jitter=0, backoff should always be 0 ms");
+ }
+ } finally {
+ ExponentialBackoff.clearTestJitterSupplier();
}
}
}
diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
index 693df7dc7fa..6ab19c9811e 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
@@ -382,7 +382,7 @@ private TimeoutContext createTimeoutContext(final TransactionOptions transaction
private static void backoff(final int transactionAttempt,
final Timeout withTransactionTimeout, final MongoException lastError) {
- long backoffMs = ExponentialBackoff.TRANSACTION.calculateDelayBeforeNextRetryMs(transactionAttempt - 1);
+ long backoffMs = ExponentialBackoff.calculateTransactionBackoffMs(transactionAttempt - 1);
withTransactionTimeout.shortenBy(backoffMs, TimeUnit.MILLISECONDS).onExpired(() -> {
throw lastError;
});
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index 514dbc4a2e6..a4bfdfa33f8 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -288,8 +288,9 @@ private static boolean canRunTests() {
private static void doWithSystemNanoTimeHandle(final Consumer action) {
long startNanos = SystemNanoTime.get();
- try (MockedStatic mockedStaticSystem = Mockito.mockStatic(SystemNanoTime.class)) {
- action.accept(change -> mockedStaticSystem.when(SystemNanoTime::get).thenReturn(startNanos + change.toNanos()));
+ try (MockedStatic mockedStaticSystemNanoTime = Mockito.mockStatic(SystemNanoTime.class)) {
+ mockedStaticSystemNanoTime.when(SystemNanoTime::get).thenReturn(startNanos);
+ action.accept(change -> mockedStaticSystemNanoTime.when(SystemNanoTime::get).thenReturn(startNanos + change.toNanos()));
}
}
From 9a925e1de68db995b40b1d42e1946a7f9529de69 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Thu, 29 Jan 2026 01:07:39 +0000
Subject: [PATCH 34/72] Reduce flakiness in CSOT tests using withTransaction,
by forcing an immediate retry without backoff.
---
...stractClientSideOperationsTimeoutProseTest.java | 14 ++++++++++----
1 file changed, 10 insertions(+), 4 deletions(-)
diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
index 7828ecde684..28f96c65f44 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
@@ -47,14 +47,11 @@
import com.mongodb.event.ConnectionClosedEvent;
import com.mongodb.event.ConnectionCreatedEvent;
import com.mongodb.event.ConnectionReadyEvent;
-
-import static com.mongodb.internal.connection.CommandHelper.HELLO;
-import static com.mongodb.internal.connection.CommandHelper.LEGACY_HELLO;
-
import com.mongodb.internal.connection.InternalStreamConnection;
import com.mongodb.internal.connection.ServerHelper;
import com.mongodb.internal.connection.TestCommandListener;
import com.mongodb.internal.connection.TestConnectionPoolListener;
+import com.mongodb.internal.time.ExponentialBackoff;
import com.mongodb.test.FlakyTest;
import org.bson.BsonDocument;
import org.bson.BsonInt32;
@@ -90,6 +87,8 @@
import static com.mongodb.ClusterFixture.sleep;
import static com.mongodb.client.Fixture.getDefaultDatabaseName;
import static com.mongodb.client.Fixture.getPrimary;
+import static com.mongodb.internal.connection.CommandHelper.HELLO;
+import static com.mongodb.internal.connection.CommandHelper.LEGACY_HELLO;
import static java.lang.Long.MAX_VALUE;
import static java.lang.String.join;
import static java.util.Arrays.asList;
@@ -1121,6 +1120,11 @@ public void setUp() {
filesCollectionHelper = new CollectionHelper<>(new BsonDocumentCodec(), gridFsFileNamespace);
chunksCollectionHelper = new CollectionHelper<>(new BsonDocumentCodec(), gridFsChunksNamespace);
commandListener = new TestCommandListener();
+
+ // setting jitter to 0 to make test using withTransaction deterministic (i.e retries immediately) otherwise we might get
+ // MongoCommandException setup in the failpoint instead of MongoOperationTimeoutException depending on the random jitter value.
+ ExponentialBackoff.setTestJitterSupplier(() -> 1.0);
+
}
@AfterEach
@@ -1146,6 +1150,8 @@ public void tearDown() throws InterruptedException {
//noinspection ResultOfMethodCallIgnored
executor.awaitTermination(MAX_VALUE, NANOSECONDS);
}
+
+ ExponentialBackoff.clearTestJitterSupplier();
}
@AfterAll
From 4bbef70f4e5e8340429787a4ec5a4c1cf51aa34d Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Thu, 29 Jan 2026 01:37:22 +0000
Subject: [PATCH 35/72] Using correct jitter value in CSOT tests using
withTransaction
---
.../client/AbstractClientSideOperationsTimeoutProseTest.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
index 28f96c65f44..01ac621781d 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
@@ -1123,7 +1123,7 @@ public void setUp() {
// setting jitter to 0 to make test using withTransaction deterministic (i.e retries immediately) otherwise we might get
// MongoCommandException setup in the failpoint instead of MongoOperationTimeoutException depending on the random jitter value.
- ExponentialBackoff.setTestJitterSupplier(() -> 1.0);
+ ExponentialBackoff.setTestJitterSupplier(() -> 0);
}
From 1c97dc7a23c89cd188715977dc771972d5a1dad6 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Thu, 29 Jan 2026 14:19:25 +0000
Subject: [PATCH 36/72] Guarding setting a custom jitter for only sync tests
---
...bstractClientSideOperationsTimeoutProseTest.java | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
index 01ac621781d..6eebcfc0089 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
@@ -1121,10 +1121,11 @@ public void setUp() {
chunksCollectionHelper = new CollectionHelper<>(new BsonDocumentCodec(), gridFsChunksNamespace);
commandListener = new TestCommandListener();
- // setting jitter to 0 to make test using withTransaction deterministic (i.e retries immediately) otherwise we might get
- // MongoCommandException setup in the failpoint instead of MongoOperationTimeoutException depending on the random jitter value.
- ExponentialBackoff.setTestJitterSupplier(() -> 0);
-
+ if (!isAsync()) {
+ // setting jitter to 0 to make test using withTransaction deterministic (i.e retries immediately) otherwise we might get
+ // MongoCommandException setup in the failpoint instead of MongoOperationTimeoutException depending on the random jitter value.
+ ExponentialBackoff.setTestJitterSupplier(() -> 0);
+ }
}
@AfterEach
@@ -1151,7 +1152,9 @@ public void tearDown() throws InterruptedException {
executor.awaitTermination(MAX_VALUE, NANOSECONDS);
}
- ExponentialBackoff.clearTestJitterSupplier();
+ if (!isAsync()) {
+ ExponentialBackoff.clearTestJitterSupplier();
+ }
}
@AfterAll
From e32f90df697b5ba4b21967c3f09ee1694592d036 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Mon, 2 Feb 2026 13:35:24 +0000
Subject: [PATCH 37/72] PR feedback
---
.../internal/time/ExponentialBackoff.java | 4 ++--
.../{ => time}/ExponentialBackoffTest.java | 16 ++++++++--------
.../client/internal/ClientSessionImpl.java | 2 +-
3 files changed, 11 insertions(+), 11 deletions(-)
rename driver-core/src/test/unit/com/mongodb/internal/{ => time}/ExponentialBackoffTest.java (87%)
diff --git a/driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java b/driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java
index 4f0d7749caa..33d7148550d 100644
--- a/driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java
+++ b/driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java
@@ -42,7 +42,7 @@ private ExponentialBackoff() {
/**
* Calculate the backoff in milliseconds for transaction retries.
*
- * @param attemptNumber The 0-based attempt number
+ * @param attemptNumber The attempt number
* @return The calculated backoff in milliseconds.
*/
public static long calculateTransactionBackoffMs(final int attemptNumber) {
@@ -50,7 +50,7 @@ public static long calculateTransactionBackoffMs(final int attemptNumber) {
? testJitterSupplier.getAsDouble()
: ThreadLocalRandom.current().nextDouble();
return Math.round(jitter * Math.min(
- TRANSACTION_BASE_MS * Math.pow(TRANSACTION_GROWTH, attemptNumber),
+ TRANSACTION_BASE_MS * Math.pow(TRANSACTION_GROWTH, attemptNumber - 1),
TRANSACTION_MAX_MS));
}
diff --git a/driver-core/src/test/unit/com/mongodb/internal/ExponentialBackoffTest.java b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
similarity index 87%
rename from driver-core/src/test/unit/com/mongodb/internal/ExponentialBackoffTest.java
rename to driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
index 0dbc5e48995..68b88666ebd 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/ExponentialBackoffTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
@@ -14,9 +14,8 @@
* limitations under the License.
*/
-package com.mongodb.internal;
+package com.mongodb.internal.time;
-import com.mongodb.internal.time.ExponentialBackoff;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -31,17 +30,18 @@ void testTransactionRetryBackoff() {
// With jitter, actual values will be between 0 and these maxima
double[] expectedMaxValues = {5.0, 7.5, 11.25, 16.875, 25.3125, 37.96875, 56.953125, 85.4296875, 128.14453125, 192.21679688, 288.32519531, 432.48779297, 500.0};
- for (int attemptNumber = 0; attemptNumber < expectedMaxValues.length; attemptNumber++) {
+ for (int attemptNumber = 1; attemptNumber <= expectedMaxValues.length; attemptNumber++) {
long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
- assertTrue(backoff >= 0 && backoff <= Math.round(expectedMaxValues[attemptNumber]),
- String.format("Attempt %d: backoff should be 0-%d ms, got: %d", attemptNumber, Math.round(expectedMaxValues[attemptNumber]), backoff));
+ assertTrue(backoff >= 0 && backoff <= Math.round(expectedMaxValues[attemptNumber - 1]),
+ String.format("Attempt %d: backoff should be 0-%d ms, got: %d", attemptNumber,
+ Math.round(expectedMaxValues[attemptNumber - 1]), backoff));
}
}
@Test
void testTransactionRetryBackoffRespectsMaximum() {
// Even at high attempt numbers, backoff should never exceed 500ms
- for (int attemptNumber = 0; attemptNumber < 25; attemptNumber++) {
+ for (int attemptNumber = 1; attemptNumber < 26; attemptNumber++) {
long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
assertTrue(backoff >= 0 && backoff <= 500,
String.format("Attempt %d: backoff should be capped at 500 ms, got: %d ms", attemptNumber, backoff));
@@ -56,9 +56,9 @@ void testCustomJitter() {
// Test with jitter = 1.0
ExponentialBackoff.setTestJitterSupplier(() -> 1.0);
try {
- for (int attemptNumber = 0; attemptNumber < expectedBackoffs.length; attemptNumber++) {
+ for (int attemptNumber = 1; attemptNumber <= expectedBackoffs.length; attemptNumber++) {
long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
- long expected = Math.round(expectedBackoffs[attemptNumber]);
+ long expected = Math.round(expectedBackoffs[attemptNumber - 1]);
assertEquals(expected, backoff,
String.format("Attempt %d: with jitter=1.0, backoff should be %d ms", attemptNumber, expected));
}
diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
index 6ab19c9811e..f7d5c770eaa 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
@@ -382,7 +382,7 @@ private TimeoutContext createTimeoutContext(final TransactionOptions transaction
private static void backoff(final int transactionAttempt,
final Timeout withTransactionTimeout, final MongoException lastError) {
- long backoffMs = ExponentialBackoff.calculateTransactionBackoffMs(transactionAttempt - 1);
+ long backoffMs = ExponentialBackoff.calculateTransactionBackoffMs(transactionAttempt);
withTransactionTimeout.shortenBy(backoffMs, TimeUnit.MILLISECONDS).onExpired(() -> {
throw lastError;
});
From 0b3ee81223e645b86a02209bce6a68b605cb376e Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Mon, 2 Feb 2026 16:26:51 +0000
Subject: [PATCH 38/72] PR feedback
---
.../main/com/mongodb/internal/time/ExponentialBackoff.java | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java b/driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java
index 33d7148550d..4b59b1f41d2 100644
--- a/driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java
+++ b/driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java
@@ -21,6 +21,7 @@
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.DoubleSupplier;
+import static com.mongodb.assertions.Assertions.assertTrue;
import static com.mongodb.internal.VisibleForTesting.AccessModifier.PRIVATE;
/**
@@ -42,10 +43,11 @@ private ExponentialBackoff() {
/**
* Calculate the backoff in milliseconds for transaction retries.
*
- * @param attemptNumber The attempt number
+ * @param attemptNumber 0-based attempt number
* @return The calculated backoff in milliseconds.
*/
public static long calculateTransactionBackoffMs(final int attemptNumber) {
+ assertTrue(attemptNumber > 0, "Attempt number must be greater than 0 in the context of transaction backoff calculation");
double jitter = testJitterSupplier != null
? testJitterSupplier.getAsDouble()
: ThreadLocalRandom.current().nextDouble();
From fda498c4317328466adfbf1dabc60dd4d867ba91 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Mon, 2 Feb 2026 17:55:26 +0000
Subject: [PATCH 39/72] Fixing unit test
---
.../unit/com/mongodb/internal/time/ExponentialBackoffTest.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
index 68b88666ebd..ba0f4804994 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
@@ -69,7 +69,7 @@ void testCustomJitter() {
// Test with jitter = 0, all backoffs should be 0
ExponentialBackoff.setTestJitterSupplier(() -> 0.0);
try {
- for (int attemptNumber = 0; attemptNumber < 10; attemptNumber++) {
+ for (int attemptNumber = 1; attemptNumber < 11; attemptNumber++) {
long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
assertEquals(0, backoff, "With jitter=0, backoff should always be 0 ms");
}
From e5ae458e2807244ee22f59ec3c96e01aef7e00b4 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Thu, 12 Feb 2026 18:36:06 +0000
Subject: [PATCH 40/72] - PR feedback - Adding logic for DRIVERS-3391
---
.../internal/time/ExponentialBackoff.java | 9 ++--
.../internal/time/ExponentialBackoffTest.java | 31 +++++------
.../ClientSideOperationTimeoutProseTest.java | 3 ++
.../client/internal/ClientSessionImpl.java | 54 ++++++++++++-------
...tClientSideOperationsTimeoutProseTest.java | 10 ----
.../client/WithTransactionProseTest.java | 49 ++++++-----------
6 files changed, 77 insertions(+), 79 deletions(-)
diff --git a/driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java b/driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java
index 4b59b1f41d2..5f63d5c01c8 100644
--- a/driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java
+++ b/driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java
@@ -30,9 +30,12 @@
public final class ExponentialBackoff {
// Constants for transaction retry backoff
- private static final double TRANSACTION_BASE_MS = 5.0;
- private static final double TRANSACTION_MAX_MS = 500.0;
- private static final double TRANSACTION_GROWTH = 1.5;
+ @VisibleForTesting(otherwise = PRIVATE)
+ static final double TRANSACTION_BASE_MS = 5.0;
+ @VisibleForTesting(otherwise = PRIVATE)
+ static final double TRANSACTION_MAX_MS = 500.0;
+ @VisibleForTesting(otherwise = PRIVATE)
+ static final double TRANSACTION_GROWTH = 1.5;
// TODO-JAVA-6079
private static DoubleSupplier testJitterSupplier = null;
diff --git a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
index ba0f4804994..70967c9ba19 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
@@ -22,43 +22,44 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
public class ExponentialBackoffTest {
+ // Expected backoffs with jitter=1.0 and growth factor ExponentialBackoff.TRANSACTION_GROWTH
+ 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,
+ 192.21679688, 288.32519531, 432.48779297, 500.0};
@Test
- void testTransactionRetryBackoff() {
- // Test that the backoff sequence follows the expected pattern with growth factor 1.5
+ void testCalculateTransactionBackoffMs() {
+ // Test that the backoff sequence follows the expected pattern with growth factor ExponentialBackoff.TRANSACTION_GROWTH
// Expected sequence (without jitter): 5, 7.5, 11.25, ...
// With jitter, actual values will be between 0 and these maxima
- double[] expectedMaxValues = {5.0, 7.5, 11.25, 16.875, 25.3125, 37.96875, 56.953125, 85.4296875, 128.14453125, 192.21679688, 288.32519531, 432.48779297, 500.0};
- for (int attemptNumber = 1; attemptNumber <= expectedMaxValues.length; attemptNumber++) {
+ for (int attemptNumber = 1; attemptNumber <= EXPECTED_BACKOFFS_MAX_VALUES.length; attemptNumber++) {
long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
- assertTrue(backoff >= 0 && backoff <= Math.round(expectedMaxValues[attemptNumber - 1]),
+ long expectedBackoff = Math.round(EXPECTED_BACKOFFS_MAX_VALUES[attemptNumber - 1]);
+ assertTrue(backoff >= 0 && backoff <= expectedBackoff,
String.format("Attempt %d: backoff should be 0-%d ms, got: %d", attemptNumber,
- Math.round(expectedMaxValues[attemptNumber - 1]), backoff));
+ expectedBackoff, backoff));
}
}
@Test
- void testTransactionRetryBackoffRespectsMaximum() {
- // Even at high attempt numbers, backoff should never exceed 500ms
+ void testCalculateTransactionBackoffMsRespectsMaximum() {
+ // Even at high attempt numbers, backoff should never exceed ExponentialBackoff.TRANSACTION_MAX_MS
for (int attemptNumber = 1; attemptNumber < 26; attemptNumber++) {
long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
- assertTrue(backoff >= 0 && backoff <= 500,
- String.format("Attempt %d: backoff should be capped at 500 ms, got: %d ms", attemptNumber, backoff));
+ assertTrue(backoff >= 0 && backoff <= ExponentialBackoff.TRANSACTION_MAX_MS,
+ String.format("Attempt %d: backoff should be capped at %f ms, got: %d ms",
+ attemptNumber, ExponentialBackoff.TRANSACTION_MAX_MS, backoff));
}
}
@Test
void testCustomJitter() {
- // Expected backoffs with jitter=1.0 and growth factor 1.5
- double[] expectedBackoffs = {5.0, 7.5, 11.25, 16.875, 25.3125, 37.96875, 56.953125, 85.4296875, 128.14453125, 192.21679688, 288.32519531, 432.48779297, 500.0};
-
// Test with jitter = 1.0
ExponentialBackoff.setTestJitterSupplier(() -> 1.0);
try {
- for (int attemptNumber = 1; attemptNumber <= expectedBackoffs.length; attemptNumber++) {
+ for (int attemptNumber = 1; attemptNumber <= EXPECTED_BACKOFFS_MAX_VALUES.length; attemptNumber++) {
long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
- long expected = Math.round(expectedBackoffs[attemptNumber - 1]);
+ long expected = Math.round(EXPECTED_BACKOFFS_MAX_VALUES[attemptNumber - 1]);
assertEquals(expected, backoff,
String.format("Attempt %d: with jitter=1.0, backoff should be %d ms", attemptNumber, expected));
}
diff --git a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientSideOperationTimeoutProseTest.java b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientSideOperationTimeoutProseTest.java
index 90446953fc1..6494cde74aa 100644
--- a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientSideOperationTimeoutProseTest.java
+++ b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientSideOperationTimeoutProseTest.java
@@ -26,6 +26,7 @@
import com.mongodb.client.model.changestream.FullDocument;
import com.mongodb.event.CommandFailedEvent;
import com.mongodb.event.CommandStartedEvent;
+import com.mongodb.internal.time.ExponentialBackoff;
import com.mongodb.reactivestreams.client.gridfs.GridFSBucket;
import com.mongodb.reactivestreams.client.gridfs.GridFSBuckets;
import com.mongodb.reactivestreams.client.syncadapter.SyncGridFSBucket;
@@ -578,6 +579,7 @@ private void assertOnlyOneCommandTimeoutFailure(final String command) {
public void setUp() {
super.setUp();
SyncMongoClient.enableSleepAfterSessionClose(postSessionCloseSleep());
+ ExponentialBackoff.setTestJitterSupplier(() -> 0);
}
@Override
@@ -585,6 +587,7 @@ public void setUp() {
public void tearDown() throws InterruptedException {
super.tearDown();
SyncMongoClient.disableSleep();
+ ExponentialBackoff.clearTestJitterSupplier();
}
@Override
diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
index f7d5c770eaa..18f782728ac 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
@@ -22,6 +22,7 @@
import com.mongodb.MongoExecutionTimeoutException;
import com.mongodb.MongoInternalException;
import com.mongodb.MongoOperationTimeoutException;
+import com.mongodb.MongoTimeoutException;
import com.mongodb.ReadConcern;
import com.mongodb.TransactionOptions;
import com.mongodb.WriteConcern;
@@ -51,6 +52,7 @@
import static com.mongodb.assertions.Assertions.assertTrue;
import static com.mongodb.assertions.Assertions.isTrue;
import static com.mongodb.assertions.Assertions.notNull;
+import static com.mongodb.internal.TimeoutContext.createMongoTimeoutException;
import static com.mongodb.internal.thread.InterruptionUtil.interruptAndCreateMongoInterruptedException;
final class ClientSessionImpl extends BaseClientSessionImpl implements ClientSession {
@@ -256,6 +258,7 @@ public T withTransaction(final TransactionBody transactionBody) {
public T withTransaction(final TransactionBody transactionBody, final TransactionOptions options) {
notNull("transactionBody", transactionBody);
TimeoutContext withTransactionTimeoutContext = createTimeoutContext(options);
+ final boolean hasTimeoutMS = withTransactionTimeoutContext.hasTimeoutMS();
Timeout withTransactionTimeout = withTransactionTimeoutContext.timeoutOrAlternative(
assertNotNull(TimeoutContext.startTimeout(MAX_RETRY_TIME_LIMIT_MS)));
BooleanSupplier withTransactionTimeoutExpired = () -> withTransactionTimeout.call(TimeUnit.MILLISECONDS,
@@ -267,7 +270,7 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
outer:
while (true) {
if (transactionAttempt > 0) {
- backoff(transactionAttempt, withTransactionTimeout, assertNotNull(lastError));
+ backoff(transactionAttempt, withTransactionTimeout, assertNotNull(lastError), hasTimeoutMS);
}
T retVal;
try {
@@ -286,12 +289,15 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
lastError = (MongoException) e;
if (!(e instanceof MongoOperationTimeoutException)) {
MongoException exceptionToHandle = OperationHelper.unwrap((MongoException) e);
- if (exceptionToHandle.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)
- && !withTransactionTimeoutExpired.getAsBoolean()) {
- if (transactionSpan != null) {
- transactionSpan.spanFinalizing(false);
+ if (exceptionToHandle.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)) {
+ if (withTransactionTimeoutExpired.getAsBoolean()) {
+ throw timeoutException(hasTimeoutMS, e);
+ } else {
+ if (transactionSpan != null) {
+ transactionSpan.spanFinalizing(false);
+ }
+ continue;
}
- continue;
}
}
}
@@ -305,18 +311,22 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
} catch (MongoException e) {
lastError = e;
clearTransactionContextOnError(e);
- if (!(e instanceof MongoOperationTimeoutException)
- && !withTransactionTimeoutExpired.getAsBoolean()) {
- applyMajorityWriteConcernToTransactionOptions();
-
- if (!(e instanceof MongoExecutionTimeoutException)
- && e.hasErrorLabel(UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)) {
- continue;
- } else if (e.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)) {
- if (transactionSpan != null) {
- transactionSpan.spanFinalizing(true);
+ if (!(e instanceof MongoOperationTimeoutException)) {
+ if (withTransactionTimeoutExpired.getAsBoolean()) {
+ throw timeoutException(hasTimeoutMS, e);
+
+ } else {
+ applyMajorityWriteConcernToTransactionOptions();
+
+ if (!(e instanceof MongoExecutionTimeoutException)
+ && e.hasErrorLabel(UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)) {
+ continue;
+ } else if (e.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)) {
+ if (transactionSpan != null) {
+ transactionSpan.spanFinalizing(true);
+ }
+ continue outer;
}
- continue outer;
}
}
throw e;
@@ -381,10 +391,10 @@ private TimeoutContext createTimeoutContext(final TransactionOptions transaction
}
private static void backoff(final int transactionAttempt,
- final Timeout withTransactionTimeout, final MongoException lastError) {
+ final Timeout withTransactionTimeout, final MongoException lastError, final boolean hasTimeoutMS) {
long backoffMs = ExponentialBackoff.calculateTransactionBackoffMs(transactionAttempt);
withTransactionTimeout.shortenBy(backoffMs, TimeUnit.MILLISECONDS).onExpired(() -> {
- throw lastError;
+ throw timeoutException(hasTimeoutMS, lastError);
});
try {
if (backoffMs > 0) {
@@ -394,4 +404,10 @@ private static void backoff(final int transactionAttempt,
throw interruptAndCreateMongoInterruptedException("Transaction retry interrupted", e);
}
}
+
+ private static MongoException timeoutException(final boolean hasTimeoutMS, final Throwable cause) {
+ return hasTimeoutMS
+ ? createMongoTimeoutException(cause) // CSOT timeout exception
+ : new MongoTimeoutException("Operation exceeded the timeout limit", cause); // Legacy timeout exception
+ }
}
diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
index 6eebcfc0089..0716bc026b3 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
@@ -51,7 +51,6 @@
import com.mongodb.internal.connection.ServerHelper;
import com.mongodb.internal.connection.TestCommandListener;
import com.mongodb.internal.connection.TestConnectionPoolListener;
-import com.mongodb.internal.time.ExponentialBackoff;
import com.mongodb.test.FlakyTest;
import org.bson.BsonDocument;
import org.bson.BsonInt32;
@@ -1121,11 +1120,6 @@ public void setUp() {
chunksCollectionHelper = new CollectionHelper<>(new BsonDocumentCodec(), gridFsChunksNamespace);
commandListener = new TestCommandListener();
- if (!isAsync()) {
- // setting jitter to 0 to make test using withTransaction deterministic (i.e retries immediately) otherwise we might get
- // MongoCommandException setup in the failpoint instead of MongoOperationTimeoutException depending on the random jitter value.
- ExponentialBackoff.setTestJitterSupplier(() -> 0);
- }
}
@AfterEach
@@ -1151,10 +1145,6 @@ public void tearDown() throws InterruptedException {
//noinspection ResultOfMethodCallIgnored
executor.awaitTermination(MAX_VALUE, NANOSECONDS);
}
-
- if (!isAsync()) {
- ExponentialBackoff.clearTestJitterSupplier();
- }
}
@AfterAll
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index a4bfdfa33f8..3f636bc57e8 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -219,44 +219,17 @@ public void testTimeoutMSAndLegacySettings() {
@DisplayName("Retry Backoff is Enforced")
@Test
public void testRetryBackoffIsEnforced() throws InterruptedException {
- // Run with jitter = 0 (no backoff)
- ExponentialBackoff.setTestJitterSupplier(() -> 0.0);
-
- BsonDocument failPointDocument = BsonDocument.parse("{'configureFailPoint': 'failCommand', 'mode': {'times': 13}, "
- + "'data': {'failCommands': ['commitTransaction'], 'errorCode': 251}}");
-
- long noBackoffTime;
- try (ClientSession session = client.startSession();
- FailPoint ignored = FailPoint.enable(failPointDocument, getPrimary())) {
- StartTime startTime = StartTime.now();
- session.withTransaction(() -> collection.insertOne(session, Document.parse("{}")));
- noBackoffTime = startTime.elapsed().toMillis();
- } finally {
- // Clear the test jitter supplier to avoid affecting other tests
- ExponentialBackoff.clearTestJitterSupplier();
- }
-
- // Run with jitter = 1 (full backoff)
- ExponentialBackoff.setTestJitterSupplier(() -> 1.0);
-
- failPointDocument = BsonDocument.parse("{'configureFailPoint': 'failCommand', 'mode': {'times': 13}, "
+ final BsonDocument failPointDocument = BsonDocument.parse("{'configureFailPoint': 'failCommand', 'mode': {'times': 13}, "
+ "'data': {'failCommands': ['commitTransaction'], 'errorCode': 251}}");
- long withBackoffTime;
- try (ClientSession session = client.startSession();
- FailPoint ignored = FailPoint.enable(failPointDocument, getPrimary())) {
- StartTime startTime = StartTime.now();
- session.withTransaction(() -> collection.insertOne(session, Document.parse("{}")));
- withBackoffTime = startTime.elapsed().toMillis();
- } finally {
- ExponentialBackoff.clearTestJitterSupplier();
- }
+ long noBackoffTime = measureTransactionLatency(0.0, failPointDocument);
+ long withBackoffTime = measureTransactionLatency(1.0, failPointDocument);
long expectedWithBackoffTime = noBackoffTime + 1800;
long actualDifference = Math.abs(withBackoffTime - expectedWithBackoffTime);
- assertTrue(actualDifference < 1000, String.format("Expected withBackoffTime to be ~% dms (noBackoffTime %d ms + 1800 ms), but"
- + " got %d ms. Difference: %d ms (tolerance: 1000 ms per spec)", expectedWithBackoffTime, noBackoffTime, withBackoffTime,
+ assertTrue(actualDifference < 500, String.format("Expected withBackoffTime to be ~% dms (noBackoffTime %d ms + 1800 ms), but"
+ + " got %d ms. Difference: %d ms (tolerance: 500 ms per spec)", expectedWithBackoffTime, noBackoffTime, withBackoffTime,
actualDifference));
}
@@ -282,6 +255,18 @@ public void testExponentialBackoffOnTransientError() throws InterruptedException
}
}
+ private long measureTransactionLatency(final double jitter, final BsonDocument failPointDocument) throws InterruptedException {
+ ExponentialBackoff.setTestJitterSupplier(() -> jitter);
+ try (ClientSession session = client.startSession();
+ FailPoint ignored = FailPoint.enable(failPointDocument, getPrimary())) {
+ StartTime startTime = StartTime.now();
+ session.withTransaction(() -> collection.insertOne(session, Document.parse("{}")));
+ return startTime.elapsed().toMillis();
+ } finally {
+ ExponentialBackoff.clearTestJitterSupplier();
+ }
+ }
+
private static boolean canRunTests() {
return isSharded() || isDiscoverableReplicaSet();
}
From 24d4ee5aec74d8e3a5790cb5742b4679a5a9276b Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Thu, 12 Feb 2026 16:33:32 +0000
Subject: [PATCH 41/72] Update
driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
Co-authored-by: Valentin Kovalenko
---
.../unit/com/mongodb/internal/time/ExponentialBackoffTest.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
index 70967c9ba19..4cafa50e3c4 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
@@ -70,7 +70,7 @@ void testCustomJitter() {
// Test with jitter = 0, all backoffs should be 0
ExponentialBackoff.setTestJitterSupplier(() -> 0.0);
try {
- for (int attemptNumber = 1; attemptNumber < 11; attemptNumber++) {
+ for (int attemptNumber = 1; attemptNumber < expectedBackoffs.length; attemptNumber++) {
long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
assertEquals(0, backoff, "With jitter=0, backoff should always be 0 ms");
}
From 4756404d6deadf2c895d01a5f6390d954b921117 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Thu, 12 Feb 2026 16:37:50 +0000
Subject: [PATCH 42/72] Update
driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
Co-authored-by: Valentin Kovalenko
---
.../functional/com/mongodb/client/WithTransactionProseTest.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index 3f636bc57e8..27cc55038c9 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -214,7 +214,7 @@ public void testTimeoutMSAndLegacySettings() {
/**
* See
- * Convenient API Prose Tests.
+ * Retry Backoff is Enforced.
*/
@DisplayName("Retry Backoff is Enforced")
@Test
From f22198e4374ceb916e2513c78ed806cdbaeaa87f Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Thu, 12 Feb 2026 19:27:54 +0000
Subject: [PATCH 43/72] moved logic of ExponentialBackoff cleanup to correct
class
---
.../internal/time/ExponentialBackoffTest.java | 2 +-
.../ClientSideOperationTimeoutProseTest.java | 3 ---
.../ClientSideOperationTimeoutProseTest.java | 17 +++++++++++++++++
3 files changed, 18 insertions(+), 4 deletions(-)
diff --git a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
index 4cafa50e3c4..1d2e52e1aaa 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
@@ -70,7 +70,7 @@ void testCustomJitter() {
// Test with jitter = 0, all backoffs should be 0
ExponentialBackoff.setTestJitterSupplier(() -> 0.0);
try {
- for (int attemptNumber = 1; attemptNumber < expectedBackoffs.length; attemptNumber++) {
+ for (int attemptNumber = 1; attemptNumber < EXPECTED_BACKOFFS_MAX_VALUES.length; attemptNumber++) {
long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
assertEquals(0, backoff, "With jitter=0, backoff should always be 0 ms");
}
diff --git a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientSideOperationTimeoutProseTest.java b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientSideOperationTimeoutProseTest.java
index 6494cde74aa..90446953fc1 100644
--- a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientSideOperationTimeoutProseTest.java
+++ b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientSideOperationTimeoutProseTest.java
@@ -26,7 +26,6 @@
import com.mongodb.client.model.changestream.FullDocument;
import com.mongodb.event.CommandFailedEvent;
import com.mongodb.event.CommandStartedEvent;
-import com.mongodb.internal.time.ExponentialBackoff;
import com.mongodb.reactivestreams.client.gridfs.GridFSBucket;
import com.mongodb.reactivestreams.client.gridfs.GridFSBuckets;
import com.mongodb.reactivestreams.client.syncadapter.SyncGridFSBucket;
@@ -579,7 +578,6 @@ private void assertOnlyOneCommandTimeoutFailure(final String command) {
public void setUp() {
super.setUp();
SyncMongoClient.enableSleepAfterSessionClose(postSessionCloseSleep());
- ExponentialBackoff.setTestJitterSupplier(() -> 0);
}
@Override
@@ -587,7 +585,6 @@ public void setUp() {
public void tearDown() throws InterruptedException {
super.tearDown();
SyncMongoClient.disableSleep();
- ExponentialBackoff.clearTestJitterSupplier();
}
@Override
diff --git a/driver-sync/src/test/functional/com/mongodb/client/ClientSideOperationTimeoutProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/ClientSideOperationTimeoutProseTest.java
index 4dcbc4d1a0f..978fece912b 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/ClientSideOperationTimeoutProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/ClientSideOperationTimeoutProseTest.java
@@ -19,6 +19,7 @@
import com.mongodb.MongoClientSettings;
import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.client.gridfs.GridFSBuckets;
+import com.mongodb.internal.time.ExponentialBackoff;
/**
@@ -36,6 +37,22 @@ protected GridFSBucket createGridFsBucket(final MongoDatabase mongoDatabase, fin
return GridFSBuckets.create(mongoDatabase, bucketName);
}
+ @Override
+ public void setUp() {
+ super.setUp();
+ ExponentialBackoff.setTestJitterSupplier(() -> 0);
+ }
+
+ @Override
+ public void tearDown() throws InterruptedException {
+ super.tearDown();
+ try {
+ super.tearDown();
+ } finally {
+ ExponentialBackoff.clearTestJitterSupplier();
+ }
+ }
+
@Override
protected boolean isAsync() {
return false;
From 9dcd5c04906a96bc0230fa1e8f3bd3585958aa8f Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Wed, 25 Feb 2026 15:35:47 +0000
Subject: [PATCH 44/72] Fixing tests
---
.../ClientSideOperationTimeoutProseTest.java | 5 +++-
.../client/WithTransactionProseTest.java | 23 ++++++++++++++-----
2 files changed, 21 insertions(+), 7 deletions(-)
diff --git a/driver-sync/src/test/functional/com/mongodb/client/ClientSideOperationTimeoutProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/ClientSideOperationTimeoutProseTest.java
index 978fece912b..bbabcd8f61a 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/ClientSideOperationTimeoutProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/ClientSideOperationTimeoutProseTest.java
@@ -20,6 +20,8 @@
import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.client.gridfs.GridFSBuckets;
import com.mongodb.internal.time.ExponentialBackoff;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
/**
@@ -37,15 +39,16 @@ protected GridFSBucket createGridFsBucket(final MongoDatabase mongoDatabase, fin
return GridFSBuckets.create(mongoDatabase, bucketName);
}
+ @BeforeEach
@Override
public void setUp() {
super.setUp();
ExponentialBackoff.setTestJitterSupplier(() -> 0);
}
+ @AfterEach
@Override
public void tearDown() throws InterruptedException {
- super.tearDown();
try {
super.tearDown();
} finally {
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index 27cc55038c9..6bc12c36c03 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -18,7 +18,10 @@
import com.mongodb.ClientSessionOptions;
import com.mongodb.MongoClientException;
+import com.mongodb.MongoCommandException;
import com.mongodb.MongoException;
+import com.mongodb.MongoNodeIsRecoveringException;
+import com.mongodb.MongoTimeoutException;
import com.mongodb.TransactionOptions;
import com.mongodb.client.model.Sorts;
import com.mongodb.internal.time.ExponentialBackoff;
@@ -42,6 +45,7 @@
import static com.mongodb.ClusterFixture.isSharded;
import static com.mongodb.client.Fixture.getPrimary;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@@ -114,8 +118,10 @@ public void testRetryTimeoutEnforcedTransientTransactionError() {
}));
fail("Test should have thrown an exception.");
} catch (Exception e) {
- assertEquals(errorMessage, e.getMessage());
- assertTrue(((MongoException) e).getErrorLabels().contains(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL));
+ assertInstanceOf(MongoTimeoutException.class, e);
+ MongoException cause = (MongoException) e.getCause();
+ assertEquals(errorMessage, cause.getMessage());
+ assertTrue(cause.getErrorLabels().contains(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL));
}
}
@@ -139,8 +145,10 @@ public void testRetryTimeoutEnforcedUnknownTransactionCommit() {
}));
fail("Test should have thrown an exception.");
} catch (Exception e) {
- assertEquals(91, ((MongoException) e).getCode());
- assertTrue(((MongoException) e).getErrorLabels().contains(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL));
+ assertInstanceOf(MongoNodeIsRecoveringException.class, e.getCause());
+ MongoNodeIsRecoveringException cause = (MongoNodeIsRecoveringException) e.getCause();
+ assertEquals(91, cause.getCode());
+ assertTrue(cause.getErrorLabels().contains(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL));
} finally {
failPointAdminDb.runCommand(Document.parse("{'configureFailPoint': 'failCommand', 'mode': 'off'}"));
}
@@ -168,8 +176,11 @@ public void testRetryTimeoutEnforcedTransientTransactionErrorOnCommit() {
}));
fail("Test should have thrown an exception.");
} catch (Exception e) {
- assertEquals(251, ((MongoException) e).getCode());
- assertTrue(((MongoException) e).getErrorLabels().contains(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL));
+ assertEquals(-4, ((MongoException) e).getCode());
+ assertInstanceOf(MongoCommandException.class, e.getCause());
+ MongoCommandException cause = (MongoCommandException) e.getCause();
+ assertEquals(251, cause.getCode());
+ assertTrue(cause.getErrorLabels().contains(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL));
} finally {
failPointAdminDb.runCommand(Document.parse("{'configureFailPoint': 'failCommand', 'mode': 'off'}"));
}
From 9676ff0a88d552d1d08907369ef245c30ef21c62 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Tue, 24 Mar 2026 16:40:18 +0000
Subject: [PATCH 45/72] Update
driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
Co-authored-by: Valentin Kovalenko
---
.../unit/com/mongodb/internal/time/ExponentialBackoffTest.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
index 1d2e52e1aaa..bbe31cfce13 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
@@ -21,7 +21,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
-public class ExponentialBackoffTest {
+class ExponentialBackoffTest {
// Expected backoffs with jitter=1.0 and growth factor ExponentialBackoff.TRANSACTION_GROWTH
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,
192.21679688, 288.32519531, 432.48779297, 500.0};
From 9153831faac2be6783e29f0154c2acfda4b2350b Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Tue, 24 Mar 2026 16:40:55 +0000
Subject: [PATCH 46/72] Update
driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
Co-authored-by: Valentin Kovalenko
---
.../unit/com/mongodb/internal/time/ExponentialBackoffTest.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
index bbe31cfce13..fb09f56c3de 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
@@ -36,7 +36,7 @@ void testCalculateTransactionBackoffMs() {
long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
long expectedBackoff = Math.round(EXPECTED_BACKOFFS_MAX_VALUES[attemptNumber - 1]);
assertTrue(backoff >= 0 && backoff <= expectedBackoff,
- String.format("Attempt %d: backoff should be 0-%d ms, got: %d", attemptNumber,
+ String.format("Attempt %d: backoff should be between 0 ms and %d ms, got: %d", attemptNumber,
expectedBackoff, backoff));
}
}
From 9eebf51034449e621e89157c6c6132c6be6923a1 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Tue, 24 Mar 2026 16:43:15 +0000
Subject: [PATCH 47/72] Update
driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
Co-authored-by: Valentin Kovalenko
---
.../unit/com/mongodb/internal/time/ExponentialBackoffTest.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
index fb09f56c3de..9613b8e0ef9 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
@@ -44,7 +44,7 @@ void testCalculateTransactionBackoffMs() {
@Test
void testCalculateTransactionBackoffMsRespectsMaximum() {
// Even at high attempt numbers, backoff should never exceed ExponentialBackoff.TRANSACTION_MAX_MS
- for (int attemptNumber = 1; attemptNumber < 26; attemptNumber++) {
+ for (int attemptNumber = 1; attemptNumber < EXPECTED_BACKOFFS_MAX_VALUES.length * 2; attemptNumber++) {
long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
assertTrue(backoff >= 0 && backoff <= ExponentialBackoff.TRANSACTION_MAX_MS,
String.format("Attempt %d: backoff should be capped at %f ms, got: %d ms",
From 0c10988b498b0558d38135dd3f1268ddaf2ce858 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Tue, 24 Mar 2026 18:08:22 +0000
Subject: [PATCH 48/72] Update
driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
Co-authored-by: Valentin Kovalenko
---
.../com/mongodb/internal/time/ExponentialBackoffTest.java | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
index 9613b8e0ef9..ee23a0df09b 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
@@ -22,7 +22,10 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
class ExponentialBackoffTest {
- // Expected backoffs with jitter=1.0 and growth factor ExponentialBackoff.TRANSACTION_GROWTH
+/**
+ * Expected {@linkplain ExponentialBackoff#calculateTransactionBackoffMs(int) backoffs} with 1.0 as
+ * {@link ExponentialBackoff#setTestJitterSupplier(DoubleSupplier) jiter}.
+ */
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,
192.21679688, 288.32519531, 432.48779297, 500.0};
From 8b247098592a8127af44211bab37daa964f69dd4 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Wed, 25 Mar 2026 12:17:39 +0000
Subject: [PATCH 49/72] Update
driver-core/src/main/com/mongodb/internal/TimeoutContext.java
Co-authored-by: Valentin Kovalenko
---
driver-core/src/main/com/mongodb/internal/TimeoutContext.java | 4 ----
1 file changed, 4 deletions(-)
diff --git a/driver-core/src/main/com/mongodb/internal/TimeoutContext.java b/driver-core/src/main/com/mongodb/internal/TimeoutContext.java
index ee61eacf7d4..54f0c68c9e9 100644
--- a/driver-core/src/main/com/mongodb/internal/TimeoutContext.java
+++ b/driver-core/src/main/com/mongodb/internal/TimeoutContext.java
@@ -168,10 +168,6 @@ public Timeout timeoutIncludingRoundTrip() {
return timeout == null ? null : timeout.shortenBy(minRoundTripTimeMS, MILLISECONDS);
}
- public Timeout timeoutOrAlternative(final Timeout alternative) {
- return timeout == null ? alternative : timeout;
- }
-
/**
* Returns the remaining {@code timeoutMS} if set or the {@code alternativeTimeoutMS}.
*
From 60acf51dc71acd569931fd6aa52715d09d83af69 Mon Sep 17 00:00:00 2001
From: Valentin Kovalenko
Date: Thu, 12 Mar 2026 08:37:51 -0600
Subject: [PATCH 50/72] Refactor `withTransaction`
Now it looks similar to the algorithm described in the specification prose.
I also addressed other concerns that I expressed in the PR review.
---
.../client/internal/ClientSessionImpl.java | 109 +++++++++---------
1 file changed, 57 insertions(+), 52 deletions(-)
diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
index 18f782728ac..a95f7d7f47f 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
@@ -21,7 +21,6 @@
import com.mongodb.MongoException;
import com.mongodb.MongoExecutionTimeoutException;
import com.mongodb.MongoInternalException;
-import com.mongodb.MongoOperationTimeoutException;
import com.mongodb.MongoTimeoutException;
import com.mongodb.ReadConcern;
import com.mongodb.TransactionOptions;
@@ -33,7 +32,6 @@
import com.mongodb.internal.observability.micrometer.TransactionSpan;
import com.mongodb.internal.operation.AbortTransactionOperation;
import com.mongodb.internal.operation.CommitTransactionOperation;
-import com.mongodb.internal.operation.OperationHelper;
import com.mongodb.internal.operation.ReadOperation;
import com.mongodb.internal.operation.WriteConcernHelper;
import com.mongodb.internal.operation.WriteOperation;
@@ -160,6 +158,12 @@ public void abortTransaction() {
}
}
+ private void abortIfInTransaction() {
+ if (transactionState == TransactionState.IN) {
+ abortTransaction();
+ }
+ }
+
private void startTransaction(final TransactionOptions transactionOptions, final TimeoutContext timeoutContext) {
Boolean snapshot = getOptions().isSnapshot();
if (snapshot != null && snapshot) {
@@ -258,9 +262,10 @@ public T withTransaction(final TransactionBody transactionBody) {
public T withTransaction(final TransactionBody transactionBody, final TransactionOptions options) {
notNull("transactionBody", transactionBody);
TimeoutContext withTransactionTimeoutContext = createTimeoutContext(options);
- final boolean hasTimeoutMS = withTransactionTimeoutContext.hasTimeoutMS();
- Timeout withTransactionTimeout = withTransactionTimeoutContext.timeoutOrAlternative(
- assertNotNull(TimeoutContext.startTimeout(MAX_RETRY_TIME_LIMIT_MS)));
+ boolean timeoutMsConfigured = withTransactionTimeoutContext.hasTimeoutMS();
+ Timeout withTransactionTimeout = assertNotNull(timeoutMsConfigured
+ ? withTransactionTimeoutContext.getTimeout()
+ : TimeoutContext.startTimeout(MAX_RETRY_TIME_LIMIT_MS));
BooleanSupplier withTransactionTimeoutExpired = () -> withTransactionTimeout.call(TimeUnit.MILLISECONDS,
() -> false, ms -> false, () -> true);
int transactionAttempt = 0;
@@ -270,35 +275,37 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
outer:
while (true) {
if (transactionAttempt > 0) {
- backoff(transactionAttempt, withTransactionTimeout, assertNotNull(lastError), hasTimeoutMS);
+ backoff(transactionAttempt, withTransactionTimeout, assertNotNull(lastError), timeoutMsConfigured);
}
- T retVal;
try {
startTransaction(options, withTransactionTimeoutContext);
transactionAttempt++;
-
if (transactionSpan != null) {
transactionSpan.setIsConvenientTransaction();
}
+ } catch (Throwable e) {
+ abortIfInTransaction();
+ throw e;
+ }
+ T retVal;
+ try {
retVal = transactionBody.execute();
} catch (Throwable e) {
- if (transactionState == TransactionState.IN) {
- abortTransaction();
- }
+ abortIfInTransaction();
if (e instanceof MongoException) {
- lastError = (MongoException) e;
- if (!(e instanceof MongoOperationTimeoutException)) {
- MongoException exceptionToHandle = OperationHelper.unwrap((MongoException) e);
- if (exceptionToHandle.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)) {
- if (withTransactionTimeoutExpired.getAsBoolean()) {
- throw timeoutException(hasTimeoutMS, e);
- } else {
- if (transactionSpan != null) {
- transactionSpan.spanFinalizing(false);
- }
- continue;
- }
+ MongoException mongoException = (MongoException) e;
+ if (mongoException.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)) {
+ if (transactionSpan != null) {
+ transactionSpan.spanFinalizing(false);
}
+ lastError = mongoException;
+ continue;
+ } else if (mongoException.hasErrorLabel(UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)) {
+ throw e;
+ } else {
+ throw withTransactionTimeoutExpired.getAsBoolean()
+ ? wrapInMongoTimeoutException(mongoException, timeoutMsConfigured)
+ : mongoException;
}
}
throw e;
@@ -308,28 +315,22 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
try {
commitTransaction(false);
break;
- } catch (MongoException e) {
- lastError = e;
- clearTransactionContextOnError(e);
- if (!(e instanceof MongoOperationTimeoutException)) {
- if (withTransactionTimeoutExpired.getAsBoolean()) {
- throw timeoutException(hasTimeoutMS, e);
-
- } else {
- applyMajorityWriteConcernToTransactionOptions();
-
- if (!(e instanceof MongoExecutionTimeoutException)
- && e.hasErrorLabel(UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)) {
- continue;
- } else if (e.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)) {
- if (transactionSpan != null) {
- transactionSpan.spanFinalizing(true);
- }
- continue outer;
- }
+ } catch (MongoException mongoException) {
+ if (mongoException.hasErrorLabel(UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)
+ && !(mongoException instanceof MongoExecutionTimeoutException)
+ && !withTransactionTimeoutExpired.getAsBoolean()) {
+ applyMajorityWriteConcernToTransactionOptions();
+ continue;
+ } else if (mongoException.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)) {
+ if (transactionSpan != null) {
+ transactionSpan.spanFinalizing(true);
}
+ lastError = mongoException;
+ continue outer;
}
- throw e;
+ throw withTransactionTimeoutExpired.getAsBoolean()
+ ? wrapInMongoTimeoutException(mongoException, timeoutMsConfigured)
+ : mongoException;
}
}
}
@@ -351,9 +352,7 @@ public TransactionSpan getTransactionSpan() {
@Override
public void close() {
try {
- if (transactionState == TransactionState.IN) {
- abortTransaction();
- }
+ abortIfInTransaction();
} finally {
clearTransactionContext();
super.close();
@@ -391,10 +390,10 @@ private TimeoutContext createTimeoutContext(final TransactionOptions transaction
}
private static void backoff(final int transactionAttempt,
- final Timeout withTransactionTimeout, final MongoException lastError, final boolean hasTimeoutMS) {
+ final Timeout withTransactionTimeout, final MongoException lastError, final boolean timeoutMsConfigured) {
long backoffMs = ExponentialBackoff.calculateTransactionBackoffMs(transactionAttempt);
withTransactionTimeout.shortenBy(backoffMs, TimeUnit.MILLISECONDS).onExpired(() -> {
- throw timeoutException(hasTimeoutMS, lastError);
+ throw wrapInMongoTimeoutException(lastError, timeoutMsConfigured);
});
try {
if (backoffMs > 0) {
@@ -405,9 +404,15 @@ private static void backoff(final int transactionAttempt,
}
}
- private static MongoException timeoutException(final boolean hasTimeoutMS, final Throwable cause) {
- return hasTimeoutMS
- ? createMongoTimeoutException(cause) // CSOT timeout exception
- : new MongoTimeoutException("Operation exceeded the timeout limit", cause); // Legacy timeout exception
+ private static MongoException wrapInMongoTimeoutException(final MongoException cause, final boolean timeoutMsConfigured) {
+ return timeoutMsConfigured
+ ? createMongoTimeoutException(cause)
+ : wrapInNonTimeoutMsMongoTimeoutException(cause);
+ }
+
+ private static MongoTimeoutException wrapInNonTimeoutMsMongoTimeoutException(final MongoException cause) {
+ return cause instanceof MongoTimeoutException
+ ? (MongoTimeoutException) cause
+ : new MongoTimeoutException("Operation exceeded the timeout limit", cause);
}
}
From 94a79dbe1af8f971b4a7de329b915d939789cda7 Mon Sep 17 00:00:00 2001
From: Valentin Kovalenko
Date: Thu, 26 Mar 2026 03:52:12 -0600
Subject: [PATCH 51/72] Use `OperationHelper.unwrap` to get to the exception
that carries labels
---
.../main/com/mongodb/client/internal/ClientSessionImpl.java | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
index a95f7d7f47f..ac0fb3141ad 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
@@ -32,6 +32,7 @@
import com.mongodb.internal.observability.micrometer.TransactionSpan;
import com.mongodb.internal.operation.AbortTransactionOperation;
import com.mongodb.internal.operation.CommitTransactionOperation;
+import com.mongodb.internal.operation.OperationHelper;
import com.mongodb.internal.operation.ReadOperation;
import com.mongodb.internal.operation.WriteConcernHelper;
import com.mongodb.internal.operation.WriteOperation;
@@ -294,13 +295,14 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
abortIfInTransaction();
if (e instanceof MongoException) {
MongoException mongoException = (MongoException) e;
- if (mongoException.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)) {
+ MongoException labelCarryingException = OperationHelper.unwrap(mongoException);
+ if (labelCarryingException.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)) {
if (transactionSpan != null) {
transactionSpan.spanFinalizing(false);
}
lastError = mongoException;
continue;
- } else if (mongoException.hasErrorLabel(UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)) {
+ } else if (labelCarryingException.hasErrorLabel(UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)) {
throw e;
} else {
throw withTransactionTimeoutExpired.getAsBoolean()
From d4bc4c70e79307608b31fd9bd916eb66ede73fe2 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Thu, 26 Mar 2026 14:22:18 +0000
Subject: [PATCH 52/72] Copy error labels from cause to wrapping timeout
exception per DRIVERS-3391 and fix timeout prose test assertions
---
.../client/internal/ClientSessionImpl.java | 6 ++++-
.../client/WithTransactionProseTest.java | 22 ++++++++++---------
2 files changed, 17 insertions(+), 11 deletions(-)
diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
index ac0fb3141ad..17e6efd7b1a 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
@@ -407,9 +407,13 @@ private static void backoff(final int transactionAttempt,
}
private static MongoException wrapInMongoTimeoutException(final MongoException cause, final boolean timeoutMsConfigured) {
- return timeoutMsConfigured
+ MongoException timeoutException = timeoutMsConfigured
? createMongoTimeoutException(cause)
: wrapInNonTimeoutMsMongoTimeoutException(cause);
+ if (timeoutException != cause) {
+ cause.getErrorLabels().forEach(timeoutException::addLabel);
+ }
+ return timeoutException;
}
private static MongoTimeoutException wrapInNonTimeoutMsMongoTimeoutException(final MongoException cause) {
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index 6bc12c36c03..797f3297bcd 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -118,10 +118,11 @@ public void testRetryTimeoutEnforcedTransientTransactionError() {
}));
fail("Test should have thrown an exception.");
} catch (Exception e) {
- assertInstanceOf(MongoTimeoutException.class, e);
- MongoException cause = (MongoException) e.getCause();
+ MongoTimeoutException exception = assertInstanceOf(MongoTimeoutException.class, e);
+ assertTrue(exception.hasErrorLabel(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL));
+ MongoException cause = assertInstanceOf(MongoException.class, exception.getCause());
assertEquals(errorMessage, cause.getMessage());
- assertTrue(cause.getErrorLabels().contains(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL));
+ assertTrue(cause.hasErrorLabel(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL));
}
}
@@ -145,10 +146,11 @@ public void testRetryTimeoutEnforcedUnknownTransactionCommit() {
}));
fail("Test should have thrown an exception.");
} catch (Exception e) {
- assertInstanceOf(MongoNodeIsRecoveringException.class, e.getCause());
- MongoNodeIsRecoveringException cause = (MongoNodeIsRecoveringException) e.getCause();
+ MongoTimeoutException exception = assertInstanceOf(MongoTimeoutException.class, e);
+ assertTrue(exception.hasErrorLabel(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL));
+ MongoNodeIsRecoveringException cause = assertInstanceOf(MongoNodeIsRecoveringException.class, exception.getCause());
assertEquals(91, cause.getCode());
- assertTrue(cause.getErrorLabels().contains(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL));
+ assertTrue(cause.hasErrorLabel(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL));
} finally {
failPointAdminDb.runCommand(Document.parse("{'configureFailPoint': 'failCommand', 'mode': 'off'}"));
}
@@ -176,11 +178,11 @@ public void testRetryTimeoutEnforcedTransientTransactionErrorOnCommit() {
}));
fail("Test should have thrown an exception.");
} catch (Exception e) {
- assertEquals(-4, ((MongoException) e).getCode());
- assertInstanceOf(MongoCommandException.class, e.getCause());
- MongoCommandException cause = (MongoCommandException) e.getCause();
+ MongoTimeoutException exception = assertInstanceOf(MongoTimeoutException.class, e);
+ assertTrue(exception.hasErrorLabel(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL));
+ MongoCommandException cause = assertInstanceOf(MongoCommandException.class, exception.getCause());
assertEquals(251, cause.getCode());
- assertTrue(cause.getErrorLabels().contains(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL));
+ assertTrue(cause.hasErrorLabel(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL));
} finally {
failPointAdminDb.runCommand(Document.parse("{'configureFailPoint': 'failCommand', 'mode': 'off'}"));
}
From 11786dd26b41de433f416b7b908b5986dd0e7dad Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Thu, 26 Mar 2026 14:27:56 +0000
Subject: [PATCH 53/72] Fixing Javadoc
---
.../com/mongodb/internal/time/ExponentialBackoffTest.java | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
index ee23a0df09b..27f5ee052ff 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
@@ -18,13 +18,15 @@
import org.junit.jupiter.api.Test;
+import java.util.function.DoubleSupplier;
+
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ExponentialBackoffTest {
-/**
+ /**
* Expected {@linkplain ExponentialBackoff#calculateTransactionBackoffMs(int) backoffs} with 1.0 as
- * {@link ExponentialBackoff#setTestJitterSupplier(DoubleSupplier) jiter}.
+ * {@link ExponentialBackoff#setTestJitterSupplier(DoubleSupplier) jitter}.
*/
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,
192.21679688, 288.32519531, 432.48779297, 500.0};
From 43dab53f44ae74026569c0864f310e2a4d286669 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Thu, 26 Mar 2026 17:36:52 +0000
Subject: [PATCH 54/72] fix import indentation
---
.../benchmark/benchmarks/RawBsonArrayEncodingBenchmark.java | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/driver-benchmarks/src/main/com/mongodb/benchmark/benchmarks/RawBsonArrayEncodingBenchmark.java b/driver-benchmarks/src/main/com/mongodb/benchmark/benchmarks/RawBsonArrayEncodingBenchmark.java
index 0768f4f63c6..f0a59967f0a 100644
--- a/driver-benchmarks/src/main/com/mongodb/benchmark/benchmarks/RawBsonArrayEncodingBenchmark.java
+++ b/driver-benchmarks/src/main/com/mongodb/benchmark/benchmarks/RawBsonArrayEncodingBenchmark.java
@@ -17,7 +17,8 @@
package com.mongodb.benchmark.benchmarks;
-import org.bson.BsonArray;import org.bson.BsonDocument;
+import org.bson.BsonArray;
+import org.bson.BsonDocument;
import org.bson.RawBsonDocument;
import org.bson.codecs.BsonDocumentCodec;
@@ -52,4 +53,4 @@ public void setUp() throws IOException {
public int getBytesPerRun() {
return documentBytes.length * NUM_INTERNAL_ITERATIONS;
}
-}
\ No newline at end of file
+}
From 69630b08fbef5f2e89cece2fe4a36ae4845d3e90 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Tue, 31 Mar 2026 13:27:33 +0100
Subject: [PATCH 55/72] Update
driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
Co-authored-by: Valentin Kovalenko
---
.../unit/com/mongodb/internal/time/ExponentialBackoffTest.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
index 27f5ee052ff..d1e02b9e4c9 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
@@ -26,7 +26,7 @@
class ExponentialBackoffTest {
/**
* Expected {@linkplain ExponentialBackoff#calculateTransactionBackoffMs(int) backoffs} with 1.0 as
- * {@link ExponentialBackoff#setTestJitterSupplier(DoubleSupplier) jitter}.
+ * {@link ExponentialBackoff#setTestJitterSupplier(DoubleSupplier) jitter}.
*/
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,
192.21679688, 288.32519531, 432.48779297, 500.0};
From ae3f140eb8e388fced84716bb466409532dcd778 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Tue, 31 Mar 2026 18:56:03 +0100
Subject: [PATCH 56/72] Restore changes lost after rebase
- Add SYSTEM_OVERLOADED_ERROR_LABEL and RETRYABLE_ERROR_LABEL constants to MongoException
- Add backpressure:true to hello command in InternalStreamConnectionInitializer
- Make CommandOperationHelper and its error label constants public
- Replace hardcoded error label strings with constants in tests and examples
- Refactor ExponentialBackoff: make TRANSACTION_BASE_MS and TRANSACTION_GROWTH private,
split testCustomJitter into two tests, minor Javadoc/assertion message fixes
- Remove redundant private constructor from TimeoutContext
- Convert block comments to Javadoc in WithTransactionProseTest, refactor testRetryBackoffIsEnforced
---
.../src/main/com/mongodb/MongoException.java | 24 ++++++
.../com/mongodb/internal/TimeoutContext.java | 4 -
.../InternalStreamConnectionInitializer.java | 3 +-
.../operation/CommandOperationHelper.java | 6 +-
.../internal/time/ExponentialBackoff.java | 13 ++-
...mConnectionInitializerSpecification.groovy | 7 +-
.../internal/time/ExponentialBackoffTest.java | 22 +++--
.../documentation/TransactionExample.java | 6 +-
...tClientSideOperationsTimeoutProseTest.java | 3 +-
...WriteConcernWithResponseExceptionTest.java | 6 +-
.../client/RetryableWritesProseTest.java | 8 +-
.../client/WithTransactionProseTest.java | 80 +++++++++----------
12 files changed, 103 insertions(+), 79 deletions(-)
diff --git a/driver-core/src/main/com/mongodb/MongoException.java b/driver-core/src/main/com/mongodb/MongoException.java
index a668dd344b7..2c585c93cf4 100644
--- a/driver-core/src/main/com/mongodb/MongoException.java
+++ b/driver-core/src/main/com/mongodb/MongoException.java
@@ -39,6 +39,7 @@ public class MongoException extends RuntimeException {
*
* @see #hasErrorLabel(String)
* @since 3.8
+ * @mongodb.driver.manual core/transactions-in-applications/#std-label-transient-transaction-error
*/
public static final String TRANSIENT_TRANSACTION_ERROR_LABEL = "TransientTransactionError";
@@ -47,9 +48,32 @@ public class MongoException extends RuntimeException {
*
* @see #hasErrorLabel(String)
* @since 3.8
+ * @mongodb.driver.manual core/transactions-in-applications/#std-label-unknown-transaction-commit-result
*/
public static final String UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL = "UnknownTransactionCommitResult";
+ /**
+ * Server is overloaded and shedding load.
+ * If you retry, use exponential backoff because the server has indicated overload.
+ * This label on its own does not mean that the operation can be safely retried.
+ *
+ * @see #hasErrorLabel(String)
+ * @since 5.7
+ * @mongodb.server.release 8.3
+ */
+ // TODO-BACKPRESSURE Valentin Add a @mongodb.driver.manual link or something similar, see `content/atlas/source/overload-errors.txt` in https://github.com/10gen/docs-mongodb-internal/pull/17281
+ public static final String SYSTEM_OVERLOADED_ERROR_LABEL = "SystemOverloadedError";
+
+ /**
+ * The operation was not executed and is safe to retry.
+ *
+ * @see #hasErrorLabel(String)
+ * @since 5.7
+ * @mongodb.server.release 8.3
+ */
+ // TODO-BACKPRESSURE Valentin Add a @mongodb.driver.manual link or something similar, see `content/atlas/source/overload-errors.txt` in https://github.com/10gen/docs-mongodb-internal/pull/17281
+ public static final String RETRYABLE_ERROR_LABEL = "RetryableError";
+
private static final long serialVersionUID = -4415279469780082174L;
private final int code;
diff --git a/driver-core/src/main/com/mongodb/internal/TimeoutContext.java b/driver-core/src/main/com/mongodb/internal/TimeoutContext.java
index 54f0c68c9e9..a263946f5ca 100644
--- a/driver-core/src/main/com/mongodb/internal/TimeoutContext.java
+++ b/driver-core/src/main/com/mongodb/internal/TimeoutContext.java
@@ -109,10 +109,6 @@ public TimeoutContext(final TimeoutSettings timeoutSettings) {
this(false, timeoutSettings, startTimeout(timeoutSettings.getTimeoutMS()));
}
- private TimeoutContext(final TimeoutSettings timeoutSettings, @Nullable final Timeout timeout) {
- this(false, timeoutSettings, timeout);
- }
-
private TimeoutContext(final boolean isMaintenanceContext,
final TimeoutSettings timeoutSettings,
@Nullable final Timeout timeout) {
diff --git a/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnectionInitializer.java b/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnectionInitializer.java
index 574a85669d0..36f6688cb0e 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnectionInitializer.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnectionInitializer.java
@@ -172,7 +172,8 @@ private InternalConnectionInitializationDescription createInitializationDescript
private BsonDocument createHelloCommand(final Authenticator authenticator, final InternalConnection connection) {
BsonDocument helloCommandDocument = new BsonDocument(getHandshakeCommandName(), new BsonInt32(1))
- .append("helloOk", BsonBoolean.TRUE);
+ .append("helloOk", BsonBoolean.TRUE)
+ .append("backpressure", BsonBoolean.TRUE);
if (clientMetadataDocument != null) {
helloCommandDocument.append("client", clientMetadataDocument);
}
diff --git a/driver-core/src/main/com/mongodb/internal/operation/CommandOperationHelper.java b/driver-core/src/main/com/mongodb/internal/operation/CommandOperationHelper.java
index 8332ad916fb..bc0d223b3db 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/CommandOperationHelper.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/CommandOperationHelper.java
@@ -51,7 +51,7 @@
import static java.util.Arrays.asList;
@SuppressWarnings("overloads")
-final class CommandOperationHelper {
+public final class CommandOperationHelper {
static WriteConcern validateAndGetEffectiveWriteConcern(final WriteConcern writeConcernSetting, final SessionContext sessionContext)
throws MongoClientException {
boolean activeTransaction = sessionContext.hasActiveTransaction();
@@ -223,8 +223,8 @@ static boolean isRetryWritesEnabled(@Nullable final BsonDocument command) {
|| command.getFirstKey().equals("commitTransaction") || command.getFirstKey().equals("abortTransaction")));
}
- static final String RETRYABLE_WRITE_ERROR_LABEL = "RetryableWriteError";
- private static final String NO_WRITES_PERFORMED_ERROR_LABEL = "NoWritesPerformed";
+ public static final String RETRYABLE_WRITE_ERROR_LABEL = "RetryableWriteError";
+ public static final String NO_WRITES_PERFORMED_ERROR_LABEL = "NoWritesPerformed";
private static boolean decideRetryableAndAddRetryableWriteErrorLabel(final Throwable t, @Nullable final Integer maxWireVersion) {
if (!(t instanceof MongoException)) {
diff --git a/driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java b/driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java
index 5f63d5c01c8..db5d2efa996 100644
--- a/driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java
+++ b/driver-core/src/main/com/mongodb/internal/time/ExponentialBackoff.java
@@ -29,13 +29,10 @@
*/
public final class ExponentialBackoff {
- // Constants for transaction retry backoff
- @VisibleForTesting(otherwise = PRIVATE)
- static final double TRANSACTION_BASE_MS = 5.0;
+ private static final double TRANSACTION_BASE_MS = 5.0;
@VisibleForTesting(otherwise = PRIVATE)
static final double TRANSACTION_MAX_MS = 500.0;
- @VisibleForTesting(otherwise = PRIVATE)
- static final double TRANSACTION_GROWTH = 1.5;
+ private static final double TRANSACTION_GROWTH = 1.5;
// TODO-JAVA-6079
private static DoubleSupplier testJitterSupplier = null;
@@ -46,11 +43,11 @@ private ExponentialBackoff() {
/**
* Calculate the backoff in milliseconds for transaction retries.
*
- * @param attemptNumber 0-based attempt number
+ * @param attemptNumber attempt number > 0
* @return The calculated backoff in milliseconds.
*/
public static long calculateTransactionBackoffMs(final int attemptNumber) {
- assertTrue(attemptNumber > 0, "Attempt number must be greater than 0 in the context of transaction backoff calculation");
+ assertTrue(attemptNumber > 0, "Attempt number must be at least 1 (1-based) in the context of transaction backoff calculation");
double jitter = testJitterSupplier != null
? testJitterSupplier.getAsDouble()
: ThreadLocalRandom.current().nextDouble();
@@ -62,7 +59,7 @@ public static long calculateTransactionBackoffMs(final int attemptNumber) {
/**
* Set a custom jitter supplier for testing purposes.
*
- * @param supplier A DoubleSupplier that returns values in [0, 1) range.
+ * @param supplier A DoubleSupplier that returns values in [0, 1] range.
*/
@VisibleForTesting(otherwise = PRIVATE)
public static void setTestJitterSupplier(final DoubleSupplier supplier) {
diff --git a/driver-core/src/test/unit/com/mongodb/internal/connection/InternalStreamConnectionInitializerSpecification.groovy b/driver-core/src/test/unit/com/mongodb/internal/connection/InternalStreamConnectionInitializerSpecification.groovy
index 1d44f8dde46..a7bfaa36cce 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/connection/InternalStreamConnectionInitializerSpecification.groovy
+++ b/driver-core/src/test/unit/com/mongodb/internal/connection/InternalStreamConnectionInitializerSpecification.groovy
@@ -201,6 +201,7 @@ class InternalStreamConnectionInitializerSpecification extends Specification {
def initializer = new InternalStreamConnectionInitializer(SINGLE, null, clientMetadataDocument, [], null)
def expectedHelloCommandDocument = new BsonDocument(LEGACY_HELLO, new BsonInt32(1))
.append('helloOk', BsonBoolean.TRUE)
+ .append('backpressure', BsonBoolean.TRUE)
.append('\$db', new BsonString('admin'))
if (clientMetadataDocument != null) {
expectedHelloCommandDocument.append('client', clientMetadataDocument)
@@ -233,6 +234,7 @@ class InternalStreamConnectionInitializerSpecification extends Specification {
def initializer = new InternalStreamConnectionInitializer(SINGLE, null, null, compressors, null)
def expectedHelloCommandDocument = new BsonDocument(LEGACY_HELLO, new BsonInt32(1))
.append('helloOk', BsonBoolean.TRUE)
+ .append('backpressure', BsonBoolean.TRUE)
.append('\$db', new BsonString('admin'))
def compressionArray = new BsonArray()
@@ -403,7 +405,8 @@ class InternalStreamConnectionInitializerSpecification extends Specification {
((SpeculativeAuthenticator) authenticator).getSpeculativeAuthenticateResponse() == null
((SpeculativeAuthenticator) authenticator)
.createSpeculativeAuthenticateCommand(internalConnection) == null
- BsonDocument.parse("{$LEGACY_HELLO: 1, helloOk: true, '\$db': 'admin'}") == decodeCommand(internalConnection.getSent()[0])
+ BsonDocument.parse("{$LEGACY_HELLO: 1, helloOk: true, backpressure: true, '\$db': 'admin'}") ==
+ decodeCommand(internalConnection.getSent()[0])
where:
async << [true, false]
@@ -500,7 +503,7 @@ class InternalStreamConnectionInitializerSpecification extends Specification {
def createHelloCommand(final String firstClientChallenge, final String mechanism,
final boolean hasSaslSupportedMechs) {
- String hello = "{$LEGACY_HELLO: 1, helloOk: true, " +
+ String hello = "{$LEGACY_HELLO: 1, helloOk: true, backpressure: true, " +
(hasSaslSupportedMechs ? 'saslSupportedMechs: "database.user", ' : '') +
(mechanism == 'MONGODB-X509' ?
'speculativeAuthenticate: { authenticate: 1, ' +
diff --git a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
index d1e02b9e4c9..a7978e23556 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
@@ -33,22 +33,17 @@ class ExponentialBackoffTest {
@Test
void testCalculateTransactionBackoffMs() {
- // Test that the backoff sequence follows the expected pattern with growth factor ExponentialBackoff.TRANSACTION_GROWTH
- // Expected sequence (without jitter): 5, 7.5, 11.25, ...
- // With jitter, actual values will be between 0 and these maxima
-
for (int attemptNumber = 1; attemptNumber <= EXPECTED_BACKOFFS_MAX_VALUES.length; attemptNumber++) {
long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
long expectedBackoff = Math.round(EXPECTED_BACKOFFS_MAX_VALUES[attemptNumber - 1]);
- assertTrue(backoff >= 0 && backoff <= expectedBackoff,
- String.format("Attempt %d: backoff should be between 0 ms and %d ms, got: %d", attemptNumber,
- expectedBackoff, backoff));
+ assertTrue(backoff >= 0 && backoff <= expectedBackoff,
+ String.format("Attempt %d: backoff should be between 0 ms and %d ms, got: %d", attemptNumber,
+ expectedBackoff, backoff));
}
}
@Test
void testCalculateTransactionBackoffMsRespectsMaximum() {
- // Even at high attempt numbers, backoff should never exceed ExponentialBackoff.TRANSACTION_MAX_MS
for (int attemptNumber = 1; attemptNumber < EXPECTED_BACKOFFS_MAX_VALUES.length * 2; attemptNumber++) {
long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
assertTrue(backoff >= 0 && backoff <= ExponentialBackoff.TRANSACTION_MAX_MS,
@@ -58,24 +53,25 @@ void testCalculateTransactionBackoffMsRespectsMaximum() {
}
@Test
- void testCustomJitter() {
- // Test with jitter = 1.0
+ void testCustomJitterWithOne() {
ExponentialBackoff.setTestJitterSupplier(() -> 1.0);
try {
for (int attemptNumber = 1; attemptNumber <= EXPECTED_BACKOFFS_MAX_VALUES.length; attemptNumber++) {
long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
long expected = Math.round(EXPECTED_BACKOFFS_MAX_VALUES[attemptNumber - 1]);
assertEquals(expected, backoff,
- String.format("Attempt %d: with jitter=1.0, backoff should be %d ms", attemptNumber, expected));
+ String.format("Attempt %d: with jitter=1.0, backoff should be %d ms", attemptNumber, expected));
}
} finally {
ExponentialBackoff.clearTestJitterSupplier();
}
+ }
- // Test with jitter = 0, all backoffs should be 0
+ @Test
+ void testCustomJitterWithZero() {
ExponentialBackoff.setTestJitterSupplier(() -> 0.0);
try {
- for (int attemptNumber = 1; attemptNumber < EXPECTED_BACKOFFS_MAX_VALUES.length; attemptNumber++) {
+ for (int attemptNumber = 1; attemptNumber <= EXPECTED_BACKOFFS_MAX_VALUES.length; attemptNumber++) {
long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
assertEquals(0, backoff, "With jitter=0, backoff should always be 0 ms");
}
diff --git a/driver-sync/src/examples/documentation/TransactionExample.java b/driver-sync/src/examples/documentation/TransactionExample.java
index 4f73122ee35..dea86b9ad4b 100644
--- a/driver-sync/src/examples/documentation/TransactionExample.java
+++ b/driver-sync/src/examples/documentation/TransactionExample.java
@@ -77,7 +77,8 @@ private void runTransactionWithRetry(final Runnable transactional) {
System.out.println("Transaction aborted. Caught exception during transaction.");
if (e.hasErrorLabel(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL)) {
- System.out.println("TransientTransactionError, aborting transaction and retrying ...");
+ System.out.printf("%s, aborting transaction and retrying ...%n",
+ MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL);
} else {
throw e;
}
@@ -94,7 +95,8 @@ private void commitWithRetry(final ClientSession clientSession) {
} catch (MongoException e) {
// can retry commit
if (e.hasErrorLabel(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)) {
- System.out.println("UnknownTransactionCommitResult, retrying commit operation ...");
+ System.out.printf("%s, retrying commit operation ...%n",
+ MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL);
} else {
System.out.println("Exception during commit ...");
throw e;
diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
index 0716bc026b3..7eecdfc4702 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
@@ -84,6 +84,7 @@
import static com.mongodb.ClusterFixture.isStandalone;
import static com.mongodb.ClusterFixture.serverVersionAtLeast;
import static com.mongodb.ClusterFixture.sleep;
+import static com.mongodb.MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL;
import static com.mongodb.client.Fixture.getDefaultDatabaseName;
import static com.mongodb.client.Fixture.getPrimary;
import static com.mongodb.internal.connection.CommandHelper.HELLO;
@@ -685,7 +686,7 @@ public void test10CustomTestWithTransactionUsesASingleTimeoutWithLock() {
+ " blockConnection: true,"
+ " blockTimeMS: " + 25
+ " errorCode: " + 24
- + " errorLabels: [\"TransientTransactionError\"]"
+ + " errorLabels: [\"" + TRANSIENT_TRANSACTION_ERROR_LABEL + "\"]"
+ " }"
+ "}");
diff --git a/driver-sync/src/test/functional/com/mongodb/client/MongoWriteConcernWithResponseExceptionTest.java b/driver-sync/src/test/functional/com/mongodb/client/MongoWriteConcernWithResponseExceptionTest.java
index 6f90b3f5f01..eccc892ce77 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/MongoWriteConcernWithResponseExceptionTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/MongoWriteConcernWithResponseExceptionTest.java
@@ -43,6 +43,8 @@
import static com.mongodb.ClusterFixture.serverVersionAtLeast;
import static com.mongodb.client.Fixture.getDefaultDatabaseName;
import static com.mongodb.client.Fixture.getMongoClientSettingsBuilder;
+import static com.mongodb.internal.operation.CommandOperationHelper.NO_WRITES_PERFORMED_ERROR_LABEL;
+import static com.mongodb.internal.operation.CommandOperationHelper.RETRYABLE_WRITE_ERROR_LABEL;
import static java.util.Collections.singletonList;
import static org.junit.Assert.assertThrows;
import static org.junit.Assume.assumeTrue;
@@ -69,7 +71,7 @@ public static void doesNotLeak(final Function
.append("data", new BsonDocument()
.append("writeConcernError", new BsonDocument()
.append("code", new BsonInt32(91))
- .append("errorLabels", new BsonArray(Stream.of("RetryableWriteError")
+ .append("errorLabels", new BsonArray(Stream.of(RETRYABLE_WRITE_ERROR_LABEL)
.map(BsonString::new).collect(Collectors.toList())))
.append("errmsg", new BsonString(""))
)
@@ -81,7 +83,7 @@ public static void doesNotLeak(final Function
.append("data", new BsonDocument()
.append("failCommands", new BsonArray(singletonList(new BsonString("insert"))))
.append("errorCode", new BsonInt32(10107))
- .append("errorLabels", new BsonArray(Stream.of("RetryableWriteError", "NoWritesPerformed")
+ .append("errorLabels", new BsonArray(Stream.of(RETRYABLE_WRITE_ERROR_LABEL, NO_WRITES_PERFORMED_ERROR_LABEL)
.map(BsonString::new).collect(Collectors.toList()))));
doesNotLeak(clientCreator, writeConcernErrorFpDoc, true, noWritesPerformedFpDoc);
doesNotLeak(clientCreator, noWritesPerformedFpDoc, false, writeConcernErrorFpDoc);
diff --git a/driver-sync/src/test/functional/com/mongodb/client/RetryableWritesProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/RetryableWritesProseTest.java
index fae39864bb9..3d5743fb459 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/RetryableWritesProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/RetryableWritesProseTest.java
@@ -67,6 +67,8 @@
import static com.mongodb.client.Fixture.getDefaultDatabaseName;
import static com.mongodb.client.Fixture.getMongoClientSettingsBuilder;
import static com.mongodb.client.Fixture.getMultiMongosMongoClientSettingsBuilder;
+import static com.mongodb.internal.operation.CommandOperationHelper.NO_WRITES_PERFORMED_ERROR_LABEL;
+import static com.mongodb.internal.operation.CommandOperationHelper.RETRYABLE_WRITE_ERROR_LABEL;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
@@ -135,7 +137,7 @@ public static void poolClearedExceptionMustBeRetryable(
.append("failCommands", new BsonArray(singletonList(new BsonString(operationName))))
.append("errorCode", new BsonInt32(91))
.append("errorLabels", write
- ? new BsonArray(singletonList(new BsonString("RetryableWriteError")))
+ ? new BsonArray(singletonList(new BsonString(RETRYABLE_WRITE_ERROR_LABEL)))
: new BsonArray())
.append("blockConnection", BsonBoolean.valueOf(true))
.append("blockTimeMS", new BsonInt32(1000)));
@@ -193,7 +195,7 @@ public void commandSucceeded(final CommandSucceededEvent event) {
.append("data", new BsonDocument()
.append("failCommands", new BsonArray(singletonList(new BsonString("insert"))))
.append("errorCode", new BsonInt32(10107))
- .append("errorLabels", new BsonArray(Stream.of("RetryableWriteError", "NoWritesPerformed")
+ .append("errorLabels", new BsonArray(Stream.of(RETRYABLE_WRITE_ERROR_LABEL, NO_WRITES_PERFORMED_ERROR_LABEL)
.map(BsonString::new).collect(Collectors.toList())))),
primaryServerAddress
)));
@@ -207,7 +209,7 @@ public void commandSucceeded(final CommandSucceededEvent event) {
.append("data", new BsonDocument()
.append("writeConcernError", new BsonDocument()
.append("code", new BsonInt32(91))
- .append("errorLabels", new BsonArray(Stream.of("RetryableWriteError")
+ .append("errorLabels", new BsonArray(Stream.of(RETRYABLE_WRITE_ERROR_LABEL)
.map(BsonString::new).collect(Collectors.toList())))
.append("errmsg", new BsonString(""))
)
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index 797f3297bcd..544a8781ed8 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -51,7 +51,9 @@
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
-// See https://github.com/mongodb/specifications/blob/master/source/transactions-convenient-api/tests/README.md#prose-tests
+/**
+ * Prose Tests.
+ */
public class WithTransactionProseTest extends DatabaseTestCase {
private static final Duration ERROR_GENERATING_INTERVAL = Duration.ofSeconds(120);
@@ -65,11 +67,10 @@ public void setUp() {
collection.insertOne(Document.parse("{ _id : 0 }"));
}
- //
- // Test that the callback raises a custom exception or error that does not include either UnknownTransactionCommitResult or
- // TransientTransactionError error labels. The callback will execute using withTransaction and assert that the callback's error
- // bypasses any retry logic within withTransaction and is propagated to the caller of withTransaction.
- //
+ /**
+ *
+ * Callback Raises a Custom Error.
+ */
@Test
public void testCallbackRaisesCustomError() {
final String exceptionMessage = "NotTransientOrUnknownError";
@@ -84,10 +85,10 @@ public void testCallbackRaisesCustomError() {
}
}
- //
- // Test that the callback that returns a custom value (e.g. boolean, string, object). Execute this callback using withTransaction
- // and assert that the callback's return value is propagated to the caller of withTransaction.
- //
+ /**
+ *
+ * Callback Returns a Value.
+ */
@Test
public void testCallbackReturnsValue() {
try (ClientSession session = client.startSession()) {
@@ -100,10 +101,10 @@ public void testCallbackReturnsValue() {
}
}
- //
- // If the callback raises an error with the TransientTransactionError label and the retry timeout has been exceeded, withTransaction
- // should propagate the error to its caller.
- //
+ /**
+ *
+ * Retry Timeout is Enforced, first scenario on the list.
+ */
@Test
public void testRetryTimeoutEnforcedTransientTransactionError() {
final String errorMessage = "transient transaction error";
@@ -126,10 +127,10 @@ public void testRetryTimeoutEnforcedTransientTransactionError() {
}
}
- //
- // If committing raises an error with the UnknownTransactionCommitResult label, the error is not a write concern timeout, and the
- // retry timeout has been exceeded, withTransaction should propagate the error to its caller.
- //
+ /**
+ *
+ * Retry Timeout is Enforced, second scenario on the list.
+ */
@Test
public void testRetryTimeoutEnforcedUnknownTransactionCommit() {
MongoDatabase failPointAdminDb = client.getDatabase("admin");
@@ -156,11 +157,10 @@ public void testRetryTimeoutEnforcedUnknownTransactionCommit() {
}
}
- //
- // If committing raises an error with the TransientTransactionError label and the retry timeout has been exceeded, withTransaction
- // should propagate the error to its caller. This case may occur if the commit was internally retried against a new primary after
- // a failover and the second primary returned a NoSuchTransaction error response.
- //
+ /**
+ *
+ * Retry Timeout is Enforced, third scenario on the list.
+ */
@Test
public void testRetryTimeoutEnforcedTransientTransactionErrorOnCommit() {
MongoDatabase failPointAdminDb = client.getDatabase("admin");
@@ -188,9 +188,9 @@ public void testRetryTimeoutEnforcedTransientTransactionErrorOnCommit() {
}
}
- //
- // Ensure cannot override timeout in transaction
- //
+ /**
+ * Ensure cannot override timeout in transaction.
+ */
@Test
public void testTimeoutMS() {
try (ClientSession session = client.startSession(ClientSessionOptions.builder()
@@ -204,9 +204,9 @@ public void testTimeoutMS() {
}
}
- //
- // Ensure legacy settings don't cause issues in sessions
- //
+ /**
+ * Ensure legacy settings don't cause issues in sessions.
+ */
@Test
public void testTimeoutMSAndLegacySettings() {
try (ClientSession session = client.startSession(ClientSessionOptions.builder()
@@ -232,18 +232,16 @@ public void testTimeoutMSAndLegacySettings() {
@DisplayName("Retry Backoff is Enforced")
@Test
public void testRetryBackoffIsEnforced() throws InterruptedException {
- final BsonDocument failPointDocument = BsonDocument.parse("{'configureFailPoint': 'failCommand', 'mode': {'times': 13}, "
- + "'data': {'failCommands': ['commitTransaction'], 'errorCode': 251}}");
+ long noBackoffTimeMs = measureTransactionLatencyMs(0.0);
+ long withBackoffTimeMs = measureTransactionLatencyMs(1.0);
- long noBackoffTime = measureTransactionLatency(0.0, failPointDocument);
- long withBackoffTime = measureTransactionLatency(1.0, failPointDocument);
+ long sumOfBackoffsMs = 1800;
+ long toleranceMs = 500;
+ long actualDifferenceMs = Math.abs(withBackoffTimeMs - (noBackoffTimeMs + sumOfBackoffsMs));
- long expectedWithBackoffTime = noBackoffTime + 1800;
- long actualDifference = Math.abs(withBackoffTime - expectedWithBackoffTime);
-
- assertTrue(actualDifference < 500, String.format("Expected withBackoffTime to be ~% dms (noBackoffTime %d ms + 1800 ms), but"
- + " got %d ms. Difference: %d ms (tolerance: 500 ms per spec)", expectedWithBackoffTime, noBackoffTime, withBackoffTime,
- actualDifference));
+ assertTrue(actualDifferenceMs < toleranceMs,
+ String.format("The actual difference between with and no backoff (%d ms) exceeds the specified tolerance (%d ms)",
+ actualDifferenceMs, toleranceMs));
}
/**
@@ -268,7 +266,9 @@ public void testExponentialBackoffOnTransientError() throws InterruptedException
}
}
- private long measureTransactionLatency(final double jitter, final BsonDocument failPointDocument) throws InterruptedException {
+ private long measureTransactionLatencyMs(final double jitter) throws InterruptedException {
+ BsonDocument failPointDocument = BsonDocument.parse("{'configureFailPoint': 'failCommand', 'mode': {'times': 13}, "
+ + "'data': {'failCommands': ['commitTransaction'], 'errorCode': 251}}");
ExponentialBackoff.setTestJitterSupplier(() -> jitter);
try (ClientSession session = client.startSession();
FailPoint ignored = FailPoint.enable(failPointDocument, getPrimary())) {
From 2f381d6342c23493758b023a3a3921d919729f7f Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Wed, 1 Apr 2026 16:41:37 +0100
Subject: [PATCH 57/72] Update
driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
.../src/main/com/mongodb/client/internal/ClientSessionImpl.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
index 17e6efd7b1a..ce4757bb612 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
@@ -419,6 +419,6 @@ private static MongoException wrapInMongoTimeoutException(final MongoException c
private static MongoTimeoutException wrapInNonTimeoutMsMongoTimeoutException(final MongoException cause) {
return cause instanceof MongoTimeoutException
? (MongoTimeoutException) cause
- : new MongoTimeoutException("Operation exceeded the timeout limit", cause);
+ : new MongoTimeoutException("Operation exceeded the timeout limit.", cause);
}
}
From 4a3d1ae1266b565a9c21dc2cc8bf623dda048c98 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Wed, 1 Apr 2026 17:51:47 +0100
Subject: [PATCH 58/72] Clarifying misleading assertion message (Copilot
feedback).
---
.../com/mongodb/client/WithTransactionProseTest.java | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index 544a8781ed8..687e9f8dccd 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -240,8 +240,7 @@ public void testRetryBackoffIsEnforced() throws InterruptedException {
long actualDifferenceMs = Math.abs(withBackoffTimeMs - (noBackoffTimeMs + sumOfBackoffsMs));
assertTrue(actualDifferenceMs < toleranceMs,
- String.format("The actual difference between with and no backoff (%d ms) exceeds the specified tolerance (%d ms)",
- actualDifferenceMs, toleranceMs));
+ String.format("Observed backoff time deviates from expected by %d ms (tolerance: %d ms)", actualDifferenceMs, toleranceMs));
}
/**
From 00cd3329d7f44721b6b3e2e7a9b8f6633f7c84f1 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Thu, 2 Apr 2026 19:05:30 +0100
Subject: [PATCH 59/72] Implement changes from DRIVERS-3436
---
.../com/mongodb/client/internal/ClientSessionImpl.java | 8 ++------
.../com/mongodb/client/WithTransactionProseTest.java | 6 ++----
2 files changed, 4 insertions(+), 10 deletions(-)
diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
index ce4757bb612..4ea30534c37 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
@@ -305,9 +305,7 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
} else if (labelCarryingException.hasErrorLabel(UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)) {
throw e;
} else {
- throw withTransactionTimeoutExpired.getAsBoolean()
- ? wrapInMongoTimeoutException(mongoException, timeoutMsConfigured)
- : mongoException;
+ throw mongoException;
}
}
throw e;
@@ -330,9 +328,7 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
lastError = mongoException;
continue outer;
}
- throw withTransactionTimeoutExpired.getAsBoolean()
- ? wrapInMongoTimeoutException(mongoException, timeoutMsConfigured)
- : mongoException;
+ throw mongoException;
}
}
}
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index 687e9f8dccd..3429ce8b298 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -147,11 +147,9 @@ public void testRetryTimeoutEnforcedUnknownTransactionCommit() {
}));
fail("Test should have thrown an exception.");
} catch (Exception e) {
- MongoTimeoutException exception = assertInstanceOf(MongoTimeoutException.class, e);
+ MongoNodeIsRecoveringException exception = assertInstanceOf(MongoNodeIsRecoveringException.class, e);
assertTrue(exception.hasErrorLabel(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL));
- MongoNodeIsRecoveringException cause = assertInstanceOf(MongoNodeIsRecoveringException.class, exception.getCause());
- assertEquals(91, cause.getCode());
- assertTrue(cause.hasErrorLabel(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL));
+ assertEquals(91, exception.getCode());
} finally {
failPointAdminDb.runCommand(Document.parse("{'configureFailPoint': 'failCommand', 'mode': 'off'}"));
}
From dbc75113e52948a10285c2e1400c98369e46c611 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Fri, 3 Apr 2026 16:42:14 +0100
Subject: [PATCH 60/72] Align with DRIVERS-3436. UnknownTransactionCommitResult
is retriable in the commit loop if we don't exceed the timeout, so it makes
sense to wrap it into a Timeout error if we exceed the timeout and want to
throw and return (as described in section 10.1.1)
---
.../main/com/mongodb/client/internal/ClientSessionImpl.java | 6 ++++--
.../com/mongodb/client/WithTransactionProseTest.java | 6 ++++--
2 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
index 4ea30534c37..99f9dc9708b 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
@@ -317,8 +317,10 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
break;
} catch (MongoException mongoException) {
if (mongoException.hasErrorLabel(UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)
- && !(mongoException instanceof MongoExecutionTimeoutException)
- && !withTransactionTimeoutExpired.getAsBoolean()) {
+ && !(mongoException instanceof MongoExecutionTimeoutException)) {
+ if (withTransactionTimeoutExpired.getAsBoolean()) {
+ throw wrapInMongoTimeoutException(mongoException, timeoutMsConfigured);
+ }
applyMajorityWriteConcernToTransactionOptions();
continue;
} else if (mongoException.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)) {
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index 3429ce8b298..687e9f8dccd 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -147,9 +147,11 @@ public void testRetryTimeoutEnforcedUnknownTransactionCommit() {
}));
fail("Test should have thrown an exception.");
} catch (Exception e) {
- MongoNodeIsRecoveringException exception = assertInstanceOf(MongoNodeIsRecoveringException.class, e);
+ MongoTimeoutException exception = assertInstanceOf(MongoTimeoutException.class, e);
assertTrue(exception.hasErrorLabel(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL));
- assertEquals(91, exception.getCode());
+ MongoNodeIsRecoveringException cause = assertInstanceOf(MongoNodeIsRecoveringException.class, exception.getCause());
+ assertEquals(91, cause.getCode());
+ assertTrue(cause.hasErrorLabel(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL));
} finally {
failPointAdminDb.runCommand(Document.parse("{'configureFailPoint': 'failCommand', 'mode': 'off'}"));
}
From bcf1a0ae14723aad54194807a5af25f99ca6a5b2 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Mon, 20 Apr 2026 18:31:57 +0100
Subject: [PATCH 61/72] Introduce WithTransactionTimeoutException for
withTransaction timeout.
Add a dedicated MongoClientException subtype thrown when the convenient transactions API exceeds its overall timeout, replacing the generic MongoTimeoutException wrap in ClientSessionImpl.
---
.../WithTransactionTimeoutException.java | 43 +++++++++++++++++++
.../client/internal/ClientSessionImpl.java | 5 ++-
.../client/WithTransactionProseTest.java | 8 ++--
3 files changed, 50 insertions(+), 6 deletions(-)
create mode 100644 driver-core/src/main/com/mongodb/WithTransactionTimeoutException.java
diff --git a/driver-core/src/main/com/mongodb/WithTransactionTimeoutException.java b/driver-core/src/main/com/mongodb/WithTransactionTimeoutException.java
new file mode 100644
index 00000000000..f3edda08c1a
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/WithTransactionTimeoutException.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2008-present MongoDB, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.mongodb;
+
+import com.mongodb.lang.Nullable;
+
+/**
+ * An exception indicating that the convenient transactions API
+ * ({@code withTransaction})
+ * exceeded its overall timeout while retrying the user-supplied callback or the commit loop.
+ * The last encountered error (if any) is attached as the
+ * {@linkplain Throwable#getCause() cause}.
+ *
+ * @since 5.7
+ */
+public class WithTransactionTimeoutException extends MongoClientException {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Construct a new instance
+ * @param message the message
+ * @param cause the cause
+ * @since 5.7
+ */
+ public WithTransactionTimeoutException(final String message, @Nullable final Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
index 99f9dc9708b..727e531eb04 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
@@ -24,6 +24,7 @@
import com.mongodb.MongoTimeoutException;
import com.mongodb.ReadConcern;
import com.mongodb.TransactionOptions;
+import com.mongodb.WithTransactionTimeoutException;
import com.mongodb.WriteConcern;
import com.mongodb.client.ClientSession;
import com.mongodb.client.TransactionBody;
@@ -414,9 +415,9 @@ private static MongoException wrapInMongoTimeoutException(final MongoException c
return timeoutException;
}
- private static MongoTimeoutException wrapInNonTimeoutMsMongoTimeoutException(final MongoException cause) {
+ private static MongoException wrapInNonTimeoutMsMongoTimeoutException(final MongoException cause) {
return cause instanceof MongoTimeoutException
? (MongoTimeoutException) cause
- : new MongoTimeoutException("Operation exceeded the timeout limit.", cause);
+ : new WithTransactionTimeoutException("Operation exceeded the timeout limit.", cause);
}
}
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index 687e9f8dccd..13506115906 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -21,8 +21,8 @@
import com.mongodb.MongoCommandException;
import com.mongodb.MongoException;
import com.mongodb.MongoNodeIsRecoveringException;
-import com.mongodb.MongoTimeoutException;
import com.mongodb.TransactionOptions;
+import com.mongodb.WithTransactionTimeoutException;
import com.mongodb.client.model.Sorts;
import com.mongodb.internal.time.ExponentialBackoff;
import com.mongodb.internal.time.StartTime;
@@ -119,7 +119,7 @@ public void testRetryTimeoutEnforcedTransientTransactionError() {
}));
fail("Test should have thrown an exception.");
} catch (Exception e) {
- MongoTimeoutException exception = assertInstanceOf(MongoTimeoutException.class, e);
+ WithTransactionTimeoutException exception = assertInstanceOf(WithTransactionTimeoutException.class, e);
assertTrue(exception.hasErrorLabel(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL));
MongoException cause = assertInstanceOf(MongoException.class, exception.getCause());
assertEquals(errorMessage, cause.getMessage());
@@ -147,7 +147,7 @@ public void testRetryTimeoutEnforcedUnknownTransactionCommit() {
}));
fail("Test should have thrown an exception.");
} catch (Exception e) {
- MongoTimeoutException exception = assertInstanceOf(MongoTimeoutException.class, e);
+ WithTransactionTimeoutException exception = assertInstanceOf(WithTransactionTimeoutException.class, e);
assertTrue(exception.hasErrorLabel(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL));
MongoNodeIsRecoveringException cause = assertInstanceOf(MongoNodeIsRecoveringException.class, exception.getCause());
assertEquals(91, cause.getCode());
@@ -178,7 +178,7 @@ public void testRetryTimeoutEnforcedTransientTransactionErrorOnCommit() {
}));
fail("Test should have thrown an exception.");
} catch (Exception e) {
- MongoTimeoutException exception = assertInstanceOf(MongoTimeoutException.class, e);
+ WithTransactionTimeoutException exception = assertInstanceOf(WithTransactionTimeoutException.class, e);
assertTrue(exception.hasErrorLabel(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL));
MongoCommandException cause = assertInstanceOf(MongoCommandException.class, exception.getCause());
assertEquals(251, cause.getCode());
From efad419362b43f267d8cc2779aa5afda678f1d1b Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Mon, 20 Apr 2026 18:38:54 +0100
Subject: [PATCH 62/72] Fix checkstyle
---
.../src/main/com/mongodb/WithTransactionTimeoutException.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/driver-core/src/main/com/mongodb/WithTransactionTimeoutException.java b/driver-core/src/main/com/mongodb/WithTransactionTimeoutException.java
index f3edda08c1a..50eb7658e31 100644
--- a/driver-core/src/main/com/mongodb/WithTransactionTimeoutException.java
+++ b/driver-core/src/main/com/mongodb/WithTransactionTimeoutException.java
@@ -23,7 +23,7 @@
* ({@code withTransaction})
* exceeded its overall timeout while retrying the user-supplied callback or the commit loop.
* The last encountered error (if any) is attached as the
- * {@linkplain Throwable#getCause() cause}.
+ * {@linkplain Throwable#getCause() cause}.
*
* @since 5.7
*/
From 6152bfa84345d325e637e98b776c3cb29b4bd12f Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Tue, 21 Apr 2026 16:09:01 +0100
Subject: [PATCH 63/72] - Mark WithTransactionTimeoutException final and switch
its docs link to the @mongodb.driver.manual taglet. - Narrow
wrapInMongoTimeoutException and wrapInNonTimeoutMsMongoTimeoutException
return types from MongoException to MongoClientException.
---
.../main/com/mongodb/WithTransactionTimeoutException.java | 6 +++---
.../main/com/mongodb/client/internal/ClientSessionImpl.java | 6 +++---
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/driver-core/src/main/com/mongodb/WithTransactionTimeoutException.java b/driver-core/src/main/com/mongodb/WithTransactionTimeoutException.java
index 50eb7658e31..118d9c59424 100644
--- a/driver-core/src/main/com/mongodb/WithTransactionTimeoutException.java
+++ b/driver-core/src/main/com/mongodb/WithTransactionTimeoutException.java
@@ -19,15 +19,15 @@
import com.mongodb.lang.Nullable;
/**
- * An exception indicating that the convenient transactions API
- * ({@code withTransaction})
+ * An exception indicating that the convenient transactions API ({@code withTransaction})
* exceeded its overall timeout while retrying the user-supplied callback or the commit loop.
* The last encountered error (if any) is attached as the
* {@linkplain Throwable#getCause() cause}.
*
* @since 5.7
+ * @mongodb.driver.manual core/transactions-in-applications/#callback-api withTransaction
*/
-public class WithTransactionTimeoutException extends MongoClientException {
+public final class WithTransactionTimeoutException extends MongoClientException {
private static final long serialVersionUID = 1L;
diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
index 727e531eb04..a1b5f4b730c 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
@@ -405,8 +405,8 @@ private static void backoff(final int transactionAttempt,
}
}
- private static MongoException wrapInMongoTimeoutException(final MongoException cause, final boolean timeoutMsConfigured) {
- MongoException timeoutException = timeoutMsConfigured
+ private static MongoClientException wrapInMongoTimeoutException(final MongoException cause, final boolean timeoutMsConfigured) {
+ MongoClientException timeoutException = timeoutMsConfigured
? createMongoTimeoutException(cause)
: wrapInNonTimeoutMsMongoTimeoutException(cause);
if (timeoutException != cause) {
@@ -415,7 +415,7 @@ private static MongoException wrapInMongoTimeoutException(final MongoException c
return timeoutException;
}
- private static MongoException wrapInNonTimeoutMsMongoTimeoutException(final MongoException cause) {
+ private static MongoClientException wrapInNonTimeoutMsMongoTimeoutException(final MongoException cause) {
return cause instanceof MongoTimeoutException
? (MongoTimeoutException) cause
: new WithTransactionTimeoutException("Operation exceeded the timeout limit.", cause);
From f205b31a4270c54d0c2bf8ec5e710f68ecd1a110 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Tue, 21 Apr 2026 16:13:47 +0100
Subject: [PATCH 64/72] Alias WithTransactionTimeoutException in
org.mongodb.scala
---
.../src/main/scala/org/mongodb/scala/package.scala | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/driver-scala/src/main/scala/org/mongodb/scala/package.scala b/driver-scala/src/main/scala/org/mongodb/scala/package.scala
index 1cdc2d0a564..c122b2c79ba 100644
--- a/driver-scala/src/main/scala/org/mongodb/scala/package.scala
+++ b/driver-scala/src/main/scala/org/mongodb/scala/package.scala
@@ -377,6 +377,14 @@ package object scala extends ClientSessionImplicits with ObservableImplicits wit
*/
type MongoOperationTimeoutException = com.mongodb.MongoOperationTimeoutException
+ /**
+ * An exception indicating that the convenient transactions API (`withTransaction`) exceeded its overall timeout
+ * while retrying the user-supplied callback or the commit loop.
+ *
+ * @since 5.7
+ */
+ type WithTransactionTimeoutException = com.mongodb.WithTransactionTimeoutException
+
/**
* An exception indicating a failure to apply the write concern to the requested write operation
*
From 68bbd16be448cdb48947568e709f396c8aa505c6 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Fri, 24 Apr 2026 13:01:55 +0100
Subject: [PATCH 65/72] Update
driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
Co-authored-by: Viacheslav Babanin
---
.../src/main/com/mongodb/client/internal/ClientSessionImpl.java | 1 +
1 file changed, 1 insertion(+)
diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
index a1b5f4b730c..831c2bb8161 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
@@ -409,6 +409,7 @@ private static MongoClientException wrapInMongoTimeoutException(final MongoExcep
MongoClientException timeoutException = timeoutMsConfigured
? createMongoTimeoutException(cause)
: wrapInNonTimeoutMsMongoTimeoutException(cause);
+ //TODO-JAVA-6154 constructor should be used.
if (timeoutException != cause) {
cause.getErrorLabels().forEach(timeoutException::addLabel);
}
From 84fd6251cff42ee9d4f8444d885b6475d62eb40b Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Fri, 24 Apr 2026 13:47:42 +0100
Subject: [PATCH 66/72] Update
driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
Co-authored-by: Viacheslav Babanin
---
.../unit/com/mongodb/internal/time/ExponentialBackoffTest.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
index a7978e23556..88a5d02f708 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
@@ -68,7 +68,7 @@ void testCustomJitterWithOne() {
}
@Test
- void testCustomJitterWithZero() {
+ void testCalculateTransactionBackoffMsWithJitterZero() {
ExponentialBackoff.setTestJitterSupplier(() -> 0.0);
try {
for (int attemptNumber = 1; attemptNumber <= EXPECTED_BACKOFFS_MAX_VALUES.length; attemptNumber++) {
From 2355934545ec50e25a17b15a766376eef50a3605 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Fri, 24 Apr 2026 13:48:25 +0100
Subject: [PATCH 67/72] Update
driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
Co-authored-by: Viacheslav Babanin
---
.../unit/com/mongodb/internal/time/ExponentialBackoffTest.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
index 88a5d02f708..db569d3016c 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
@@ -53,7 +53,7 @@ void testCalculateTransactionBackoffMsRespectsMaximum() {
}
@Test
- void testCustomJitterWithOne() {
+ void testCalculateTransactionBackoffMsWithJitterOne() {
ExponentialBackoff.setTestJitterSupplier(() -> 1.0);
try {
for (int attemptNumber = 1; attemptNumber <= EXPECTED_BACKOFFS_MAX_VALUES.length; attemptNumber++) {
From 2b32dd5c86f3af8a9d7d093097f154a1b206c4d7 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Fri, 24 Apr 2026 13:48:39 +0100
Subject: [PATCH 68/72] Update
driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
Co-authored-by: Viacheslav Babanin
---
.../client/AbstractClientSideOperationsTimeoutProseTest.java | 1 -
1 file changed, 1 deletion(-)
diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
index 7eecdfc4702..420c6fae002 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java
@@ -1120,7 +1120,6 @@ public void setUp() {
filesCollectionHelper = new CollectionHelper<>(new BsonDocumentCodec(), gridFsFileNamespace);
chunksCollectionHelper = new CollectionHelper<>(new BsonDocumentCodec(), gridFsChunksNamespace);
commandListener = new TestCommandListener();
-
}
@AfterEach
From fb9e64288cd9d13df935181668cade4efcfc9cb7 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Fri, 24 Apr 2026 13:49:02 +0100
Subject: [PATCH 69/72] Update
driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
Co-authored-by: Viacheslav Babanin
---
.../unit/com/mongodb/internal/time/ExponentialBackoffTest.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
index db569d3016c..dec8ac31e85 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
@@ -73,7 +73,7 @@ void testCalculateTransactionBackoffMsWithJitterZero() {
try {
for (int attemptNumber = 1; attemptNumber <= EXPECTED_BACKOFFS_MAX_VALUES.length; attemptNumber++) {
long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
- assertEquals(0, backoff, "With jitter=0, backoff should always be 0 ms");
+ assertEquals(0, backoff, "Attempt %d: with jitter=0, backoff should always be 0 ms");
}
} finally {
ExponentialBackoff.clearTestJitterSupplier();
From 6e3349855abc1b1f179d2cf3cc7f3bd4f5397c94 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Fri, 24 Apr 2026 14:00:15 +0100
Subject: [PATCH 70/72] PR feedback
---
.../internal/time/ExponentialBackoffTest.java | 2 +-
.../com/mongodb/client/WithTransactionProseTest.java | 12 ++++++------
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
index dec8ac31e85..504a9840e73 100644
--- a/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
+++ b/driver-core/src/test/unit/com/mongodb/internal/time/ExponentialBackoffTest.java
@@ -73,7 +73,7 @@ void testCalculateTransactionBackoffMsWithJitterZero() {
try {
for (int attemptNumber = 1; attemptNumber <= EXPECTED_BACKOFFS_MAX_VALUES.length; attemptNumber++) {
long backoff = ExponentialBackoff.calculateTransactionBackoffMs(attemptNumber);
- assertEquals(0, backoff, "Attempt %d: with jitter=0, backoff should always be 0 ms");
+ assertEquals(0, backoff, String.format("Attempt %d: with jitter=0, backoff should always be 0 ms", attemptNumber));
}
} finally {
ExponentialBackoff.clearTestJitterSupplier();
diff --git a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
index 13506115906..6d2b928e8ec 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java
@@ -55,7 +55,7 @@
* Prose Tests.
*/
public class WithTransactionProseTest extends DatabaseTestCase {
- private static final Duration ERROR_GENERATING_INTERVAL = Duration.ofSeconds(120);
+ private static final Duration TIMEOUT_EXCEEDING_DURATION = Duration.ofSeconds(120);
@BeforeEach
@Override
@@ -112,7 +112,7 @@ public void testRetryTimeoutEnforcedTransientTransactionError() {
try (ClientSession session = client.startSession()) {
doWithSystemNanoTimeHandle(systemNanoTimeHandle ->
session.withTransaction(() -> {
- systemNanoTimeHandle.setRelativeToStart(ERROR_GENERATING_INTERVAL);
+ systemNanoTimeHandle.setRelativeToStart(TIMEOUT_EXCEEDING_DURATION);
MongoException e = new MongoException(112, errorMessage);
e.addLabel(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL);
throw e;
@@ -141,7 +141,7 @@ public void testRetryTimeoutEnforcedUnknownTransactionCommit() {
try (ClientSession session = client.startSession()) {
doWithSystemNanoTimeHandle(systemNanoTimeHandle ->
session.withTransaction(() -> {
- systemNanoTimeHandle.setRelativeToStart(ERROR_GENERATING_INTERVAL);
+ systemNanoTimeHandle.setRelativeToStart(TIMEOUT_EXCEEDING_DURATION);
collection.insertOne(session, new Document("_id", 2));
return null;
}));
@@ -172,7 +172,7 @@ public void testRetryTimeoutEnforcedTransientTransactionErrorOnCommit() {
try (ClientSession session = client.startSession()) {
doWithSystemNanoTimeHandle(systemNanoTimeHandle ->
session.withTransaction(() -> {
- systemNanoTimeHandle.setRelativeToStart(ERROR_GENERATING_INTERVAL);
+ systemNanoTimeHandle.setRelativeToStart(TIMEOUT_EXCEEDING_DURATION);
collection.insertOne(session, Document.parse("{ _id : 1 }"));
return null;
}));
@@ -189,7 +189,7 @@ public void testRetryTimeoutEnforcedTransientTransactionErrorOnCommit() {
}
/**
- * Ensure cannot override timeout in transaction.
+ * This test is not from the specification. Ensures cannot override timeout in transaction.
*/
@Test
public void testTimeoutMS() {
@@ -205,7 +205,7 @@ public void testTimeoutMS() {
}
/**
- * Ensure legacy settings don't cause issues in sessions.
+ * This test is not from the specification. Ensures legacy settings don't cause issues in sessions.
*/
@Test
public void testTimeoutMSAndLegacySettings() {
From d685ffa46663d60ff62792ffb216ae75e18df572 Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Tue, 28 Apr 2026 11:15:46 +0100
Subject: [PATCH 71/72] Update
driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
Co-authored-by: Viacheslav Babanin
---
.../main/com/mongodb/client/internal/ClientSessionImpl.java | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
index 831c2bb8161..cf8c54dd0d5 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
@@ -303,11 +303,7 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
}
lastError = mongoException;
continue;
- } else if (labelCarryingException.hasErrorLabel(UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL)) {
- throw e;
- } else {
- throw mongoException;
- }
+}
}
throw e;
}
From 7b2301dd49deabc9d861d21dcc9d38fdba372aed Mon Sep 17 00:00:00 2001
From: Nabil Hachicha
Date: Tue, 28 Apr 2026 11:29:16 +0100
Subject: [PATCH 72/72] Fixing checkstyle
---
.../main/com/mongodb/client/internal/ClientSessionImpl.java | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
index cf8c54dd0d5..3ff86f64d3b 100644
--- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
+++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java
@@ -303,7 +303,7 @@ public T withTransaction(final TransactionBody transactionBody, final Tra
}
lastError = mongoException;
continue;
-}
+ }
}
throw e;
}
@@ -405,7 +405,7 @@ private static MongoClientException wrapInMongoTimeoutException(final MongoExcep
MongoClientException timeoutException = timeoutMsConfigured
? createMongoTimeoutException(cause)
: wrapInNonTimeoutMsMongoTimeoutException(cause);
- //TODO-JAVA-6154 constructor should be used.
+ //TODO-JAVA-6154 constructor should be used.
if (timeoutException != cause) {
cause.getErrorLabels().forEach(timeoutException::addLabel);
}