Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
24 changes: 24 additions & 0 deletions driver-core/src/main/com/mongodb/MongoException.java
Copy link
Copy Markdown
Member Author

@stIncMale stIncMale Mar 24, 2026

Choose a reason for hiding this comment

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

TODO-BACKPRESSURE Valentin

Before merging in main, make sure that all the code in the backpressure branch uses the MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL/UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL/SYSTEM_OVERLOADED_ERROR_LABEL/RETRYABLE_ERROR_LABEL and CommandOperationHelper.RETRYABLE_WRITE_ERROR_LABEL/NO_WRITES_PERFORMED_ERROR_LABEL constants instead of using the TransientTransactionError/UnknownTransactionCommitResult/SystemOverloadedError/RetryableError and RetryableWriteError/NoWritesPerformed literals.

Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Copy Markdown
Member Author

@stIncMale stIncMale Mar 24, 2026

Choose a reason for hiding this comment

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

@nhachicha

The commit ac1c120 introduced by #1906 for JAVA-6035 should have implemented, as far as I understand, the Test 9: Handshake documents include backpressure: true prose test (though the PR says nothing about the relevant spec changes it was meant to implement).

If the above is correct, let's reopen JAVA-6035 and create another PR for that ticket that will implement the prose test.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for catching this. I reopened https://jira.mongodb.org/browse/JAVA-6035

if (clientMetadataDocument != null) {
helloCommandDocument.append("client", clientMetadataDocument);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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, ' +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import com.mongodb.event.ConnectionCreatedEvent;
import com.mongodb.event.ConnectionReadyEvent;

import static com.mongodb.MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL;
import static com.mongodb.internal.connection.CommandHelper.HELLO;
import static com.mongodb.internal.connection.CommandHelper.LEGACY_HELLO;

Expand Down Expand Up @@ -687,7 +688,7 @@ public void test10CustomTestWithTransactionUsesASingleTimeoutWithLock() {
+ " blockConnection: true,"
+ " blockTimeMS: " + 25
+ " errorCode: " + 24
+ " errorLabels: [\"TransientTransactionError\"]"
+ " errorLabels: [\"" + TRANSIENT_TRANSACTION_ERROR_LABEL + "\"]"
+ " }"
+ "}");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -69,7 +71,7 @@ public static void doesNotLeak(final Function<MongoClientSettings, MongoClient>
.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(""))
)
Expand All @@ -81,7 +83,7 @@ public static void doesNotLeak(final Function<MongoClientSettings, MongoClient>
.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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -135,7 +137,7 @@ public static <R> 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)));
Expand Down Expand Up @@ -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
)));
Expand All @@ -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(""))
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,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
/**
* <a href="https://github.com/mongodb/specifications/blob/master/source/transactions-convenient-api/tests/README.md#prose-tests">Prose Tests</a>.
*/
public class WithTransactionProseTest extends DatabaseTestCase {
Copy link
Copy Markdown
Collaborator

@nhachicha nhachicha Apr 1, 2026

Choose a reason for hiding this comment

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

TODO-BACKPRESSURE Nabil
Git submodule must point to mongodb/specifications@1125200 at the very least or latest main

private static final long START_TIME_MS = 1L;
private static final long ERROR_GENERATING_INTERVAL = 121000L;
Expand All @@ -52,11 +54,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.
//
/**
* <a href="https://github.com/mongodb/specifications/blob/master/source/transactions-convenient-api/tests/README.md#callback-raises-a-custom-error">
* Callback Raises a Custom Error</a>.
*/
@Test
public void testCallbackRaisesCustomError() {
final String exceptionMessage = "NotTransientOrUnknownError";
Expand All @@ -71,10 +72,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.
//
/**
* <a href="https://github.com/mongodb/specifications/blob/master/source/transactions-convenient-api/tests/README.md#callback-returns-a-value">
* Callback Returns a Value</a>.
*/
@Test
public void testCallbackReturnsValue() {
try (ClientSession session = client.startSession()) {
Expand All @@ -87,10 +88,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.
//
/**
* <a href="https://github.com/mongodb/specifications/blob/master/source/transactions-convenient-api/tests/README.md#retry-timeout-is-enforced">
* Retry Timeout is Enforced</a>, first scenario on the list.
*/
@Test
public void testRetryTimeoutEnforcedTransientTransactionError() {
final String errorMessage = "transient transaction error";
Expand All @@ -110,10 +111,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.
//
/**
* <a href="https://github.com/mongodb/specifications/blob/master/source/transactions-convenient-api/tests/README.md#retry-timeout-is-enforced">
* Retry Timeout is Enforced</a>, second scenario on the list.
*/
@Test
public void testRetryTimeoutEnforcedUnknownTransactionCommit() {
MongoDatabase failPointAdminDb = client.getDatabase("admin");
Expand All @@ -137,11 +138,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.
//
/**
* <a href="https://github.com/mongodb/specifications/blob/master/source/transactions-convenient-api/tests/README.md#retry-timeout-is-enforced">
* Retry Timeout is Enforced</a>, third scenario on the list.
*/
@Test
public void testRetryTimeoutEnforcedTransientTransactionErrorOnCommit() {
MongoDatabase failPointAdminDb = client.getDatabase("admin");
Expand All @@ -166,9 +166,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()
Expand All @@ -182,9 +182,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()
Expand Down