From 4ac8827bfe502383f8c466118b6bb505842aec02 Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Tue, 24 Mar 2026 13:17:49 -0600 Subject: [PATCH] Add `MongoException.SYSTEM_OVERLOADED_ERROR_LABEL`/`RETRYABLE_ERROR_LABEL` JAVA-5956, JAVA-6117, JAVA-6113, JAVA-6119 --- .../src/main/com/mongodb/MongoException.java | 24 ++++++++ .../operation/CommandOperationHelper.java | 6 +- .../documentation/TransactionExample.java | 6 +- ...tClientSideOperationsTimeoutProseTest.java | 3 +- ...WriteConcernWithResponseExceptionTest.java | 6 +- .../client/RetryableWritesProseTest.java | 8 ++- .../client/WithTransactionProseTest.java | 58 +++++++++---------- 7 files changed, 71 insertions(+), 40 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/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-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 7828ecde684..3d2d58dc4c8 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java @@ -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; @@ -687,7 +688,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 1afbf61565e..a840a83babb 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/WithTransactionProseTest.java @@ -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 +/** + * Prose Tests. + */ public class WithTransactionProseTest extends DatabaseTestCase { private static final long START_TIME_MS = 1L; private static final long ERROR_GENERATING_INTERVAL = 121000L; @@ -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. - // + /** + * + * Callback Raises a Custom Error. + */ @Test public void testCallbackRaisesCustomError() { final String exceptionMessage = "NotTransientOrUnknownError"; @@ -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. - // + /** + * + * Callback Returns a Value. + */ @Test public void testCallbackReturnsValue() { try (ClientSession session = client.startSession()) { @@ -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. - // + /** + * + * Retry Timeout is Enforced, first scenario on the list. + */ @Test public void testRetryTimeoutEnforcedTransientTransactionError() { final String errorMessage = "transient transaction error"; @@ -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. - // + /** + * + * Retry Timeout is Enforced, second scenario on the list. + */ @Test public void testRetryTimeoutEnforcedUnknownTransactionCommit() { MongoDatabase failPointAdminDb = client.getDatabase("admin"); @@ -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. - // + /** + * + * Retry Timeout is Enforced, third scenario on the list. + */ @Test public void testRetryTimeoutEnforcedTransientTransactionErrorOnCommit() { MongoDatabase failPointAdminDb = client.getDatabase("admin"); @@ -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() @@ -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()