Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1ed116f
JAVA-5950 - Update Transactions Convenient API with exponential back…
nhachicha Dec 3, 2025
eb8b4ad
Simplifying test, clean up.
nhachicha Dec 9, 2025
b8b0e1a
Fixing test
nhachicha Dec 9, 2025
c05ce05
Update driver-sync/src/main/com/mongodb/client/internal/ClientSession…
nhachicha Dec 9, 2025
aa96659
retrigger checks
nhachicha Dec 9, 2025
98fc57b
retrigger checks
nhachicha Dec 9, 2025
f98262e
retrigger checks
nhachicha Dec 9, 2025
bfc89fc
retrigger checks
nhachicha Dec 9, 2025
d9405ef
test cleanup
nhachicha Dec 9, 2025
5de452b
retrigger checks
nhachicha Dec 9, 2025
1867ff5
Test cleanup
nhachicha Dec 9, 2025
f89d62d
retrigger checks
nhachicha Dec 10, 2025
3d646ae
Update the implementation according to the spec
nhachicha Dec 10, 2025
9b4bf15
Added prose test
nhachicha Dec 10, 2025
da83704
Flaky test
nhachicha Dec 10, 2025
cb95167
Remove extra Test annotation
nhachicha Dec 11, 2025
ef734a0
Throwing correct exception when CSOT is used
nhachicha Dec 14, 2025
96b5ed7
Simplifying implementation by relying on CSOT to throw when timeout i…
nhachicha Dec 14, 2025
43eda52
Fixing implementation according to spec changes in JAVA-6046 and http…
nhachicha Jan 13, 2026
a4193dd
Update driver-sync/src/test/functional/com/mongodb/client/WithTransac…
nhachicha Jan 13, 2026
f2d8263
Update driver-sync/src/test/functional/com/mongodb/client/WithTransac…
nhachicha Jan 14, 2026
f3daab0
Update driver-sync/src/test/functional/com/mongodb/client/WithTransac…
nhachicha Jan 14, 2026
7afd9d4
Update driver-sync/src/test/functional/com/mongodb/client/WithTransac…
nhachicha Jan 14, 2026
ccb8d03
Update driver-sync/src/test/functional/com/mongodb/client/WithTransac…
nhachicha Jan 14, 2026
bbb9a68
Update driver-sync/src/test/functional/com/mongodb/client/WithTransac…
nhachicha Jan 14, 2026
c44872d
Update driver-sync/src/test/functional/com/mongodb/client/WithTransac…
nhachicha Jan 14, 2026
f0dd916
Update driver-core/src/main/com/mongodb/internal/ExponentialBackoff.java
nhachicha Jan 14, 2026
0e00c90
PR feedback
nhachicha Jan 15, 2026
36ecbf9
remove annotation
nhachicha Jan 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* 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.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.
*/
public enum ExponentialBackoff {
TRANSACTION(5.0, 500.0, 1.5);

private final double baseMs, maxMs, growth;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't use this declaration style in the Java driver codebase. Let's declare each instance field separately.


// TODO remove this global state once https://jira.mongodb.org/browse/JAVA-6060 is done
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • The tag format is TODO-<ticket ID>. This way we can find all such tags by searching the codebase for "TODO-", and not have them mixed them with the (useless) TODO tags that were introduced to the code before we started using the new approach.
  • Comments / error messages / etc. with such tags is a mechanism we resort to if the description of a ticket is not enough to conveniently specify all the information. In this particular case, the description of a ticket is very much enough, as we can just say in it something like "Use InternalMongoClientSettings to get rid of the ExponentialBackoff.testJitterSupplier global state". So we should probably not resort to leaving a comment with the TODO-<ticket ID> tag.
  • When we resort to this mechanism, we should also leave a note in the ticket description that addressing comments with the TODO-<ticket ID> tag is in scope of the ticket. See https://jira.mongodb.org/browse/JAVA-6005, https://jira.mongodb.org/browse/JAVA-6059 as examples (I updated the latter, as well as some other tickets, because they were missing the note). Such notes are important because they draw attention of the assignee to the tagged comments. Without a note, the assignee is more likely to not even realize there are relevant tagged comments.

https://jira.mongodb.org/browse/JAVA-6060 is about introducing InternalMongoClientSettings and getting rid of InternalStreamConnection.setRecordEverything, but is not about getting rid of ExponentialBackoff.testJitterSupplier. Therefore, we should

  • create another ticket that will use InternalMongoClientSettings to get rid of the ExponentialBackoff.testJitterSupplier global state;
  • link it to https://jira.mongodb.org/browse/JAVA-6060 via the "depends on" link.

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);
}
Comment on lines +43 to +55
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class name uses the term "backoff", but the method uses the term "delay" (both in its name and in the documentation comment). Let's not use two different terms to refer to the same thing.

I am guessing the above is an indirect result of you deciding to call backoff the intermediate result when computing the backoff. Given that this intermediate result does not serve any purpose, we don't have to make it a thing and name it, we can just write the whole formula like return Math.round(jitter * Math.min(baseMs * Math.pow(growth, retryCount), maxMs)).

Comment on lines +43 to +55
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method accepts retryCount despite:

  • both the specification and ClientSessionImpl.withTransaction operating 0-based number of a transaction attempt that it is called transactionAttempt;
    • As a result, ClientSessionImpl has to pass transactionAttempt - 1 when calling calculateDelayBeforeNextRetryMs to compute the backoff for the attempt with number transactionAttempt.
  • RetryState.attempt() returning 0-based attempt number.

Let's change this method so that it also operates 0-based attempt number by accepting attempt documented as the 0-based number of the transaction attempt for which backoff is to be calculated.


/**
* 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);
}
Comment on lines +57 to +67
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. This method is called only from a test that tests this method. This makes the method effectively dead. If the method is introduced in anticipation to become useful in the future, then we should introduce it when and in the PR where it is going to be actually used.
  2. Just for information: if the method were not dead, I would have suggest that the implementation of the calculateDelayBeforeNextRetryMs method should be done by calling calculateDelayBeforeNextRetryMs, to reuse the code instead of duplicating it. But I am not suggesting that because of item 1 above.


/**
* 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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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.internal.time.ExponentialBackoff;
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() {
// Test that the backoff sequence follows the expected pattern with growth factor 1.5
// 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};

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 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));
}
}

@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;

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));
}

// 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");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import com.mongodb.client.ClientSession;
import com.mongodb.client.TransactionBody;
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;
Expand All @@ -36,8 +38,7 @@
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.internal.time.ExponentialBackoff;
import com.mongodb.lang.Nullable;

import static com.mongodb.MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL;
Expand All @@ -46,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 {

Expand Down Expand Up @@ -251,13 +253,21 @@ public <T> T withTransaction(final TransactionBody<T> transactionBody, final Tra
notNull("transactionBody", transactionBody);
long startTime = ClientSessionClock.INSTANCE.now();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[just a comment on a code that wasn't changed in this PR]

I have just noticed this ClientSessionClock - it uses non-monotonic clock. Horrendous.

TimeoutContext withTransactionTimeoutContext = createTimeoutContext(options);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an explanatory comment for the changes proposed in stIncMale@08171aa.

Taking into account

  • recent team discussion about doing maintenance as we go, when possible;
  • the fact that should use timeout from withTransactionTimeoutContext instead of always using MAX_RETRY_TIME_LIMIT_MS;
  • the fact that ClientSessionClock has only a few usages in production code (all are in the withTransaction method)

we should replace it with a correct internal API that uses monotonic clock. We already have such API in the com.mongodb.internal.time package, and use it extensively in CSOT code.

  • We should use Timeout to track the elapsed time against the time budget.
  • We should use Mockito.mockStatic to tamper with the clock instead of making the tampering code part of the production code, like it is done in ClientSessionClock and com.mongodb.internal.connection.Time.

See SystemNanoTime for a bit more explanation and see the changes in WithTransactionProseTest, where ClientSessionClock.INSTANCE.setTime was used previously, for the Mockito.mockStatic usage.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an explanatory comment for the changes proposed in stIncMale@08171aa.

We should create Timeout withTransactionTimeout from withTransactionTimeoutContext, taking into account that if CSOT is not enabled, then MAX_RETRY_TIME_LIMIT_MS is to be used. To achieve this, let's introduce TimeoutContext.timeoutOrAlternative(Timeout alternative)1.

See also the comment stIncMale@08171aa#r175640155 for additional related explanations.


1 TimeoutContext already has timeoutOrAlternative(alternativeTimeoutMS), but that method really is just an implementation detail, that is also used in one of the test classes. It is not too suitable as internal API, because we should strive to use types (like Timeout, StartTime, Duration) when working with time, rather than faceless long values that do not carry enough type information.

ExponentialBackoff transactionBackoff = ExponentialBackoff.TRANSACTION;
int transactionAttempt = 0;
MongoException lastError = null;

try {
outer:
while (true) {
if (transactionAttempt > 0) {
backoff(transactionBackoff, transactionAttempt, startTime, lastError);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an explanatory comment for the changes proposed in stIncMale@08171aa.

lastError cannot be null at this point in execution, but javac or a static checker can't know that. We should use assertNotNull here, and stop handing null lastErrors in the backoff method.

}
T retVal;
try {
startTransaction(options, withTransactionTimeoutContext.copyTimeoutContext());
transactionAttempt++;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an explanatory comment for the changes proposed in stIncMale@08171aa.

copyTimeoutContext is a method that should have been removed when TimeoutContext was made immutable, but it accidentally was not. Let's remove it now.


if (transactionSpan != null) {
transactionSpan.setIsConvenientTransaction();
}
Expand All @@ -266,14 +276,17 @@ public <T> T withTransaction(final TransactionBody<T> 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;
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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The withTransaction method must be bounded by timeoutMS according to the specification. But here and below it still always uses 120 seconds (MAX_RETRY_TIME_LIMIT_MS).

The changes I propose for ClientSessionImpl.java are expressed in stIncMale@08171aa. I am leaving comments in this PR that explain the changes proposed, but those comments are purely explanatory, and link to the commit as the suggestion.

if (transactionSpan != null) {
transactionSpan.spanFinalizing(false);
}
continue;
}
continue;
}
}
throw e;
Expand All @@ -296,6 +309,7 @@ public <T> T withTransaction(final TransactionBody<T> transactionBody, final Tra
if (transactionSpan != null) {
transactionSpan.spanFinalizing(true);
}
lastError = e;
continue outer;
}
}
Expand Down Expand Up @@ -359,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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an explanatory comment for the changes proposed in stIncMale@08171aa.

What is the reason behind passing exponentialBackoff as a parameter here? If there is no good reason, the backoff method should just use ExponentialBackoff.TRANSACTION.calculateDelayBeforeNextRetryMs.

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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,20 @@
import com.mongodb.TransactionOptions;
import com.mongodb.client.internal.ClientSessionClock;
import com.mongodb.client.model.Sorts;
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;
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;
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;
Expand Down Expand Up @@ -203,6 +208,76 @@ public void testTimeoutMSAndLegacySettings() {
}
}

/**
* See
* <a href="https://github.com/mongodb/specifications/blob/master/source/transactions-convenient-api/tests/README.md#retry-backoff-is-enforceds">Convenient API Prose Tests</a>.
*/
@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())) {
long startNanos = System.nanoTime();
session.withTransaction(() -> collection.insertOne(session, Document.parse("{}")));
noBackoffTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
Comment on lines +227 to +229
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an explanatory comment for the changes proposed in stIncMale@08171aa.

We have StartTime for this scenario, let's use it:

StartTime startTime = StartTime.now();
...
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}, "
+ "'data': {'failCommands': ['commitTransaction'], 'errorCode': 251}}");

long withBackoffTime;
try (ClientSession session = client.startSession();
FailPoint ignored = FailPoint.enable(failPointDocument, getPrimary())) {
long startNanos = System.nanoTime();
session.withTransaction(() -> collection.insertOne(session, Document.parse("{}")));
withBackoffTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
} finally {
ExponentialBackoff.clearTestJitterSupplier();
}

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,
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();
}
Comment on lines 281 to 283
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an explanatory comment for the changes proposed in stIncMale@08171aa.

This method can be static, so I made it static.

Expand Down