From 76f549a27d95f1f2d0ba8330b6c473f0cba1764e Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Thu, 18 Jun 2026 17:02:23 -0600 Subject: [PATCH 1/4] Do misc improvements JAVA-6229 --- .../com/mongodb/internal/TimeoutContext.java | 21 ++++++------ .../async/function/AsyncCallbackFunction.java | 4 +-- .../async/function/AsyncCallbackLoop.java | 10 +++--- .../async/function/AsyncCallbackRunnable.java | 4 +-- .../RetryingAsyncCallbackSupplier.java | 15 ++++----- .../async/function/RetryingSyncSupplier.java | 8 ++--- .../internal/async/function/package-info.java | 1 - .../model/AbstractConstructibleBson.java | 6 ++-- .../AbstractConstructibleBsonElement.java | 6 ++-- .../connection/OidcAuthenticator.java | 2 +- .../operation/ClientBulkWriteOperation.java | 3 +- .../operation/MixedBulkWriteOperation.java | 3 +- ...FindAndDeleteOperationSpecification.groovy | 2 +- .../client/internal/TimeoutHelper.java | 3 +- .../client/internal/ClientSessionImpl.java | 3 +- .../client/internal/TimeoutHelper.java | 3 +- .../com/mongodb/client/CrudProseTest.java | 32 ++++++++++--------- 17 files changed, 61 insertions(+), 65 deletions(-) diff --git a/driver-core/src/main/com/mongodb/internal/TimeoutContext.java b/driver-core/src/main/com/mongodb/internal/TimeoutContext.java index d27975b4187..2e2917485a4 100644 --- a/driver-core/src/main/com/mongodb/internal/TimeoutContext.java +++ b/driver-core/src/main/com/mongodb/internal/TimeoutContext.java @@ -40,6 +40,7 @@ *

The context for handling timeouts in relation to the Client Side Operation Timeout specification.

*/ public class TimeoutContext { + public static final String DEFAULT_TIMEOUT_MESSAGE = "Operation exceeded the timeout limit."; private static final int NO_ROUND_TRIP_TIME_MS = 0; private final TimeoutSettings timeoutSettings; /** @@ -64,11 +65,11 @@ public static T throwMongoTimeoutException(final String message) { throw new MongoOperationTimeoutException(message); } public static T throwMongoTimeoutException() { - throw new MongoOperationTimeoutException("The operation exceeded the timeout limit."); + throw new MongoOperationTimeoutException(DEFAULT_TIMEOUT_MESSAGE); } public static MongoOperationTimeoutException createMongoTimeoutException(final Throwable cause) { - return createMongoTimeoutException("Operation exceeded the timeout limit: " + cause.getMessage(), cause); + return createMongoTimeoutException(DEFAULT_TIMEOUT_MESSAGE, cause); } public static MongoOperationTimeoutException createMongoTimeoutException(final String message, @Nullable final Throwable cause) { @@ -185,7 +186,7 @@ public long timeoutOrAlternative(final long alternativeTimeoutMS) { return timeout.call(MILLISECONDS, () -> 0L, (ms) -> ms, - () -> throwMongoTimeoutException("The operation exceeded the timeout limit.")); + () -> throwMongoTimeoutException()); } } @@ -229,7 +230,7 @@ public int getConnectTimeoutMs() { return Math.toIntExact(Timeout.nullAsInfinite(timeout).call(MILLISECONDS, () -> connectTimeoutMS, (ms) -> connectTimeoutMS == 0 ? ms : Math.min(ms, connectTimeoutMS), - () -> throwMongoTimeoutException("The operation exceeded the timeout limit."))); + () -> throwMongoTimeoutException())); } /** @@ -250,13 +251,11 @@ public TimeoutContext withMaxTimeAsMaxAwaitTimeOverride() { * The override will be provided as the remaining value in * {@link #runMaxTimeMS}, where 0 is ignored. This is useful for setting timeout * in {@link CommandMessage} as an extra element before we send it to the server. - * *

- * NOTE: Suitable for static user-defined values only (i.e MaxAwaitTimeMS), + * Suitable for static user-defined values only (i.e. {@code MaxAwaitTimeMS}), * not for running timeouts that adjust dynamically (CSOT). - * + *

* If remaining CSOT timeout is less than this static timeout, then CSOT timeout will be used. - * */ public TimeoutContext withMaxTimeOverride(final long maxTimeMS) { return new TimeoutContext( @@ -480,10 +479,10 @@ private void runMinTimeout(final LongConsumer onRemaining, final long fixedMs) { timeout.run(MILLISECONDS, () -> { onRemaining.accept(fixedMs); }, - (renamingMs) -> { - onRemaining.accept(Math.min(renamingMs, fixedMs)); + (remainingMs) -> { + onRemaining.accept(Math.min(remainingMs, fixedMs)); }, () -> { - throwMongoTimeoutException("The operation exceeded the timeout limit."); + throwMongoTimeoutException(); }); } else { onRemaining.accept(fixedMs); diff --git a/driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackFunction.java b/driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackFunction.java index cf2fedbc1ef..a869af91a39 100644 --- a/driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackFunction.java +++ b/driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackFunction.java @@ -35,8 +35,8 @@ * "normal" and "abrupt completion" * are used as they defined by the Java Language Specification, while the terms "successful" and "failed completion" are used to refer to a * situation when the function produces either a successful or a failed result respectively. - * - *

This class is not part of the public API and may be removed or changed at any time

+ *

+ * This class is not part of the public API and may be removed or changed at any time. * * @param

The type of the first parameter to the function. * @param The type of successful result. A failed result is of the {@link Throwable} type diff --git a/driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackLoop.java b/driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackLoop.java index a1021d15483..2dadef942af 100644 --- a/driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackLoop.java +++ b/driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackLoop.java @@ -29,14 +29,14 @@ * This class emulates the {@code while(true)} * statement. *

- * The original function may additionally observe or control looping via {@link LoopState}. - * Looping continues until either of the following happens: + * The original function may additionally observe or control the loop via {@link LoopState}. + * The loop continues until either of the following happens: *

    *
  • the original function fails as specified by {@link AsyncCallbackFunction};
  • *
  • the original function calls {@link LoopState#breakAndCompleteIf(Supplier, SingleResultCallback)}.
  • *
- * - *

This class is not part of the public API and may be removed or changed at any time

+ *

+ * This class is not part of the public API and may be removed or changed at any time. */ @NotThreadSafe public final class AsyncCallbackLoop implements AsyncCallbackRunnable { @@ -44,7 +44,7 @@ public final class AsyncCallbackLoop implements AsyncCallbackRunnable { private final AsyncCallbackRunnable body; /** - * @param state The {@link LoopState} to be deemed as initial for the purpose of the new {@link AsyncCallbackLoop}. + * @param state The {@link LoopState} to control the new {@link AsyncCallbackLoop}. * @param body The body of the loop. */ public AsyncCallbackLoop(final LoopState state, final AsyncCallbackRunnable body) { diff --git a/driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackRunnable.java b/driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackRunnable.java index 02fdbdf9699..68bc2d7cc51 100644 --- a/driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackRunnable.java +++ b/driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackRunnable.java @@ -20,8 +20,8 @@ /** * An {@linkplain AsyncCallbackFunction asynchronous callback-based function} of no parameters and no successful result. * This class is a callback-based counterpart of {@link Runnable}. - * - *

This class is not part of the public API and may be removed or changed at any time

+ *

+ * This class is not part of the public API and may be removed or changed at any time. * * @see AsyncCallbackFunction */ diff --git a/driver-core/src/main/com/mongodb/internal/async/function/RetryingAsyncCallbackSupplier.java b/driver-core/src/main/com/mongodb/internal/async/function/RetryingAsyncCallbackSupplier.java index 8d12e82e19e..7dc0ea705d1 100644 --- a/driver-core/src/main/com/mongodb/internal/async/function/RetryingAsyncCallbackSupplier.java +++ b/driver-core/src/main/com/mongodb/internal/async/function/RetryingAsyncCallbackSupplier.java @@ -22,18 +22,15 @@ import java.util.function.BiPredicate; import java.util.function.BinaryOperator; -import java.util.function.Supplier; /** * A decorator that implements automatic retrying of failed executions of an {@link AsyncCallbackSupplier}. * {@link RetryingAsyncCallbackSupplier} may execute the original retryable asynchronous function multiple times sequentially, * while guaranteeing that the callback passed to {@link #get(SingleResultCallback)} is completed at most once. *

- * The original function may additionally observe or control retrying via {@link RetryState}. - * For example, the {@link RetryState#breakAndCompleteIfRetryAnd(Supplier, SingleResultCallback)} method may be used to - * break retrying if the original function decides so. - * - *

This class is not part of the public API and may be removed or changed at any time

+ * The original function may additionally observe or control the retry loop via {@link RetryState}. + *

+ * This class is not part of the public API and may be removed or changed at any time. * * @see RetryingSyncSupplier */ @@ -45,7 +42,7 @@ public final class RetryingAsyncCallbackSupplier implements AsyncCallbackSupp private final AsyncCallbackSupplier asyncFunction; /** - * @param state The {@link RetryState} to be deemed as initial for the purpose of the new {@link RetryingAsyncCallbackSupplier}. + * @param state The {@link RetryState} to control the new {@link RetryingAsyncCallbackSupplier}. * @param onAttemptFailureOperator The action that is called once per failed attempt before (in the happens-before order) the * {@code retryPredicate}, regardless of whether the {@code retryPredicate} is called. * This action is allowed to have side effects. @@ -89,8 +86,8 @@ public RetryingAsyncCallbackSupplier( @Override public void get(final SingleResultCallback callback) { - /* `asyncFunction` and `callback` are the only externally provided pieces of code for which we do not need to care about - * them throwing exceptions. If they do, that violates their contract and there is nothing we should do about it. */ + // `asyncFunction` and `callback` are the only externally provided pieces of code for which we do not need to care about + // them throwing exceptions. If they do, that violates their contract and there is nothing we should do about it. asyncFunction.get(new RetryingCallback(callback)); } diff --git a/driver-core/src/main/com/mongodb/internal/async/function/RetryingSyncSupplier.java b/driver-core/src/main/com/mongodb/internal/async/function/RetryingSyncSupplier.java index ad3e4b2b807..b2f82b90865 100644 --- a/driver-core/src/main/com/mongodb/internal/async/function/RetryingSyncSupplier.java +++ b/driver-core/src/main/com/mongodb/internal/async/function/RetryingSyncSupplier.java @@ -25,11 +25,9 @@ * A decorator that implements automatic retrying of failed executions of a {@link Supplier}. * {@link RetryingSyncSupplier} may execute the original retryable function multiple times sequentially. *

- * The original function may additionally observe or control retrying via {@link RetryState}. - * For example, the {@link RetryState#breakAndThrowIfRetryAnd(Supplier)} method may be used to - * break retrying if the original function decides so. - * - *

This class is not part of the public API and may be removed or changed at any time

+ * The original function may additionally observe or control the retry loop via {@link RetryState}. + *

+ * This class is not part of the public API and may be removed or changed at any time. * * @see RetryingAsyncCallbackSupplier */ diff --git a/driver-core/src/main/com/mongodb/internal/async/function/package-info.java b/driver-core/src/main/com/mongodb/internal/async/function/package-info.java index 2a89dc73a54..1b289f3d5bf 100644 --- a/driver-core/src/main/com/mongodb/internal/async/function/package-info.java +++ b/driver-core/src/main/com/mongodb/internal/async/function/package-info.java @@ -17,7 +17,6 @@ /** * This package contains internal functionality that may change at any time. */ - @Internal @NonNullApi package com.mongodb.internal.async.function; diff --git a/driver-core/src/main/com/mongodb/internal/client/model/AbstractConstructibleBson.java b/driver-core/src/main/com/mongodb/internal/client/model/AbstractConstructibleBson.java index 278f7e273be..43a06911785 100644 --- a/driver-core/src/main/com/mongodb/internal/client/model/AbstractConstructibleBson.java +++ b/driver-core/src/main/com/mongodb/internal/client/model/AbstractConstructibleBson.java @@ -32,10 +32,10 @@ /** * A {@link Bson} that allows constructing new instances via {@link #newAppended(String, Object)} instead of mutating {@code this}. * See {@link #AbstractConstructibleBson(Bson, Document)} for the note on mutability. + *

+ * This class is not part of the public API and may be removed or changed at any time. * - *

This class is not part of the public API and may be removed or changed at any time

- * - * @param A type introduced by the concrete class that extends this abstract class. + * @param Self. The type introduced by a subclass of {@link AbstractConstructibleBson}. * @see AbstractConstructibleBsonElement */ public abstract class AbstractConstructibleBson> implements Bson, ToMap { diff --git a/driver-core/src/main/com/mongodb/internal/client/model/AbstractConstructibleBsonElement.java b/driver-core/src/main/com/mongodb/internal/client/model/AbstractConstructibleBsonElement.java index b6ef3391430..43c209bf821 100644 --- a/driver-core/src/main/com/mongodb/internal/client/model/AbstractConstructibleBsonElement.java +++ b/driver-core/src/main/com/mongodb/internal/client/model/AbstractConstructibleBsonElement.java @@ -35,10 +35,10 @@ * A {@link Bson} that contains exactly one name/value pair * and allows constructing new instances via {@link #newWithAppendedValue(String, Object)} instead of mutating {@code this}. * The value must itself be a {@code Bson}. + *

+ * This class is not part of the public API and may be removed or changed at any time. * - *

This class is not part of the public API and may be removed or changed at any time

- * - * @param A type introduced by the concrete class that extends this abstract class. + * @param Self. The type introduced by a subclass of {@link AbstractConstructibleBsonElement}. * @see AbstractConstructibleBson */ public abstract class AbstractConstructibleBsonElement> implements Bson, ToMap { diff --git a/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java index e002ed33975..45809e1b077 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java +++ b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java @@ -134,7 +134,7 @@ private Duration getCallbackTimeout(final TimeoutContext timeoutContext) { () -> // we can get here if server selection timeout was set to infinite. ChronoUnit.FOREVER.getDuration(), - (renamingMs) -> Duration.ofMillis(renamingMs), + (remainingMs) -> Duration.ofMillis(remainingMs), () -> throwMongoTimeoutException()); } diff --git a/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java b/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java index ff078b5b09c..82aabb1279d 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java @@ -208,6 +208,7 @@ public void executeAsync( final OperationContext operationContext, final SingleResultCallback callback) { beginAsync().thenSupply(c -> { + binding.retain(); WriteConcern effectiveWriteConcern = validateAndGetEffectiveWriteConcern(operationContext.getSessionContext()); ResultAccumulator resultAccumulator = new ResultAccumulator(); MutableValue transformedTopLevelError = new MutableValue<>(); @@ -220,7 +221,7 @@ public void executeAsync( }).thenApply((ignored, buildResultCallback) -> { buildResultCallback.complete(resultAccumulator.build(transformedTopLevelError.getNullable(), effectiveWriteConcern)); }).finish(c); - }).finish(callback); + }).thenAlwaysRunAndFinish(binding::release, callback); } /** diff --git a/driver-core/src/main/com/mongodb/internal/operation/MixedBulkWriteOperation.java b/driver-core/src/main/com/mongodb/internal/operation/MixedBulkWriteOperation.java index f43007677ca..a5bb11bed9f 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/MixedBulkWriteOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/MixedBulkWriteOperation.java @@ -164,13 +164,14 @@ public void executeAsync( final OperationContext operationContext, final SingleResultCallback callback) { beginAsync().thenSupply(c -> { + binding.retain(); WriteConcern effectiveWriteConcern = validateAndGetEffectiveWriteConcern(writeConcern, operationContext.getSessionContext()); beginAsync().thenSupply(executeAllBatchesCallback -> { executeAllBatchesAsync(effectiveWriteConcern, binding, operationContext, executeAllBatchesCallback); }).onErrorIf(e -> e instanceof MongoException, (e, onErrorCallback) -> { throw transformWriteException((MongoException) e); }).finish(c); - }).finish(callback); + }).thenAlwaysRunAndFinish(binding::release, callback); } private BulkWriteResult executeAllBatches( diff --git a/driver-core/src/test/functional/com/mongodb/internal/operation/FindAndDeleteOperationSpecification.groovy b/driver-core/src/test/functional/com/mongodb/internal/operation/FindAndDeleteOperationSpecification.groovy index 64c6123a84b..6da8050811b 100644 --- a/driver-core/src/test/functional/com/mongodb/internal/operation/FindAndDeleteOperationSpecification.groovy +++ b/driver-core/src/test/functional/com/mongodb/internal/operation/FindAndDeleteOperationSpecification.groovy @@ -299,7 +299,7 @@ class FindAndDeleteOperationSpecification extends OperationFunctionalSpecificati commandException == originalException where: - async << [false] + async << [true, false] } def 'should support collation'() { diff --git a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/TimeoutHelper.java b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/TimeoutHelper.java index cefdf7184d8..cd538202a2b 100644 --- a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/TimeoutHelper.java +++ b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/TimeoutHelper.java @@ -24,14 +24,13 @@ import com.mongodb.reactivestreams.client.MongoDatabase; import reactor.core.publisher.Mono; +import static com.mongodb.internal.TimeoutContext.DEFAULT_TIMEOUT_MESSAGE; import static java.util.concurrent.TimeUnit.MILLISECONDS; /** *

This class is not part of the public API and may be removed or changed at any time

*/ public final class TimeoutHelper { - private static final String DEFAULT_TIMEOUT_MESSAGE = "Operation exceeded the timeout limit."; - private TimeoutHelper() { //NOP } 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 1a2d90f3969..75c6c34a206 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java +++ b/driver-sync/src/main/com/mongodb/client/internal/ClientSessionImpl.java @@ -52,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.DEFAULT_TIMEOUT_MESSAGE; import static com.mongodb.internal.TimeoutContext.createMongoTimeoutException; import static com.mongodb.internal.thread.InterruptionUtil.interruptAndCreateMongoInterruptedException; @@ -416,6 +417,6 @@ private static MongoClientException wrapInMongoTimeoutException(final MongoExcep private static MongoClientException wrapInNonTimeoutMsMongoTimeoutException(final MongoException cause) { return cause instanceof MongoTimeoutException ? (MongoTimeoutException) cause - : new WithTransactionTimeoutException("Operation exceeded the timeout limit.", cause); + : new WithTransactionTimeoutException(DEFAULT_TIMEOUT_MESSAGE, cause); } } diff --git a/driver-sync/src/main/com/mongodb/client/internal/TimeoutHelper.java b/driver-sync/src/main/com/mongodb/client/internal/TimeoutHelper.java index 6a5ef68e615..2556388aed7 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/TimeoutHelper.java +++ b/driver-sync/src/main/com/mongodb/client/internal/TimeoutHelper.java @@ -22,14 +22,13 @@ import com.mongodb.internal.time.Timeout; import com.mongodb.lang.Nullable; +import static com.mongodb.internal.TimeoutContext.DEFAULT_TIMEOUT_MESSAGE; import static java.util.concurrent.TimeUnit.MILLISECONDS; /** *

This class is not part of the public API and may be removed or changed at any time

*/ public final class TimeoutHelper { - private static final String DEFAULT_TIMEOUT_MESSAGE = "Operation exceeded the timeout limit."; - private TimeoutHelper() { //NOP } diff --git a/driver-sync/src/test/functional/com/mongodb/client/CrudProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/CrudProseTest.java index d269a3cad57..9a002b94137 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/CrudProseTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/CrudProseTest.java @@ -117,12 +117,14 @@ void testWriteConcernErrInfoIsPropagated() throws InterruptedException { FailPoint ignored = FailPoint.enable(failPointDocument, getPrimary())) { MongoWriteConcernException actual = assertThrows(MongoWriteConcernException.class, () -> droppedCollection(client, Document.class).insertOne(Document.parse("{ x: 1 }"))); - assertEquals(actual.getWriteConcernError().getCode(), 100); + assertEquals(100, actual.getWriteConcernError().getCode()); assertEquals("UnsatisfiableWriteConcern", actual.getWriteConcernError().getCodeName()); - assertEquals(actual.getWriteConcernError().getDetails(), new BsonDocument("writeConcern", - new BsonDocument("w", new BsonInt32(2)) - .append("wtimeout", new BsonInt32(0)) - .append("provenance", new BsonString("clientSupplied")))); + assertEquals( + new BsonDocument("writeConcern", + new BsonDocument("w", new BsonInt32(2)) + .append("wtimeout", new BsonInt32(0)) + .append("provenance", new BsonString("clientSupplied"))), + actual.getWriteConcernError().getDetails()); } } @@ -211,7 +213,7 @@ void testBulkWriteSplitsWhenExceedingMaxMessageSizeBytes() { @DisplayName("5. MongoClient.bulkWrite collects WriteConcernErrors across batches") @Test @SuppressWarnings("try") - protected void testBulkWriteCollectsWriteConcernErrorsAcrossBatches() throws InterruptedException { + void testBulkWriteCollectsWriteConcernErrorsAcrossBatches() throws InterruptedException { assumeTrue(serverVersionAtLeast(8, 0)); TestCommandListener commandListener = new TestCommandListener(); BsonDocument failPointDocument = new BsonDocument("configureFailPoint", new BsonString("failCommand")) @@ -240,7 +242,7 @@ protected void testBulkWriteCollectsWriteConcernErrorsAcrossBatches() throws Int @DisplayName("6. MongoClient.bulkWrite handles individual WriteErrors across batches") @ParameterizedTest(name = "6. MongoClient.bulkWrite handles individual WriteErrors across batches--ordered:{0}") @ValueSource(booleans = {false, true}) - protected void testBulkWriteHandlesWriteErrorsAcrossBatches(final boolean ordered) { + void testBulkWriteHandlesWriteErrorsAcrossBatches(final boolean ordered) { assumeTrue(serverVersionAtLeast(8, 0)); TestCommandListener commandListener = new TestCommandListener(); try (MongoClient client = createMongoClient(getMongoClientSettingsBuilder() @@ -270,7 +272,7 @@ void testBulkWriteHandlesCursorRequiringGetMore() { @DisplayName("8. MongoClient.bulkWrite handles a cursor requiring getMore within a transaction") @Test - protected void testBulkWriteHandlesCursorRequiringGetMoreWithinTransaction() { + void testBulkWriteHandlesCursorRequiringGetMoreWithinTransaction() { assumeTrue(serverVersionAtLeast(8, 0)); assumeFalse(isStandalone()); assertBulkWriteHandlesCursorRequiringGetMore(true); @@ -311,7 +313,7 @@ private void assertBulkWriteHandlesCursorRequiringGetMore(final boolean transact @DisplayName("11. MongoClient.bulkWrite batch splits when the addition of a new namespace exceeds the maximum message size") @Test - protected void testBulkWriteSplitsWhenExceedingMaxMessageSizeBytesDueToNsInfo() { + void testBulkWriteSplitsWhenExceedingMaxMessageSizeBytesDueToNsInfo() { assumeTrue(serverVersionAtLeast(8, 0)); assertAll( () -> { @@ -382,7 +384,7 @@ private void testBulkWriteSplitsWhenExceedingMaxMessageSizeBytesDueToNsInfo( @DisplayName("12. MongoClient.bulkWrite returns an error if no operations can be added to ops") @ParameterizedTest(name = "12. MongoClient.bulkWrite returns an error if no operations can be added to ops--tooLarge:{0}") @ValueSource(strings = {"document", "namespace"}) - protected void testBulkWriteSplitsErrorsForTooLargeOpsOrNsInfo(final String tooLarge) { + void testBulkWriteSplitsErrorsForTooLargeOpsOrNsInfo(final String tooLarge) { assumeTrue(serverVersionAtLeast(8, 0)); try (MongoClient client = createMongoClient(getMongoClientSettingsBuilder())) { int maxMessageSizeBytes = droppedDatabase(client).runCommand(new Document("hello", 1)).getInteger("maxMessageSizeBytes"); @@ -410,7 +412,7 @@ protected void testBulkWriteSplitsErrorsForTooLargeOpsOrNsInfo(final String tooL @DisplayName("13. MongoClient.bulkWrite returns an error if auto-encryption is configured") @Test - protected void testBulkWriteErrorsForAutoEncryption() { + void testBulkWriteErrorsForAutoEncryption() { assumeTrue(serverVersionAtLeast(8, 0)); HashMap awsKmsProviderProperties = new HashMap<>(); awsKmsProviderProperties.put("accessKeyId", "foo"); @@ -431,7 +433,7 @@ protected void testBulkWriteErrorsForAutoEncryption() { @DisplayName("15. MongoClient.bulkWrite with unacknowledged write concern uses w:0 for all batches") @Test - protected void testWriteConcernOfAllBatchesWhenUnacknowledgedRequested() { + void testWriteConcernOfAllBatchesWhenUnacknowledgedRequested() { assumeTrue(serverVersionAtLeast(8, 0)); TestCommandListener commandListener = new TestCommandListener(); try (MongoClient client = createMongoClient(getMongoClientSettingsBuilder().addCommandListener(commandListener) @@ -468,7 +470,7 @@ protected void testWriteConcernOfAllBatchesWhenUnacknowledgedRequested() { @DisplayName("insertMustGenerateIdAtMostOnce") @ParameterizedTest(name = "insertMustGenerateIdAtMostOnce--documentClass:{0}, expectIdGenerated:{1}") @MethodSource("insertMustGenerateIdAtMostOnceArgs") - protected void insertMustGenerateIdAtMostOnce( + void insertMustGenerateIdAtMostOnce( final Class documentClass, final boolean expectIdGenerated, final Supplier documentSupplier) { @@ -564,11 +566,11 @@ protected MongoClient createMongoClient(final MongoClientSettings.Builder mongoC return MongoClients.create(mongoClientSettingsBuilder.build()); } - private MongoCollection droppedCollection(final MongoClient client, final Class documentClass) { + private static MongoCollection droppedCollection(final MongoClient client, final Class documentClass) { return droppedDatabase(client).getCollection(NAMESPACE.getCollectionName(), documentClass); } - private MongoDatabase droppedDatabase(final MongoClient client) { + private static MongoDatabase droppedDatabase(final MongoClient client) { MongoDatabase database = client.getDatabase(NAMESPACE.getDatabaseName()); database.drop(); return database; From 2972f0be28f9188a121c8aa02e39f0511ee9b4b8 Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Mon, 22 Jun 2026 01:57:05 -0600 Subject: [PATCH 2/4] Rename `LoopState` -> `LoopControl` JAVA-6229 --- .../mongodb/internal/async/AsyncRunnable.java | 14 +-- .../async/function/AsyncCallbackLoop.java | 14 +-- .../{LoopState.java => LoopControl.java} | 12 +-- .../internal/async/function/RetryState.java | 32 +++--- .../operation/retry/AttachmentKeys.java | 2 +- .../async/function/LoopControlTest.java | 102 ++++++++++++++++++ .../async/function/LoopStateTest.java | 102 ------------------ .../async/function/RetryStateTest.java | 2 +- 8 files changed, 140 insertions(+), 140 deletions(-) rename driver-core/src/main/com/mongodb/internal/async/function/{LoopState.java => LoopControl.java} (95%) create mode 100644 driver-core/src/test/unit/com/mongodb/internal/async/function/LoopControlTest.java delete mode 100644 driver-core/src/test/unit/com/mongodb/internal/async/function/LoopStateTest.java diff --git a/driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java b/driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java index 9468e787dcb..5ad4e9f55b2 100644 --- a/driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java +++ b/driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java @@ -17,7 +17,7 @@ package com.mongodb.internal.async; import com.mongodb.internal.async.function.AsyncCallbackLoop; -import com.mongodb.internal.async.function.LoopState; +import com.mongodb.internal.async.function.LoopControl; import com.mongodb.internal.async.function.RetryState; import com.mongodb.internal.async.function.RetryingAsyncCallbackSupplier; @@ -255,10 +255,10 @@ default AsyncRunnable thenRunRetryingWhile(final AsyncRunnable runnable, final P */ default AsyncRunnable thenRunWhileLoop(final BooleanSupplier whileCheck, final AsyncRunnable loopBodyRunnable) { return thenRun(finalCallback -> { - LoopState loopState = new LoopState(); - new AsyncCallbackLoop(loopState, iterationCallback -> { + LoopControl loopControl = new LoopControl(); + new AsyncCallbackLoop(loopControl, iterationCallback -> { - if (loopState.breakAndCompleteIf(() -> !whileCheck.getAsBoolean(), iterationCallback)) { + if (loopControl.breakAndCompleteIf(() -> !whileCheck.getAsBoolean(), iterationCallback)) { return; } loopBodyRunnable.finish((result, t) -> { @@ -284,15 +284,15 @@ default AsyncRunnable thenRunWhileLoop(final BooleanSupplier whileCheck, final A */ default AsyncRunnable thenRunDoWhileLoop(final AsyncRunnable loopBodyRunnable, final BooleanSupplier whileCheck) { return thenRun(finalCallback -> { - LoopState loopState = new LoopState(); - new AsyncCallbackLoop(loopState, iterationCallback -> { + LoopControl loopControl = new LoopControl(); + new AsyncCallbackLoop(loopControl, iterationCallback -> { loopBodyRunnable.finish((result, t) -> { if (t != null) { iterationCallback.completeExceptionally(t); return; } - if (loopState.breakAndCompleteIf(() -> !whileCheck.getAsBoolean(), iterationCallback)) { + if (loopControl.breakAndCompleteIf(() -> !whileCheck.getAsBoolean(), iterationCallback)) { return; } iterationCallback.complete(iterationCallback); diff --git a/driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackLoop.java b/driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackLoop.java index 2dadef942af..46936a10ecb 100644 --- a/driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackLoop.java +++ b/driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackLoop.java @@ -29,26 +29,26 @@ * This class emulates the {@code while(true)} * statement. *

- * The original function may additionally observe or control the loop via {@link LoopState}. + * The original function may additionally observe or control the loop via {@link LoopControl}. * The loop continues until either of the following happens: *

    *
  • the original function fails as specified by {@link AsyncCallbackFunction};
  • - *
  • the original function calls {@link LoopState#breakAndCompleteIf(Supplier, SingleResultCallback)}.
  • + *
  • the original function calls {@link LoopControl#breakAndCompleteIf(Supplier, SingleResultCallback)}.
  • *
*

* This class is not part of the public API and may be removed or changed at any time. */ @NotThreadSafe public final class AsyncCallbackLoop implements AsyncCallbackRunnable { - private final LoopState state; + private final LoopControl control; private final AsyncCallbackRunnable body; /** - * @param state The {@link LoopState} to control the new {@link AsyncCallbackLoop}. + * @param control The {@link LoopControl} to control the new {@link AsyncCallbackLoop}. * @param body The body of the loop. */ - public AsyncCallbackLoop(final LoopState state, final AsyncCallbackRunnable body) { - this.state = state; + public AsyncCallbackLoop(final LoopControl control, final AsyncCallbackRunnable body) { + this.control = control; this.body = body; } @@ -77,7 +77,7 @@ public void onResult(@Nullable final Void result, @Nullable final Throwable t) { } else { boolean continueLooping; try { - continueLooping = state.advance(); + continueLooping = control.advance(); } catch (Throwable e) { wrapped.onResult(null, e); return; diff --git a/driver-core/src/main/com/mongodb/internal/async/function/LoopState.java b/driver-core/src/main/com/mongodb/internal/async/function/LoopControl.java similarity index 95% rename from driver-core/src/main/com/mongodb/internal/async/function/LoopState.java rename to driver-core/src/main/com/mongodb/internal/async/function/LoopControl.java index f3b19fecde7..d385c1630ba 100644 --- a/driver-core/src/main/com/mongodb/internal/async/function/LoopState.java +++ b/driver-core/src/main/com/mongodb/internal/async/function/LoopControl.java @@ -39,18 +39,18 @@ * @see AsyncCallbackLoop */ @NotThreadSafe -public final class LoopState { +public final class LoopControl { private int iteration; private boolean lastIteration; @Nullable private Map, AttachmentValueContainer> attachments; - public LoopState() { + public LoopControl() { iteration = 0; } /** - * Advances this {@link LoopState} such that it represents the state of a new iteration. + * Advances this {@link LoopControl} such that it represents the state of a new iteration. * Must not be called before the {@linkplain #isFirstIteration() first iteration}, must be called before each subsequent iteration. * * @return {@code true} if the next iteration must be executed; {@code false} iff the loop was {@link #isLastIteration() broken}. @@ -90,7 +90,7 @@ public int iteration() { /** * This method emulates executing the - * {@code break} statement. Must not be called more than once per {@link LoopState}. + * {@code break} statement. Must not be called more than once per {@link LoopControl}. * * @param predicate {@code true} iff the associated loop needs to be broken. * @return {@code true} iff the {@code callback} was completed, which happens iff any of the following is true: @@ -137,7 +137,7 @@ void markAsLastIteration() { * @return {@code this}. * @see #attachment(AttachmentKey) */ - public LoopState attach(final AttachmentKey key, final V value, final boolean autoRemove) { + public LoopControl attach(final AttachmentKey key, final V value, final boolean autoRemove) { attachments().put(assertNotNull(key), new AttachmentValueContainer(assertNotNull(value), autoRemove)); return this; } @@ -167,7 +167,7 @@ private void removeAutoRemovableAttachments() { @Override public String toString() { - return "LoopState{" + return "LoopControl{" + "iteration=" + iteration + ", attachments=" + attachments + '}'; diff --git a/driver-core/src/main/com/mongodb/internal/async/function/RetryState.java b/driver-core/src/main/com/mongodb/internal/async/function/RetryState.java index 9742116f239..8f451268bcd 100644 --- a/driver-core/src/main/com/mongodb/internal/async/function/RetryState.java +++ b/driver-core/src/main/com/mongodb/internal/async/function/RetryState.java @@ -18,7 +18,7 @@ import com.mongodb.MongoOperationTimeoutException; import com.mongodb.annotations.NotThreadSafe; import com.mongodb.internal.async.SingleResultCallback; -import com.mongodb.internal.async.function.LoopState.AttachmentKey; +import com.mongodb.internal.async.function.LoopControl.AttachmentKey; import com.mongodb.lang.NonNull; import com.mongodb.lang.Nullable; @@ -48,7 +48,7 @@ public final class RetryState { public static final int MAX_RETRIES = 1; private static final int INFINITE_RETRIES = Integer.MAX_VALUE; - private final LoopState loopState; + private final LoopControl loopControl; private final int attempts; @Nullable private Throwable previouslyChosenException; @@ -68,7 +68,7 @@ public RetryState() { */ public RetryState(final int retries) { assertTrue(retries >= 0); - loopState = new LoopState(); + loopControl = new LoopControl(); attempts = retries == INFINITE_RETRIES ? INFINITE_RETRIES : retries + 1; } @@ -162,11 +162,11 @@ private void doAdvanceOrThrow(final Throwable attemptException, } throw previouslyChosenException; } else { - // note that we must not update the state, e.g, `previouslyChosenException`, `loopState`, before calling `retryPredicate` + // note that we must not update the state, e.g, `previouslyChosenException`, `loopControl`, before calling `retryPredicate` boolean retry = shouldRetry(this, attemptException, newlyChosenException, onlyRuntimeExceptions, retryPredicate); previouslyChosenException = newlyChosenException; if (retry) { - assertTrue(loopState.advance()); + assertTrue(loopControl.advance()); } else { throw previouslyChosenException; } @@ -254,20 +254,20 @@ private static boolean isRuntime(@Nullable final Throwable exception) { * @see #breakAndCompleteIfRetryAnd(Supplier, SingleResultCallback) */ public void breakAndThrowIfRetryAnd(final Supplier predicate) throws RuntimeException { - assertFalse(loopState.isLastIteration()); + assertFalse(loopControl.isLastIteration()); if (!isFirstAttempt()) { assertNotNull(previouslyChosenException); assertTrue(previouslyChosenException instanceof RuntimeException); RuntimeException localException = (RuntimeException) previouslyChosenException; try { if (predicate.get()) { - loopState.markAsLastIteration(); + loopControl.markAsLastIteration(); } } catch (Exception predicateException) { predicateException.addSuppressed(localException); throw predicateException; } - if (loopState.isLastIteration()) { + if (loopControl.isLastIteration()) { throw localException; } } @@ -302,7 +302,7 @@ public boolean breakAndCompleteIfRetryAnd(final Supplier predicate, fin * @see #attempt() */ public boolean isFirstAttempt() { - return loopState.isFirstIteration(); + return loopControl.isFirstIteration(); } /** @@ -319,7 +319,7 @@ public boolean isFirstAttempt() { private boolean isLastAttempt(final Throwable attemptException) { boolean operationTimeout = attemptException instanceof MongoOperationTimeoutException; boolean attemptLimit = attempt() == attempts - 1; - return loopState.isLastIteration() || operationTimeout || attemptLimit; + return loopControl.isLastIteration() || operationTimeout || attemptLimit; } /** @@ -328,7 +328,7 @@ private boolean isLastAttempt(final Throwable attemptException) { * @see #isFirstAttempt() */ public int attempt() { - return loopState.iteration(); + return loopControl.iteration(); } /** @@ -344,24 +344,24 @@ public Optional exception() { } /** - * @see LoopState#attach(AttachmentKey, Object, boolean) + * @see LoopControl#attach(AttachmentKey, Object, boolean) */ public RetryState attach(final AttachmentKey key, final V value, final boolean autoRemove) { - loopState.attach(key, value, autoRemove); + loopControl.attach(key, value, autoRemove); return this; } /** - * @see LoopState#attachment(AttachmentKey) + * @see LoopControl#attachment(AttachmentKey) */ public Optional attachment(final AttachmentKey key) { - return loopState.attachment(key); + return loopControl.attachment(key); } @Override public String toString() { return "RetryState{" - + "loopState=" + loopState + + "loopControl=" + loopControl + ", attempts=" + (attempts == INFINITE_RETRIES ? "infinite" : attempts) + ", exception=" + previouslyChosenException + '}'; diff --git a/driver-core/src/main/com/mongodb/internal/operation/retry/AttachmentKeys.java b/driver-core/src/main/com/mongodb/internal/operation/retry/AttachmentKeys.java index eb453dd455e..d3c5cc32346 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/retry/AttachmentKeys.java +++ b/driver-core/src/main/com/mongodb/internal/operation/retry/AttachmentKeys.java @@ -17,7 +17,7 @@ import com.mongodb.MongoConnectionPoolClearedException; import com.mongodb.annotations.Immutable; -import com.mongodb.internal.async.function.LoopState.AttachmentKey; +import com.mongodb.internal.async.function.LoopControl.AttachmentKey; import org.bson.BsonDocument; import java.util.HashSet; diff --git a/driver-core/src/test/unit/com/mongodb/internal/async/function/LoopControlTest.java b/driver-core/src/test/unit/com/mongodb/internal/async/function/LoopControlTest.java new file mode 100644 index 00000000000..84e07d48d5b --- /dev/null +++ b/driver-core/src/test/unit/com/mongodb/internal/async/function/LoopControlTest.java @@ -0,0 +1,102 @@ +/* + * 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.async.function; + +import com.mongodb.client.syncadapter.SupplyingCallback; +import com.mongodb.internal.async.function.LoopControl.AttachmentKey; +import com.mongodb.internal.operation.retry.AttachmentKeys; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class LoopControlTest { + @Test + void iterationsAndAdvance() { + LoopControl loopControl = new LoopControl(); + assertAll( + () -> assertTrue(loopControl.isFirstIteration()), + () -> assertEquals(0, loopControl.iteration()), + () -> assertFalse(loopControl.isLastIteration()), + () -> assertTrue(loopControl.advance()), + () -> assertFalse(loopControl.isFirstIteration()), + () -> assertEquals(1, loopControl.iteration()), + () -> assertFalse(loopControl.isLastIteration()) + ); + loopControl.markAsLastIteration(); + assertAll( + () -> assertFalse(loopControl.isFirstIteration()), + () -> assertEquals(1, loopControl.iteration()), + () -> assertTrue(loopControl.isLastIteration()), + () -> assertFalse(loopControl.advance()) + ); + } + + @Test + void maskAsLastIteration() { + LoopControl loopControl = new LoopControl(); + loopControl.markAsLastIteration(); + assertTrue(loopControl.isLastIteration()); + assertFalse(loopControl.advance()); + } + + @Test + void breakAndCompleteIfFalse() { + LoopControl loopControl = new LoopControl(); + SupplyingCallback callback = new SupplyingCallback<>(); + assertFalse(loopControl.breakAndCompleteIf(() -> false, callback)); + assertFalse(callback.completed()); + } + + @Test + void breakAndCompleteIfTrue() { + LoopControl loopControl = new LoopControl(); + SupplyingCallback callback = new SupplyingCallback<>(); + assertTrue(loopControl.breakAndCompleteIf(() -> true, callback)); + assertTrue(callback.completed()); + } + + @Test + void breakAndCompleteIfPredicateThrows() { + LoopControl loopControl = new LoopControl(); + SupplyingCallback callback = new SupplyingCallback<>(); + RuntimeException e = new RuntimeException() { + }; + assertTrue(loopControl.breakAndCompleteIf(() -> { + throw e; + }, callback)); + assertThrows(e.getClass(), callback::get); + } + + @Test + void attachAndAttachment() { + LoopControl loopControl = new LoopControl(); + AttachmentKey attachmentKey = AttachmentKeys.maxWireVersion(); + int attachmentValue = 1; + assertFalse(loopControl.attachment(attachmentKey).isPresent()); + loopControl.attach(attachmentKey, attachmentValue, false); + assertEquals(attachmentValue, loopControl.attachment(attachmentKey).get()); + loopControl.advance(); + assertEquals(attachmentValue, loopControl.attachment(attachmentKey).get()); + loopControl.attach(attachmentKey, attachmentValue, true); + assertEquals(attachmentValue, loopControl.attachment(attachmentKey).get()); + loopControl.advance(); + assertFalse(loopControl.attachment(attachmentKey).isPresent()); + } +} diff --git a/driver-core/src/test/unit/com/mongodb/internal/async/function/LoopStateTest.java b/driver-core/src/test/unit/com/mongodb/internal/async/function/LoopStateTest.java deleted file mode 100644 index c9a8ada7c0c..00000000000 --- a/driver-core/src/test/unit/com/mongodb/internal/async/function/LoopStateTest.java +++ /dev/null @@ -1,102 +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.async.function; - -import com.mongodb.client.syncadapter.SupplyingCallback; -import com.mongodb.internal.async.function.LoopState.AttachmentKey; -import com.mongodb.internal.operation.retry.AttachmentKeys; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -final class LoopStateTest { - @Test - void iterationsAndAdvance() { - LoopState loopState = new LoopState(); - assertAll( - () -> assertTrue(loopState.isFirstIteration()), - () -> assertEquals(0, loopState.iteration()), - () -> assertFalse(loopState.isLastIteration()), - () -> assertTrue(loopState.advance()), - () -> assertFalse(loopState.isFirstIteration()), - () -> assertEquals(1, loopState.iteration()), - () -> assertFalse(loopState.isLastIteration()) - ); - loopState.markAsLastIteration(); - assertAll( - () -> assertFalse(loopState.isFirstIteration()), - () -> assertEquals(1, loopState.iteration()), - () -> assertTrue(loopState.isLastIteration()), - () -> assertFalse(loopState.advance()) - ); - } - - @Test - void maskAsLastIteration() { - LoopState loopState = new LoopState(); - loopState.markAsLastIteration(); - assertTrue(loopState.isLastIteration()); - assertFalse(loopState.advance()); - } - - @Test - void breakAndCompleteIfFalse() { - LoopState loopState = new LoopState(); - SupplyingCallback callback = new SupplyingCallback<>(); - assertFalse(loopState.breakAndCompleteIf(() -> false, callback)); - assertFalse(callback.completed()); - } - - @Test - void breakAndCompleteIfTrue() { - LoopState loopState = new LoopState(); - SupplyingCallback callback = new SupplyingCallback<>(); - assertTrue(loopState.breakAndCompleteIf(() -> true, callback)); - assertTrue(callback.completed()); - } - - @Test - void breakAndCompleteIfPredicateThrows() { - LoopState loopState = new LoopState(); - SupplyingCallback callback = new SupplyingCallback<>(); - RuntimeException e = new RuntimeException() { - }; - assertTrue(loopState.breakAndCompleteIf(() -> { - throw e; - }, callback)); - assertThrows(e.getClass(), callback::get); - } - - @Test - void attachAndAttachment() { - LoopState loopState = new LoopState(); - AttachmentKey attachmentKey = AttachmentKeys.maxWireVersion(); - int attachmentValue = 1; - assertFalse(loopState.attachment(attachmentKey).isPresent()); - loopState.attach(attachmentKey, attachmentValue, false); - assertEquals(attachmentValue, loopState.attachment(attachmentKey).get()); - loopState.advance(); - assertEquals(attachmentValue, loopState.attachment(attachmentKey).get()); - loopState.attach(attachmentKey, attachmentValue, true); - assertEquals(attachmentValue, loopState.attachment(attachmentKey).get()); - loopState.advance(); - assertFalse(loopState.attachment(attachmentKey).isPresent()); - } -} diff --git a/driver-core/src/test/unit/com/mongodb/internal/async/function/RetryStateTest.java b/driver-core/src/test/unit/com/mongodb/internal/async/function/RetryStateTest.java index d428318fa20..eec5763c775 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/async/function/RetryStateTest.java +++ b/driver-core/src/test/unit/com/mongodb/internal/async/function/RetryStateTest.java @@ -18,7 +18,7 @@ import com.mongodb.MongoOperationTimeoutException; import com.mongodb.client.syncadapter.SupplyingCallback; import com.mongodb.internal.TimeoutContext; -import com.mongodb.internal.async.function.LoopState.AttachmentKey; +import com.mongodb.internal.async.function.LoopControl.AttachmentKey; import com.mongodb.internal.operation.retry.AttachmentKeys; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; From 0685fa5437ed8e835410be8a61a29c3f24bdca6b Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Mon, 22 Jun 2026 10:54:54 -0600 Subject: [PATCH 3/4] Rename `RetryState` -> `RetryControl` JAVA-6229 --- .../mongodb/internal/async/AsyncRunnable.java | 4 +- .../{RetryState.java => RetryControl.java} | 44 ++-- .../RetryingAsyncCallbackSupplier.java | 20 +- .../async/function/RetryingSyncSupplier.java | 18 +- .../operation/AsyncOperationHelper.java | 38 ++-- .../operation/ClientBulkWriteOperation.java | 70 +++---- .../operation/CommandOperationHelper.java | 42 ++-- .../internal/operation/FindOperation.java | 18 +- .../operation/ListCollectionsOperation.java | 18 +- .../operation/ListIndexesOperation.java | 18 +- .../operation/MixedBulkWriteOperation.java | 42 ++-- .../operation/SyncOperationHelper.java | 38 ++-- ...ryStateTest.java => RetryControlTest.java} | 198 +++++++++--------- 13 files changed, 284 insertions(+), 284 deletions(-) rename driver-core/src/main/com/mongodb/internal/async/function/{RetryState.java => RetryControl.java} (90%) rename driver-core/src/test/unit/com/mongodb/internal/async/function/{RetryStateTest.java => RetryControlTest.java} (65%) diff --git a/driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java b/driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java index 5ad4e9f55b2..760d8053df5 100644 --- a/driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java +++ b/driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java @@ -18,7 +18,7 @@ import com.mongodb.internal.async.function.AsyncCallbackLoop; import com.mongodb.internal.async.function.LoopControl; -import com.mongodb.internal.async.function.RetryState; +import com.mongodb.internal.async.function.RetryControl; import com.mongodb.internal.async.function.RetryingAsyncCallbackSupplier; import java.util.function.BooleanSupplier; @@ -233,7 +233,7 @@ default AsyncSupplier thenSupply(final AsyncSupplier supplier) { default AsyncRunnable thenRunRetryingWhile(final AsyncRunnable runnable, final Predicate shouldRetry) { return thenRun(callback -> { new RetryingAsyncCallbackSupplier( - new RetryState(), + new RetryControl(), (previouslyChosenFailure, lastAttemptFailure) -> lastAttemptFailure, (rs, lastAttemptFailure) -> shouldRetry.test(lastAttemptFailure), // `finish` is required here instead of `unsafeFinish` diff --git a/driver-core/src/main/com/mongodb/internal/async/function/RetryState.java b/driver-core/src/main/com/mongodb/internal/async/function/RetryControl.java similarity index 90% rename from driver-core/src/main/com/mongodb/internal/async/function/RetryState.java rename to driver-core/src/main/com/mongodb/internal/async/function/RetryControl.java index 8f451268bcd..e189bbde255 100644 --- a/driver-core/src/main/com/mongodb/internal/async/function/RetryState.java +++ b/driver-core/src/main/com/mongodb/internal/async/function/RetryControl.java @@ -44,7 +44,7 @@ * @see RetryingAsyncCallbackSupplier */ @NotThreadSafe -public final class RetryState { +public final class RetryControl { public static final int MAX_RETRIES = 1; private static final int INFINITE_RETRIES = Integer.MAX_VALUE; @@ -54,26 +54,26 @@ public final class RetryState { private Throwable previouslyChosenException; /** - * Creates a {@link RetryState} that does not explicitly limit the number of attempts. + * Creates a {@link RetryControl} that does not explicitly limit the number of attempts. * Retrying still may be stopped because, for example, * the failed result from the most recent attempt is {@link MongoOperationTimeoutException}. */ - public RetryState() { + public RetryControl() { this(INFINITE_RETRIES); } /** * @param retries A non-negative number of allowed retry attempts. - * {@value #INFINITE_RETRIES} is interpreted as {@linkplain #RetryState() absence of explicit limit}. + * {@value #INFINITE_RETRIES} is interpreted as {@linkplain #RetryControl() absence of explicit limit}. */ - public RetryState(final int retries) { + public RetryControl(final int retries) { assertTrue(retries >= 0); loopControl = new LoopControl(); attempts = retries == INFINITE_RETRIES ? INFINITE_RETRIES : retries + 1; } /** - * Advances this {@link RetryState} such that it represents the state of a new attempt. + * Advances this {@link RetryControl} such that it represents the state of a new attempt. * If there is at least one more attempt left, it is consumed by this method. * Must not be called before the {@linkplain #isFirstAttempt() first attempt}, must be called before each subsequent attempt. *

@@ -102,8 +102,8 @@ public RetryState(final int retries) { *

  • {@code onAttemptFailureOperator} completed normally;
  • *
  • the most recent attempt is not known to be the {@linkplain #isLastAttempt(Throwable) last} one.
  • * - * The {@code retryPredicate} accepts this {@link RetryState} and the exception from the most recent attempt, - * and may mutate the exception. The {@linkplain RetryState} advances to represent the state of a new attempt + * The {@code retryPredicate} accepts this {@link RetryControl} and the exception from the most recent attempt, + * and may mutate the exception. The {@linkplain RetryControl} advances to represent the state of a new attempt * after (in the happens-before order) testing the {@code retryPredicate}, and only if the predicate completes normally. * @throws RuntimeException Iff any of the following is true: *
      @@ -117,7 +117,7 @@ public RetryState(final int retries) { * @see #advanceOrThrow(Throwable, BinaryOperator, BiPredicate) */ void advanceOrThrow(final RuntimeException attemptException, final BinaryOperator onAttemptFailureOperator, - final BiPredicate retryPredicate) throws RuntimeException { + final BiPredicate retryPredicate) throws RuntimeException { try { doAdvanceOrThrow(attemptException, onAttemptFailureOperator, retryPredicate, true); } catch (RuntimeException | Error unchecked) { @@ -134,19 +134,19 @@ void advanceOrThrow(final RuntimeException attemptException, final BinaryOperato * @see #advanceOrThrow(RuntimeException, BinaryOperator, BiPredicate) */ void advanceOrThrow(final Throwable attemptException, final BinaryOperator onAttemptFailureOperator, - final BiPredicate retryPredicate) throws Throwable { + final BiPredicate retryPredicate) throws Throwable { doAdvanceOrThrow(attemptException, onAttemptFailureOperator, retryPredicate, false); } /** * @param onlyRuntimeExceptions {@code true} iff the method must expect {@link #previouslyChosenException} and {@code attemptException} to be * {@link RuntimeException}s and must not explicitly handle other {@link Throwable} types, of which only {@link Error} is possible - * as {@link RetryState} does not have any source of {@link Exception}s. + * as {@link RetryControl} does not have any source of {@link Exception}s. * @param onAttemptFailureOperator See {@link #advanceOrThrow(RuntimeException, BinaryOperator, BiPredicate)}. */ private void doAdvanceOrThrow(final Throwable attemptException, final BinaryOperator onAttemptFailureOperator, - final BiPredicate retryPredicate, + final BiPredicate retryPredicate, final boolean onlyRuntimeExceptions) throws Throwable { assertTrue(attempt() < attempts); assertNotNull(attemptException); @@ -205,14 +205,14 @@ private static Throwable callOnAttemptFailureOperator( } /** - * @param readOnlyRetryState Must not be mutated by this method. + * @param readOnlyRetryControl Must not be mutated by this method. * @param onlyRuntimeExceptions See {@link #doAdvanceOrThrow(Throwable, BinaryOperator, BiPredicate, boolean)}. */ - private static boolean shouldRetry(final RetryState readOnlyRetryState, final Throwable attemptException, - final Throwable newlyChosenException, - final boolean onlyRuntimeExceptions, final BiPredicate retryPredicate) { + private static boolean shouldRetry(final RetryControl readOnlyRetryControl, final Throwable attemptException, + final Throwable newlyChosenException, + final boolean onlyRuntimeExceptions, final BiPredicate retryPredicate) { try { - return retryPredicate.test(readOnlyRetryState, attemptException); + return retryPredicate.test(readOnlyRetryControl, attemptException); } catch (Throwable retryPredicateException) { if (onlyRuntimeExceptions && !isRuntime(retryPredicateException)) { throw retryPredicateException; @@ -232,7 +232,7 @@ private static boolean isRuntime(@Nullable final Throwable exception) { * that breaking results in throwing an exception because the retry loop has more than one iteration only if the first iteration fails. * Does nothing and completes normally if called during the {@linkplain #isFirstAttempt() first attempt}. * This method is useful when the associated retryable activity detects that a retry attempt should not happen - * despite having been started. Must not be called more than once per {@link RetryState}. + * despite having been started. Must not be called more than once per {@link RetryControl}. *

      * If the {@code predicate} completes abruptly, this method also completes abruptly with the same exception but does not break retrying; * if the {@code predicate} is {@code true}, then the method breaks retrying and completes abruptly by throwing the exception that is @@ -240,7 +240,7 @@ private static boolean isRuntime(@Nullable final Throwable exception) { * by the caller to complete the ongoing attempt. *

      * If this method is called from - * {@linkplain RetryingSyncSupplier#RetryingSyncSupplier(RetryState, BinaryOperator, BiPredicate, Supplier) + * {@linkplain RetryingSyncSupplier#RetryingSyncSupplier(RetryControl, BinaryOperator, BiPredicate, Supplier) * retry predicate / failed result transformer}, the behavior is unspecified. * * @param predicate {@code true} iff retrying needs to be broken. @@ -278,7 +278,7 @@ public void breakAndThrowIfRetryAnd(final Supplier predicate) throws Ru * but instead of throwing an exception, it relays it to the {@code callback}. *

      * If this method is called from - * {@linkplain RetryingAsyncCallbackSupplier#RetryingAsyncCallbackSupplier(RetryState, BinaryOperator, BiPredicate, AsyncCallbackSupplier) + * {@linkplain RetryingAsyncCallbackSupplier#RetryingAsyncCallbackSupplier(RetryControl, BinaryOperator, BiPredicate, AsyncCallbackSupplier) * retry predicate / failed result transformer}, the behavior is unspecified. * * @return {@code true} iff the {@code callback} was completed, which happens in the same situations in which @@ -346,7 +346,7 @@ public Optional exception() { /** * @see LoopControl#attach(AttachmentKey, Object, boolean) */ - public RetryState attach(final AttachmentKey key, final V value, final boolean autoRemove) { + public RetryControl attach(final AttachmentKey key, final V value, final boolean autoRemove) { loopControl.attach(key, value, autoRemove); return this; } @@ -360,7 +360,7 @@ public Optional attachment(final AttachmentKey key) { @Override public String toString() { - return "RetryState{" + return "RetryControl{" + "loopControl=" + loopControl + ", attempts=" + (attempts == INFINITE_RETRIES ? "infinite" : attempts) + ", exception=" + previouslyChosenException diff --git a/driver-core/src/main/com/mongodb/internal/async/function/RetryingAsyncCallbackSupplier.java b/driver-core/src/main/com/mongodb/internal/async/function/RetryingAsyncCallbackSupplier.java index 7dc0ea705d1..f21c537a19a 100644 --- a/driver-core/src/main/com/mongodb/internal/async/function/RetryingAsyncCallbackSupplier.java +++ b/driver-core/src/main/com/mongodb/internal/async/function/RetryingAsyncCallbackSupplier.java @@ -28,7 +28,7 @@ * {@link RetryingAsyncCallbackSupplier} may execute the original retryable asynchronous function multiple times sequentially, * while guaranteeing that the callback passed to {@link #get(SingleResultCallback)} is completed at most once. *

      - * The original function may additionally observe or control the retry loop via {@link RetryState}. + * The original function may additionally observe or control the retry loop via {@link RetryControl}. *

      * This class is not part of the public API and may be removed or changed at any time. * @@ -36,13 +36,13 @@ */ @NotThreadSafe public final class RetryingAsyncCallbackSupplier implements AsyncCallbackSupplier { - private final RetryState state; - private final BiPredicate retryPredicate; + private final RetryControl control; + private final BiPredicate retryPredicate; private final BinaryOperator onAttemptFailureOperator; private final AsyncCallbackSupplier asyncFunction; /** - * @param state The {@link RetryState} to control the new {@link RetryingAsyncCallbackSupplier}. + * @param control The {@link RetryControl} to control the new {@link RetryingAsyncCallbackSupplier}. * @param onAttemptFailureOperator The action that is called once per failed attempt before (in the happens-before order) the * {@code retryPredicate}, regardless of whether the {@code retryPredicate} is called. * This action is allowed to have side effects. @@ -68,17 +68,17 @@ public final class RetryingAsyncCallbackSupplier implements AsyncCallbackSupp *

    • {@code onAttemptFailureOperator} completed normally;
    • *
    • the most recent attempt is not known to be the last one.
    • *
    - * The {@code retryPredicate} accepts this {@link RetryState} and the exception from the most recent attempt, - * and may mutate the exception. The {@linkplain RetryState} advances to represent the state of a new attempt + * The {@code retryPredicate} accepts this {@link RetryControl} and the exception from the most recent attempt, + * and may mutate the exception. The {@linkplain RetryControl} advances to represent the state of a new attempt * after (in the happens-before order) testing the {@code retryPredicate}, and only if the predicate completes normally. * @param asyncFunction The retryable {@link AsyncCallbackSupplier} to be decorated. */ public RetryingAsyncCallbackSupplier( - final RetryState state, + final RetryControl control, final BinaryOperator onAttemptFailureOperator, - final BiPredicate retryPredicate, + final BiPredicate retryPredicate, final AsyncCallbackSupplier asyncFunction) { - this.state = state; + this.control = control; this.retryPredicate = retryPredicate; this.onAttemptFailureOperator = onAttemptFailureOperator; this.asyncFunction = asyncFunction; @@ -106,7 +106,7 @@ private class RetryingCallback implements SingleResultCallback { public void onResult(@Nullable final R result, @Nullable final Throwable t) { if (t != null) { try { - state.advanceOrThrow(t, onAttemptFailureOperator, retryPredicate); + control.advanceOrThrow(t, onAttemptFailureOperator, retryPredicate); } catch (Throwable failedResult) { wrapped.onResult(null, failedResult); return; diff --git a/driver-core/src/main/com/mongodb/internal/async/function/RetryingSyncSupplier.java b/driver-core/src/main/com/mongodb/internal/async/function/RetryingSyncSupplier.java index b2f82b90865..5bf988602c9 100644 --- a/driver-core/src/main/com/mongodb/internal/async/function/RetryingSyncSupplier.java +++ b/driver-core/src/main/com/mongodb/internal/async/function/RetryingSyncSupplier.java @@ -25,7 +25,7 @@ * A decorator that implements automatic retrying of failed executions of a {@link Supplier}. * {@link RetryingSyncSupplier} may execute the original retryable function multiple times sequentially. *

    - * The original function may additionally observe or control the retry loop via {@link RetryState}. + * The original function may additionally observe or control the retry loop via {@link RetryControl}. *

    * This class is not part of the public API and may be removed or changed at any time. * @@ -33,13 +33,13 @@ */ @NotThreadSafe public final class RetryingSyncSupplier implements Supplier { - private final RetryState state; - private final BiPredicate retryPredicate; + private final RetryControl control; + private final BiPredicate retryPredicate; private final BinaryOperator onAttemptFailureOperator; private final Supplier syncFunction; /** - * See {@link RetryingAsyncCallbackSupplier#RetryingAsyncCallbackSupplier(RetryState, BinaryOperator, BiPredicate, AsyncCallbackSupplier)} + * See {@link RetryingAsyncCallbackSupplier#RetryingAsyncCallbackSupplier(RetryControl, BinaryOperator, BiPredicate, AsyncCallbackSupplier)} * for the documentation of the parameters. * * @param onAttemptFailureOperator Even though the {@code onAttemptFailureOperator} accepts {@link Throwable}, @@ -48,11 +48,11 @@ public final class RetryingSyncSupplier implements Supplier { * only {@link RuntimeException}s are passed to it. */ public RetryingSyncSupplier( - final RetryState state, + final RetryControl control, final BinaryOperator onAttemptFailureOperator, - final BiPredicate retryPredicate, + final BiPredicate retryPredicate, final Supplier syncFunction) { - this.state = state; + this.control = control; this.retryPredicate = retryPredicate; this.onAttemptFailureOperator = onAttemptFailureOperator; this.syncFunction = syncFunction; @@ -64,10 +64,10 @@ public R get() { try { return syncFunction.get(); } catch (RuntimeException attemptException) { - state.advanceOrThrow(attemptException, onAttemptFailureOperator, retryPredicate); + control.advanceOrThrow(attemptException, onAttemptFailureOperator, retryPredicate); } catch (Exception attemptException) { // wrap potential sneaky / Kotlin exceptions - state.advanceOrThrow(new RuntimeException(attemptException), onAttemptFailureOperator, retryPredicate); + control.advanceOrThrow(new RuntimeException(attemptException), onAttemptFailureOperator, retryPredicate); } } } diff --git a/driver-core/src/main/com/mongodb/internal/operation/AsyncOperationHelper.java b/driver-core/src/main/com/mongodb/internal/operation/AsyncOperationHelper.java index 90c013dd293..ecf77db3728 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/AsyncOperationHelper.java +++ b/driver-core/src/main/com/mongodb/internal/operation/AsyncOperationHelper.java @@ -28,7 +28,7 @@ import com.mongodb.internal.async.function.AsyncCallbackFunction; import com.mongodb.internal.async.function.AsyncCallbackSupplier; import com.mongodb.internal.async.function.AsyncCallbackTriFunction; -import com.mongodb.internal.async.function.RetryState; +import com.mongodb.internal.async.function.RetryControl; import com.mongodb.internal.async.function.RetryingAsyncCallbackSupplier; import com.mongodb.internal.binding.AsyncConnectionSource; import com.mongodb.internal.binding.AsyncReadBinding; @@ -190,17 +190,17 @@ static void executeRetryableReadAsync( final CommandReadTransformerAsync transformer, final boolean retryReads, final SingleResultCallback callback) { - RetryState retryState = initialRetryState(retryReads, operationContext.getTimeoutContext()); + RetryControl retryControl = initialRetryState(retryReads, operationContext.getTimeoutContext()); binding.retain(); - AsyncCallbackSupplier asyncRead = decorateReadWithRetriesAsync(retryState, operationContext, + AsyncCallbackSupplier asyncRead = decorateReadWithRetriesAsync(retryControl, operationContext, (AsyncCallbackSupplier) funcCallback -> withAsyncSourceAndConnection(sourceAsyncFunction, false, operationContext, funcCallback, (source, connection, operationContextWithMinRtt, releasingCallback) -> { - if (retryState.breakAndCompleteIfRetryAnd( + if (retryControl.breakAndCompleteIfRetryAnd( () -> !OperationHelper.canRetryRead(operationContextWithMinRtt), releasingCallback)) { return; } - createReadCommandAndExecuteAsync(retryState, operationContextWithMinRtt, source, database, + createReadCommandAndExecuteAsync(retryControl, operationContextWithMinRtt, source, database, commandCreator, decoder, transformer, connection, releasingCallback); }) ).whenComplete(binding::release); @@ -258,12 +258,12 @@ static void executeRetryableWriteAsync( final Function retryCommandModifier, final SingleResultCallback callback) { - RetryState retryState = initialRetryState(true, operationContext.getTimeoutContext()); + RetryControl retryControl = initialRetryState(true, operationContext.getTimeoutContext()); binding.retain(); - AsyncCallbackSupplier asyncWrite = decorateWriteWithRetriesAsync(retryState, operationContext, + AsyncCallbackSupplier asyncWrite = decorateWriteWithRetriesAsync(retryControl, operationContext, (AsyncCallbackSupplier) funcCallback -> { - boolean firstAttempt = retryState.isFirstAttempt(); + boolean firstAttempt = retryControl.isFirstAttempt(); if (!firstAttempt && operationContext.getSessionContext().hasActiveTransaction()) { operationContext.getSessionContext().clearTransactionContext(); } @@ -273,13 +273,13 @@ static void executeRetryableWriteAsync( SingleResultCallback addingRetryableLabelCallback = firstAttempt ? releasingCallback : addingRetryableLabelCallback(releasingCallback, maxWireVersion); - if (retryState.breakAndCompleteIfRetryAnd(() -> + if (retryControl.breakAndCompleteIfRetryAnd(() -> !OperationHelper.canRetryWrite(connection.getDescription()), addingRetryableLabelCallback)) { return; } BsonDocument command; try { - command = retryState.attachment(AttachmentKeys.command()) + command = retryControl.attachment(AttachmentKeys.command()) .map(previousAttemptCommand -> { Assertions.assertFalse(firstAttempt); return retryCommandModifier.apply(previousAttemptCommand); @@ -288,7 +288,7 @@ static void executeRetryableWriteAsync( source.getServerDescription(), connection.getDescription())); // attach `maxWireVersion`, `retryableWriteCommandFlag` ASAP because they are used to check whether we should retry - retryState.attach(AttachmentKeys.maxWireVersion(), maxWireVersion, true) + retryControl.attach(AttachmentKeys.maxWireVersion(), maxWireVersion, true) .attach(AttachmentKeys.retryableWriteCommandFlag(), isRetryableWriteCommand(command), true) .attach(AttachmentKeys.commandDescriptionSupplier(), command::getFirstKey, false) .attach(AttachmentKeys.command(), command, false); @@ -306,7 +306,7 @@ static void executeRetryableWriteAsync( } static void createReadCommandAndExecuteAsync( - final RetryState retryState, + final RetryControl retryControl, final OperationContext operationContext, final AsyncConnectionSource source, final String database, @@ -318,7 +318,7 @@ static void createReadCommandAndExecuteAsync( BsonDocument command; try { command = commandCreator.create(operationContext, source.getServerDescription(), connection.getDescription()); - retryState.attach(AttachmentKeys.commandDescriptionSupplier(), command::getFirstKey, false); + retryControl.attach(AttachmentKeys.commandDescriptionSupplier(), command::getFirstKey, false); } catch (IllegalArgumentException e) { callback.onResult(null, e); return; @@ -327,20 +327,20 @@ static void createReadCommandAndExecuteAsync( operationContext, transformingReadCallback(transformer, source, connection, operationContext, callback)); } - static AsyncCallbackSupplier decorateReadWithRetriesAsync(final RetryState retryState, final OperationContext operationContext, + static AsyncCallbackSupplier decorateReadWithRetriesAsync(final RetryControl retryControl, final OperationContext operationContext, final AsyncCallbackSupplier asyncReadFunction) { - return new RetryingAsyncCallbackSupplier<>(retryState, onRetryableReadAttemptFailure(operationContext.getServerDeprioritization()), + return new RetryingAsyncCallbackSupplier<>(retryControl, onRetryableReadAttemptFailure(operationContext.getServerDeprioritization()), CommandOperationHelper::loggingShouldAttemptToRetryRead, callback -> { - logRetryCommand(retryState, operationContext); + logRetryCommand(retryControl, operationContext); asyncReadFunction.get(callback); }); } - static AsyncCallbackSupplier decorateWriteWithRetriesAsync(final RetryState retryState, final OperationContext operationContext, + static AsyncCallbackSupplier decorateWriteWithRetriesAsync(final RetryControl retryControl, final OperationContext operationContext, final AsyncCallbackSupplier asyncWriteFunction) { - return new RetryingAsyncCallbackSupplier<>(retryState, onRetryableWriteAttemptFailure(operationContext.getServerDeprioritization()), + return new RetryingAsyncCallbackSupplier<>(retryControl, onRetryableWriteAttemptFailure(operationContext.getServerDeprioritization()), CommandOperationHelper::loggingShouldAttemptToRetryWriteAndAddRetryableLabel, callback -> { - logRetryCommand(retryState, operationContext); + logRetryCommand(retryControl, operationContext); asyncWriteFunction.get(callback); }); } diff --git a/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java b/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java index 82aabb1279d..cad557283b7 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java @@ -45,7 +45,7 @@ import com.mongodb.internal.async.MutableValue; import com.mongodb.internal.async.SingleResultCallback; import com.mongodb.internal.async.function.AsyncCallbackSupplier; -import com.mongodb.internal.async.function.RetryState; +import com.mongodb.internal.async.function.RetryControl; import com.mongodb.internal.async.function.RetryingSyncSupplier; import com.mongodb.internal.binding.AsyncConnectionSource; import com.mongodb.internal.binding.AsyncWriteBinding; @@ -284,11 +284,11 @@ private Integer executeBatch( assertFalse(unexecutedModels.isEmpty()); SessionContext sessionContext = operationContext.getSessionContext(); TimeoutContext timeoutContext = operationContext.getTimeoutContext(); - RetryState retryState = initialRetryState(retryWritesSetting, timeoutContext); + RetryControl retryControl = initialRetryState(retryWritesSetting, timeoutContext); BatchEncoder batchEncoder = new BatchEncoder(); Supplier retryingBatchExecutor = decorateWriteWithRetries( - retryState, operationContext, + retryControl, operationContext, // Each batch re-selects a server and re-checks out a connection because this is simpler, // and it is allowed by https://jira.mongodb.org/browse/DRIVERS-2502. // If connection pinning is required, `binding` handles that, @@ -298,15 +298,15 @@ private Integer executeBatch( ConnectionDescription connectionDescription = connection.getDescription(); boolean effectiveRetryWrites = isRetryableWrite( retryWritesSetting, effectiveWriteConcern, connectionDescription, sessionContext); - retryState.breakAndThrowIfRetryAnd(() -> !effectiveRetryWrites); + retryControl.breakAndThrowIfRetryAnd(() -> !effectiveRetryWrites); resultAccumulator.onNewServerAddress(connectionDescription.getServerAddress()); - retryState.attach(AttachmentKeys.maxWireVersion(), connectionDescription.getMaxWireVersion(), true) + retryControl.attach(AttachmentKeys.maxWireVersion(), connectionDescription.getMaxWireVersion(), true) .attach(AttachmentKeys.commandDescriptionSupplier(), () -> BULK_WRITE_COMMAND_NAME, false); ClientBulkWriteCommand bulkWriteCommand = createBulkWriteCommand( - retryState, effectiveRetryWrites, effectiveWriteConcern, sessionContext, unexecutedModels, batchEncoder, - () -> retryState.attach(AttachmentKeys.retryableWriteCommandFlag(), true, true)); + retryControl, effectiveRetryWrites, effectiveWriteConcern, sessionContext, unexecutedModels, batchEncoder, + () -> retryControl.attach(AttachmentKeys.retryableWriteCommandFlag(), true, true)); return executeBulkWriteCommandAndExhaustOkResponse( - retryState, connectionSource, connection, bulkWriteCommand, effectiveWriteConcern, operationContextWithMinRtt); + retryControl, connectionSource, connection, bulkWriteCommand, effectiveWriteConcern, operationContextWithMinRtt); }) ); @@ -326,7 +326,7 @@ private Integer executeBatch( // Adding the `RetryableWriteError` label here is unnecessary at this point: // applications cannot use it for implementing retries, and it is not even part of the public driver API. // Unfortunately, certain unified tests incorrectly rely on this label to verify retries, resulting in this redundant code. - addRetryableLabelOrGetWriteAttemptFailureNotToBeRetried(retryState, mongoException); + addRetryableLabelOrGetWriteAttemptFailureNotToBeRetried(retryControl, mongoException); } throw mongoException; } @@ -347,11 +347,11 @@ private void executeBatchAsync( assertFalse(unexecutedModels.isEmpty()); SessionContext sessionContext = operationContext.getSessionContext(); TimeoutContext timeoutContext = operationContext.getTimeoutContext(); - RetryState retryState = initialRetryState(retryWritesSetting, timeoutContext); + RetryControl retryControl = initialRetryState(retryWritesSetting, timeoutContext); BatchEncoder batchEncoder = new BatchEncoder(); AsyncCallbackSupplier retryingBatchExecutor = decorateWriteWithRetriesAsync( - retryState, operationContext, + retryControl, operationContext, // Each batch re-selects a server and re-checks out a connection because this is simpler, // and it is allowed by https://jira.mongodb.org/browse/DRIVERS-2502. // If connection pinning is required, `binding` handles that, @@ -362,15 +362,15 @@ private void executeBatchAsync( ConnectionDescription connectionDescription = connection.getDescription(); boolean effectiveRetryWrites = isRetryableWrite( retryWritesSetting, effectiveWriteConcern, connectionDescription, sessionContext); - retryState.breakAndThrowIfRetryAnd(() -> !effectiveRetryWrites); + retryControl.breakAndThrowIfRetryAnd(() -> !effectiveRetryWrites); resultAccumulator.onNewServerAddress(connectionDescription.getServerAddress()); - retryState.attach(AttachmentKeys.maxWireVersion(), connectionDescription.getMaxWireVersion(), true) + retryControl.attach(AttachmentKeys.maxWireVersion(), connectionDescription.getMaxWireVersion(), true) .attach(AttachmentKeys.commandDescriptionSupplier(), () -> BULK_WRITE_COMMAND_NAME, false); ClientBulkWriteCommand bulkWriteCommand = createBulkWriteCommand( - retryState, effectiveRetryWrites, effectiveWriteConcern, sessionContext, unexecutedModels, batchEncoder, - () -> retryState.attach(AttachmentKeys.retryableWriteCommandFlag(), true, true)); + retryControl, effectiveRetryWrites, effectiveWriteConcern, sessionContext, unexecutedModels, batchEncoder, + () -> retryControl.attach(AttachmentKeys.retryableWriteCommandFlag(), true, true)); executeBulkWriteCommandAndExhaustOkResponseAsync( - retryState, connectionSource, connection, bulkWriteCommand, effectiveWriteConcern, operationContextWithMinRtt, executeAndExhaustCallback); + retryControl, connectionSource, connection, bulkWriteCommand, effectiveWriteConcern, operationContextWithMinRtt, executeAndExhaustCallback); }).finish(functionCallback); }) ); @@ -396,7 +396,7 @@ private void executeBatchAsync( // Adding the `RetryableWriteError` label here is unnecessary at this point: // applications cannot use it for implementing retries, and it is not even part of the public driver API. // Unfortunately, certain unified tests incorrectly rely on this label to verify retries, resulting in this redundant code. - addRetryableLabelOrGetWriteAttemptFailureNotToBeRetried(retryState, mongoException); + addRetryableLabelOrGetWriteAttemptFailureNotToBeRetried(retryControl, mongoException); } throw mongoException; } else { @@ -415,7 +415,7 @@ private void executeBatchAsync( */ @Nullable private ExhaustiveClientBulkWriteCommandOkResponse executeBulkWriteCommandAndExhaustOkResponse( - final RetryState retryState, + final RetryControl retryControl, final ConnectionSource connectionSource, final Connection connection, final ClientBulkWriteCommand bulkWriteCommand, @@ -434,7 +434,7 @@ private ExhaustiveClientBulkWriteCommandOkResponse executeBulkWriteCommandAndExh return null; } ClientBulkWriteCommandOkResponse response = new ClientBulkWriteCommandOkResponse(okResponseDocument); - List> cursorExhaustBatches = doWithRetriesDisabled(retryState, () -> + List> cursorExhaustBatches = doWithRetriesDisabled(retryControl, () -> exhaustBulkWriteCommandOkResponseCursor(connectionSource, operationContext, connection, response)); return createExhaustiveClientBulkWriteCommandOkResponse( response, @@ -443,10 +443,10 @@ private ExhaustiveClientBulkWriteCommandOkResponse executeBulkWriteCommandAndExh } /** - * @see #executeBulkWriteCommandAndExhaustOkResponse(RetryState, ConnectionSource, Connection, ClientBulkWriteCommand, WriteConcern, OperationContext) + * @see #executeBulkWriteCommandAndExhaustOkResponse(RetryControl, ConnectionSource, Connection, ClientBulkWriteCommand, WriteConcern, OperationContext) */ private void executeBulkWriteCommandAndExhaustOkResponseAsync( - final RetryState retryState, + final RetryControl retryControl, final AsyncConnectionSource connectionSource, final AsyncConnection connection, final ClientBulkWriteCommand bulkWriteCommand, @@ -470,7 +470,7 @@ private void executeBulkWriteCommandAndExhaustOkResponseAsync( } ClientBulkWriteCommandOkResponse response = new ClientBulkWriteCommandOkResponse(okResponseDocument); beginAsync().>>thenSupply(exhaustCallback -> { - doWithRetriesDisabledAsync(retryState, (actionCallback) -> { + doWithRetriesDisabledAsync(retryControl, (actionCallback) -> { exhaustBulkWriteCommandOkResponseCursorAsync(connectionSource, connection, response, operationContext, actionCallback); }, exhaustCallback); }).thenApply((cursorExhaustBatches, transformExhaustionResultCallback) -> { @@ -483,7 +483,7 @@ private void executeBulkWriteCommandAndExhaustOkResponseAsync( } /** - * @see #executeBulkWriteCommandAndExhaustOkResponse(RetryState, ConnectionSource, Connection, ClientBulkWriteCommand, WriteConcern, OperationContext) + * @see #executeBulkWriteCommandAndExhaustOkResponse(RetryControl, ConnectionSource, Connection, ClientBulkWriteCommand, WriteConcern, OperationContext) */ private static ExhaustiveClientBulkWriteCommandOkResponse createExhaustiveClientBulkWriteCommandOkResponse( final ClientBulkWriteCommandOkResponse response, @@ -503,40 +503,40 @@ private static ExhaustiveClientBulkWriteCommandOkResponse createExhaustiveClient } /** - * This method disables retries on {@code outerRetryState} while executing the {@code action}. + * This method disables retries on {@code outerRetryControl} while executing the {@code action}. * This way, if the {@code action} completes abruptly, the outer {@link RetryingSyncSupplier} the execution is part of * does not make another attempt based on that exception. */ private R doWithRetriesDisabled( - final RetryState outerRetryState, + final RetryControl outerRetryControl, final Supplier action) { // TODO-JAVA-5956 The current implementation incorrectly uses `retryableWriteCommandFlag` to achieve the behavior needed. - Optional originalRetryableWriteCommandFlag = outerRetryState.attachment(AttachmentKeys.retryableWriteCommandFlag()); + Optional originalRetryableWriteCommandFlag = outerRetryControl.attachment(AttachmentKeys.retryableWriteCommandFlag()); try { - outerRetryState.attach(AttachmentKeys.retryableWriteCommandFlag(), false, true); + outerRetryControl.attach(AttachmentKeys.retryableWriteCommandFlag(), false, true); return action.get(); } finally { - originalRetryableWriteCommandFlag.ifPresent(value -> outerRetryState.attach(AttachmentKeys.retryableWriteCommandFlag(), value, true)); + originalRetryableWriteCommandFlag.ifPresent(value -> outerRetryControl.attach(AttachmentKeys.retryableWriteCommandFlag(), value, true)); } } /** - * @see #doWithRetriesDisabled(RetryState, Supplier) + * @see #doWithRetriesDisabled(RetryControl, Supplier) */ private void doWithRetriesDisabledAsync( - final RetryState retryState, + final RetryControl retryControl, final AsyncSupplier action, final SingleResultCallback callback) { beginAsync().thenSupply(c -> { // TODO-JAVA-5956 The current implementation incorrectly uses `retryableWriteCommandFlag` to achieve the behavior needed. - Optional originalRetryableWriteCommandFlag = retryState.attachment(AttachmentKeys.retryableWriteCommandFlag()); + Optional originalRetryableWriteCommandFlag = retryControl.attachment(AttachmentKeys.retryableWriteCommandFlag()); beginAsync().thenSupply(actionCallback -> { - retryState.attach(AttachmentKeys.retryableWriteCommandFlag(), false, true); + retryControl.attach(AttachmentKeys.retryableWriteCommandFlag(), false, true); action.finish(actionCallback); }).thenAlwaysRunAndFinish(() -> { - originalRetryableWriteCommandFlag.ifPresent(value -> retryState.attach(AttachmentKeys.retryableWriteCommandFlag(), value, true)); + originalRetryableWriteCommandFlag.ifPresent(value -> retryControl.attach(AttachmentKeys.retryableWriteCommandFlag(), value, true)); }, c); }).finish(callback); } @@ -586,7 +586,7 @@ private void exhaustBulkWriteCommandOkResponseCursorAsync( } private ClientBulkWriteCommand createBulkWriteCommand( - final RetryState retryState, + final RetryControl retryControl, final boolean effectiveRetryWrites, final WriteConcern effectiveWriteConcern, final SessionContext sessionContext, @@ -612,7 +612,7 @@ private ClientBulkWriteCommand createBulkWriteCommand( options, () -> { retriesEnabler.run(); - return retryState.isFirstAttempt() + return retryControl.isFirstAttempt() ? sessionContext.advanceTransactionNumber() : sessionContext.getTransactionNumber(); })); 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 a0acea83485..464663260c6 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/CommandOperationHelper.java +++ b/driver-core/src/main/com/mongodb/internal/operation/CommandOperationHelper.java @@ -30,7 +30,7 @@ import com.mongodb.connection.ConnectionDescription; import com.mongodb.connection.ServerDescription; import com.mongodb.internal.TimeoutContext; -import com.mongodb.internal.async.function.RetryState; +import com.mongodb.internal.async.function.RetryControl; import com.mongodb.internal.connection.OperationContext; import com.mongodb.internal.connection.OperationContext.ServerDeprioritization; import com.mongodb.internal.operation.OperationHelper.ResourceSupplierInternalException; @@ -46,7 +46,7 @@ import static com.mongodb.assertions.Assertions.assertFalse; import static com.mongodb.assertions.Assertions.assertNotNull; -import static com.mongodb.internal.async.function.RetryState.MAX_RETRIES; +import static com.mongodb.internal.async.function.RetryControl.MAX_RETRIES; import static com.mongodb.internal.operation.OperationHelper.LOGGER; import static java.lang.String.format; import static java.util.Arrays.asList; @@ -122,11 +122,11 @@ private static Throwable chooseRetryableWriteException( /* Read Binding Helpers */ - static RetryState initialRetryState(final boolean retry, final TimeoutContext timeoutContext) { + static RetryControl initialRetryState(final boolean retry, final TimeoutContext timeoutContext) { if (retry) { - return timeoutContext.hasTimeoutMS() ? new RetryState() : new RetryState(MAX_RETRIES); + return timeoutContext.hasTimeoutMS() ? new RetryControl() : new RetryControl(MAX_RETRIES); } - return new RetryState(0); + return new RetryControl(0); } private static final List RETRYABLE_ERROR_CODES = asList(6, 7, 89, 91, 134, 189, 262, 9001, 13436, 13435, 11602, 11600, 10107); @@ -165,22 +165,22 @@ static boolean isNamespaceError(final Throwable t) { } } - static boolean loggingShouldAttemptToRetryRead(final RetryState retryState, final Throwable attemptFailure) { + static boolean loggingShouldAttemptToRetryRead(final RetryControl retryControl, final Throwable attemptFailure) { assertFalse(attemptFailure instanceof ResourceSupplierInternalException); boolean decision = isRetryableException(attemptFailure) || (attemptFailure instanceof MongoSecurityException && attemptFailure.getCause() != null && isRetryableException(attemptFailure.getCause())); if (!decision) { - logUnableToRetryCommand(retryState, attemptFailure); + logUnableToRetryCommand(retryControl, attemptFailure); } return decision; } - static boolean loggingShouldAttemptToRetryWriteAndAddRetryableLabel(final RetryState retryState, final Throwable attemptFailure) { - Throwable attemptFailureNotToBeRetried = addRetryableLabelOrGetWriteAttemptFailureNotToBeRetried(retryState, attemptFailure); + static boolean loggingShouldAttemptToRetryWriteAndAddRetryableLabel(final RetryControl retryControl, final Throwable attemptFailure) { + Throwable attemptFailureNotToBeRetried = addRetryableLabelOrGetWriteAttemptFailureNotToBeRetried(retryControl, attemptFailure); boolean decision = attemptFailureNotToBeRetried == null; - if (!decision && retryState.attachment(AttachmentKeys.retryableWriteCommandFlag()).orElse(false)) { - logUnableToRetryCommand(retryState, assertNotNull(attemptFailureNotToBeRetried)); + if (!decision && retryControl.attachment(AttachmentKeys.retryableWriteCommandFlag()).orElse(false)) { + logUnableToRetryCommand(retryControl, assertNotNull(attemptFailureNotToBeRetried)); } return decision; } @@ -191,7 +191,7 @@ static boolean loggingShouldAttemptToRetryWriteAndAddRetryableLabel(final RetryS * Otherwise, returns a {@link Throwable} that must not be retried. */ @Nullable - static Throwable addRetryableLabelOrGetWriteAttemptFailureNotToBeRetried(final RetryState retryState, final Throwable attemptFailure) { + static Throwable addRetryableLabelOrGetWriteAttemptFailureNotToBeRetried(final RetryControl retryControl, final Throwable attemptFailure) { Throwable failure = attemptFailure instanceof ResourceSupplierInternalException ? attemptFailure.getCause() : attemptFailure; boolean decision = false; MongoException exceptionRetryableRegardlessOfCommand = null; @@ -200,12 +200,12 @@ static Throwable addRetryableLabelOrGetWriteAttemptFailureNotToBeRetried(final R decision = true; exceptionRetryableRegardlessOfCommand = (MongoException) failure; } - if (retryState.attachment(AttachmentKeys.retryableWriteCommandFlag()).orElse(false)) { + if (retryControl.attachment(AttachmentKeys.retryableWriteCommandFlag()).orElse(false)) { if (exceptionRetryableRegardlessOfCommand != null) { /* We are going to retry even if `retryableWriteCommandFlag` is false, * but we add the retryable label only if `retryableWriteCommandFlag` is true. */ exceptionRetryableRegardlessOfCommand.addLabel(RETRYABLE_WRITE_ERROR_LABEL); - } else if (decideRetryableAndAddRetryableWriteErrorLabel(failure, retryState.attachment(AttachmentKeys.maxWireVersion()) + } else if (decideRetryableAndAddRetryableWriteErrorLabel(failure, retryControl.attachment(AttachmentKeys.maxWireVersion()) .orElse(null))) { decision = true; } @@ -251,11 +251,11 @@ static void addRetryableWriteErrorLabel(final MongoException exception, final in } } - static void logRetryCommand(final RetryState retryState, final OperationContext operationContext) { - if (LOGGER.isDebugEnabled() && !retryState.isFirstAttempt()) { - String commandDescription = retryState.attachment(AttachmentKeys.commandDescriptionSupplier()).map(Supplier::get).orElse(null); - Throwable exception = retryState.exception().orElseThrow(Assertions::fail); - int oneBasedAttempt = retryState.attempt() + 1; + static void logRetryCommand(final RetryControl retryControl, final OperationContext operationContext) { + if (LOGGER.isDebugEnabled() && !retryControl.isFirstAttempt()) { + String commandDescription = retryControl.attachment(AttachmentKeys.commandDescriptionSupplier()).map(Supplier::get).orElse(null); + Throwable exception = retryControl.exception().orElseThrow(Assertions::fail); + int oneBasedAttempt = retryControl.attempt() + 1; long operationId = operationContext.getId(); LOGGER.debug(commandDescription == null ? format("Retrying a command within the operation with operation ID %s due to the error \"%s\". Attempt number: #%d", @@ -265,9 +265,9 @@ static void logRetryCommand(final RetryState retryState, final OperationContext } } - private static void logUnableToRetryCommand(final RetryState retryState, final Throwable originalError) { + private static void logUnableToRetryCommand(final RetryControl retryControl, final Throwable originalError) { if (LOGGER.isDebugEnabled()) { - String commandDescription = retryState.attachment(AttachmentKeys.commandDescriptionSupplier()).map(Supplier::get).orElse(null); + String commandDescription = retryControl.attachment(AttachmentKeys.commandDescriptionSupplier()).map(Supplier::get).orElse(null); LOGGER.debug(commandDescription == null ? format("Unable to retry a command due to the error \"%s\"", originalError) : format("Unable to retry the command '%s' due to the error \"%s\"", commandDescription, originalError)); diff --git a/driver-core/src/main/com/mongodb/internal/operation/FindOperation.java b/driver-core/src/main/com/mongodb/internal/operation/FindOperation.java index 958a0dc5c62..906a16420df 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/FindOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/FindOperation.java @@ -27,7 +27,7 @@ import com.mongodb.internal.async.AsyncBatchCursor; import com.mongodb.internal.async.SingleResultCallback; import com.mongodb.internal.async.function.AsyncCallbackSupplier; -import com.mongodb.internal.async.function.RetryState; +import com.mongodb.internal.async.function.RetryControl; import com.mongodb.internal.binding.AsyncReadBinding; import com.mongodb.internal.binding.ReadBinding; import com.mongodb.internal.connection.OperationContext; @@ -299,13 +299,13 @@ public BatchCursor execute(final ReadBinding binding, final OperationContext } OperationContext findOperationContext = getFindOperationContext(operationContext); - RetryState retryState = initialRetryState(retryReads, findOperationContext.getTimeoutContext()); - Supplier> read = decorateReadWithRetries(retryState, findOperationContext, () -> + RetryControl retryControl = initialRetryState(retryReads, findOperationContext.getTimeoutContext()); + Supplier> read = decorateReadWithRetries(retryControl, findOperationContext, () -> withSourceAndConnection(binding::getReadConnectionSource, false, findOperationContext, (source, connection, commandOperationContext) -> { - retryState.breakAndThrowIfRetryAnd(() -> !canRetryRead(commandOperationContext)); + retryControl.breakAndThrowIfRetryAnd(() -> !canRetryRead(commandOperationContext)); try { - return createReadCommandAndExecute(retryState, commandOperationContext, source, namespace.getDatabaseName(), + return createReadCommandAndExecute(retryControl, commandOperationContext, source, namespace.getDatabaseName(), getCommandCreator(), CommandResultDocumentCodec.create(decoder, FIRST_BATCH), transformer(), connection); } catch (MongoCommandException e) { @@ -325,17 +325,17 @@ public void executeAsync(final AsyncReadBinding binding, final OperationContext } OperationContext findOperationContext = getFindOperationContext(operationContext); - RetryState retryState = initialRetryState(retryReads, findOperationContext.getTimeoutContext()); + RetryControl retryControl = initialRetryState(retryReads, findOperationContext.getTimeoutContext()); binding.retain(); AsyncCallbackSupplier> asyncRead = decorateReadWithRetriesAsync( - retryState, operationContext, (AsyncCallbackSupplier>) funcCallback -> + retryControl, operationContext, (AsyncCallbackSupplier>) funcCallback -> withAsyncSourceAndConnection(binding::getReadConnectionSource, false, findOperationContext, funcCallback, (source, connection, operationContextWithMinRTT, releasingCallback) -> { - if (retryState.breakAndCompleteIfRetryAnd(() -> !canRetryRead(findOperationContext), releasingCallback)) { + if (retryControl.breakAndCompleteIfRetryAnd(() -> !canRetryRead(findOperationContext), releasingCallback)) { return; } SingleResultCallback> wrappedCallback = exceptionTransformingCallback(releasingCallback); - createReadCommandAndExecuteAsync(retryState, operationContextWithMinRTT, source, + createReadCommandAndExecuteAsync(retryControl, operationContextWithMinRTT, source, namespace.getDatabaseName(), getCommandCreator(), CommandResultDocumentCodec.create(decoder, FIRST_BATCH), asyncTransformer(), connection, wrappedCallback); diff --git a/driver-core/src/main/com/mongodb/internal/operation/ListCollectionsOperation.java b/driver-core/src/main/com/mongodb/internal/operation/ListCollectionsOperation.java index fe4929a27be..340f60901a3 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/ListCollectionsOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/ListCollectionsOperation.java @@ -23,7 +23,7 @@ import com.mongodb.internal.async.AsyncBatchCursor; import com.mongodb.internal.async.SingleResultCallback; import com.mongodb.internal.async.function.AsyncCallbackSupplier; -import com.mongodb.internal.async.function.RetryState; +import com.mongodb.internal.async.function.RetryControl; import com.mongodb.internal.binding.AsyncReadBinding; import com.mongodb.internal.binding.ReadBinding; import com.mongodb.internal.connection.OperationContext; @@ -175,12 +175,12 @@ public String getCommandName() { public BatchCursor execute(final ReadBinding binding, final OperationContext operationContext) { OperationContext listCollectionsOperationContext = applyTimeoutModeToOperationContext(timeoutMode, operationContext); - RetryState retryState = initialRetryState(retryReads, listCollectionsOperationContext.getTimeoutContext()); - Supplier> read = decorateReadWithRetries(retryState, listCollectionsOperationContext, () -> + RetryControl retryControl = initialRetryState(retryReads, listCollectionsOperationContext.getTimeoutContext()); + Supplier> read = decorateReadWithRetries(retryControl, listCollectionsOperationContext, () -> withSourceAndConnection(binding::getReadConnectionSource, false, listCollectionsOperationContext, (source, connection, operationContextWithMinRTT) -> { - retryState.breakAndThrowIfRetryAnd(() -> !canRetryRead(operationContextWithMinRTT)); + retryControl.breakAndThrowIfRetryAnd(() -> !canRetryRead(operationContextWithMinRTT)); try { - return createReadCommandAndExecute(retryState, operationContextWithMinRTT, source, databaseName, + return createReadCommandAndExecute(retryControl, operationContextWithMinRTT, source, databaseName, getCommandCreator(), createCommandDecoder(), transformer(), connection); } catch (MongoCommandException e) { return rethrowIfNotNamespaceError(e, @@ -196,16 +196,16 @@ public void executeAsync(final AsyncReadBinding binding, final OperationContext final SingleResultCallback> callback) { OperationContext listCollectionsOperationContext = applyTimeoutModeToOperationContext(timeoutMode, operationContext); - RetryState retryState = initialRetryState(retryReads, listCollectionsOperationContext.getTimeoutContext()); + RetryControl retryControl = initialRetryState(retryReads, listCollectionsOperationContext.getTimeoutContext()); binding.retain(); AsyncCallbackSupplier> asyncRead = decorateReadWithRetriesAsync( - retryState, listCollectionsOperationContext, (AsyncCallbackSupplier>) funcCallback -> + retryControl, listCollectionsOperationContext, (AsyncCallbackSupplier>) funcCallback -> withAsyncSourceAndConnection(binding::getReadConnectionSource, false, listCollectionsOperationContext, funcCallback, (source, connection, operationContextWithMinRtt, releasingCallback) -> { - if (retryState.breakAndCompleteIfRetryAnd(() -> !canRetryRead(operationContextWithMinRtt), releasingCallback)) { + if (retryControl.breakAndCompleteIfRetryAnd(() -> !canRetryRead(operationContextWithMinRtt), releasingCallback)) { return; } - createReadCommandAndExecuteAsync(retryState, operationContextWithMinRtt, source, databaseName, + createReadCommandAndExecuteAsync(retryControl, operationContextWithMinRtt, source, databaseName, getCommandCreator(), createCommandDecoder(), asyncTransformer(), connection, (result, t) -> { if (t != null && !isNamespaceError(t)) { diff --git a/driver-core/src/main/com/mongodb/internal/operation/ListIndexesOperation.java b/driver-core/src/main/com/mongodb/internal/operation/ListIndexesOperation.java index 75e29b946bd..6905b968ad3 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/ListIndexesOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/ListIndexesOperation.java @@ -22,7 +22,7 @@ import com.mongodb.internal.async.AsyncBatchCursor; import com.mongodb.internal.async.SingleResultCallback; import com.mongodb.internal.async.function.AsyncCallbackSupplier; -import com.mongodb.internal.async.function.RetryState; +import com.mongodb.internal.async.function.RetryControl; import com.mongodb.internal.binding.AsyncReadBinding; import com.mongodb.internal.binding.ReadBinding; import com.mongodb.internal.connection.OperationContext; @@ -132,12 +132,12 @@ public MongoNamespace getNamespace() { public BatchCursor execute(final ReadBinding binding, final OperationContext operationContext) { OperationContext listIndexesOperationContext = applyTimeoutModeToOperationContext(timeoutMode, operationContext); - RetryState retryState = initialRetryState(retryReads, listIndexesOperationContext.getTimeoutContext()); - Supplier> read = decorateReadWithRetries(retryState, listIndexesOperationContext, () -> + RetryControl retryControl = initialRetryState(retryReads, listIndexesOperationContext.getTimeoutContext()); + Supplier> read = decorateReadWithRetries(retryControl, listIndexesOperationContext, () -> withSourceAndConnection(binding::getReadConnectionSource, false, listIndexesOperationContext, (source, connection, operationContextWithMinRTT) -> { - retryState.breakAndThrowIfRetryAnd(() -> !canRetryRead(operationContextWithMinRTT)); + retryControl.breakAndThrowIfRetryAnd(() -> !canRetryRead(operationContextWithMinRTT)); try { - return createReadCommandAndExecute(retryState, operationContextWithMinRTT, source, namespace.getDatabaseName(), + return createReadCommandAndExecute(retryControl, operationContextWithMinRTT, source, namespace.getDatabaseName(), getCommandCreator(), createCommandDecoder(), transformer(), connection); } catch (MongoCommandException e) { return rethrowIfNotNamespaceError(e, @@ -152,16 +152,16 @@ public BatchCursor execute(final ReadBinding binding, final OperationContext public void executeAsync(final AsyncReadBinding binding, final OperationContext operationContext, final SingleResultCallback> callback) { OperationContext listIndexesOperationContext = applyTimeoutModeToOperationContext(timeoutMode, operationContext); - RetryState retryState = initialRetryState(retryReads, operationContext.getTimeoutContext()); + RetryControl retryControl = initialRetryState(retryReads, operationContext.getTimeoutContext()); binding.retain(); AsyncCallbackSupplier> asyncRead = decorateReadWithRetriesAsync( - retryState, listIndexesOperationContext, (AsyncCallbackSupplier>) funcCallback -> + retryControl, listIndexesOperationContext, (AsyncCallbackSupplier>) funcCallback -> withAsyncSourceAndConnection(binding::getReadConnectionSource, false, listIndexesOperationContext, funcCallback, (source, connection, operationContextWithMinRtt, releasingCallback) -> { - if (retryState.breakAndCompleteIfRetryAnd(() -> !canRetryRead(operationContextWithMinRtt), releasingCallback)) { + if (retryControl.breakAndCompleteIfRetryAnd(() -> !canRetryRead(operationContextWithMinRtt), releasingCallback)) { return; } - createReadCommandAndExecuteAsync(retryState, operationContextWithMinRtt, source, + createReadCommandAndExecuteAsync(retryControl, operationContextWithMinRtt, source, namespace.getDatabaseName(), getCommandCreator(), createCommandDecoder(), asyncTransformer(), connection, (result, t) -> { diff --git a/driver-core/src/main/com/mongodb/internal/operation/MixedBulkWriteOperation.java b/driver-core/src/main/com/mongodb/internal/operation/MixedBulkWriteOperation.java index a5bb11bed9f..2433256c46c 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/MixedBulkWriteOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/MixedBulkWriteOperation.java @@ -26,7 +26,7 @@ import com.mongodb.internal.async.function.AsyncCallbackFunction; import com.mongodb.internal.async.function.AsyncCallbackSupplier; import com.mongodb.internal.async.function.AsyncCallbackTriFunction; -import com.mongodb.internal.async.function.RetryState; +import com.mongodb.internal.async.function.RetryControl; import com.mongodb.internal.binding.AsyncConnectionSource; import com.mongodb.internal.binding.AsyncWriteBinding; import com.mongodb.internal.binding.ConnectionSource; @@ -241,23 +241,23 @@ private BatchWithSourceAndConnection executeBatchReusingCon final OperationContext operationContext) { MutableValue batch = new MutableValue<>(maybeBatch); MutableValue sourceAndConnection = new MutableValue<>(maybeSourceAndConnection); - RetryState retryState = initialRetryState( + RetryControl retryControl = initialRetryState( retryWrites, operationContext.getTimeoutContext()); Supplier> retryingBatchExecutor = decorateWriteWithRetries( - retryState, + retryControl, operationContext, () -> { SourceAndConnection reusedOrNewSourceAndConnection = reuseOrSelectServerAndCheckoutConnectionIfClosed( sourceAndConnection.getNullable(), effectiveWriteConcern, binding, - operationContext, retryState); + operationContext, retryControl); try { sourceAndConnection.set(reusedOrNewSourceAndConnection); ConnectionDescription connectionDescription = reusedOrNewSourceAndConnection.getConnection().getDescription(); - retryState.attach(AttachmentKeys.maxWireVersion(), connectionDescription.getMaxWireVersion(), true); + retryControl.attach(AttachmentKeys.maxWireVersion(), connectionDescription.getMaxWireVersion(), true); batch.set(batch.getNullable() != null ? batch.get() : createFirstBatch(connectionDescription, reusedOrNewSourceAndConnection.getOperationContext(), effectiveWriteConcern)); - onBatch(batch.get(), retryState); + onBatch(batch.get(), retryControl); executeBatch(batch.get(), reusedOrNewSourceAndConnection, effectiveWriteConcern); return new BatchWithSourceAndConnection<>(batch.get().getNextBatch(), reusedOrNewSourceAndConnection); } catch (Throwable e) { @@ -280,7 +280,7 @@ private BatchWithSourceAndConnection executeBatchReusingCon // Adding the `RetryableWriteError` label here is unnecessary at this point: // applications cannot use it for implementing retries, and it is not even part of the public driver API. // Unfortunately, certain unified tests incorrectly rely on this label to verify retries, resulting in this redundant code. - addRetryableLabelOrGetWriteAttemptFailureNotToBeRetried(retryState, e); + addRetryableLabelOrGetWriteAttemptFailureNotToBeRetried(retryControl, e); } throw e; } @@ -300,25 +300,25 @@ private void executeBatchReusingConnectionAsync( beginAsync().>thenSupply(c -> { MutableValue batch = new MutableValue<>(maybeBatch); MutableValue sourceAndConnection = new MutableValue<>(maybeSourceAndConnection); - RetryState retryState = initialRetryState( + RetryControl retryControl = initialRetryState( retryWrites, operationContext.getTimeoutContext()); AsyncCallbackSupplier> retryingBatchExecutor = decorateWriteWithRetriesAsync( - retryState, + retryControl, operationContext, supplierCallback -> { beginAsync().thenSupply(reuseOrSelectServerAndCheckoutConnectionCallback -> { reuseOrSelectServerAndCheckoutConnectionIfClosedAsync( sourceAndConnection.getNullable(), effectiveWriteConcern, binding, - operationContext, retryState, reuseOrSelectServerAndCheckoutConnectionCallback); + operationContext, retryControl, reuseOrSelectServerAndCheckoutConnectionCallback); }).>thenApply((reusedOrNewSourceAndConnection, setSourceAndConnectionCallback) -> { beginAsync().thenRun(executeBatchCallback -> { sourceAndConnection.set(reusedOrNewSourceAndConnection); ConnectionDescription connectionDescription = reusedOrNewSourceAndConnection.getConnection().getDescription(); - retryState.attach(AttachmentKeys.maxWireVersion(), connectionDescription.getMaxWireVersion(), true); + retryControl.attach(AttachmentKeys.maxWireVersion(), connectionDescription.getMaxWireVersion(), true); batch.set(batch.getNullable() != null ? batch.get() : createFirstBatch(connectionDescription, reusedOrNewSourceAndConnection.getOperationContext(), effectiveWriteConcern)); - onBatch(batch.get(), retryState); + onBatch(batch.get(), retryControl); executeBatchAsync(batch.get(), reusedOrNewSourceAndConnection, effectiveWriteConcern, executeBatchCallback); }).>thenSupply(createNextBatchCallback -> { createNextBatchCallback.complete(new BatchWithSourceAndConnection<>(batch.get().getNextBatch(), reusedOrNewSourceAndConnection)); @@ -344,7 +344,7 @@ private void executeBatchReusingConnectionAsync( // Adding the `RetryableWriteError` label here is unnecessary at this point: // applications cannot use it for implementing retries, and it is not even part of the public driver API. // Unfortunately, certain unified tests incorrectly rely on this label to verify retries, resulting in this redundant code. - addRetryableLabelOrGetWriteAttemptFailureNotToBeRetried(retryState, e); + addRetryableLabelOrGetWriteAttemptFailureNotToBeRetried(retryControl, e); } onErrorCallback.completeExceptionally(e); }).finish(c); @@ -356,14 +356,14 @@ private SourceAndConnection reuseOrSelectServerAndCheckoutConnectionIfClosed( final WriteConcern effectiveWriteConcern, final WriteBinding binding, final OperationContext operationContext, - final RetryState retryState) { + final RetryControl retryControl) { if (sourceAndConnection == null || sourceAndConnection.isClosed()) { SourceAndConnection newSourceAndConnection = selectServerAndCheckoutConnection(binding, operationContext); try { onNewConnection( newSourceAndConnection.getConnection().getDescription(), newSourceAndConnection.getOperationContext().getSessionContext(), - effectiveWriteConcern, retryState); + effectiveWriteConcern, retryControl); } catch (Throwable e) { newSourceAndConnection.close(); throw e; @@ -379,7 +379,7 @@ private void reuseOrSelectServerAndCheckoutConnectionIfClosedAsync( final WriteConcern effectiveWriteConcern, final AsyncWriteBinding binding, final OperationContext operationContext, - final RetryState retryState, + final RetryControl retryControl, final SingleResultCallback callback) { beginAsync().thenSupply(c -> { if (sourceAndConnection == null || sourceAndConnection.isClosed()) { @@ -390,7 +390,7 @@ private void reuseOrSelectServerAndCheckoutConnectionIfClosedAsync( onNewConnection( newSourceAndConnection.getConnection().getDescription(), newSourceAndConnection.getOperationContext().getSessionContext(), - effectiveWriteConcern, retryState); + effectiveWriteConcern, retryControl); } catch (Throwable e) { newSourceAndConnection.close(); throw e; @@ -433,10 +433,10 @@ private void onNewConnection( final ConnectionDescription connectionDescription, final SessionContext sessionContext, final WriteConcern effectiveWriteConcern, - final RetryState retryState) { + final RetryControl retryControl) { boolean effectiveRetryWrites = isRetryableWrite( retryWrites, effectiveWriteConcern, connectionDescription, sessionContext); - retryState.breakAndThrowIfRetryAnd(() -> !effectiveRetryWrites); + retryControl.breakAndThrowIfRetryAnd(() -> !effectiveRetryWrites); validateWriteRequests(connectionDescription, bypassDocumentValidation, writeRequests, effectiveWriteConcern); } @@ -450,10 +450,10 @@ private BulkWriteBatch createFirstBatch( writeRequests, operationContext, comment, variables); } - private void onBatch(final BulkWriteBatch batch, final RetryState retryState) { + private void onBatch(final BulkWriteBatch batch, final RetryControl retryControl) { commandName = batch.getCommand().getFirstKey(); String commandDescriptionToCapture = commandName; - retryState.attach(AttachmentKeys.retryableWriteCommandFlag(), batch.getRetryWrites(), false) + retryControl.attach(AttachmentKeys.retryableWriteCommandFlag(), batch.getRetryWrites(), false) .attach(AttachmentKeys.commandDescriptionSupplier(), () -> commandDescriptionToCapture, false); } diff --git a/driver-core/src/main/com/mongodb/internal/operation/SyncOperationHelper.java b/driver-core/src/main/com/mongodb/internal/operation/SyncOperationHelper.java index edcd266ff8e..e6368e0f4a4 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/SyncOperationHelper.java +++ b/driver-core/src/main/com/mongodb/internal/operation/SyncOperationHelper.java @@ -21,7 +21,7 @@ import com.mongodb.client.cursor.TimeoutMode; import com.mongodb.internal.TimeoutContext; import com.mongodb.internal.VisibleForTesting; -import com.mongodb.internal.async.function.RetryState; +import com.mongodb.internal.async.function.RetryControl; import com.mongodb.internal.async.function.RetryingSyncSupplier; import com.mongodb.internal.binding.ConnectionSource; import com.mongodb.internal.binding.ReadBinding; @@ -201,12 +201,12 @@ static T executeRetryableRead( final Decoder decoder, final CommandReadTransformer transformer, final boolean retryReads) { - RetryState retryState = CommandOperationHelper.initialRetryState(retryReads, operationContext.getTimeoutContext()); + RetryControl retryControl = CommandOperationHelper.initialRetryState(retryReads, operationContext.getTimeoutContext()); - Supplier read = decorateReadWithRetries(retryState, operationContext, () -> + Supplier read = decorateReadWithRetries(retryControl, operationContext, () -> withSourceAndConnection(readConnectionSourceSupplier, false, operationContext, (source, connection, operationContextWithMinRtt) -> { - retryState.breakAndThrowIfRetryAnd(() -> !canRetryRead(operationContextWithMinRtt)); - return createReadCommandAndExecute(retryState, operationContextWithMinRtt, source, database, + retryControl.breakAndThrowIfRetryAnd(() -> !canRetryRead(operationContextWithMinRtt)); + return createReadCommandAndExecute(retryControl, operationContextWithMinRtt, source, database, commandCreator, decoder, transformer, connection); }) ); @@ -259,9 +259,9 @@ static R executeRetryableWrite( final CommandCreator commandCreator, final CommandWriteTransformer transformer, final com.mongodb.Function retryCommandModifier) { - RetryState retryState = CommandOperationHelper.initialRetryState(true, operationContext.getTimeoutContext()); - Supplier retryingWrite = decorateWriteWithRetries(retryState, operationContext, () -> { - boolean firstAttempt = retryState.isFirstAttempt(); + RetryControl retryControl = CommandOperationHelper.initialRetryState(true, operationContext.getTimeoutContext()); + Supplier retryingWrite = decorateWriteWithRetries(retryControl, operationContext, () -> { + boolean firstAttempt = retryControl.isFirstAttempt(); SessionContext sessionContext = operationContext.getSessionContext(); if (!firstAttempt && sessionContext.hasActiveTransaction()) { sessionContext.clearTransactionContext(); @@ -269,15 +269,15 @@ static R executeRetryableWrite( return withSourceAndConnection(binding::getWriteConnectionSource, true, operationContext, (source, connection, operationContextWithMinRtt) -> { int maxWireVersion = connection.getDescription().getMaxWireVersion(); try { - retryState.breakAndThrowIfRetryAnd(() -> !canRetryWrite(connection.getDescription())); - BsonDocument command = retryState.attachment(AttachmentKeys.command()) + retryControl.breakAndThrowIfRetryAnd(() -> !canRetryWrite(connection.getDescription())); + BsonDocument command = retryControl.attachment(AttachmentKeys.command()) .map(previousAttemptCommand -> { assertFalse(firstAttempt); return retryCommandModifier.apply(previousAttemptCommand); }).orElseGet(() -> commandCreator.create(operationContextWithMinRtt, source.getServerDescription(), connection.getDescription())); // attach `maxWireVersion`, `retryableWriteCommandFlag` ASAP because they are used to check whether we should retry - retryState.attach(AttachmentKeys.maxWireVersion(), maxWireVersion, true) + retryControl.attach(AttachmentKeys.maxWireVersion(), maxWireVersion, true) .attach(AttachmentKeys.retryableWriteCommandFlag(), isRetryableWriteCommand(command), true) .attach(AttachmentKeys.commandDescriptionSupplier(), command::getFirstKey, false) .attach(AttachmentKeys.command(), command, false); @@ -301,7 +301,7 @@ static R executeRetryableWrite( @Nullable static T createReadCommandAndExecute( - final RetryState retryState, + final RetryControl retryControl, final OperationContext operationContext, final ConnectionSource source, final String database, @@ -311,7 +311,7 @@ static T createReadCommandAndExecute( final Connection connection) { BsonDocument command = commandCreator.create(operationContext, source.getServerDescription(), connection.getDescription()); - retryState.attach(AttachmentKeys.commandDescriptionSupplier(), command::getFirstKey, false); + retryControl.attach(AttachmentKeys.commandDescriptionSupplier(), command::getFirstKey, false); D result = assertNotNull(connection.command(database, command, NoOpFieldNameValidator.INSTANCE, source.getReadPreference(), decoder, operationContext)); @@ -320,20 +320,20 @@ static T createReadCommandAndExecute( } - static Supplier decorateWriteWithRetries(final RetryState retryState, + static Supplier decorateWriteWithRetries(final RetryControl retryControl, final OperationContext operationContext, final Supplier writeFunction) { - return new RetryingSyncSupplier<>(retryState, onRetryableWriteAttemptFailure(operationContext.getServerDeprioritization()), + return new RetryingSyncSupplier<>(retryControl, onRetryableWriteAttemptFailure(operationContext.getServerDeprioritization()), CommandOperationHelper::loggingShouldAttemptToRetryWriteAndAddRetryableLabel, () -> { - logRetryCommand(retryState, operationContext); + logRetryCommand(retryControl, operationContext); return writeFunction.get(); }); } - static Supplier decorateReadWithRetries(final RetryState retryState, final OperationContext operationContext, + static Supplier decorateReadWithRetries(final RetryControl retryControl, final OperationContext operationContext, final Supplier readFunction) { - return new RetryingSyncSupplier<>(retryState, onRetryableReadAttemptFailure(operationContext.getServerDeprioritization()), + return new RetryingSyncSupplier<>(retryControl, onRetryableReadAttemptFailure(operationContext.getServerDeprioritization()), CommandOperationHelper::loggingShouldAttemptToRetryRead, () -> { - logRetryCommand(retryState, operationContext); + logRetryCommand(retryControl, operationContext); return readFunction.get(); }); } diff --git a/driver-core/src/test/unit/com/mongodb/internal/async/function/RetryStateTest.java b/driver-core/src/test/unit/com/mongodb/internal/async/function/RetryControlTest.java similarity index 65% rename from driver-core/src/test/unit/com/mongodb/internal/async/function/RetryStateTest.java rename to driver-core/src/test/unit/com/mongodb/internal/async/function/RetryControlTest.java index eec5763c775..d5a450ac26b 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/async/function/RetryStateTest.java +++ b/driver-core/src/test/unit/com/mongodb/internal/async/function/RetryControlTest.java @@ -44,144 +44,144 @@ import static org.junit.jupiter.api.Named.named; import static org.junit.jupiter.params.provider.Arguments.arguments; -final class RetryStateTest { +final class RetryControlTest { private static final String EXPECTED_TIMEOUT_MESSAGE = "Retry attempt exceeded the timeout limit."; private static Stream atMostTwoRetriesAndUnlimitedRetries() { return Stream.of( - arguments(named("at most two retries", new RetryState(2))), - arguments(named("unlimited retries", new RetryState()))); + arguments(named("at most two retries", new RetryControl(2))), + arguments(named("unlimited retries", new RetryControl()))); } private static Stream noRetries() { return Stream.of( - arguments(named("no retries", new RetryState(0)))); + arguments(named("no retries", new RetryControl(0)))); } @Test void unlimitedAttemptsAndAdvance() { - final RetryState retryState = new RetryState(); + final RetryControl retryControl = new RetryControl(); RuntimeException attemptException = new RuntimeException(); assertAll( - () -> assertTrue(retryState.isFirstAttempt()), - () -> assertEquals(0, retryState.attempt()) + () -> assertTrue(retryControl.isFirstAttempt()), + () -> assertEquals(0, retryControl.attempt()) ); - retryState.advanceOrThrow(attemptException, (e1, e2) -> e2, (rs, e) -> true); + retryControl.advanceOrThrow(attemptException, (e1, e2) -> e2, (rs, e) -> true); assertAll( - () -> assertFalse(retryState.isFirstAttempt()), - () -> assertEquals(1, retryState.attempt()) + () -> assertFalse(retryControl.isFirstAttempt()), + () -> assertEquals(1, retryControl.attempt()) ); } @Test void limitedAttemptsAndAdvance() { - RetryState retryState = new RetryState(0); + RetryControl retryControl = new RetryControl(0); RuntimeException attemptException = new RuntimeException(); assertAll( - () -> assertTrue(retryState.isFirstAttempt()), - () -> assertEquals(0, retryState.attempt()), - () -> assertAdvanceOrThrowThrows(attemptException, retryState, attemptException), + () -> assertTrue(retryControl.isFirstAttempt()), + () -> assertEquals(0, retryControl.attempt()), + () -> assertAdvanceOrThrowThrows(attemptException, retryControl, attemptException), // when there is only one attempt, it is both the first and the last one - () -> assertTrue(retryState.isFirstAttempt()), - () -> assertEquals(0, retryState.attempt()) + () -> assertTrue(retryControl.isFirstAttempt()), + () -> assertEquals(0, retryControl.attempt()) ); } @ParameterizedTest @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void breakAndThrowIfRetryAndFirstAttempt(final RetryState retryState) { - retryState.breakAndThrowIfRetryAnd(Assertions::fail); - assertAdvanceOrThrowDoesNotThrow(retryState, new RuntimeException()); + void breakAndThrowIfRetryAndFirstAttempt(final RetryControl retryControl) { + retryControl.breakAndThrowIfRetryAnd(Assertions::fail); + assertAdvanceOrThrowDoesNotThrow(retryControl, new RuntimeException()); } @ParameterizedTest @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void breakAndThrowIfRetryAndFalse(final RetryState retryState) { - advance(retryState); - retryState.breakAndThrowIfRetryAnd(() -> false); - assertAdvanceOrThrowDoesNotThrow(retryState, new RuntimeException()); + void breakAndThrowIfRetryAndFalse(final RetryControl retryControl) { + advance(retryControl); + retryControl.breakAndThrowIfRetryAnd(() -> false); + assertAdvanceOrThrowDoesNotThrow(retryControl, new RuntimeException()); } @ParameterizedTest @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void breakAndThrowIfRetryAndTrue(final RetryState retryState) { - advance(retryState); - assertThrows(RuntimeException.class, () -> retryState.breakAndThrowIfRetryAnd(() -> true)); + void breakAndThrowIfRetryAndTrue(final RetryControl retryControl) { + advance(retryControl); + assertThrows(RuntimeException.class, () -> retryControl.breakAndThrowIfRetryAnd(() -> true)); RuntimeException attemptException = new RuntimeException(); - assertAdvanceOrThrowThrows(attemptException, retryState, attemptException); + assertAdvanceOrThrowThrows(attemptException, retryControl, attemptException); } @ParameterizedTest @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void breakAndThrowIfRetryIfPredicateThrows(final RetryState retryState) { - advance(retryState); + void breakAndThrowIfRetryIfPredicateThrows(final RetryControl retryControl) { + advance(retryControl); RuntimeException exception = new RuntimeException(); assertSame( exception, - assertThrows(exception.getClass(), () -> retryState.breakAndThrowIfRetryAnd(() -> { + assertThrows(exception.getClass(), () -> retryControl.breakAndThrowIfRetryAnd(() -> { throw exception; }))); - assertAdvanceOrThrowDoesNotThrow(retryState, exception); + assertAdvanceOrThrowDoesNotThrow(retryControl, exception); } @ParameterizedTest @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void breakAndCompleteIfRetryAndFirstAttempt(final RetryState retryState) { + void breakAndCompleteIfRetryAndFirstAttempt(final RetryControl retryControl) { SupplyingCallback callback = new SupplyingCallback<>(); - assertFalse(retryState.breakAndCompleteIfRetryAnd(Assertions::fail, callback)); + assertFalse(retryControl.breakAndCompleteIfRetryAnd(Assertions::fail, callback)); assertFalse(callback.completed()); - assertAdvanceOrThrowDoesNotThrow(retryState, new RuntimeException()); + assertAdvanceOrThrowDoesNotThrow(retryControl, new RuntimeException()); } @ParameterizedTest @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void breakAndCompleteIfRetryAndFalse(final RetryState retryState) { - advance(retryState); + void breakAndCompleteIfRetryAndFalse(final RetryControl retryControl) { + advance(retryControl); SupplyingCallback callback = new SupplyingCallback<>(); - assertFalse(retryState.breakAndCompleteIfRetryAnd(() -> false, callback)); + assertFalse(retryControl.breakAndCompleteIfRetryAnd(() -> false, callback)); assertFalse(callback.completed()); - assertAdvanceOrThrowDoesNotThrow(retryState, new RuntimeException()); + assertAdvanceOrThrowDoesNotThrow(retryControl, new RuntimeException()); } @ParameterizedTest @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void breakAndCompleteIfRetryAndTrue(final RetryState retryState) { - advance(retryState); + void breakAndCompleteIfRetryAndTrue(final RetryControl retryControl) { + advance(retryControl); SupplyingCallback callback = new SupplyingCallback<>(); - assertTrue(retryState.breakAndCompleteIfRetryAnd(() -> true, callback)); + assertTrue(retryControl.breakAndCompleteIfRetryAnd(() -> true, callback)); assertThrows(RuntimeException.class, callback::get); RuntimeException attemptException = new RuntimeException(); - assertAdvanceOrThrowThrows(attemptException, retryState, attemptException); + assertAdvanceOrThrowThrows(attemptException, retryControl, attemptException); } @ParameterizedTest @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void breakAndCompleteIfRetryAndPredicateThrows(final RetryState retryState) { - advance(retryState); + void breakAndCompleteIfRetryAndPredicateThrows(final RetryControl retryControl) { + advance(retryControl); Error exception = new Error(); SupplyingCallback callback = new SupplyingCallback<>(); - assertTrue(retryState.breakAndCompleteIfRetryAnd(() -> { + assertTrue(retryControl.breakAndCompleteIfRetryAnd(() -> { throw exception; }, callback)); assertSame( exception, assertThrows(exception.getClass(), callback::get)); - assertAdvanceOrThrowDoesNotThrow(retryState, exception); + assertAdvanceOrThrowDoesNotThrow(retryControl, exception); } @ParameterizedTest @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void advanceOrThrowPredicateFalse(final RetryState retryState) { + void advanceOrThrowPredicateFalse(final RetryControl retryControl) { RuntimeException attemptException = new RuntimeException(); - assertAdvanceOrThrowThrows(attemptException, retryState, attemptException, (rs, e) -> false); + assertAdvanceOrThrowThrows(attemptException, retryControl, attemptException, (rs, e) -> false); } @ParameterizedTest @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) @DisplayName("should rethrow detected timeout exception") - void advanceReThrowDetectedTimeoutException(final RetryState retryState) { + void advanceReThrowDetectedTimeoutException(final RetryControl retryControl) { MongoOperationTimeoutException expectedTimeoutException = TimeoutContext.createMongoTimeoutException("Server selection failed"); - assertAdvanceOrThrowThrows(expectedTimeoutException, retryState, expectedTimeoutException, + assertAdvanceOrThrowThrows(expectedTimeoutException, retryControl, expectedTimeoutException, (e1, e2) -> expectedTimeoutException, (rs, e) -> false); } @@ -189,16 +189,16 @@ void advanceReThrowDetectedTimeoutException(final RetryState retryState) { @Test @DisplayName("should throw timeout exception from retry, when transformer swallows original timeout exception") void advanceThrowTimeoutExceptionWhenTransformerSwallowOriginalTimeoutException() { - RetryState retryState = new RetryState(); + RetryControl retryControl = new RetryControl(); RuntimeException previousAttemptException = new RuntimeException(); MongoOperationTimeoutException latestAttemptException = TimeoutContext.createMongoTimeoutException("Server selection failed"); - retryState.advanceOrThrow(previousAttemptException, + retryControl.advanceOrThrow(previousAttemptException, (e1, e2) -> previousAttemptException, (rs, e) -> true); MongoOperationTimeoutException actualTimeoutException = - assertThrows(MongoOperationTimeoutException.class, () -> retryState.advanceOrThrow(latestAttemptException, + assertThrows(MongoOperationTimeoutException.class, () -> retryControl.advanceOrThrow(latestAttemptException, (e1, e2) -> previousAttemptException, (rs, e) -> false)); @@ -212,33 +212,33 @@ void advanceThrowTimeoutExceptionWhenTransformerSwallowOriginalTimeoutException( @Test @DisplayName("should throw original timeout exception from retry, when transformer returns original timeout exception") void advanceThrowOriginalTimeoutExceptionWhenTransformerReturnsOriginalTimeoutException() { - RetryState retryState = new RetryState(); + RetryControl retryControl = new RetryControl(); RuntimeException previousAttemptException = new RuntimeException(); MongoOperationTimeoutException expectedTimeoutException = TimeoutContext .createMongoTimeoutException("Server selection failed"); - retryState.advanceOrThrow(previousAttemptException, + retryControl.advanceOrThrow(previousAttemptException, (e1, e2) -> previousAttemptException, (rs, e) -> true); - assertAdvanceOrThrowThrows(expectedTimeoutException, retryState, expectedTimeoutException, + assertAdvanceOrThrowThrows(expectedTimeoutException, retryControl, expectedTimeoutException, (e1, e2) -> expectedTimeoutException, (rs, e) -> false); } @Test void advanceOrThrowPredicateTrueAndLastAttempt() { - RetryState retryState = new RetryState(0); + RetryControl retryControl = new RetryControl(0); Error attemptException = new Error(); - assertAdvanceOrThrowThrows(attemptException, retryState, attemptException); + assertAdvanceOrThrowThrows(attemptException, retryControl, attemptException); } @ParameterizedTest @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void advanceOrThrowPredicateThrowsAfterFirstAttempt(final RetryState retryState) { + void advanceOrThrowPredicateThrowsAfterFirstAttempt(final RetryControl retryControl) { RuntimeException predicateException = new RuntimeException(); RuntimeException attemptException = new RuntimeException(); - assertAdvanceOrThrowThrows(predicateException, retryState, attemptException, + assertAdvanceOrThrowThrows(predicateException, retryControl, attemptException, (e1, e2) -> e2, (rs, e) -> { assertTrue(rs.isFirstAttempt()); @@ -249,11 +249,11 @@ void advanceOrThrowPredicateThrowsAfterFirstAttempt(final RetryState retryState) @Test void advanceOrThrowPredicateThrowsTimeoutAfterFirstAttempt() { - RetryState retryState = new RetryState(); + RetryControl retryControl = new RetryControl(); RuntimeException predicateException = new RuntimeException(); RuntimeException attemptException = new MongoOperationTimeoutException(EXPECTED_TIMEOUT_MESSAGE); MongoOperationTimeoutException mongoOperationTimeoutException = assertThrows(MongoOperationTimeoutException.class, - () -> retryState.advanceOrThrow(attemptException, (e1, e2) -> e2, (rs, e) -> { + () -> retryControl.advanceOrThrow(attemptException, (e1, e2) -> e2, (rs, e) -> { assertTrue(rs.isFirstAttempt()); assertSame(attemptException, e); throw predicateException; @@ -265,12 +265,12 @@ void advanceOrThrowPredicateThrowsTimeoutAfterFirstAttempt() { @ParameterizedTest @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void advanceOrThrowPredicateThrows(final RetryState retryState) { + void advanceOrThrowPredicateThrows(final RetryControl retryControl) { RuntimeException firstAttemptException = new RuntimeException(); - retryState.advanceOrThrow(firstAttemptException, (e1, e2) -> e2, (rs, e) -> true); + retryControl.advanceOrThrow(firstAttemptException, (e1, e2) -> e2, (rs, e) -> true); RuntimeException secondAttemptException = new RuntimeException(); RuntimeException predicateException = new RuntimeException(); - assertAdvanceOrThrowThrows(predicateException, retryState, secondAttemptException, + assertAdvanceOrThrowThrows(predicateException, retryControl, secondAttemptException, (e1, e2) -> e2, (rs, e) -> { assertEquals(1, rs.attempt()); @@ -281,9 +281,9 @@ void advanceOrThrowPredicateThrows(final RetryState retryState) { @ParameterizedTest @MethodSource({"noRetries", "atMostTwoRetriesAndUnlimitedRetries"}) - void advanceOrThrowTransformerThrowsAfterFirstAttempt(final RetryState retryState) { + void advanceOrThrowTransformerThrowsAfterFirstAttempt(final RetryControl retryControl) { RuntimeException transformerException = new RuntimeException(); - assertAdvanceOrThrowThrows(transformerException, retryState, new AssertionError(), + assertAdvanceOrThrowThrows(transformerException, retryControl, new AssertionError(), (e1, e2) -> { throw transformerException; }, @@ -292,11 +292,11 @@ void advanceOrThrowTransformerThrowsAfterFirstAttempt(final RetryState retryStat @ParameterizedTest @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void advanceOrThrowTransformerThrows(final RetryState retryState) throws Throwable { + void advanceOrThrowTransformerThrows(final RetryControl retryControl) throws Throwable { Error firstAttemptException = new Error(); - retryState.advanceOrThrow(firstAttemptException, (e1, e2) -> e2, (rs, e) -> true); + retryControl.advanceOrThrow(firstAttemptException, (e1, e2) -> e2, (rs, e) -> true); RuntimeException transformerException = new RuntimeException(); - assertAdvanceOrThrowThrows(transformerException, retryState, new AssertionError(), + assertAdvanceOrThrowThrows(transformerException, retryControl, new AssertionError(), (e1, e2) -> { throw transformerException; }, @@ -305,10 +305,10 @@ void advanceOrThrowTransformerThrows(final RetryState retryState) throws Throwab @ParameterizedTest @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void advanceOrThrowTransformAfterFirstAttempt(final RetryState retryState) { + void advanceOrThrowTransformAfterFirstAttempt(final RetryControl retryControl) { RuntimeException attemptException = new RuntimeException(); RuntimeException transformerResult = new RuntimeException(); - assertAdvanceOrThrowThrows(transformerResult, retryState, attemptException, + assertAdvanceOrThrowThrows(transformerResult, retryControl, attemptException, (e1, e2) -> { assertNull(e1); assertSame(attemptException, e2); @@ -322,13 +322,13 @@ void advanceOrThrowTransformAfterFirstAttempt(final RetryState retryState) { @Test void advanceOrThrowTransformThrowsTimeoutExceptionAfterFirstAttempt() { - RetryState retryState = new RetryState(); + RetryControl retryControl = new RetryControl(); RuntimeException attemptException = new MongoOperationTimeoutException(EXPECTED_TIMEOUT_MESSAGE); RuntimeException transformerResult = new RuntimeException(); MongoOperationTimeoutException mongoOperationTimeoutException = - assertThrows(MongoOperationTimeoutException.class, () -> retryState.advanceOrThrow(attemptException, + assertThrows(MongoOperationTimeoutException.class, () -> retryControl.advanceOrThrow(attemptException, (e1, e2) -> { assertNull(e1); assertSame(attemptException, e2); @@ -345,12 +345,12 @@ void advanceOrThrowTransformThrowsTimeoutExceptionAfterFirstAttempt() { @ParameterizedTest @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void advanceOrThrowTransform(final RetryState retryState) { + void advanceOrThrowTransform(final RetryControl retryControl) { RuntimeException firstAttemptException = new RuntimeException(); - retryState.advanceOrThrow(firstAttemptException, (e1, e2) -> e2, (rs, e) -> true); + retryControl.advanceOrThrow(firstAttemptException, (e1, e2) -> e2, (rs, e) -> true); RuntimeException secondAttemptException = new RuntimeException(); RuntimeException transformerResult = new RuntimeException(); - assertAdvanceOrThrowThrows(transformerResult, retryState, secondAttemptException, + assertAdvanceOrThrowThrows(transformerResult, retryControl, secondAttemptException, (e1, e2) -> { assertSame(firstAttemptException, e1); assertSame(secondAttemptException, e2); @@ -364,59 +364,59 @@ void advanceOrThrowTransform(final RetryState retryState) { @ParameterizedTest @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void attachAndAttachment(final RetryState retryState) { + void attachAndAttachment(final RetryControl retryControl) { AttachmentKey attachmentKey = AttachmentKeys.maxWireVersion(); int attachmentValue = 1; - assertFalse(retryState.attachment(attachmentKey).isPresent()); - retryState.attach(attachmentKey, attachmentValue, false); - assertEquals(attachmentValue, retryState.attachment(attachmentKey).get()); - advance(retryState); - assertEquals(attachmentValue, retryState.attachment(attachmentKey).get()); - retryState.attach(attachmentKey, attachmentValue, true); - assertEquals(attachmentValue, retryState.attachment(attachmentKey).get()); - advance(retryState); - assertFalse(retryState.attachment(attachmentKey).isPresent()); + assertFalse(retryControl.attachment(attachmentKey).isPresent()); + retryControl.attach(attachmentKey, attachmentValue, false); + assertEquals(attachmentValue, retryControl.attachment(attachmentKey).get()); + advance(retryControl); + assertEquals(attachmentValue, retryControl.attachment(attachmentKey).get()); + retryControl.attach(attachmentKey, attachmentValue, true); + assertEquals(attachmentValue, retryControl.attachment(attachmentKey).get()); + advance(retryControl); + assertFalse(retryControl.attachment(attachmentKey).isPresent()); } - private static void advance(final RetryState retryState) { - retryState.advanceOrThrow(new RuntimeException(), (e1, e2) -> e2, (rs, e) -> true); + private static void advance(final RetryControl retryControl) { + retryControl.advanceOrThrow(new RuntimeException(), (e1, e2) -> e2, (rs, e) -> true); } private static void assertAdvanceOrThrowDoesNotThrow( - final RetryState retryState, + final RetryControl retryControl, final Throwable attemptException) { - assertDoesNotThrow(() -> retryState.advanceOrThrow(attemptException, (e1, e2) -> e2, (rs, e) -> true)); + assertDoesNotThrow(() -> retryControl.advanceOrThrow(attemptException, (e1, e2) -> e2, (rs, e) -> true)); } private static void assertAdvanceOrThrowThrows( final Throwable expectedException, - final RetryState retryState, + final RetryControl retryControl, final Throwable attemptException) { assertAdvanceOrThrowThrows( com.mongodb.assertions.Assertions.assertNotNull(expectedException), - retryState, attemptException, (rs, e) -> true); + retryControl, attemptException, (rs, e) -> true); } private static void assertAdvanceOrThrowThrows( final Throwable expectedException, - final RetryState retryState, + final RetryControl retryControl, final Throwable attemptException, - final BiPredicate retryPredicate) { + final BiPredicate retryPredicate) { assertAdvanceOrThrowThrows( com.mongodb.assertions.Assertions.assertNotNull(expectedException), - retryState, attemptException, (e1, e2) -> e2, retryPredicate); + retryControl, attemptException, (e1, e2) -> e2, retryPredicate); } private static void assertAdvanceOrThrowThrows( final Throwable expectedException, - final RetryState retryState, + final RetryControl retryControl, final Throwable attemptException, final BinaryOperator onAttemptFailureOperator, - final BiPredicate retryPredicate) { + final BiPredicate retryPredicate) { com.mongodb.assertions.Assertions.assertNotNull(expectedException); assertSame( expectedException, assertThrows(expectedException.getClass(), () -> - retryState.advanceOrThrow(attemptException, onAttemptFailureOperator, retryPredicate))); + retryControl.advanceOrThrow(attemptException, onAttemptFailureOperator, retryPredicate))); } } From 69ad9b46012eb58c3901778ce3be5cdb301921c3 Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Wed, 13 May 2026 14:41:21 -0600 Subject: [PATCH 4/4] Introduce the `RetryPolicy` abstraction JAVA-6229 --- .../mongodb/internal/async/AsyncRunnable.java | 4 +- .../internal/async/SimpleRetryPolicy.java | 35 ++ .../internal/async/function/LoopControl.java | 133 ++--- .../internal/async/function/RetryContext.java | 54 ++ .../internal/async/function/RetryControl.java | 403 +++++---------- .../internal/async/function/RetryPolicy.java | 99 ++++ .../RetryingAsyncCallbackSupplier.java | 60 +-- .../async/function/RetryingSyncSupplier.java | 33 +- .../internal/connection/OperationContext.java | 8 +- .../operation/AbortTransactionOperation.java | 2 +- .../operation/AsyncOperationHelper.java | 180 +++---- .../operation/BaseFindAndModifyOperation.java | 9 +- .../internal/operation/BulkWriteBatch.java | 17 +- .../operation/ClientBulkWriteOperation.java | 169 +++--- .../operation/CommandOperationHelper.java | 170 +------ .../operation/CommitTransactionOperation.java | 2 +- .../internal/operation/FindOperation.java | 27 +- .../operation/ListCollectionsOperation.java | 22 +- .../operation/ListIndexesOperation.java | 22 +- .../operation/MixedBulkWriteOperation.java | 80 ++- .../internal/operation/OperationHelper.java | 13 +- .../internal/operation/SpecRetryPolicy.java | 382 ++++++++++++++ .../operation/SyncOperationHelper.java | 103 ++-- .../operation/TransactionOperation.java | 3 +- .../operation/WriteConcernHelper.java | 4 +- .../operation/retry/AttachmentKeys.java | 108 ---- .../operation/retry/package-info.java | 25 - ...ixedBulkWriteOperationSpecification.groovy | 4 +- .../async/function/LoopControlTest.java | 26 +- .../async/function/RetryControlTest.java | 479 +++++------------- .../RetryingAsyncCallbackSupplierTest.java | 102 ++++ .../function/RetryingSyncSupplierTest.java | 114 +++++ .../AsyncOperationHelperSpecification.groovy | 2 +- .../BulkWriteBatchSpecification.groovy | 2 +- .../OperationHelperSpecification.groovy | 12 +- .../SyncOperationHelperSpecification.groovy | 4 +- .../com/mongodb/client/CrudProseTest.java | 90 +++- 37 files changed, 1477 insertions(+), 1525 deletions(-) create mode 100644 driver-core/src/main/com/mongodb/internal/async/SimpleRetryPolicy.java create mode 100644 driver-core/src/main/com/mongodb/internal/async/function/RetryContext.java create mode 100644 driver-core/src/main/com/mongodb/internal/async/function/RetryPolicy.java create mode 100644 driver-core/src/main/com/mongodb/internal/operation/SpecRetryPolicy.java delete mode 100644 driver-core/src/main/com/mongodb/internal/operation/retry/AttachmentKeys.java delete mode 100644 driver-core/src/main/com/mongodb/internal/operation/retry/package-info.java create mode 100644 driver-core/src/test/unit/com/mongodb/internal/async/function/RetryingAsyncCallbackSupplierTest.java create mode 100644 driver-core/src/test/unit/com/mongodb/internal/async/function/RetryingSyncSupplierTest.java diff --git a/driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java b/driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java index 760d8053df5..f5fb6358b87 100644 --- a/driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java +++ b/driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java @@ -233,9 +233,7 @@ default AsyncSupplier thenSupply(final AsyncSupplier supplier) { default AsyncRunnable thenRunRetryingWhile(final AsyncRunnable runnable, final Predicate shouldRetry) { return thenRun(callback -> { new RetryingAsyncCallbackSupplier( - new RetryControl(), - (previouslyChosenFailure, lastAttemptFailure) -> lastAttemptFailure, - (rs, lastAttemptFailure) -> shouldRetry.test(lastAttemptFailure), + new RetryControl<>(new SimpleRetryPolicy(shouldRetry)), // `finish` is required here instead of `unsafeFinish` // because only `finish` meets the contract of // `AsyncCallbackSupplier.get`, which we implement here diff --git a/driver-core/src/main/com/mongodb/internal/async/SimpleRetryPolicy.java b/driver-core/src/main/com/mongodb/internal/async/SimpleRetryPolicy.java new file mode 100644 index 00000000000..ade2a70c119 --- /dev/null +++ b/driver-core/src/main/com/mongodb/internal/async/SimpleRetryPolicy.java @@ -0,0 +1,35 @@ +/* + * 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.async; + +import com.mongodb.internal.async.function.RetryContext; +import com.mongodb.internal.async.function.RetryPolicy; +import com.mongodb.internal.async.function.RetryPolicy.Decision.RetryAttemptInfo; + +import java.util.function.Predicate; + +final class SimpleRetryPolicy implements RetryPolicy { + private final Predicate shouldRetry; + + SimpleRetryPolicy(final Predicate shouldRetry) { + this.shouldRetry = shouldRetry; + } + + @Override + public Decision onAttemptFailure(final RetryContext retryContext, final Throwable attemptFailedResult) { + return new Decision(attemptFailedResult, shouldRetry.test(attemptFailedResult) ? new RetryAttemptInfo() : null); + } +} diff --git a/driver-core/src/main/com/mongodb/internal/async/function/LoopControl.java b/driver-core/src/main/com/mongodb/internal/async/function/LoopControl.java index d385c1630ba..5b89ce55434 100644 --- a/driver-core/src/main/com/mongodb/internal/async/function/LoopControl.java +++ b/driver-core/src/main/com/mongodb/internal/async/function/LoopControl.java @@ -15,26 +15,20 @@ */ package com.mongodb.internal.async.function; -import com.mongodb.annotations.Immutable; import com.mongodb.annotations.NotThreadSafe; +import com.mongodb.internal.async.MutableValue; import com.mongodb.internal.async.SingleResultCallback; -import com.mongodb.lang.Nullable; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; import java.util.function.Supplier; import static com.mongodb.assertions.Assertions.assertFalse; -import static com.mongodb.assertions.Assertions.assertNotNull; /** - * Represents both the state associated with a loop and a handle that can be used to affect looping, e.g., + * A stateful controller of a loop that can be used to control it, for example, * to {@linkplain #breakAndCompleteIf(Supplier, SingleResultCallback) break} it. - * {@linkplain #attachment(AttachmentKey) Attachments} may be used by the associated loop - * to preserve a state between iterations. - * - *

    This class is not part of the public API and may be removed or changed at any time

    + * {@linkplain MutableValue} may be used by the loop to preserve state between iterations. + *

    + * This class is not part of the public API and may be removed or changed at any time. * * @see AsyncCallbackLoop */ @@ -42,25 +36,23 @@ public final class LoopControl { private int iteration; private boolean lastIteration; - @Nullable - private Map, AttachmentValueContainer> attachments; public LoopControl() { iteration = 0; } /** - * Advances this {@link LoopControl} such that it represents the state of a new iteration. + * Advances this {@link LoopControl} such that it represents the state of the immediate next iteration, if any. * Must not be called before the {@linkplain #isFirstIteration() first iteration}, must be called before each subsequent iteration. * - * @return {@code true} if the next iteration must be executed; {@code false} iff the loop was {@link #isLastIteration() broken}. + * @return {@code true} if another iteration must be executed; + * otherwise the loop was {@link #isLastIteration() broken} and {@code false} is returned. */ boolean advance() { if (lastIteration) { return false; } else { iteration++; - removeAutoRemovableAttachments(); return true; } } @@ -70,7 +62,7 @@ boolean advance() { * * @see #iteration() */ - public boolean isFirstIteration() { + boolean isFirstIteration() { return iteration == 0; } @@ -84,29 +76,39 @@ boolean isLastIteration() { /** * A 0-based iteration number. */ - public int iteration() { + int iteration() { return iteration; } /** - * This method emulates executing the - * {@code break} statement. Must not be called more than once per {@link LoopControl}. + * This method emulates executing the {@code break} statement + * in callback-based code. If {@code true} is returned, the caller must complete the current attempt. + *

    + * Must not be called after breaking the loop. * - * @param predicate {@code true} iff the associated loop needs to be broken. + * @param predicate {@code true} iff the loop needs to be broken. + *

      + *
    • + * If the {@code predicate} completes abruptly, this method completes the {@code callback} with the same exception but does not break the loop;
    • + *
    • + * if the {@code predicate} is {@code true}, then this method breaks the retry loop;
    • + *
    • + * if the {@code predicate} is {@code false}, then this method does nothing. + *
    * @return {@code true} iff the {@code callback} was completed, which happens iff any of the following is true: *
      - *
    • the {@code predicate} completed abruptly, in which case the exception thrown is relayed to the {@code callback};
    • - *
    • this method broke the associated loop.
    • + *
    • the {@code predicate} completed abruptly;
    • + *
    • this method broke the loop.
    • *
    - * If {@code true} is returned, the caller must complete the ongoing attempt. + * * @see #isLastIteration() */ public boolean breakAndCompleteIf(final Supplier predicate, final SingleResultCallback callback) { assertFalse(lastIteration); try { lastIteration = predicate.get(); - } catch (Throwable t) { - callback.onResult(null, t); + } catch (Throwable predicateException) { + callback.onResult(null, predicateException); return true; } if (lastIteration) { @@ -128,88 +130,11 @@ void markAsLastIteration() { lastIteration = true; } - /** - * The associated loop may use this method to preserve a state between iterations. - * - * @param autoRemove Specifies whether the attachment must be automatically removed before (in the happens-before order) the next - * {@linkplain #iteration() iteration} as if this removal were the very first action of the iteration. - * Note that there is no guarantee that the attachment is removed after the {@linkplain #isLastIteration() last iteration}. - * @return {@code this}. - * @see #attachment(AttachmentKey) - */ - public LoopControl attach(final AttachmentKey key, final V value, final boolean autoRemove) { - attachments().put(assertNotNull(key), new AttachmentValueContainer(assertNotNull(value), autoRemove)); - return this; - } - - /** - * @see #attach(AttachmentKey, Object, boolean) - */ - public Optional attachment(final AttachmentKey key) { - AttachmentValueContainer valueContainer = attachments().get(assertNotNull(key)); - @SuppressWarnings("unchecked") V value = valueContainer == null ? null : (V) valueContainer.value(); - return Optional.ofNullable(value); - } - - private Map, AttachmentValueContainer> attachments() { - if (attachments == null) { - attachments = new HashMap<>(); - } - return attachments; - } - - private void removeAutoRemovableAttachments() { - if (attachments == null) { - return; - } - attachments.entrySet().removeIf(entry -> entry.getValue().autoRemove()); - } - @Override public String toString() { return "LoopControl{" + "iteration=" + iteration - + ", attachments=" + attachments + + ", lastIteration=" + lastIteration + '}'; } - - /** - * A value-based - * identifier of an attachment. - * - * @param The type of the corresponding attachment value. - */ - @Immutable - // the type parameter V is of the essence even though it is not used in the interface itself - @SuppressWarnings("unused") - public interface AttachmentKey { - } - - private static final class AttachmentValueContainer { - @Nullable - private final Object value; - private final boolean autoRemove; - - AttachmentValueContainer(@Nullable final Object value, final boolean autoRemove) { - this.value = value; - this.autoRemove = autoRemove; - } - - @Nullable - Object value() { - return value; - } - - boolean autoRemove() { - return autoRemove; - } - - @Override - public String toString() { - return "AttachmentValueContainer{" - + "value=" + value - + ", autoRemove=" + autoRemove - + '}'; - } - } } diff --git a/driver-core/src/main/com/mongodb/internal/async/function/RetryContext.java b/driver-core/src/main/com/mongodb/internal/async/function/RetryContext.java new file mode 100644 index 00000000000..419224d4f80 --- /dev/null +++ b/driver-core/src/main/com/mongodb/internal/async/function/RetryContext.java @@ -0,0 +1,54 @@ +/* + * 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.async.function; + +import com.mongodb.annotations.NotThreadSafe; + +import java.util.Optional; +import java.util.function.Supplier; + +/** + * The part of {@link RetryControl} accessible to the methods of the {@link RetryPolicy} interface. + * It prevents, for example, the method {@link RetryPolicy#onAttemptFailure(RetryContext, Throwable)} from calling + * {@link RetryControl#breakAndThrowIfRetryAnd(Supplier)}, as that is forbidden. + * A non-overriding method of a {@link RetryPolicy} implementation is free to access the full {@link RetryControl} + * as opposed to {@link RetryContext}. + */ +@NotThreadSafe +public interface RetryContext { + /** + * Returns {@code true} iff the current attempt is the first one, i.e., no retry attempts have been made. + * + * @see #attempt() + */ + boolean isFirstAttempt(); + + /** + * A 0-based attempt number. + * + * @see #isFirstAttempt() + */ + int attempt(); + + /** + * Returns the exception that is currently deemed to be the prospective failed result of the retryable activity. + * Note that it is not necessary the failed result of the most recent failed attempt. + * Returns an {@linkplain Optional#isEmpty() empty} {@link Optional} iff called during the {@linkplain #isFirstAttempt() first attempt}. + * + * @see RetryPolicy.Decision#getProspectiveFailedResult() + */ + Optional getProspectiveFailedResult(); +} diff --git a/driver-core/src/main/com/mongodb/internal/async/function/RetryControl.java b/driver-core/src/main/com/mongodb/internal/async/function/RetryControl.java index e189bbde255..d192ebbe3ee 100644 --- a/driver-core/src/main/com/mongodb/internal/async/function/RetryControl.java +++ b/driver-core/src/main/com/mongodb/internal/async/function/RetryControl.java @@ -15,355 +15,220 @@ */ package com.mongodb.internal.async.function; -import com.mongodb.MongoOperationTimeoutException; import com.mongodb.annotations.NotThreadSafe; +import com.mongodb.assertions.Assertions; +import com.mongodb.internal.async.AsyncSupplier; +import com.mongodb.internal.async.MutableValue; import com.mongodb.internal.async.SingleResultCallback; -import com.mongodb.internal.async.function.LoopControl.AttachmentKey; -import com.mongodb.lang.NonNull; +import com.mongodb.internal.async.function.RetryPolicy.Decision; +import com.mongodb.internal.async.function.RetryPolicy.Decision.RetryAttemptInfo; import com.mongodb.lang.Nullable; import java.util.Optional; -import java.util.function.BiPredicate; -import java.util.function.BinaryOperator; import java.util.function.Supplier; import static com.mongodb.assertions.Assertions.assertFalse; import static com.mongodb.assertions.Assertions.assertNotNull; import static com.mongodb.assertions.Assertions.assertTrue; -import static com.mongodb.internal.TimeoutContext.createMongoTimeoutException; +import static com.mongodb.internal.async.AsyncRunnable.beginAsync; /** - * Represents both the state associated with a retryable activity and a handle that can be used to affect retrying, e.g., + * A stateful controller of a retryable activity that can be used to control it, for example, * to {@linkplain #breakAndThrowIfRetryAnd(Supplier) break} it. - * {@linkplain #attachment(AttachmentKey) Attachments} may be used by the associated retryable activity either - * to preserve a state between attempts. - * - *

    This class is not part of the public API and may be removed or changed at any time

    + * Either {@linkplain MutableValue} or an implementation of {@link RetryPolicy} may be used by the retryable activity + * to preserve state between attempts. + *

    + * This class is not part of the public API and may be removed or changed at any time. * * @see RetryingSyncSupplier * @see RetryingAsyncCallbackSupplier */ @NotThreadSafe -public final class RetryControl { - public static final int MAX_RETRIES = 1; - private static final int INFINITE_RETRIES = Integer.MAX_VALUE; - +public final class RetryControl

    implements RetryContext { private final LoopControl loopControl; - private final int attempts; + private boolean disabled; @Nullable - private Throwable previouslyChosenException; + private Decision mostRecentDecision; + private final P policy; - /** - * Creates a {@link RetryControl} that does not explicitly limit the number of attempts. - * Retrying still may be stopped because, for example, - * the failed result from the most recent attempt is {@link MongoOperationTimeoutException}. - */ - public RetryControl() { - this(INFINITE_RETRIES); + public RetryControl(final P policy) { + loopControl = new LoopControl(); + this.policy = policy; + disabled = false; + mostRecentDecision = null; } - /** - * @param retries A non-negative number of allowed retry attempts. - * {@value #INFINITE_RETRIES} is interpreted as {@linkplain #RetryControl() absence of explicit limit}. - */ - public RetryControl(final int retries) { - assertTrue(retries >= 0); - loopControl = new LoopControl(); - attempts = retries == INFINITE_RETRIES ? INFINITE_RETRIES : retries + 1; + @Override + public boolean isFirstAttempt() { + return loopControl.isFirstIteration(); } - /** - * Advances this {@link RetryControl} such that it represents the state of a new attempt. - * If there is at least one more attempt left, it is consumed by this method. - * Must not be called before the {@linkplain #isFirstAttempt() first attempt}, must be called before each subsequent attempt. - *

    - * This method is intended to be used by code that generally does not handle {@link Error}s explicitly, - * which is usually synchronous code. - * - * @param attemptException The exception produced by the most recent attempt. - * It is passed to the {@code retryPredicate} and to the {@code onAttemptFailureOperator}. - * @param onAttemptFailureOperator The action that is called once per failed attempt before (in the happens-before order) the - * {@code retryPredicate}, regardless of whether the {@code retryPredicate} is called. - * This action is allowed to have side effects. - *

    - * It also has to choose which exception to preserve as a prospective failed result of the associated retryable activity. - * The {@code onAttemptFailureOperator} may mutate its arguments, choose from the arguments, or return a different exception, - * but it must return a {@code @}{@link NonNull} value. - * The choice is between

    - *
      - *
    • the previously chosen exception or {@code null} if none has been chosen - * (the first argument of the {@code onAttemptFailureOperator})
    • - *
    • and the exception from the most recent attempt (the second argument of the {@code onAttemptFailureOperator}).
    • - *
    - * The result of the {@code onAttemptFailureOperator} does not affect the exception passed to the {@code retryPredicate}. - * @param retryPredicate {@code true} iff another attempt needs to be made. The {@code retryPredicate} is called not more than once - * per attempt and only if all the following is true: - *
      - *
    • {@code onAttemptFailureOperator} completed normally;
    • - *
    • the most recent attempt is not known to be the {@linkplain #isLastAttempt(Throwable) last} one.
    • - *
    - * The {@code retryPredicate} accepts this {@link RetryControl} and the exception from the most recent attempt, - * and may mutate the exception. The {@linkplain RetryControl} advances to represent the state of a new attempt - * after (in the happens-before order) testing the {@code retryPredicate}, and only if the predicate completes normally. - * @throws RuntimeException Iff any of the following is true: - *
      - *
    • the {@code onAttemptFailureOperator} completed abruptly;
    • - *
    • the most recent attempt is known to be the {@linkplain #isLastAttempt(Throwable) last} one;
    • - *
    • the {@code retryPredicate} completed abruptly;
    • - *
    • the {@code retryPredicate} is {@code false}.
    • - *
    - * The exception thrown represents the failed result of the associated retryable activity, - * i.e., the caller must not make any more attempts. - * @see #advanceOrThrow(Throwable, BinaryOperator, BiPredicate) - */ - void advanceOrThrow(final RuntimeException attemptException, final BinaryOperator onAttemptFailureOperator, - final BiPredicate retryPredicate) throws RuntimeException { - try { - doAdvanceOrThrow(attemptException, onAttemptFailureOperator, retryPredicate, true); - } catch (RuntimeException | Error unchecked) { - throw unchecked; - } catch (Throwable checked) { - throw new AssertionError(checked); - } + @Override + public int attempt() { + return loopControl.iteration(); } - /** - * This method is intended to be used by code that generally handles all {@link Throwable} types explicitly, - * which is usually asynchronous code. - * - * @see #advanceOrThrow(RuntimeException, BinaryOperator, BiPredicate) - */ - void advanceOrThrow(final Throwable attemptException, final BinaryOperator onAttemptFailureOperator, - final BiPredicate retryPredicate) throws Throwable { - doAdvanceOrThrow(attemptException, onAttemptFailureOperator, retryPredicate, false); + public P getPolicy() { + return policy; } /** - * @param onlyRuntimeExceptions {@code true} iff the method must expect {@link #previouslyChosenException} and {@code attemptException} to be - * {@link RuntimeException}s and must not explicitly handle other {@link Throwable} types, of which only {@link Error} is possible - * as {@link RetryControl} does not have any source of {@link Exception}s. - * @param onAttemptFailureOperator See {@link #advanceOrThrow(RuntimeException, BinaryOperator, BiPredicate)}. + * Advances this {@link RetryControl} such that it represents the state of the immediate next attempt, if any. + * Must not be called before the {@linkplain #isFirstAttempt() first attempt}, must be called before each subsequent attempt. + * + * @param attemptFailedResult The failed result of the most recent attempt. + * @return {@link RetryAttemptInfo} iff another attempt must be executed. + * @throws RuntimeException If another attempt must not be executed. + * The exception thrown represents the failed result of the retryable activity. */ - private void doAdvanceOrThrow(final Throwable attemptException, - final BinaryOperator onAttemptFailureOperator, - final BiPredicate retryPredicate, - final boolean onlyRuntimeExceptions) throws Throwable { - assertTrue(attempt() < attempts); - assertNotNull(attemptException); - if (onlyRuntimeExceptions) { - assertTrue(isRuntime(attemptException)); - } - assertTrue(!isFirstAttempt() || previouslyChosenException == null); - Throwable newlyChosenException = callOnAttemptFailureOperator(previouslyChosenException, attemptException, onlyRuntimeExceptions, onAttemptFailureOperator); - if (isLastAttempt(attemptException)) { - previouslyChosenException = newlyChosenException; - if (attemptException instanceof MongoOperationTimeoutException) { - previouslyChosenException = createMongoTimeoutException("Retry attempt exceeded the timeout limit.", previouslyChosenException); + RetryAttemptInfo advanceOrThrow(final Throwable attemptFailedResult) throws RuntimeException { + assertNotNull(attemptFailedResult); + try { + if (disabled) { + throw attemptFailedResult; } - throw previouslyChosenException; - } else { - // note that we must not update the state, e.g, `previouslyChosenException`, `loopControl`, before calling `retryPredicate` - boolean retry = shouldRetry(this, attemptException, newlyChosenException, onlyRuntimeExceptions, retryPredicate); - previouslyChosenException = newlyChosenException; - if (retry) { - assertTrue(loopControl.advance()); + // this `RetryControl` must not be mutated before calling `onAttemptFailure` + Decision decision = onAttemptFailure(policy, this, prospectiveFailedResult(), attemptFailedResult); + mostRecentDecision = decision; + if (loopControl.isLastIteration() || !decision.getImmediateNextAttemptInfo().isPresent()) { + throw decision.getProspectiveFailedResult(); } else { - throw previouslyChosenException; + assertTrue(loopControl.advance()); + return decision.getImmediateNextAttemptInfo().orElseThrow(Assertions::fail); } + } catch (RuntimeException | Error uncheckedFailedResult) { + throw uncheckedFailedResult; + } catch (Throwable checkedFailedResult) { + throw new RuntimeException(checkedFailedResult); } } - /** - * @param onlyRuntimeExceptions See {@link #doAdvanceOrThrow(Throwable, BinaryOperator, BiPredicate, boolean)}. - * @param onAttemptFailureOperator See {@link #advanceOrThrow(RuntimeException, BinaryOperator, BiPredicate)}. - */ - private static Throwable callOnAttemptFailureOperator( - @Nullable final Throwable previouslyChosenException, - final Throwable attemptException, - final boolean onlyRuntimeExceptions, - final BinaryOperator onAttemptFailureOperator) { - if (onlyRuntimeExceptions && previouslyChosenException != null) { - assertTrue(isRuntime(previouslyChosenException)); - } - Throwable result; + private static

    Decision onAttemptFailure( + final P policy, + final RetryContext retryContext, + @Nullable final Throwable prospectiveFailedResult, + final Throwable attemptFailedResult) throws RuntimeException { + Decision decision; try { - result = assertNotNull(onAttemptFailureOperator.apply(previouslyChosenException, attemptException)); - if (onlyRuntimeExceptions) { - assertTrue(isRuntime(result)); - } - } catch (Throwable onAttemptFailureOperatorException) { - if (onlyRuntimeExceptions && !isRuntime(onAttemptFailureOperatorException)) { - throw onAttemptFailureOperatorException; + decision = assertNotNull(policy.onAttemptFailure(retryContext, attemptFailedResult)); + } catch (Throwable onAttemptFailureException) { + if (prospectiveFailedResult != null && prospectiveFailedResult != onAttemptFailureException) { + onAttemptFailureException.addSuppressed(prospectiveFailedResult); } - if (previouslyChosenException != null) { - onAttemptFailureOperatorException.addSuppressed(previouslyChosenException); + if (attemptFailedResult != onAttemptFailureException) { + onAttemptFailureException.addSuppressed(attemptFailedResult); } - onAttemptFailureOperatorException.addSuppressed(attemptException); - throw onAttemptFailureOperatorException; + throw onAttemptFailureException; } - return result; + return decision; } - /** - * @param readOnlyRetryControl Must not be mutated by this method. - * @param onlyRuntimeExceptions See {@link #doAdvanceOrThrow(Throwable, BinaryOperator, BiPredicate, boolean)}. - */ - private static boolean shouldRetry(final RetryControl readOnlyRetryControl, final Throwable attemptException, - final Throwable newlyChosenException, - final boolean onlyRuntimeExceptions, final BiPredicate retryPredicate) { - try { - return retryPredicate.test(readOnlyRetryControl, attemptException); - } catch (Throwable retryPredicateException) { - if (onlyRuntimeExceptions && !isRuntime(retryPredicateException)) { - throw retryPredicateException; - } - retryPredicateException.addSuppressed(newlyChosenException); - throw retryPredicateException; - } + @Override + public Optional getProspectiveFailedResult() { + assertTrue(isFirstAttempt() ^ mostRecentDecision != null); + return Optional.ofNullable(prospectiveFailedResult()); } - private static boolean isRuntime(@Nullable final Throwable exception) { - return exception instanceof RuntimeException; + @Nullable + private Throwable prospectiveFailedResult() { + return mostRecentDecision == null ? null : mostRecentDecision.getProspectiveFailedResult(); } /** * This method is similar to the semantics of the - * {@code break} statement, with the difference - * that breaking results in throwing an exception because the retry loop has more than one iteration only if the first iteration fails. + * {@code break} statement, + * with the difference that breaking results in throwing an exception. + * That thrown exception must be used by the caller to complete the current attempt. * Does nothing and completes normally if called during the {@linkplain #isFirstAttempt() first attempt}. - * This method is useful when the associated retryable activity detects that a retry attempt should not happen - * despite having been started. Must not be called more than once per {@link RetryControl}. *

    - * If the {@code predicate} completes abruptly, this method also completes abruptly with the same exception but does not break retrying; - * if the {@code predicate} is {@code true}, then the method breaks retrying and completes abruptly by throwing the exception that is - * currently deemed to be a prospective failed result of the associated retryable activity. The thrown exception must also be used - * by the caller to complete the ongoing attempt. + * This method is useful when the retryable activity detects that a retry attempt should not happen + * despite having been started. *

    - * If this method is called from - * {@linkplain RetryingSyncSupplier#RetryingSyncSupplier(RetryControl, BinaryOperator, BiPredicate, Supplier) - * retry predicate / failed result transformer}, the behavior is unspecified. + * Must not be called after breaking the retry loop. * - * @param predicate {@code true} iff retrying needs to be broken. + * @param predicate {@code true} iff the retry loop needs to be broken. * The {@code predicate} is not called during the {@linkplain #isFirstAttempt() first attempt}. + *

      + *
    • + * If the {@code predicate} completes abruptly, this method completes abruptly with the same exception, but does not break the retry loop;
    • + *
    • + * if the {@code predicate} is {@code true}, then this method breaks the retry loop and completes abruptly by throwing {@link #getProspectiveFailedResult()};
    • + *
    • + * if the {@code predicate} is {@code false}, then this method does nothing. + *
    * @throws RuntimeException Iff any of the following is true: *
      *
    • the {@code predicate} completed abruptly;
    • - *
    • this method broke retrying.
    • + *
    • this method broke the retry loop.
    • *
    - * The exception thrown represents the failed result of the associated retryable activity. - * @see #breakAndCompleteIfRetryAnd(Supplier, SingleResultCallback) */ public void breakAndThrowIfRetryAnd(final Supplier predicate) throws RuntimeException { assertFalse(loopControl.isLastIteration()); - if (!isFirstAttempt()) { - assertNotNull(previouslyChosenException); - assertTrue(previouslyChosenException instanceof RuntimeException); - RuntimeException localException = (RuntimeException) previouslyChosenException; - try { - if (predicate.get()) { - loopControl.markAsLastIteration(); - } - } catch (Exception predicateException) { - predicateException.addSuppressed(localException); - throw predicateException; + if (isFirstAttempt()) { + return; + } + Throwable prospectiveFailedResult = assertNotNull(prospectiveFailedResult()); + try { + if (predicate.get()) { + loopControl.markAsLastIteration(); } - if (loopControl.isLastIteration()) { - throw localException; + } catch (Throwable predicateException) { + if (prospectiveFailedResult != predicateException) { + predicateException.addSuppressed(prospectiveFailedResult); } + throw predicateException; } - } - - /** - * This method is intended to be used by callback-based code. It is similar to {@link #breakAndThrowIfRetryAnd(Supplier)}, - * but instead of throwing an exception, it relays it to the {@code callback}. - *

    - * If this method is called from - * {@linkplain RetryingAsyncCallbackSupplier#RetryingAsyncCallbackSupplier(RetryControl, BinaryOperator, BiPredicate, AsyncCallbackSupplier) - * retry predicate / failed result transformer}, the behavior is unspecified. - * - * @return {@code true} iff the {@code callback} was completed, which happens in the same situations in which - * {@link #breakAndThrowIfRetryAnd(Supplier)} throws an exception. If {@code true} is returned, the caller must complete - * the ongoing attempt. - * @see #breakAndThrowIfRetryAnd(Supplier) - */ - public boolean breakAndCompleteIfRetryAnd(final Supplier predicate, final SingleResultCallback callback) { - try { - breakAndThrowIfRetryAnd(predicate); - return false; - } catch (Throwable t) { - callback.onResult(null, t); - return true; + if (loopControl.isLastIteration()) { + try { + throw prospectiveFailedResult; + } catch (RuntimeException | Error unchecked) { + throw unchecked; + } catch (Throwable checked) { + throw new RuntimeException(checked); + } } } /** - * Returns {@code true} iff the current attempt is the first one, i.e., no retry attempts have been made. + * This method allows to execute {@code action} within the encompassing retryable activity, as if it were not retryable. + * If {@code action} throws an exception, then this method throws that same exception, and, if the current attempt fails, + * the {@link RetryPolicy#onAttemptFailure(RetryContext, Throwable)} is not called, and the failed result of the attempt + * becomes the failed result of the retryable activity disregarding {@link #getProspectiveFailedResult()}. * - * @see #attempt() + * @see #doWhileDisabledAsync(AsyncSupplier, SingleResultCallback) */ - public boolean isFirstAttempt() { - return loopControl.isFirstIteration(); - } - - /** - * Returns {@code true} iff the current attempt is known to be the last one, i.e., it is known that no more attempts will be made. - * An attempt is known to be the last one iff any of the following applies: - *

      - *
    • {@link #breakAndThrowIfRetryAnd(Supplier)} / {@link #breakAndCompleteIfRetryAnd(Supplier, SingleResultCallback)} was called.
    • - *
    • {@code attemptException} is a {@link MongoOperationTimeoutException}.
    • - *
    • The number of attempts is limited, and the current attempt is the last one.
    • - *
    - * - * @see #attempt() - */ - private boolean isLastAttempt(final Throwable attemptException) { - boolean operationTimeout = attemptException instanceof MongoOperationTimeoutException; - boolean attemptLimit = attempt() == attempts - 1; - return loopControl.isLastIteration() || operationTimeout || attemptLimit; - } - - /** - * A 0-based attempt number. - * - * @see #isFirstAttempt() - */ - public int attempt() { - return loopControl.iteration(); - } - - /** - * Returns the exception that is currently deemed to be a prospective failed result of the associated retryable activity. - * Note that this exception is not necessary the one from the most recent failed attempt. - * Returns an {@linkplain Optional#isEmpty() empty} {@link Optional} iff called during the {@linkplain #isFirstAttempt() first attempt}. - *

    - * In synchronous code the returned exception is of the type {@link RuntimeException}. - */ - public Optional exception() { - assertTrue(previouslyChosenException == null || !isFirstAttempt()); - return Optional.ofNullable(previouslyChosenException); - } - - /** - * @see LoopControl#attach(AttachmentKey, Object, boolean) - */ - public RetryControl attach(final AttachmentKey key, final V value, final boolean autoRemove) { - loopControl.attach(key, value, autoRemove); - return this; + public R doWhileDisabled(final Supplier action) { + boolean originalDisabled = disabled; + disabled = true; + R result = action.get(); + // `disabled` must be reverted to its original value only if `action` completes normally + disabled = originalDisabled; + return result; } /** - * @see LoopControl#attachment(AttachmentKey) + * This method is similar to {@link #doWhileDisabled(Supplier)}, + * but instead of throwing an exception, it completes the {@code callback} with it. + * This method is intended to be used in callback-based code. */ - public Optional attachment(final AttachmentKey key) { - return loopControl.attachment(key); + public void doWhileDisabledAsync(final AsyncSupplier action, final SingleResultCallback callback) { + boolean originalDisabled = disabled; + disabled = true; + beginAsync().thenSupply(c -> { + action.finish(c); + }).thenRunAndFinish(() -> { + // `disabled` must be reverted to its original value only if `action` completes normally + disabled = originalDisabled; + }, callback); } @Override public String toString() { return "RetryControl{" + "loopControl=" + loopControl - + ", attempts=" + (attempts == INFINITE_RETRIES ? "infinite" : attempts) - + ", exception=" + previouslyChosenException + + ", disabled=" + disabled + + ", mostRecentDecision=" + mostRecentDecision + + ", policy=" + policy + '}'; } } diff --git a/driver-core/src/main/com/mongodb/internal/async/function/RetryPolicy.java b/driver-core/src/main/com/mongodb/internal/async/function/RetryPolicy.java new file mode 100644 index 00000000000..6cbedb1ac2c --- /dev/null +++ b/driver-core/src/main/com/mongodb/internal/async/function/RetryPolicy.java @@ -0,0 +1,99 @@ +/* + * 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.async.function; + +import com.mongodb.annotations.NotThreadSafe; +import com.mongodb.lang.Nullable; + +import java.util.Optional; +import java.util.function.Supplier; + +import static com.mongodb.assertions.Assertions.assertNotNull; + +/** + * Customizes retrying and may allow for control beyond what {@link RetryControl} itself provides, depending on the implementation. + *

    + * An implementation may be stateful and does not have to be thread-safe. + *

    + * This class is not part of the public API and may be removed or changed at any time. + */ +@NotThreadSafe +public interface RetryPolicy { + /** + * This method is called exactly once per failed attempt, + * even if that is the {@linkplain RetryControl#breakAndThrowIfRetryAnd(Supplier) last attempt}, + * provided that retrying is not {@linkplain RetryControl#doWhileDisabled(Supplier) disabled}. + * If this method completes abruptly, then another attempt is not executed, + * and the exception thrown by the method is used as the failed result of the retryable activity. + *

    + * This method may have side effects, and may mutate {@link RetryContext#getProspectiveFailedResult()}, {@code attemptFailedResult}. + * + * @param attemptFailedResult The failed result of the most recent attempt. + */ + Decision onAttemptFailure(RetryContext retryContext, Throwable attemptFailedResult); + + final class Decision { + private final Throwable prospectiveFailedResult; + @Nullable + private final RetryAttemptInfo immediateNextAttemptInfo; + + /** + * @param immediateNextAttemptInfo See {@link #getImmediateNextAttemptInfo()}. + */ + public Decision(final Throwable prospectiveFailedResult, @Nullable final RetryAttemptInfo immediateNextAttemptInfo) { + assertNotNull(prospectiveFailedResult); + this.prospectiveFailedResult = prospectiveFailedResult; + this.immediateNextAttemptInfo = immediateNextAttemptInfo; + } + + /** + * @see RetryControl#getProspectiveFailedResult() + */ + public Throwable getProspectiveFailedResult() { + return prospectiveFailedResult; + } + + /** + * Returns {@link Optional#isEmpty()} to signal that another attempt must not be executed. + * If {@link RetryAttemptInfo} is {@linkplain Optional#isPresent() present}, + * another attempt is still not executed if most recent attempt was the {@linkplain RetryControl#breakAndThrowIfRetryAnd(Supplier) last one}. + */ + public Optional getImmediateNextAttemptInfo() { + return Optional.ofNullable(immediateNextAttemptInfo); + } + + @Override + public String toString() { + return "Decision{" + + "prospectiveFailedResult=" + prospectiveFailedResult + + ", immediateNextAttemptInfo=" + immediateNextAttemptInfo + + '}'; + } + + /** + * The information needed to start a retry attempt. + */ + public static final class RetryAttemptInfo { + public RetryAttemptInfo() { + } + + @Override + public String toString() { + return "RetryAttemptInfo{}"; + } + } + } +} diff --git a/driver-core/src/main/com/mongodb/internal/async/function/RetryingAsyncCallbackSupplier.java b/driver-core/src/main/com/mongodb/internal/async/function/RetryingAsyncCallbackSupplier.java index f21c537a19a..abdfefe55e4 100644 --- a/driver-core/src/main/com/mongodb/internal/async/function/RetryingAsyncCallbackSupplier.java +++ b/driver-core/src/main/com/mongodb/internal/async/function/RetryingAsyncCallbackSupplier.java @@ -17,11 +17,9 @@ import com.mongodb.annotations.NotThreadSafe; import com.mongodb.internal.async.SingleResultCallback; -import com.mongodb.lang.NonNull; import com.mongodb.lang.Nullable; -import java.util.function.BiPredicate; -import java.util.function.BinaryOperator; +import static com.mongodb.assertions.Assertions.assertNotNull; /** * A decorator that implements automatic retrying of failed executions of an {@link AsyncCallbackSupplier}. @@ -36,51 +34,15 @@ */ @NotThreadSafe public final class RetryingAsyncCallbackSupplier implements AsyncCallbackSupplier { - private final RetryControl control; - private final BiPredicate retryPredicate; - private final BinaryOperator onAttemptFailureOperator; + private final RetryControl control; private final AsyncCallbackSupplier asyncFunction; /** * @param control The {@link RetryControl} to control the new {@link RetryingAsyncCallbackSupplier}. - * @param onAttemptFailureOperator The action that is called once per failed attempt before (in the happens-before order) the - * {@code retryPredicate}, regardless of whether the {@code retryPredicate} is called. - * This action is allowed to have side effects. - *

    - * It also has to choose which exception to preserve as a prospective failed result of this {@link RetryingAsyncCallbackSupplier}. - * The {@code onAttemptFailureOperator} may mutate its arguments, choose from the arguments, or return a different exception, - * but it must return a {@code @}{@link NonNull} value. - * The choice is between

    - *
      - *
    • the previously chosen failed result or {@code null} if none has been chosen - * (the first argument of the {@code onAttemptFailureOperator})
    • - *
    • and the failed result from the most recent attempt (the second argument of the {@code onAttemptFailureOperator}).
    • - *
    - * The result of the {@code onAttemptFailureOperator} does not affect the exception passed to the {@code retryPredicate}. - *

    - * If {@code onAttemptFailureOperator} completes abruptly, then the {@code asyncFunction} cannot be retried and the exception thrown by - * the {@code onAttemptFailureOperator} is used as a failed result of this {@link RetryingAsyncCallbackSupplier}.

    - * @param retryPredicate {@code true} iff another attempt needs to be made. If it completes abruptly, - * then the {@code asyncFunction} cannot be retried and the exception thrown by the {@code retryPredicate} - * is used as a failed result of this {@link RetryingAsyncCallbackSupplier}. The {@code retryPredicate} is called not more than once - * per attempt and only if all the following is true: - *
      - *
    • {@code onAttemptFailureOperator} completed normally;
    • - *
    • the most recent attempt is not known to be the last one.
    • - *
    - * The {@code retryPredicate} accepts this {@link RetryControl} and the exception from the most recent attempt, - * and may mutate the exception. The {@linkplain RetryControl} advances to represent the state of a new attempt - * after (in the happens-before order) testing the {@code retryPredicate}, and only if the predicate completes normally. * @param asyncFunction The retryable {@link AsyncCallbackSupplier} to be decorated. */ - public RetryingAsyncCallbackSupplier( - final RetryControl control, - final BinaryOperator onAttemptFailureOperator, - final BiPredicate retryPredicate, - final AsyncCallbackSupplier asyncFunction) { + public RetryingAsyncCallbackSupplier(final RetryControl control, final AsyncCallbackSupplier asyncFunction) { this.control = control; - this.retryPredicate = retryPredicate; - this.onAttemptFailureOperator = onAttemptFailureOperator; this.asyncFunction = asyncFunction; } @@ -103,17 +65,21 @@ private class RetryingCallback implements SingleResultCallback { } @Override - public void onResult(@Nullable final R result, @Nullable final Throwable t) { - if (t != null) { + public void onResult(@Nullable final R attemptSuccessfulResult, @Nullable final Throwable attemptFailedResult) { + if (attemptFailedResult != null) { + if (attemptFailedResult instanceof Error) { + wrapped.onResult(null, attemptFailedResult); + return; + } try { - control.advanceOrThrow(t, onAttemptFailureOperator, retryPredicate); - } catch (Throwable failedResult) { - wrapped.onResult(null, failedResult); + assertNotNull(control.advanceOrThrow(attemptFailedResult)); + } catch (Throwable retryingSupplierFailedResult) { + wrapped.onResult(null, retryingSupplierFailedResult); return; } asyncFunction.get(this); } else { - wrapped.onResult(result, null); + wrapped.onResult(attemptSuccessfulResult, null); } } } diff --git a/driver-core/src/main/com/mongodb/internal/async/function/RetryingSyncSupplier.java b/driver-core/src/main/com/mongodb/internal/async/function/RetryingSyncSupplier.java index 5bf988602c9..3af14b46786 100644 --- a/driver-core/src/main/com/mongodb/internal/async/function/RetryingSyncSupplier.java +++ b/driver-core/src/main/com/mongodb/internal/async/function/RetryingSyncSupplier.java @@ -17,10 +17,10 @@ import com.mongodb.annotations.NotThreadSafe; -import java.util.function.BiPredicate; -import java.util.function.BinaryOperator; import java.util.function.Supplier; +import static com.mongodb.assertions.Assertions.assertNotNull; + /** * A decorator that implements automatic retrying of failed executions of a {@link Supplier}. * {@link RetryingSyncSupplier} may execute the original retryable function multiple times sequentially. @@ -33,28 +33,14 @@ */ @NotThreadSafe public final class RetryingSyncSupplier implements Supplier { - private final RetryControl control; - private final BiPredicate retryPredicate; - private final BinaryOperator onAttemptFailureOperator; + private final RetryControl control; private final Supplier syncFunction; /** - * See {@link RetryingAsyncCallbackSupplier#RetryingAsyncCallbackSupplier(RetryControl, BinaryOperator, BiPredicate, AsyncCallbackSupplier)} - * for the documentation of the parameters. - * - * @param onAttemptFailureOperator Even though the {@code onAttemptFailureOperator} accepts {@link Throwable}, - * only {@link RuntimeException}s are passed to it. - * @param retryPredicate Even though the {@code retryPredicate} accepts {@link Throwable}, - * only {@link RuntimeException}s are passed to it. + * See {@link RetryingAsyncCallbackSupplier#RetryingAsyncCallbackSupplier(RetryControl, AsyncCallbackSupplier)}. */ - public RetryingSyncSupplier( - final RetryControl control, - final BinaryOperator onAttemptFailureOperator, - final BiPredicate retryPredicate, - final Supplier syncFunction) { + public RetryingSyncSupplier(final RetryControl control, final Supplier syncFunction) { this.control = control; - this.retryPredicate = retryPredicate; - this.onAttemptFailureOperator = onAttemptFailureOperator; this.syncFunction = syncFunction; } @@ -63,11 +49,10 @@ public R get() { while (true) { try { return syncFunction.get(); - } catch (RuntimeException attemptException) { - control.advanceOrThrow(attemptException, onAttemptFailureOperator, retryPredicate); - } catch (Exception attemptException) { - // wrap potential sneaky / Kotlin exceptions - control.advanceOrThrow(new RuntimeException(attemptException), onAttemptFailureOperator, retryPredicate); + } catch (Error attemptFailedResult) { + throw attemptFailedResult; + } catch (Throwable attemptFailedResult) { + assertNotNull(control.advanceOrThrow(attemptFailedResult)); } } } diff --git a/driver-core/src/main/com/mongodb/internal/connection/OperationContext.java b/driver-core/src/main/com/mongodb/internal/connection/OperationContext.java index 06c2c9b9358..a4f772eccce 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/OperationContext.java +++ b/driver-core/src/main/com/mongodb/internal/connection/OperationContext.java @@ -264,16 +264,16 @@ void updateCandidate(final ServerAddress serverAddress, final ClusterType cluste this.clusterType = clusterType; } - public void onAttemptFailure(final Throwable failure) { - if (candidate == null || failure instanceof MongoConnectionPoolClearedException) { + public void onAttemptFailure(final Throwable attemptFailedResult) { + if (candidate == null || attemptFailedResult instanceof MongoConnectionPoolClearedException) { candidate = null; return; } // As per spec: sharded clusters deprioritize on any error, // other topologies deprioritize on overload only when retargeting is enabled. - boolean isSystemOverloadedError = failure instanceof MongoException - && ((MongoException) failure).hasErrorLabel(SYSTEM_OVERLOADED_ERROR_LABEL); + boolean isSystemOverloadedError = attemptFailedResult instanceof MongoException + && ((MongoException) attemptFailedResult).hasErrorLabel(SYSTEM_OVERLOADED_ERROR_LABEL); if (clusterType == ClusterType.SHARDED || (isSystemOverloadedError && enableOverloadRetargeting)) { deprioritized.add(candidate); diff --git a/driver-core/src/main/com/mongodb/internal/operation/AbortTransactionOperation.java b/driver-core/src/main/com/mongodb/internal/operation/AbortTransactionOperation.java index 21981fa968a..9d655cd3d7a 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/AbortTransactionOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/AbortTransactionOperation.java @@ -21,10 +21,10 @@ import com.mongodb.WriteConcern; import com.mongodb.internal.MongoNamespaceHelper; import com.mongodb.internal.TimeoutContext; +import com.mongodb.internal.operation.CommandOperationHelper.CommandCreator; import com.mongodb.lang.Nullable; import org.bson.BsonDocument; -import static com.mongodb.internal.operation.CommandOperationHelper.CommandCreator; import static com.mongodb.internal.operation.DocumentHelper.putIfNotNull; /** diff --git a/driver-core/src/main/com/mongodb/internal/operation/AsyncOperationHelper.java b/driver-core/src/main/com/mongodb/internal/operation/AsyncOperationHelper.java index ecf77db3728..9bed9d84a6e 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/AsyncOperationHelper.java +++ b/driver-core/src/main/com/mongodb/internal/operation/AsyncOperationHelper.java @@ -21,9 +21,11 @@ import com.mongodb.ReadPreference; import com.mongodb.assertions.Assertions; import com.mongodb.client.cursor.TimeoutMode; +import com.mongodb.connection.ConnectionDescription; import com.mongodb.connection.ServerDescription; import com.mongodb.internal.TimeoutContext; import com.mongodb.internal.async.AsyncBatchCursor; +import com.mongodb.internal.async.MutableValue; import com.mongodb.internal.async.SingleResultCallback; import com.mongodb.internal.async.function.AsyncCallbackFunction; import com.mongodb.internal.async.function.AsyncCallbackSupplier; @@ -36,7 +38,7 @@ import com.mongodb.internal.binding.ReferenceCounted; import com.mongodb.internal.connection.AsyncConnection; import com.mongodb.internal.connection.OperationContext; -import com.mongodb.internal.operation.retry.AttachmentKeys; +import com.mongodb.internal.session.SessionContext; import com.mongodb.internal.validator.NoOpFieldNameValidator; import com.mongodb.lang.Nullable; import org.bson.BsonDocument; @@ -46,18 +48,21 @@ import org.bson.codecs.Decoder; import java.util.Collections; +import java.util.EnumSet; import java.util.List; +import static com.mongodb.assertions.Assertions.assertFalse; import static com.mongodb.assertions.Assertions.assertNotNull; +import static com.mongodb.internal.async.AsyncRunnable.beginAsync; import static com.mongodb.internal.async.ErrorHandlingResultCallback.errorHandlingCallback; import static com.mongodb.internal.operation.CommandOperationHelper.CommandCreator; -import static com.mongodb.internal.operation.CommandOperationHelper.addRetryableWriteErrorLabel; -import static com.mongodb.internal.operation.CommandOperationHelper.initialRetryState; -import static com.mongodb.internal.operation.CommandOperationHelper.isRetryableWriteCommand; -import static com.mongodb.internal.operation.CommandOperationHelper.logRetryCommand; -import static com.mongodb.internal.operation.CommandOperationHelper.onRetryableReadAttemptFailure; -import static com.mongodb.internal.operation.CommandOperationHelper.onRetryableWriteAttemptFailure; +import static com.mongodb.internal.operation.CommandOperationHelper.createSpecRetryControl; import static com.mongodb.internal.operation.CommandOperationHelper.transformWriteException; +import static com.mongodb.internal.operation.CommandOperationHelper.isWriteRetryRequirementsMet; +import static com.mongodb.internal.operation.OperationHelper.isReadRetryRequirementsMet; +import static com.mongodb.internal.operation.OperationHelper.isServerWriteRetryRequirementsMet; +import static com.mongodb.internal.operation.SpecRetryPolicy.Descriptor.READ; +import static com.mongodb.internal.operation.SpecRetryPolicy.Descriptor.WRITE; import static com.mongodb.internal.operation.WriteConcernHelper.throwOnWriteConcernError; final class AsyncOperationHelper { @@ -174,10 +179,10 @@ static void executeRetryableReadAsync( final CommandCreator commandCreator, final Decoder decoder, final CommandReadTransformerAsync transformer, - final boolean retryReads, + final boolean retryReadsSetting, final SingleResultCallback callback) { executeRetryableReadAsync(binding, operationContext, binding::getReadConnectionSource, database, commandCreator, - decoder, transformer, retryReads, callback); + decoder, transformer, retryReadsSetting, callback); } static void executeRetryableReadAsync( @@ -188,18 +193,14 @@ static void executeRetryableReadAsync( final CommandCreator commandCreator, final Decoder decoder, final CommandReadTransformerAsync transformer, - final boolean retryReads, + final boolean retryReadsSetting, final SingleResultCallback callback) { - RetryControl retryControl = initialRetryState(retryReads, operationContext.getTimeoutContext()); + RetryControl retryControl = createSpecRetryControl(EnumSet.of(READ), retryReadsSetting, isReadRetryRequirementsMet(retryReadsSetting, operationContext), operationContext); binding.retain(); - AsyncCallbackSupplier asyncRead = decorateReadWithRetriesAsync(retryControl, operationContext, + AsyncCallbackSupplier asyncRead = decorateWithRetriesAsync(retryControl, operationContext, (AsyncCallbackSupplier) funcCallback -> withAsyncSourceAndConnection(sourceAsyncFunction, false, operationContext, funcCallback, (source, connection, operationContextWithMinRtt, releasingCallback) -> { - if (retryControl.breakAndCompleteIfRetryAnd( - () -> !OperationHelper.canRetryRead(operationContextWithMinRtt), releasingCallback)) { - return; - } createReadCommandAndExecuteAsync(retryControl, operationContextWithMinRtt, source, database, commandCreator, decoder, transformer, connection, releasingCallback); }) @@ -240,12 +241,13 @@ static void executeCommandAsync(final AsyncWriteBinding binding, final CommandWriteTransformerAsync transformer, final SingleResultCallback callback) { Assertions.notNull("binding", binding); - SingleResultCallback addingRetryableLabelCallback = addingRetryableLabelCallback(callback, - connection.getDescription().getMaxWireVersion()); connection.commandAsync(database, command, NoOpFieldNameValidator.INSTANCE, ReadPreference.primary(), new BsonDocumentCodec(), - operationContext, transformingWriteCallback(transformer, connection, addingRetryableLabelCallback)); + operationContext, transformingWriteCallback(transformer, connection, callback)); } + /** + * @param effectiveRetryWritesSetting See {@link SpecRetryPolicy}. + */ static void executeRetryableWriteAsync( final AsyncWriteBinding binding, final OperationContext operationContext, @@ -256,57 +258,51 @@ static void executeRetryableWriteAsync( final CommandCreator commandCreator, final CommandWriteTransformerAsync transformer, final Function retryCommandModifier, + final boolean effectiveRetryWritesSetting, final SingleResultCallback callback) { - - RetryControl retryControl = initialRetryState(true, operationContext.getTimeoutContext()); - binding.retain(); - - AsyncCallbackSupplier asyncWrite = decorateWriteWithRetriesAsync(retryControl, operationContext, - (AsyncCallbackSupplier) funcCallback -> { - boolean firstAttempt = retryControl.isFirstAttempt(); - if (!firstAttempt && operationContext.getSessionContext().hasActiveTransaction()) { - operationContext.getSessionContext().clearTransactionContext(); - } - withAsyncSourceAndConnection(binding::getWriteConnectionSource, true, operationContext, funcCallback, - (source, connection, operationContextWithMinRtt, releasingCallback) -> { - int maxWireVersion = connection.getDescription().getMaxWireVersion(); - SingleResultCallback addingRetryableLabelCallback = firstAttempt - ? releasingCallback - : addingRetryableLabelCallback(releasingCallback, maxWireVersion); - if (retryControl.breakAndCompleteIfRetryAnd(() -> - !OperationHelper.canRetryWrite(connection.getDescription()), addingRetryableLabelCallback)) { - return; - } - BsonDocument command; - try { - command = retryControl.attachment(AttachmentKeys.command()) - .map(previousAttemptCommand -> { - Assertions.assertFalse(firstAttempt); - return retryCommandModifier.apply(previousAttemptCommand); - }).orElseGet(() -> commandCreator.create( - operationContextWithMinRtt, - source.getServerDescription(), - connection.getDescription())); - // attach `maxWireVersion`, `retryableWriteCommandFlag` ASAP because they are used to check whether we should retry - retryControl.attach(AttachmentKeys.maxWireVersion(), maxWireVersion, true) - .attach(AttachmentKeys.retryableWriteCommandFlag(), isRetryableWriteCommand(command), true) - .attach(AttachmentKeys.commandDescriptionSupplier(), command::getFirstKey, false) - .attach(AttachmentKeys.command(), command, false); - } catch (Throwable t) { - addingRetryableLabelCallback.onResult(null, t); - return; - } - connection.commandAsync(database, command, fieldNameValidator, readPreference, commandResultDecoder, - operationContextWithMinRtt, - transformingWriteCallback(transformer, connection, addingRetryableLabelCallback)); - }); - }).whenComplete(binding::release); - - asyncWrite.get(exceptionTransformingCallback(errorHandlingCallback(callback, OperationHelper.LOGGER))); + beginAsync().thenSupply(c -> { + binding.retain(); + MutableValue command = new MutableValue<>(); + RetryControl retryControl = createSpecRetryControl(EnumSet.of(WRITE), effectiveRetryWritesSetting, effectiveRetryWritesSetting, operationContext); + AsyncCallbackSupplier retryingWrite = decorateWithRetriesAsync(retryControl, operationContext, supplierCallback -> { + beginAsync().thenSupply(withSourceAndConnectionCallback -> { + boolean firstAttempt = retryControl.isFirstAttempt(); + SessionContext sessionContext = operationContext.getSessionContext(); + if (!firstAttempt && sessionContext.hasActiveTransaction()) { + sessionContext.clearTransactionContext(); + } + withAsyncSourceAndConnection(binding::getWriteConnectionSource, true, operationContext, withSourceAndConnectionCallback, + (source, connection, operationContextWithMinRtt, functionCallback) -> { + beginAsync().thenSupply(executeCommandCallback -> { + ConnectionDescription connectionDescription = connection.getDescription(); + retryControl.breakAndThrowIfRetryAnd(() -> !isServerWriteRetryRequirementsMet(connectionDescription)); + if (command.getNullable() == null) { + command.set(commandCreator.create(operationContextWithMinRtt, source.getServerDescription(), connectionDescription)); + } else { + assertFalse(firstAttempt); + command.set(retryCommandModifier.apply(command.get())); + } + retryControl.getPolicy() + .onCommand(() -> command.get().getFirstKey()) + .onWriteRetryRequirements(isWriteRetryRequirementsMet(command.get()), connectionDescription); + connection.commandAsync(database, command.get(), fieldNameValidator, readPreference, + commandResultDecoder, operationContextWithMinRtt, executeCommandCallback); + }).thenApply((result, transformResultCallback) -> { + transformResultCallback.complete(transformer.apply(assertNotNull(result), connection)); + }).finish(functionCallback); + }); + }).finish(supplierCallback); + }); + beginAsync().thenSupply(retryingWriteCallback -> { + retryingWrite.get(retryingWriteCallback); + }).onErrorIf(e -> e instanceof MongoException, (e, onErrorCallback) -> { + throw transformWriteException((MongoException) e); + }).finish(c); + }).thenAlwaysRunAndFinish(binding::release, callback); } static void createReadCommandAndExecuteAsync( - final RetryControl retryControl, + final RetryControl retryControl, final OperationContext operationContext, final AsyncConnectionSource source, final String database, @@ -318,7 +314,7 @@ static void createReadCommandAndExecuteAsync( BsonDocument command; try { command = commandCreator.create(operationContext, source.getServerDescription(), connection.getDescription()); - retryControl.attach(AttachmentKeys.commandDescriptionSupplier(), command::getFirstKey, false); + retryControl.getPolicy().onCommand(command::getFirstKey); } catch (IllegalArgumentException e) { callback.onResult(null, e); return; @@ -327,21 +323,13 @@ static void createReadCommandAndExecuteAsync( operationContext, transformingReadCallback(transformer, source, connection, operationContext, callback)); } - static AsyncCallbackSupplier decorateReadWithRetriesAsync(final RetryControl retryControl, final OperationContext operationContext, - final AsyncCallbackSupplier asyncReadFunction) { - return new RetryingAsyncCallbackSupplier<>(retryControl, onRetryableReadAttemptFailure(operationContext.getServerDeprioritization()), - CommandOperationHelper::loggingShouldAttemptToRetryRead, callback -> { - logRetryCommand(retryControl, operationContext); - asyncReadFunction.get(callback); - }); - } - - static AsyncCallbackSupplier decorateWriteWithRetriesAsync(final RetryControl retryControl, final OperationContext operationContext, - final AsyncCallbackSupplier asyncWriteFunction) { - return new RetryingAsyncCallbackSupplier<>(retryControl, onRetryableWriteAttemptFailure(operationContext.getServerDeprioritization()), - CommandOperationHelper::loggingShouldAttemptToRetryWriteAndAddRetryableLabel, callback -> { - logRetryCommand(retryControl, operationContext); - asyncWriteFunction.get(callback); + static AsyncCallbackSupplier decorateWithRetriesAsync( + final RetryControl retryControl, + final OperationContext operationContext, + final AsyncCallbackSupplier supplier) { + return new RetryingAsyncCallbackSupplier<>(retryControl, callback -> { + retryControl.getPolicy().onAttemptStart(retryControl, operationContext); + supplier.get(callback); }); } @@ -377,20 +365,6 @@ static SingleResultCallback releasingCallback(final SingleResultCallback< return new ReferenceCountedReleasingWrappedCallback<>(wrapped, Collections.singletonList(connection)); } - static SingleResultCallback exceptionTransformingCallback(final SingleResultCallback callback) { - return (result, t) -> { - if (t != null) { - if (t instanceof MongoException) { - callback.onResult(null, transformWriteException((MongoException) t)); - } else { - callback.onResult(null, t); - } - } else { - callback.onResult(result, null); - } - }; - } - private static SingleResultCallback transformingWriteCallback(final CommandWriteTransformerAsync transformer, final AsyncConnection connection, final SingleResultCallback callback) { return (result, t) -> { @@ -483,20 +457,6 @@ public void onResult(@Nullable final T result, @Nullable final Throwable t) { } } - private static SingleResultCallback addingRetryableLabelCallback(final SingleResultCallback callback, - final int maxWireVersion) { - return (result, t) -> { - if (t != null) { - if (t instanceof MongoException) { - addRetryableWriteErrorLabel((MongoException) t, maxWireVersion); - } - callback.onResult(null, t); - } else { - callback.onResult(result, null); - } - }; - } - private static SingleResultCallback transformingReadCallback(final CommandReadTransformerAsync transformer, final AsyncConnectionSource source, final AsyncConnection connection, final OperationContext operationContext, final SingleResultCallback callback) { return (result, t) -> { diff --git a/driver-core/src/main/com/mongodb/internal/operation/BaseFindAndModifyOperation.java b/driver-core/src/main/com/mongodb/internal/operation/BaseFindAndModifyOperation.java index f503ebc428a..fdb24752b62 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/BaseFindAndModifyOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/BaseFindAndModifyOperation.java @@ -37,7 +37,7 @@ import static com.mongodb.internal.operation.AsyncOperationHelper.executeRetryableWriteAsync; import static com.mongodb.internal.operation.CommandOperationHelper.CommandCreator; import static com.mongodb.internal.operation.DocumentHelper.putIfNotNull; -import static com.mongodb.internal.operation.OperationHelper.isRetryableWrite; +import static com.mongodb.internal.operation.OperationHelper.isNonCommandWriteRetryRequirementsMet; import static com.mongodb.internal.operation.OperationHelper.validateHintForFindAndModify; import static com.mongodb.internal.operation.SyncOperationHelper.executeRetryableWrite; @@ -82,7 +82,8 @@ public T execute(final WriteBinding binding, final OperationContext operationCon CommandResultDocumentCodec.create(getDecoder(), "value"), getCommandCreator(), FindAndModifyHelper.transformer(), - cmd -> cmd); + cmd -> cmd, + retryWrites); } @Override @@ -90,7 +91,7 @@ public void executeAsync(final AsyncWriteBinding binding, final OperationContext executeRetryableWriteAsync(binding, operationContext, getDatabaseName(), null, getFieldNameValidator(), CommandResultDocumentCodec.create(getDecoder(), "value"), getCommandCreator(), - FindAndModifyHelper.asyncTransformer(), cmd -> cmd, callback); + FindAndModifyHelper.asyncTransformer(), cmd -> cmd, retryWrites, callback); } @Override @@ -218,7 +219,7 @@ private CommandCreator getCommandCreator() { putIfNotNull(commandDocument, "comment", getComment()); putIfNotNull(commandDocument, "let", getLet()); - if (isRetryableWrite(isRetryWrites(), getWriteConcern(), connectionDescription, sessionContext)) { + if (isNonCommandWriteRetryRequirementsMet(isRetryWrites(), getWriteConcern(), connectionDescription, sessionContext)) { commandDocument.put("txnNumber", new BsonInt64(sessionContext.advanceTransactionNumber())); } return commandDocument; diff --git a/driver-core/src/main/com/mongodb/internal/operation/BulkWriteBatch.java b/driver-core/src/main/com/mongodb/internal/operation/BulkWriteBatch.java index 1064bee14d3..5f684d45e56 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/BulkWriteBatch.java +++ b/driver-core/src/main/com/mongodb/internal/operation/BulkWriteBatch.java @@ -66,7 +66,7 @@ import static com.mongodb.internal.operation.DocumentHelper.putIfNotNull; import static com.mongodb.internal.operation.CommandOperationHelper.commandWriteConcern; import static com.mongodb.internal.operation.OperationHelper.LOGGER; -import static com.mongodb.internal.operation.OperationHelper.isRetryableWrite; +import static com.mongodb.internal.operation.OperationHelper.isNonCommandWriteRetryRequirementsMet; import static com.mongodb.internal.operation.WriteConcernHelper.createWriteConcernError; import static java.util.Collections.singletonMap; import static org.bson.codecs.configuration.CodecRegistries.fromProviders; @@ -101,20 +101,19 @@ static BulkWriteBatch createBulkWriteBatch(final MongoNamespace namespace, final List writeRequests, final OperationContext operationContext, @Nullable final BsonValue comment, @Nullable final BsonDocument variables) { - boolean canRetryWrites = isRetryableWrite(retryWrites, writeConcern, connectionDescription, operationContext.getSessionContext()); + boolean nonCommandWriteRetryRequirementsMet = isNonCommandWriteRetryRequirementsMet(retryWrites, writeConcern, connectionDescription, operationContext.getSessionContext()); List writeRequestsWithIndex = new ArrayList<>(); - boolean writeRequestsAreRetryable = true; + boolean commandWriteRetryRequirementsMet = true; for (int i = 0; i < writeRequests.size(); i++) { WriteRequest writeRequest = writeRequests.get(i); - writeRequestsAreRetryable = writeRequestsAreRetryable && isRetryable(writeRequest); + commandWriteRetryRequirementsMet = commandWriteRetryRequirementsMet && isCommandWriteRetryRequirementsMet(writeRequest); writeRequestsWithIndex.add(new WriteRequestWithIndex(writeRequest, i)); } - if (canRetryWrites && !writeRequestsAreRetryable) { - canRetryWrites = false; + if (nonCommandWriteRetryRequirementsMet && !commandWriteRetryRequirementsMet) { logWriteModelDoesNotSupportRetries(); } return new BulkWriteBatch(namespace, connectionDescription, ordered, writeConcern, bypassDocumentValidation, - canRetryWrites, new BulkWriteBatchCombiner(connectionDescription.getServerAddress(), ordered, writeConcern), + nonCommandWriteRetryRequirementsMet && commandWriteRetryRequirementsMet, new BulkWriteBatchCombiner(connectionDescription.getServerAddress(), ordered, writeConcern), writeRequestsWithIndex, operationContext, comment, variables); } @@ -214,7 +213,7 @@ void addResult(@Nullable final BsonDocument result) { } } - boolean getRetryWrites() { + boolean isWriteRetryRequirementsMet() { return retryWrites; } @@ -377,7 +376,7 @@ private SplittablePayload.Type getPayloadType(final WriteRequest.Type batchType) } } - private static boolean isRetryable(final WriteRequest writeRequest) { + private static boolean isCommandWriteRetryRequirementsMet(final WriteRequest writeRequest) { if (writeRequest.getType() == UPDATE || writeRequest.getType() == REPLACE) { return !((UpdateRequest) writeRequest).isMulti(); } else if (writeRequest.getType() == DELETE) { diff --git a/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java b/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java index cad557283b7..8c46fa472c4 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java @@ -38,15 +38,12 @@ import com.mongodb.client.model.bulk.ClientNamespacedWriteModel; import com.mongodb.client.model.bulk.ClientUpdateResult; import com.mongodb.connection.ConnectionDescription; -import com.mongodb.internal.TimeoutContext; import com.mongodb.internal.VisibleForTesting; import com.mongodb.internal.async.AsyncBatchCursor; -import com.mongodb.internal.async.AsyncSupplier; import com.mongodb.internal.async.MutableValue; import com.mongodb.internal.async.SingleResultCallback; import com.mongodb.internal.async.function.AsyncCallbackSupplier; import com.mongodb.internal.async.function.RetryControl; -import com.mongodb.internal.async.function.RetryingSyncSupplier; import com.mongodb.internal.binding.AsyncConnectionSource; import com.mongodb.internal.binding.AsyncWriteBinding; import com.mongodb.internal.binding.ConnectionSource; @@ -83,7 +80,6 @@ import com.mongodb.internal.connection.IdHoldingBsonWriter; import com.mongodb.internal.connection.MongoWriteConcernWithResponseException; import com.mongodb.internal.connection.OperationContext; -import com.mongodb.internal.operation.retry.AttachmentKeys; import com.mongodb.internal.session.SessionContext; import com.mongodb.internal.validator.NoOpFieldNameValidator; import com.mongodb.internal.validator.ReplacingDocumentFieldNameValidator; @@ -105,12 +101,14 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.EnumSet; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.LongSupplier; import java.util.function.Supplier; import java.util.stream.Stream; @@ -125,16 +123,17 @@ import static com.mongodb.internal.connection.DualMessageSequences.WritersProviderAndLimitsChecker.WriteResult.FAIL_LIMIT_EXCEEDED; import static com.mongodb.internal.connection.DualMessageSequences.WritersProviderAndLimitsChecker.WriteResult.OK_LIMIT_NOT_REACHED; import static com.mongodb.internal.operation.AsyncOperationHelper.cursorDocumentToAsyncBatchCursor; -import static com.mongodb.internal.operation.AsyncOperationHelper.decorateWriteWithRetriesAsync; +import static com.mongodb.internal.operation.AsyncOperationHelper.decorateWithRetriesAsync; import static com.mongodb.internal.operation.AsyncOperationHelper.withAsyncSourceAndConnection; import static com.mongodb.internal.operation.BulkWriteBatch.logWriteModelDoesNotSupportRetries; import static com.mongodb.internal.operation.CommandOperationHelper.commandWriteConcern; -import static com.mongodb.internal.operation.CommandOperationHelper.addRetryableLabelOrGetWriteAttemptFailureNotToBeRetried; -import static com.mongodb.internal.operation.CommandOperationHelper.initialRetryState; +import static com.mongodb.internal.operation.CommandOperationHelper.createSpecRetryControl; import static com.mongodb.internal.operation.CommandOperationHelper.transformWriteException; -import static com.mongodb.internal.operation.OperationHelper.isRetryableWrite; +import static com.mongodb.internal.operation.OperationHelper.isNonCommandWriteRetryRequirementsMet; +import static com.mongodb.internal.operation.OperationHelper.isServerWriteRetryRequirementsMet; +import static com.mongodb.internal.operation.SpecRetryPolicy.Descriptor.WRITE; import static com.mongodb.internal.operation.SyncOperationHelper.cursorDocumentToBatchCursor; -import static com.mongodb.internal.operation.SyncOperationHelper.decorateWriteWithRetries; +import static com.mongodb.internal.operation.SyncOperationHelper.decorateWithRetries; import static com.mongodb.internal.operation.SyncOperationHelper.withSourceAndConnection; import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; @@ -283,11 +282,10 @@ private Integer executeBatch( List unexecutedModels = models.subList(batchStartModelIndex, models.size()); assertFalse(unexecutedModels.isEmpty()); SessionContext sessionContext = operationContext.getSessionContext(); - TimeoutContext timeoutContext = operationContext.getTimeoutContext(); - RetryControl retryControl = initialRetryState(retryWritesSetting, timeoutContext); + RetryControl retryControl = createSpecRetryControl(EnumSet.of(WRITE), retryWritesSetting, retryWritesSetting, operationContext); BatchEncoder batchEncoder = new BatchEncoder(); - Supplier retryingBatchExecutor = decorateWriteWithRetries( + Supplier retryingBatchExecutor = decorateWithRetries( retryControl, operationContext, // Each batch re-selects a server and re-checks out a connection because this is simpler, // and it is allowed by https://jira.mongodb.org/browse/DRIVERS-2502. @@ -295,16 +293,13 @@ private Integer executeBatch( // and `ClientSession`, `TransactionContext` are aware of that. () -> withSourceAndConnection(binding::getWriteConnectionSource, true, operationContext, (connectionSource, connection, operationContextWithMinRtt) -> { + SpecRetryPolicy retryPolicy = retryControl.getPolicy().onCommand(() -> BULK_WRITE_COMMAND_NAME); ConnectionDescription connectionDescription = connection.getDescription(); - boolean effectiveRetryWrites = isRetryableWrite( - retryWritesSetting, effectiveWriteConcern, connectionDescription, sessionContext); - retryControl.breakAndThrowIfRetryAnd(() -> !effectiveRetryWrites); + retryControl.breakAndThrowIfRetryAnd(() -> !isServerWriteRetryRequirementsMet(connectionDescription)); resultAccumulator.onNewServerAddress(connectionDescription.getServerAddress()); - retryControl.attach(AttachmentKeys.maxWireVersion(), connectionDescription.getMaxWireVersion(), true) - .attach(AttachmentKeys.commandDescriptionSupplier(), () -> BULK_WRITE_COMMAND_NAME, false); ClientBulkWriteCommand bulkWriteCommand = createBulkWriteCommand( - retryControl, effectiveRetryWrites, effectiveWriteConcern, sessionContext, unexecutedModels, batchEncoder, - () -> retryControl.attach(AttachmentKeys.retryableWriteCommandFlag(), true, true)); + retryControl, connectionDescription, effectiveWriteConcern, sessionContext, unexecutedModels, batchEncoder, + () -> retryPolicy.onWriteRetryRequirements(true, connectionDescription)); return executeBulkWriteCommandAndExhaustOkResponse( retryControl, connectionSource, connection, bulkWriteCommand, effectiveWriteConcern, operationContextWithMinRtt); }) @@ -322,12 +317,6 @@ private Integer executeBatch( throw bulkWriteCommandException; } catch (MongoException mongoException) { resultAccumulator.onBulkWriteCommandErrorWithoutResponse(mongoException); - if (retryWritesSetting) { - // Adding the `RetryableWriteError` label here is unnecessary at this point: - // applications cannot use it for implementing retries, and it is not even part of the public driver API. - // Unfortunately, certain unified tests incorrectly rely on this label to verify retries, resulting in this redundant code. - addRetryableLabelOrGetWriteAttemptFailureNotToBeRetried(retryControl, mongoException); - } throw mongoException; } } @@ -346,11 +335,10 @@ private void executeBatchAsync( List unexecutedModels = models.subList(batchStartModelIndex, models.size()); assertFalse(unexecutedModels.isEmpty()); SessionContext sessionContext = operationContext.getSessionContext(); - TimeoutContext timeoutContext = operationContext.getTimeoutContext(); - RetryControl retryControl = initialRetryState(retryWritesSetting, timeoutContext); + RetryControl retryControl = createSpecRetryControl(EnumSet.of(WRITE), retryWritesSetting, retryWritesSetting, operationContext); BatchEncoder batchEncoder = new BatchEncoder(); - AsyncCallbackSupplier retryingBatchExecutor = decorateWriteWithRetriesAsync( + AsyncCallbackSupplier retryingBatchExecutor = decorateWithRetriesAsync( retryControl, operationContext, // Each batch re-selects a server and re-checks out a connection because this is simpler, // and it is allowed by https://jira.mongodb.org/browse/DRIVERS-2502. @@ -359,16 +347,13 @@ private void executeBatchAsync( supplierCallback -> withAsyncSourceAndConnection(binding::getWriteConnectionSource, true, operationContext, supplierCallback, (connectionSource, connection, operationContextWithMinRtt, functionCallback) -> { beginAsync().thenSupply(executeAndExhaustCallback -> { + SpecRetryPolicy retryPolicy = retryControl.getPolicy().onCommand(() -> BULK_WRITE_COMMAND_NAME); ConnectionDescription connectionDescription = connection.getDescription(); - boolean effectiveRetryWrites = isRetryableWrite( - retryWritesSetting, effectiveWriteConcern, connectionDescription, sessionContext); - retryControl.breakAndThrowIfRetryAnd(() -> !effectiveRetryWrites); + retryControl.breakAndThrowIfRetryAnd(() -> !isServerWriteRetryRequirementsMet(connectionDescription)); resultAccumulator.onNewServerAddress(connectionDescription.getServerAddress()); - retryControl.attach(AttachmentKeys.maxWireVersion(), connectionDescription.getMaxWireVersion(), true) - .attach(AttachmentKeys.commandDescriptionSupplier(), () -> BULK_WRITE_COMMAND_NAME, false); ClientBulkWriteCommand bulkWriteCommand = createBulkWriteCommand( - retryControl, effectiveRetryWrites, effectiveWriteConcern, sessionContext, unexecutedModels, batchEncoder, - () -> retryControl.attach(AttachmentKeys.retryableWriteCommandFlag(), true, true)); + retryControl, connectionDescription, effectiveWriteConcern, sessionContext, unexecutedModels, batchEncoder, + () -> retryPolicy.onWriteRetryRequirements(true, connectionDescription)); executeBulkWriteCommandAndExhaustOkResponseAsync( retryControl, connectionSource, connection, bulkWriteCommand, effectiveWriteConcern, operationContextWithMinRtt, executeAndExhaustCallback); }).finish(functionCallback); @@ -392,12 +377,6 @@ private void executeBatchAsync( } else if (t instanceof MongoException) { MongoException mongoException = (MongoException) t; resultAccumulator.onBulkWriteCommandErrorWithoutResponse(mongoException); - if (retryWritesSetting) { - // Adding the `RetryableWriteError` label here is unnecessary at this point: - // applications cannot use it for implementing retries, and it is not even part of the public driver API. - // Unfortunately, certain unified tests incorrectly rely on this label to verify retries, resulting in this redundant code. - addRetryableLabelOrGetWriteAttemptFailureNotToBeRetried(retryControl, mongoException); - } throw mongoException; } else { onErrorCallback.completeExceptionally(t); @@ -415,7 +394,7 @@ private void executeBatchAsync( */ @Nullable private ExhaustiveClientBulkWriteCommandOkResponse executeBulkWriteCommandAndExhaustOkResponse( - final RetryControl retryControl, + final RetryControl retryControl, final ConnectionSource connectionSource, final Connection connection, final ClientBulkWriteCommand bulkWriteCommand, @@ -434,7 +413,7 @@ private ExhaustiveClientBulkWriteCommandOkResponse executeBulkWriteCommandAndExh return null; } ClientBulkWriteCommandOkResponse response = new ClientBulkWriteCommandOkResponse(okResponseDocument); - List> cursorExhaustBatches = doWithRetriesDisabled(retryControl, () -> + List> cursorExhaustBatches = retryControl.doWhileDisabled(() -> exhaustBulkWriteCommandOkResponseCursor(connectionSource, operationContext, connection, response)); return createExhaustiveClientBulkWriteCommandOkResponse( response, @@ -446,7 +425,7 @@ private ExhaustiveClientBulkWriteCommandOkResponse executeBulkWriteCommandAndExh * @see #executeBulkWriteCommandAndExhaustOkResponse(RetryControl, ConnectionSource, Connection, ClientBulkWriteCommand, WriteConcern, OperationContext) */ private void executeBulkWriteCommandAndExhaustOkResponseAsync( - final RetryControl retryControl, + final RetryControl retryControl, final AsyncConnectionSource connectionSource, final AsyncConnection connection, final ClientBulkWriteCommand bulkWriteCommand, @@ -470,7 +449,7 @@ private void executeBulkWriteCommandAndExhaustOkResponseAsync( } ClientBulkWriteCommandOkResponse response = new ClientBulkWriteCommandOkResponse(okResponseDocument); beginAsync().>>thenSupply(exhaustCallback -> { - doWithRetriesDisabledAsync(retryControl, (actionCallback) -> { + retryControl.doWhileDisabledAsync((actionCallback) -> { exhaustBulkWriteCommandOkResponseCursorAsync(connectionSource, connection, response, operationContext, actionCallback); }, exhaustCallback); }).thenApply((cursorExhaustBatches, transformExhaustionResultCallback) -> { @@ -502,45 +481,6 @@ private static ExhaustiveClientBulkWriteCommandOkResponse createExhaustiveClient return exhaustiveResponse; } - /** - * This method disables retries on {@code outerRetryControl} while executing the {@code action}. - * This way, if the {@code action} completes abruptly, the outer {@link RetryingSyncSupplier} the execution is part of - * does not make another attempt based on that exception. - */ - private R doWithRetriesDisabled( - final RetryControl outerRetryControl, - final Supplier action) { - // TODO-JAVA-5956 The current implementation incorrectly uses `retryableWriteCommandFlag` to achieve the behavior needed. - Optional originalRetryableWriteCommandFlag = outerRetryControl.attachment(AttachmentKeys.retryableWriteCommandFlag()); - - try { - outerRetryControl.attach(AttachmentKeys.retryableWriteCommandFlag(), false, true); - return action.get(); - } finally { - originalRetryableWriteCommandFlag.ifPresent(value -> outerRetryControl.attach(AttachmentKeys.retryableWriteCommandFlag(), value, true)); - } - } - - /** - * @see #doWithRetriesDisabled(RetryControl, Supplier) - */ - private void doWithRetriesDisabledAsync( - final RetryControl retryControl, - final AsyncSupplier action, - final SingleResultCallback callback) { - beginAsync().thenSupply(c -> { - // TODO-JAVA-5956 The current implementation incorrectly uses `retryableWriteCommandFlag` to achieve the behavior needed. - Optional originalRetryableWriteCommandFlag = retryControl.attachment(AttachmentKeys.retryableWriteCommandFlag()); - - beginAsync().thenSupply(actionCallback -> { - retryControl.attach(AttachmentKeys.retryableWriteCommandFlag(), false, true); - action.finish(actionCallback); - }).thenAlwaysRunAndFinish(() -> { - originalRetryableWriteCommandFlag.ifPresent(value -> retryControl.attach(AttachmentKeys.retryableWriteCommandFlag(), value, true)); - }, c); - }).finish(callback); - } - private List> exhaustBulkWriteCommandOkResponseCursor( final ConnectionSource connectionSource, final OperationContext operationContext, @@ -586,13 +526,13 @@ private void exhaustBulkWriteCommandOkResponseCursorAsync( } private ClientBulkWriteCommand createBulkWriteCommand( - final RetryControl retryControl, - final boolean effectiveRetryWrites, + final RetryControl retryControl, + final ConnectionDescription connectionDescription, final WriteConcern effectiveWriteConcern, final SessionContext sessionContext, final List unexecutedModels, final BatchEncoder batchEncoder, - final Runnable retriesEnabler) { + final Runnable onWriteRetryRequirementsMet) { BsonDocument commandDocument = new BsonDocument(BULK_WRITE_COMMAND_NAME, new BsonInt32(1)) .append("errorsOnly", BsonBoolean.valueOf(!options.isVerboseResults())) .append("ordered", BsonBoolean.valueOf(options.isOrdered())); @@ -607,11 +547,12 @@ private ClientBulkWriteCommand createBulkWriteCommand( return new ClientBulkWriteCommand( commandDocument, new ClientBulkWriteCommand.OpsAndNsInfo( - effectiveRetryWrites, unexecutedModels, + isNonCommandWriteRetryRequirementsMet(retryWritesSetting, effectiveWriteConcern, connectionDescription, sessionContext), + unexecutedModels, batchEncoder, options, () -> { - retriesEnabler.run(); + onWriteRetryRequirementsMet.run(); return retryControl.isFirstAttempt() ? sessionContext.advanceTransactionNumber() : sessionContext.getTransactionNumber(); @@ -976,25 +917,32 @@ OpsAndNsInfo getOpsAndNsInfo() { } public static final class OpsAndNsInfo extends DualMessageSequences { - private final boolean effectiveRetryWrites; + private final boolean nonCommandWriteRetryRequirementsMet; private final List models; private final BatchEncoder batchEncoder; private final ConcreteClientBulkWriteOptions options; - private final Supplier doIfCommandIsRetryableAndAdvanceGetTxnNumber; + /** + * We use {@link MemoizingLongSupplier} because the wrapped {@link LongSupplier} must be executed at most once, + * even if {@link #encodeDocuments(WritersProviderAndLimitsChecker)} is executed multiple times, + * which may happen if, for example, the command needs to be re-encoded and re-sent due to the + * {@code ReauthenticationRequired} + * error. + */ + private final MemoizingLongSupplier doIfRetryRequirementsMetAndAdvanceGetTxnNumber; @VisibleForTesting(otherwise = PACKAGE) public OpsAndNsInfo( - final boolean effectiveRetryWrites, + final boolean nonCommandWriteRetryRequirementsMet, final List models, final BatchEncoder batchEncoder, final ConcreteClientBulkWriteOptions options, - final Supplier doIfCommandIsRetryableAndAdvanceGetTxnNumber) { + final LongSupplier doIfRetryRequirementsMetAndAdvanceGetTxnNumber) { super("ops", new OpsFieldNameValidator(models), "nsInfo", NoOpFieldNameValidator.INSTANCE); - this.effectiveRetryWrites = effectiveRetryWrites; + this.nonCommandWriteRetryRequirementsMet = nonCommandWriteRetryRequirementsMet; this.models = models; this.batchEncoder = batchEncoder; this.options = options; - this.doIfCommandIsRetryableAndAdvanceGetTxnNumber = doIfCommandIsRetryableAndAdvanceGetTxnNumber; + this.doIfRetryRequirementsMetAndAdvanceGetTxnNumber = new MemoizingLongSupplier(doIfRetryRequirementsMetAndAdvanceGetTxnNumber); } @Override @@ -1005,7 +953,7 @@ public EncodeDocumentsResult encodeDocuments(final WritersProviderAndLimitsCheck batchEncoder.reset(); LinkedHashMap indexedNamespaces = new LinkedHashMap<>(); WritersProviderAndLimitsChecker.WriteResult writeResult = OK_LIMIT_NOT_REACHED; - boolean commandIsRetryable = effectiveRetryWrites; + boolean writeRetryRequirementsMet = nonCommandWriteRetryRequirementsMet; int maxModelIndexInBatch = -1; for (int modelIndexInBatch = 0; modelIndexInBatch < models.size() && writeResult == OK_LIMIT_NOT_REACHED; modelIndexInBatch++) { AbstractClientNamespacedWriteModel namespacedModel = getNamespacedModel(models, modelIndexInBatch); @@ -1027,8 +975,8 @@ public EncodeDocumentsResult encodeDocuments(final WritersProviderAndLimitsCheck batchEncoder.reset(finalModelIndexInBatch); } else { maxModelIndexInBatch = finalModelIndexInBatch; - if (commandIsRetryable && doesNotSupportRetries(namespacedModel)) { - commandIsRetryable = false; + if (writeRetryRequirementsMet && !isCommandWriteRetryRequirementsMet(namespacedModel)) { + writeRetryRequirementsMet = false; logWriteModelDoesNotSupportRetries(); } } @@ -1036,13 +984,13 @@ public EncodeDocumentsResult encodeDocuments(final WritersProviderAndLimitsCheck return new EncodeDocumentsResult( // we will execute more batches, so we must request a response to maintain the order of individual write operations options.isOrdered() && maxModelIndexInBatch < models.size() - 1, - commandIsRetryable - ? singletonList(new BsonElement("txnNumber", new BsonInt64(doIfCommandIsRetryableAndAdvanceGetTxnNumber.get()))) + writeRetryRequirementsMet + ? singletonList(new BsonElement("txnNumber", new BsonInt64(doIfRetryRequirementsMetAndAdvanceGetTxnNumber.get()))) : emptyList()); } - private static boolean doesNotSupportRetries(final AbstractClientNamespacedWriteModel model) { - return model instanceof ConcreteClientNamespacedUpdateManyModel || model instanceof ConcreteClientNamespacedDeleteManyModel; + private static boolean isCommandWriteRetryRequirementsMet(final AbstractClientNamespacedWriteModel model) { + return !(model instanceof ConcreteClientNamespacedUpdateManyModel || model instanceof ConcreteClientNamespacedDeleteManyModel); } /** @@ -1171,6 +1119,23 @@ UpdatingUpdateModsFieldValidator reset() { } } } + + private static final class MemoizingLongSupplier { + private final LongSupplier wrapped; + @Nullable + private Long supplied; + + MemoizingLongSupplier(final LongSupplier wrapped) { + this.wrapped = wrapped; + } + + public long get() { + if (supplied == null) { + supplied = wrapped.getAsLong(); + } + return supplied; + } + } } } 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 464663260c6..fccdc48be28 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/CommandOperationHelper.java +++ b/driver-core/src/main/com/mongodb/internal/operation/CommandOperationHelper.java @@ -22,33 +22,24 @@ import com.mongodb.MongoException; import com.mongodb.MongoNodeIsRecoveringException; import com.mongodb.MongoNotPrimaryException; -import com.mongodb.MongoSecurityException; -import com.mongodb.MongoServerException; import com.mongodb.MongoSocketException; import com.mongodb.WriteConcern; -import com.mongodb.assertions.Assertions; import com.mongodb.connection.ConnectionDescription; import com.mongodb.connection.ServerDescription; -import com.mongodb.internal.TimeoutContext; import com.mongodb.internal.async.function.RetryControl; import com.mongodb.internal.connection.OperationContext; -import com.mongodb.internal.connection.OperationContext.ServerDeprioritization; -import com.mongodb.internal.operation.OperationHelper.ResourceSupplierInternalException; -import com.mongodb.internal.operation.retry.AttachmentKeys; +import com.mongodb.internal.operation.SpecRetryPolicy.ExplicitMaxRetries; import com.mongodb.internal.session.SessionContext; import com.mongodb.lang.Nullable; import org.bson.BsonDocument; import java.util.List; import java.util.Optional; -import java.util.function.BinaryOperator; -import java.util.function.Supplier; +import java.util.Set; -import static com.mongodb.assertions.Assertions.assertFalse; -import static com.mongodb.assertions.Assertions.assertNotNull; -import static com.mongodb.internal.async.function.RetryControl.MAX_RETRIES; -import static com.mongodb.internal.operation.OperationHelper.LOGGER; -import static java.lang.String.format; +import static com.mongodb.internal.operation.SpecRetryPolicy.ExplicitMaxRetries.RETRIES_LIMITED_BY_DESCRIPTORS; +import static com.mongodb.internal.operation.SpecRetryPolicy.ExplicitMaxRetries.NO_RETRIES_LIMIT; +import static com.mongodb.internal.operation.SpecRetryPolicy.ExplicitMaxRetries.NO_RETRIES; import static java.util.Arrays.asList; @SuppressWarnings("overloads") @@ -78,55 +69,24 @@ BsonDocument create( ConnectionDescription connectionDescription); } - static BinaryOperator onRetryableReadAttemptFailure(final ServerDeprioritization serverDeprioritization) { - return (@Nullable Throwable previouslyChosenException, Throwable mostRecentAttemptException) -> { - serverDeprioritization.onAttemptFailure(mostRecentAttemptException); - return chooseRetryableReadException(previouslyChosenException, mostRecentAttemptException); - }; - } - - private static Throwable chooseRetryableReadException( - @Nullable final Throwable previouslyChosenException, final Throwable mostRecentAttemptException) { - assertFalse(mostRecentAttemptException instanceof ResourceSupplierInternalException); - if (previouslyChosenException == null - || mostRecentAttemptException instanceof MongoSocketException - || mostRecentAttemptException instanceof MongoServerException) { - return mostRecentAttemptException; - } else { - return previouslyChosenException; - } - } - - static BinaryOperator onRetryableWriteAttemptFailure(final ServerDeprioritization serverDeprioritization) { - return (@Nullable Throwable previouslyChosenException, Throwable mostRecentAttemptException) -> { - serverDeprioritization.onAttemptFailure(mostRecentAttemptException); - return chooseRetryableWriteException(previouslyChosenException, mostRecentAttemptException); - }; - } - - private static Throwable chooseRetryableWriteException( - @Nullable final Throwable previouslyChosenException, final Throwable mostRecentAttemptException) { - if (previouslyChosenException == null) { - if (mostRecentAttemptException instanceof ResourceSupplierInternalException) { - return mostRecentAttemptException.getCause(); - } - return mostRecentAttemptException; - } else if (mostRecentAttemptException instanceof ResourceSupplierInternalException - || (mostRecentAttemptException instanceof MongoException - && ((MongoException) mostRecentAttemptException).hasErrorLabel(NO_WRITES_PERFORMED_ERROR_LABEL))) { - return previouslyChosenException; - } else { - return mostRecentAttemptException; - } - } - /* Read Binding Helpers */ - static RetryControl initialRetryState(final boolean retry, final TimeoutContext timeoutContext) { - if (retry) { - return timeoutContext.hasTimeoutMS() ? new RetryControl() : new RetryControl(MAX_RETRIES); + /** + * See {@link SpecRetryPolicy#SpecRetryPolicy(Set, boolean, boolean, ExplicitMaxRetries, OperationContext.ServerDeprioritization)}. + */ + static RetryControl createSpecRetryControl( + final Set retryPolicyDescriptors, + final boolean effectiveRetrySetting, + final boolean retryRequirementsMaybeMet, + final OperationContext operationContext) { + ExplicitMaxRetries explicitMaxRetries; + if (effectiveRetrySetting && retryRequirementsMaybeMet) { + explicitMaxRetries = operationContext.getTimeoutContext().hasTimeoutMS() ? NO_RETRIES_LIMIT : RETRIES_LIMITED_BY_DESCRIPTORS; + } else { + explicitMaxRetries = NO_RETRIES; } - return new RetryControl(0); + return new RetryControl<>(new SpecRetryPolicy( + retryPolicyDescriptors, effectiveRetrySetting, retryRequirementsMaybeMet, explicitMaxRetries, operationContext.getServerDeprioritization())); } private static final List RETRYABLE_ERROR_CODES = asList(6, 7, 89, 91, 134, 189, 262, 9001, 13436, 13435, 11602, 11600, 10107); @@ -165,59 +125,7 @@ static boolean isNamespaceError(final Throwable t) { } } - static boolean loggingShouldAttemptToRetryRead(final RetryControl retryControl, final Throwable attemptFailure) { - assertFalse(attemptFailure instanceof ResourceSupplierInternalException); - boolean decision = isRetryableException(attemptFailure) - || (attemptFailure instanceof MongoSecurityException - && attemptFailure.getCause() != null && isRetryableException(attemptFailure.getCause())); - if (!decision) { - logUnableToRetryCommand(retryControl, attemptFailure); - } - return decision; - } - - static boolean loggingShouldAttemptToRetryWriteAndAddRetryableLabel(final RetryControl retryControl, final Throwable attemptFailure) { - Throwable attemptFailureNotToBeRetried = addRetryableLabelOrGetWriteAttemptFailureNotToBeRetried(retryControl, attemptFailure); - boolean decision = attemptFailureNotToBeRetried == null; - if (!decision && retryControl.attachment(AttachmentKeys.retryableWriteCommandFlag()).orElse(false)) { - logUnableToRetryCommand(retryControl, assertNotNull(attemptFailureNotToBeRetried)); - } - return decision; - } - - /** - * Returns {@code null} if the failed attempt should be retried; - * in this case, also adds the {@value #RETRYABLE_WRITE_ERROR_LABEL} label if needed. - * Otherwise, returns a {@link Throwable} that must not be retried. - */ - @Nullable - static Throwable addRetryableLabelOrGetWriteAttemptFailureNotToBeRetried(final RetryControl retryControl, final Throwable attemptFailure) { - Throwable failure = attemptFailure instanceof ResourceSupplierInternalException ? attemptFailure.getCause() : attemptFailure; - boolean decision = false; - MongoException exceptionRetryableRegardlessOfCommand = null; - if (failure instanceof MongoConnectionPoolClearedException - || (failure instanceof MongoSecurityException && failure.getCause() != null && isRetryableException(failure.getCause()))) { - decision = true; - exceptionRetryableRegardlessOfCommand = (MongoException) failure; - } - if (retryControl.attachment(AttachmentKeys.retryableWriteCommandFlag()).orElse(false)) { - if (exceptionRetryableRegardlessOfCommand != null) { - /* We are going to retry even if `retryableWriteCommandFlag` is false, - * but we add the retryable label only if `retryableWriteCommandFlag` is true. */ - exceptionRetryableRegardlessOfCommand.addLabel(RETRYABLE_WRITE_ERROR_LABEL); - } else if (decideRetryableAndAddRetryableWriteErrorLabel(failure, retryControl.attachment(AttachmentKeys.maxWireVersion()) - .orElse(null))) { - decision = true; - } - } - return decision ? null : assertNotNull(failure); - } - - /** - * Returns {@code true} if the {@code command} is intended to be executed outside a transaction and supports being retried, - * or if the {@code command} is {@code commitTransaction}/{@code abortTransaction}; {@code false} otherwise. - */ - static boolean isRetryableWriteCommand(final BsonDocument command) { + static boolean isWriteRetryRequirementsMet(final BsonDocument command) { // Given the requirement // https://github.com/mongodb/specifications/blame/7039e69945d463a14b1b727d16db063e21f48f53/source/transactions/transactions.md#L584-L586: // When executing the `commitTransaction` and `abortTransaction` commands within a transaction @@ -232,18 +140,7 @@ static boolean isRetryableWriteCommand(final BsonDocument command) { 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)) { - return false; - } - MongoException exception = (MongoException) t; - if (maxWireVersion != null) { - addRetryableWriteErrorLabel(exception, maxWireVersion); - } - return exception.hasErrorLabel(RETRYABLE_WRITE_ERROR_LABEL); - } - - static void addRetryableWriteErrorLabel(final MongoException exception, final int maxWireVersion) { + static void addRetryableWriteErrorLabelIfNeeded(final MongoException exception, final int maxWireVersion) { if (maxWireVersion >= 9 && exception instanceof MongoSocketException) { exception.addLabel(RETRYABLE_WRITE_ERROR_LABEL); } else if (maxWireVersion < 9 && isRetryableException(exception)) { @@ -251,29 +148,6 @@ static void addRetryableWriteErrorLabel(final MongoException exception, final in } } - static void logRetryCommand(final RetryControl retryControl, final OperationContext operationContext) { - if (LOGGER.isDebugEnabled() && !retryControl.isFirstAttempt()) { - String commandDescription = retryControl.attachment(AttachmentKeys.commandDescriptionSupplier()).map(Supplier::get).orElse(null); - Throwable exception = retryControl.exception().orElseThrow(Assertions::fail); - int oneBasedAttempt = retryControl.attempt() + 1; - long operationId = operationContext.getId(); - LOGGER.debug(commandDescription == null - ? format("Retrying a command within the operation with operation ID %s due to the error \"%s\". Attempt number: #%d", - operationId, exception, oneBasedAttempt) - : format("Retrying the command '%s' within the operation with operation ID %s due to the error \"%s\". Attempt number: #%d", - commandDescription, operationId, exception, oneBasedAttempt)); - } - } - - private static void logUnableToRetryCommand(final RetryControl retryControl, final Throwable originalError) { - if (LOGGER.isDebugEnabled()) { - String commandDescription = retryControl.attachment(AttachmentKeys.commandDescriptionSupplier()).map(Supplier::get).orElse(null); - LOGGER.debug(commandDescription == null - ? format("Unable to retry a command due to the error \"%s\"", originalError) - : format("Unable to retry the command '%s' due to the error \"%s\"", commandDescription, originalError)); - } - } - static MongoException transformWriteException(final MongoException exception) { if (exception.getCode() == 20 && exception.getMessage().contains("Transaction numbers")) { MongoException clientException = new MongoClientException("This MongoDB deployment does not support retryable writes. " diff --git a/driver-core/src/main/com/mongodb/internal/operation/CommitTransactionOperation.java b/driver-core/src/main/com/mongodb/internal/operation/CommitTransactionOperation.java index ca3c8ac5e6f..a7a60ba7206 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/CommitTransactionOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/CommitTransactionOperation.java @@ -32,13 +32,13 @@ import com.mongodb.internal.binding.AsyncWriteBinding; import com.mongodb.internal.binding.WriteBinding; import com.mongodb.internal.connection.OperationContext; +import com.mongodb.internal.operation.CommandOperationHelper.CommandCreator; import com.mongodb.lang.Nullable; import org.bson.BsonDocument; import java.util.List; import static com.mongodb.MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL; -import static com.mongodb.internal.operation.CommandOperationHelper.CommandCreator; import static com.mongodb.internal.operation.CommandOperationHelper.RETRYABLE_WRITE_ERROR_LABEL; import static java.util.Arrays.asList; import static java.util.concurrent.TimeUnit.MILLISECONDS; diff --git a/driver-core/src/main/com/mongodb/internal/operation/FindOperation.java b/driver-core/src/main/com/mongodb/internal/operation/FindOperation.java index 906a16420df..cc0021232e5 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/FindOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/FindOperation.java @@ -39,6 +39,7 @@ import org.bson.BsonValue; import org.bson.codecs.Decoder; +import java.util.EnumSet; import java.util.function.Supplier; import static com.mongodb.assertions.Assertions.notNull; @@ -46,20 +47,21 @@ import static com.mongodb.internal.connection.CommandHelper.applyMaxTimeMS; import static com.mongodb.internal.operation.AsyncOperationHelper.CommandReadTransformerAsync; import static com.mongodb.internal.operation.AsyncOperationHelper.createReadCommandAndExecuteAsync; -import static com.mongodb.internal.operation.AsyncOperationHelper.decorateReadWithRetriesAsync; +import static com.mongodb.internal.operation.AsyncOperationHelper.decorateWithRetriesAsync; import static com.mongodb.internal.operation.AsyncOperationHelper.withAsyncSourceAndConnection; import static com.mongodb.internal.operation.CommandOperationHelper.CommandCreator; -import static com.mongodb.internal.operation.CommandOperationHelper.initialRetryState; +import static com.mongodb.internal.operation.CommandOperationHelper.createSpecRetryControl; import static com.mongodb.internal.operation.DocumentHelper.putIfNotNull; import static com.mongodb.internal.operation.DocumentHelper.putIfNotNullOrEmpty; import static com.mongodb.internal.operation.ExplainHelper.asExplainCommand; import static com.mongodb.internal.operation.OperationHelper.LOGGER; -import static com.mongodb.internal.operation.OperationHelper.canRetryRead; +import static com.mongodb.internal.operation.OperationHelper.isReadRetryRequirementsMet; import static com.mongodb.internal.operation.OperationReadConcernHelper.appendReadConcernToCommand; import static com.mongodb.internal.operation.ServerVersionHelper.UNKNOWN_WIRE_VERSION; +import static com.mongodb.internal.operation.SpecRetryPolicy.Descriptor.READ; import static com.mongodb.internal.operation.SyncOperationHelper.CommandReadTransformer; import static com.mongodb.internal.operation.SyncOperationHelper.createReadCommandAndExecute; -import static com.mongodb.internal.operation.SyncOperationHelper.decorateReadWithRetries; +import static com.mongodb.internal.operation.SyncOperationHelper.decorateWithRetries; import static com.mongodb.internal.operation.SyncOperationHelper.withSourceAndConnection; /** @@ -299,13 +301,13 @@ public BatchCursor execute(final ReadBinding binding, final OperationContext } OperationContext findOperationContext = getFindOperationContext(operationContext); - RetryControl retryControl = initialRetryState(retryReads, findOperationContext.getTimeoutContext()); - Supplier> read = decorateReadWithRetries(retryControl, findOperationContext, () -> + RetryControl retryControl = createSpecRetryControl(EnumSet.of(READ), retryReads, isReadRetryRequirementsMet(retryReads, findOperationContext), findOperationContext); + Supplier> read = decorateWithRetries(retryControl, findOperationContext, () -> withSourceAndConnection(binding::getReadConnectionSource, false, findOperationContext, (source, connection, commandOperationContext) -> { - retryControl.breakAndThrowIfRetryAnd(() -> !canRetryRead(commandOperationContext)); try { - return createReadCommandAndExecute(retryControl, commandOperationContext, source, namespace.getDatabaseName(), + return createReadCommandAndExecute(retryControl, commandOperationContext, source, + namespace.getDatabaseName(), getCommandCreator(), CommandResultDocumentCodec.create(decoder, FIRST_BATCH), transformer(), connection); } catch (MongoCommandException e) { @@ -325,15 +327,12 @@ public void executeAsync(final AsyncReadBinding binding, final OperationContext } OperationContext findOperationContext = getFindOperationContext(operationContext); - RetryControl retryControl = initialRetryState(retryReads, findOperationContext.getTimeoutContext()); + RetryControl retryControl = createSpecRetryControl(EnumSet.of(READ), retryReads, isReadRetryRequirementsMet(retryReads, findOperationContext), findOperationContext); binding.retain(); - AsyncCallbackSupplier> asyncRead = decorateReadWithRetriesAsync( + AsyncCallbackSupplier> asyncRead = decorateWithRetriesAsync( retryControl, operationContext, (AsyncCallbackSupplier>) funcCallback -> withAsyncSourceAndConnection(binding::getReadConnectionSource, false, findOperationContext, funcCallback, - (source, connection, operationContextWithMinRTT, releasingCallback) -> { - if (retryControl.breakAndCompleteIfRetryAnd(() -> !canRetryRead(findOperationContext), releasingCallback)) { - return; - } + (source, connection, operationContextWithMinRTT, releasingCallback) -> { SingleResultCallback> wrappedCallback = exceptionTransformingCallback(releasingCallback); createReadCommandAndExecuteAsync(retryControl, operationContextWithMinRTT, source, namespace.getDatabaseName(), getCommandCreator(), diff --git a/driver-core/src/main/com/mongodb/internal/operation/ListCollectionsOperation.java b/driver-core/src/main/com/mongodb/internal/operation/ListCollectionsOperation.java index 340f60901a3..52d370ec481 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/ListCollectionsOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/ListCollectionsOperation.java @@ -34,6 +34,7 @@ import org.bson.codecs.Codec; import org.bson.codecs.Decoder; +import java.util.EnumSet; import java.util.function.Supplier; import static com.mongodb.internal.MongoNamespaceHelper.COMMAND_COLLECTION_NAME; @@ -43,11 +44,11 @@ import static com.mongodb.internal.operation.AsyncOperationHelper.CommandReadTransformerAsync; import static com.mongodb.internal.operation.AsyncOperationHelper.createReadCommandAndExecuteAsync; import static com.mongodb.internal.operation.AsyncOperationHelper.cursorDocumentToAsyncBatchCursor; -import static com.mongodb.internal.operation.AsyncOperationHelper.decorateReadWithRetriesAsync; +import static com.mongodb.internal.operation.AsyncOperationHelper.decorateWithRetriesAsync; import static com.mongodb.internal.operation.AsyncOperationHelper.withAsyncSourceAndConnection; import static com.mongodb.internal.operation.AsyncSingleBatchCursor.createEmptyAsyncSingleBatchCursor; import static com.mongodb.internal.operation.CommandOperationHelper.CommandCreator; -import static com.mongodb.internal.operation.CommandOperationHelper.initialRetryState; +import static com.mongodb.internal.operation.CommandOperationHelper.createSpecRetryControl; import static com.mongodb.internal.operation.CommandOperationHelper.isNamespaceError; import static com.mongodb.internal.operation.CommandOperationHelper.rethrowIfNotNamespaceError; import static com.mongodb.internal.operation.CursorHelper.getCursorDocumentFromBatchSize; @@ -55,12 +56,13 @@ import static com.mongodb.internal.operation.DocumentHelper.putIfTrue; import static com.mongodb.internal.operation.OperationHelper.LOGGER; import static com.mongodb.internal.operation.OperationHelper.applyTimeoutModeToOperationContext; -import static com.mongodb.internal.operation.OperationHelper.canRetryRead; +import static com.mongodb.internal.operation.OperationHelper.isReadRetryRequirementsMet; import static com.mongodb.internal.operation.SingleBatchCursor.createEmptySingleBatchCursor; +import static com.mongodb.internal.operation.SpecRetryPolicy.Descriptor.READ; import static com.mongodb.internal.operation.SyncOperationHelper.CommandReadTransformer; import static com.mongodb.internal.operation.SyncOperationHelper.createReadCommandAndExecute; import static com.mongodb.internal.operation.SyncOperationHelper.cursorDocumentToBatchCursor; -import static com.mongodb.internal.operation.SyncOperationHelper.decorateReadWithRetries; +import static com.mongodb.internal.operation.SyncOperationHelper.decorateWithRetries; import static com.mongodb.internal.operation.SyncOperationHelper.withSourceAndConnection; /** @@ -175,10 +177,9 @@ public String getCommandName() { public BatchCursor execute(final ReadBinding binding, final OperationContext operationContext) { OperationContext listCollectionsOperationContext = applyTimeoutModeToOperationContext(timeoutMode, operationContext); - RetryControl retryControl = initialRetryState(retryReads, listCollectionsOperationContext.getTimeoutContext()); - Supplier> read = decorateReadWithRetries(retryControl, listCollectionsOperationContext, () -> + RetryControl retryControl = createSpecRetryControl(EnumSet.of(READ), retryReads, isReadRetryRequirementsMet(retryReads, listCollectionsOperationContext), listCollectionsOperationContext); + Supplier> read = decorateWithRetries(retryControl, listCollectionsOperationContext, () -> withSourceAndConnection(binding::getReadConnectionSource, false, listCollectionsOperationContext, (source, connection, operationContextWithMinRTT) -> { - retryControl.breakAndThrowIfRetryAnd(() -> !canRetryRead(operationContextWithMinRTT)); try { return createReadCommandAndExecute(retryControl, operationContextWithMinRTT, source, databaseName, getCommandCreator(), createCommandDecoder(), transformer(), connection); @@ -196,15 +197,12 @@ public void executeAsync(final AsyncReadBinding binding, final OperationContext final SingleResultCallback> callback) { OperationContext listCollectionsOperationContext = applyTimeoutModeToOperationContext(timeoutMode, operationContext); - RetryControl retryControl = initialRetryState(retryReads, listCollectionsOperationContext.getTimeoutContext()); + RetryControl retryControl = createSpecRetryControl(EnumSet.of(READ), retryReads, isReadRetryRequirementsMet(retryReads, listCollectionsOperationContext), listCollectionsOperationContext); binding.retain(); - AsyncCallbackSupplier> asyncRead = decorateReadWithRetriesAsync( + AsyncCallbackSupplier> asyncRead = decorateWithRetriesAsync( retryControl, listCollectionsOperationContext, (AsyncCallbackSupplier>) funcCallback -> withAsyncSourceAndConnection(binding::getReadConnectionSource, false, listCollectionsOperationContext, funcCallback, (source, connection, operationContextWithMinRtt, releasingCallback) -> { - if (retryControl.breakAndCompleteIfRetryAnd(() -> !canRetryRead(operationContextWithMinRtt), releasingCallback)) { - return; - } createReadCommandAndExecuteAsync(retryControl, operationContextWithMinRtt, source, databaseName, getCommandCreator(), createCommandDecoder(), asyncTransformer(), connection, (result, t) -> { diff --git a/driver-core/src/main/com/mongodb/internal/operation/ListIndexesOperation.java b/driver-core/src/main/com/mongodb/internal/operation/ListIndexesOperation.java index 6905b968ad3..0caf03cbd94 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/ListIndexesOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/ListIndexesOperation.java @@ -33,6 +33,7 @@ import org.bson.codecs.Codec; import org.bson.codecs.Decoder; +import java.util.EnumSet; import java.util.function.Supplier; import static com.mongodb.assertions.Assertions.notNull; @@ -40,23 +41,24 @@ import static com.mongodb.internal.operation.AsyncOperationHelper.CommandReadTransformerAsync; import static com.mongodb.internal.operation.AsyncOperationHelper.createReadCommandAndExecuteAsync; import static com.mongodb.internal.operation.AsyncOperationHelper.cursorDocumentToAsyncBatchCursor; -import static com.mongodb.internal.operation.AsyncOperationHelper.decorateReadWithRetriesAsync; +import static com.mongodb.internal.operation.AsyncOperationHelper.decorateWithRetriesAsync; import static com.mongodb.internal.operation.AsyncOperationHelper.withAsyncSourceAndConnection; import static com.mongodb.internal.operation.AsyncSingleBatchCursor.createEmptyAsyncSingleBatchCursor; import static com.mongodb.internal.operation.CommandOperationHelper.CommandCreator; -import static com.mongodb.internal.operation.CommandOperationHelper.initialRetryState; +import static com.mongodb.internal.operation.CommandOperationHelper.createSpecRetryControl; import static com.mongodb.internal.operation.CommandOperationHelper.isNamespaceError; import static com.mongodb.internal.operation.CommandOperationHelper.rethrowIfNotNamespaceError; import static com.mongodb.internal.operation.CursorHelper.getCursorDocumentFromBatchSize; import static com.mongodb.internal.operation.DocumentHelper.putIfNotNull; import static com.mongodb.internal.operation.OperationHelper.LOGGER; import static com.mongodb.internal.operation.OperationHelper.applyTimeoutModeToOperationContext; -import static com.mongodb.internal.operation.OperationHelper.canRetryRead; +import static com.mongodb.internal.operation.OperationHelper.isReadRetryRequirementsMet; import static com.mongodb.internal.operation.SingleBatchCursor.createEmptySingleBatchCursor; +import static com.mongodb.internal.operation.SpecRetryPolicy.Descriptor.READ; import static com.mongodb.internal.operation.SyncOperationHelper.CommandReadTransformer; import static com.mongodb.internal.operation.SyncOperationHelper.createReadCommandAndExecute; import static com.mongodb.internal.operation.SyncOperationHelper.cursorDocumentToBatchCursor; -import static com.mongodb.internal.operation.SyncOperationHelper.decorateReadWithRetries; +import static com.mongodb.internal.operation.SyncOperationHelper.decorateWithRetries; import static com.mongodb.internal.operation.SyncOperationHelper.withSourceAndConnection; /** @@ -132,10 +134,9 @@ public MongoNamespace getNamespace() { public BatchCursor execute(final ReadBinding binding, final OperationContext operationContext) { OperationContext listIndexesOperationContext = applyTimeoutModeToOperationContext(timeoutMode, operationContext); - RetryControl retryControl = initialRetryState(retryReads, listIndexesOperationContext.getTimeoutContext()); - Supplier> read = decorateReadWithRetries(retryControl, listIndexesOperationContext, () -> + RetryControl retryControl = createSpecRetryControl(EnumSet.of(READ), retryReads, isReadRetryRequirementsMet(retryReads, listIndexesOperationContext), listIndexesOperationContext); + Supplier> read = decorateWithRetries(retryControl, listIndexesOperationContext, () -> withSourceAndConnection(binding::getReadConnectionSource, false, listIndexesOperationContext, (source, connection, operationContextWithMinRTT) -> { - retryControl.breakAndThrowIfRetryAnd(() -> !canRetryRead(operationContextWithMinRTT)); try { return createReadCommandAndExecute(retryControl, operationContextWithMinRTT, source, namespace.getDatabaseName(), getCommandCreator(), createCommandDecoder(), transformer(), connection); @@ -152,15 +153,12 @@ public BatchCursor execute(final ReadBinding binding, final OperationContext public void executeAsync(final AsyncReadBinding binding, final OperationContext operationContext, final SingleResultCallback> callback) { OperationContext listIndexesOperationContext = applyTimeoutModeToOperationContext(timeoutMode, operationContext); - RetryControl retryControl = initialRetryState(retryReads, operationContext.getTimeoutContext()); + RetryControl retryControl = createSpecRetryControl(EnumSet.of(READ), retryReads, isReadRetryRequirementsMet(retryReads, listIndexesOperationContext), listIndexesOperationContext); binding.retain(); - AsyncCallbackSupplier> asyncRead = decorateReadWithRetriesAsync( + AsyncCallbackSupplier> asyncRead = decorateWithRetriesAsync( retryControl, listIndexesOperationContext, (AsyncCallbackSupplier>) funcCallback -> withAsyncSourceAndConnection(binding::getReadConnectionSource, false, listIndexesOperationContext, funcCallback, (source, connection, operationContextWithMinRtt, releasingCallback) -> { - if (retryControl.breakAndCompleteIfRetryAnd(() -> !canRetryRead(operationContextWithMinRtt), releasingCallback)) { - return; - } createReadCommandAndExecuteAsync(retryControl, operationContextWithMinRtt, source, namespace.getDatabaseName(), getCommandCreator(), createCommandDecoder(), asyncTransformer(), connection, diff --git a/driver-core/src/main/com/mongodb/internal/operation/MixedBulkWriteOperation.java b/driver-core/src/main/com/mongodb/internal/operation/MixedBulkWriteOperation.java index 2433256c46c..931d99ab256 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/MixedBulkWriteOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/MixedBulkWriteOperation.java @@ -38,8 +38,6 @@ import com.mongodb.internal.connection.MongoWriteConcernWithResponseException; import com.mongodb.internal.connection.OperationContext; import com.mongodb.internal.connection.ProtocolHelper; -import com.mongodb.internal.operation.retry.AttachmentKeys; -import com.mongodb.internal.session.SessionContext; import com.mongodb.internal.validator.NoOpFieldNameValidator; import com.mongodb.lang.Nullable; import org.bson.BsonArray; @@ -47,6 +45,7 @@ import org.bson.BsonString; import org.bson.BsonValue; +import java.util.EnumSet; import java.util.List; import java.util.Locale; import java.util.Set; @@ -59,16 +58,16 @@ import static com.mongodb.assertions.Assertions.isTrueArgument; import static com.mongodb.assertions.Assertions.notNull; import static com.mongodb.internal.async.AsyncRunnable.beginAsync; -import static com.mongodb.internal.operation.AsyncOperationHelper.decorateWriteWithRetriesAsync; +import static com.mongodb.internal.operation.AsyncOperationHelper.decorateWithRetriesAsync; import static com.mongodb.internal.operation.AsyncOperationHelper.withAsyncSourceAndConnection; -import static com.mongodb.internal.operation.CommandOperationHelper.addRetryableWriteErrorLabel; -import static com.mongodb.internal.operation.CommandOperationHelper.addRetryableLabelOrGetWriteAttemptFailureNotToBeRetried; -import static com.mongodb.internal.operation.CommandOperationHelper.initialRetryState; +import static com.mongodb.internal.operation.CommandOperationHelper.addRetryableWriteErrorLabelIfNeeded; +import static com.mongodb.internal.operation.CommandOperationHelper.createSpecRetryControl; import static com.mongodb.internal.operation.CommandOperationHelper.transformWriteException; import static com.mongodb.internal.operation.CommandOperationHelper.validateAndGetEffectiveWriteConcern; -import static com.mongodb.internal.operation.OperationHelper.isRetryableWrite; +import static com.mongodb.internal.operation.OperationHelper.isServerWriteRetryRequirementsMet; import static com.mongodb.internal.operation.OperationHelper.validateWriteRequests; -import static com.mongodb.internal.operation.SyncOperationHelper.decorateWriteWithRetries; +import static com.mongodb.internal.operation.SpecRetryPolicy.Descriptor.WRITE; +import static com.mongodb.internal.operation.SyncOperationHelper.decorateWithRetries; import static com.mongodb.internal.operation.SyncOperationHelper.withSourceAndConnection; /** @@ -241,9 +240,9 @@ private BatchWithSourceAndConnection executeBatchReusingCon final OperationContext operationContext) { MutableValue batch = new MutableValue<>(maybeBatch); MutableValue sourceAndConnection = new MutableValue<>(maybeSourceAndConnection); - RetryControl retryControl = initialRetryState( - retryWrites, operationContext.getTimeoutContext()); - Supplier> retryingBatchExecutor = decorateWriteWithRetries( + RetryControl retryControl = createSpecRetryControl( + EnumSet.of(WRITE), retryWrites, retryWrites, operationContext); + Supplier> retryingBatchExecutor = decorateWithRetries( retryControl, operationContext, () -> { @@ -253,11 +252,10 @@ private BatchWithSourceAndConnection executeBatchReusingCon try { sourceAndConnection.set(reusedOrNewSourceAndConnection); ConnectionDescription connectionDescription = reusedOrNewSourceAndConnection.getConnection().getDescription(); - retryControl.attach(AttachmentKeys.maxWireVersion(), connectionDescription.getMaxWireVersion(), true); batch.set(batch.getNullable() != null ? batch.get() : createFirstBatch(connectionDescription, reusedOrNewSourceAndConnection.getOperationContext(), effectiveWriteConcern)); - onBatch(batch.get(), retryControl); + onBatch(batch.get(), retryControl, connectionDescription); executeBatch(batch.get(), reusedOrNewSourceAndConnection, effectiveWriteConcern); return new BatchWithSourceAndConnection<>(batch.get().getNextBatch(), reusedOrNewSourceAndConnection); } catch (Throwable e) { @@ -276,12 +274,6 @@ private BatchWithSourceAndConnection executeBatchReusingCon batch.get().addResult((BsonDocument) ((MongoWriteConcernWithResponseException) e).getResponse()); return new BatchWithSourceAndConnection<>(batch.get().getNextBatch(), null); } - if (retryWrites && e instanceof MongoException) { - // Adding the `RetryableWriteError` label here is unnecessary at this point: - // applications cannot use it for implementing retries, and it is not even part of the public driver API. - // Unfortunately, certain unified tests incorrectly rely on this label to verify retries, resulting in this redundant code. - addRetryableLabelOrGetWriteAttemptFailureNotToBeRetried(retryControl, e); - } throw e; } } @@ -300,9 +292,9 @@ private void executeBatchReusingConnectionAsync( beginAsync().>thenSupply(c -> { MutableValue batch = new MutableValue<>(maybeBatch); MutableValue sourceAndConnection = new MutableValue<>(maybeSourceAndConnection); - RetryControl retryControl = initialRetryState( - retryWrites, operationContext.getTimeoutContext()); - AsyncCallbackSupplier> retryingBatchExecutor = decorateWriteWithRetriesAsync( + RetryControl retryControl = createSpecRetryControl( + EnumSet.of(WRITE), retryWrites, retryWrites, operationContext); + AsyncCallbackSupplier> retryingBatchExecutor = decorateWithRetriesAsync( retryControl, operationContext, supplierCallback -> { @@ -314,11 +306,10 @@ private void executeBatchReusingConnectionAsync( beginAsync().thenRun(executeBatchCallback -> { sourceAndConnection.set(reusedOrNewSourceAndConnection); ConnectionDescription connectionDescription = reusedOrNewSourceAndConnection.getConnection().getDescription(); - retryControl.attach(AttachmentKeys.maxWireVersion(), connectionDescription.getMaxWireVersion(), true); batch.set(batch.getNullable() != null ? batch.get() : createFirstBatch(connectionDescription, reusedOrNewSourceAndConnection.getOperationContext(), effectiveWriteConcern)); - onBatch(batch.get(), retryControl); + onBatch(batch.get(), retryControl, connectionDescription); executeBatchAsync(batch.get(), reusedOrNewSourceAndConnection, effectiveWriteConcern, executeBatchCallback); }).>thenSupply(createNextBatchCallback -> { createNextBatchCallback.complete(new BatchWithSourceAndConnection<>(batch.get().getNextBatch(), reusedOrNewSourceAndConnection)); @@ -340,12 +331,6 @@ private void executeBatchReusingConnectionAsync( onErrorCallback.complete(new BatchWithSourceAndConnection<>(batch.get().getNextBatch(), null)); return; } - if (retryWrites && e instanceof MongoException) { - // Adding the `RetryableWriteError` label here is unnecessary at this point: - // applications cannot use it for implementing retries, and it is not even part of the public driver API. - // Unfortunately, certain unified tests incorrectly rely on this label to verify retries, resulting in this redundant code. - addRetryableLabelOrGetWriteAttemptFailureNotToBeRetried(retryControl, e); - } onErrorCallback.completeExceptionally(e); }).finish(c); }).finish(callback); @@ -356,14 +341,11 @@ private SourceAndConnection reuseOrSelectServerAndCheckoutConnectionIfClosed( final WriteConcern effectiveWriteConcern, final WriteBinding binding, final OperationContext operationContext, - final RetryControl retryControl) { + final RetryControl retryControl) { if (sourceAndConnection == null || sourceAndConnection.isClosed()) { SourceAndConnection newSourceAndConnection = selectServerAndCheckoutConnection(binding, operationContext); try { - onNewConnection( - newSourceAndConnection.getConnection().getDescription(), - newSourceAndConnection.getOperationContext().getSessionContext(), - effectiveWriteConcern, retryControl); + onNewConnection(newSourceAndConnection.getConnection().getDescription(), effectiveWriteConcern, retryControl); } catch (Throwable e) { newSourceAndConnection.close(); throw e; @@ -379,7 +361,7 @@ private void reuseOrSelectServerAndCheckoutConnectionIfClosedAsync( final WriteConcern effectiveWriteConcern, final AsyncWriteBinding binding, final OperationContext operationContext, - final RetryControl retryControl, + final RetryControl retryControl, final SingleResultCallback callback) { beginAsync().thenSupply(c -> { if (sourceAndConnection == null || sourceAndConnection.isClosed()) { @@ -387,10 +369,7 @@ private void reuseOrSelectServerAndCheckoutConnectionIfClosedAsync( selectServerAndCheckoutConnectionAsync(binding, operationContext, selectServerAndCheckoutConnectionCallback); }).thenApply((newSourceAndConnection, onNewConnectionCallback) -> { try { - onNewConnection( - newSourceAndConnection.getConnection().getDescription(), - newSourceAndConnection.getOperationContext().getSessionContext(), - effectiveWriteConcern, retryControl); + onNewConnection(newSourceAndConnection.getConnection().getDescription(), effectiveWriteConcern, retryControl); } catch (Throwable e) { newSourceAndConnection.close(); throw e; @@ -431,12 +410,9 @@ private static void selectServerAndCheckoutConnectionAsync( private void onNewConnection( final ConnectionDescription connectionDescription, - final SessionContext sessionContext, final WriteConcern effectiveWriteConcern, - final RetryControl retryControl) { - boolean effectiveRetryWrites = isRetryableWrite( - retryWrites, effectiveWriteConcern, connectionDescription, sessionContext); - retryControl.breakAndThrowIfRetryAnd(() -> !effectiveRetryWrites); + final RetryControl retryControl) { + retryControl.breakAndThrowIfRetryAnd(() -> !isServerWriteRetryRequirementsMet(connectionDescription)); validateWriteRequests(connectionDescription, bypassDocumentValidation, writeRequests, effectiveWriteConcern); } @@ -450,11 +426,15 @@ private BulkWriteBatch createFirstBatch( writeRequests, operationContext, comment, variables); } - private void onBatch(final BulkWriteBatch batch, final RetryControl retryControl) { + private void onBatch( + final BulkWriteBatch batch, + final RetryControl retryControl, + final ConnectionDescription connectionDescription) { commandName = batch.getCommand().getFirstKey(); String commandDescriptionToCapture = commandName; - retryControl.attach(AttachmentKeys.retryableWriteCommandFlag(), batch.getRetryWrites(), false) - .attach(AttachmentKeys.commandDescriptionSupplier(), () -> commandDescriptionToCapture, false); + retryControl.getPolicy() + .onCommand(() -> commandDescriptionToCapture) + .onWriteRetryRequirements(batch.isWriteRetryRequirementsMet(), connectionDescription); } private void executeBatch( @@ -501,12 +481,12 @@ private static void addResultOrThrowWriteConcernWithResponseException( @Nullable final BsonDocument result, final ConnectionDescription connectionDescription, final OperationContext operationContext) throws MongoWriteConcernWithResponseException { - if (batch.getRetryWrites()) { + if (batch.isWriteRetryRequirementsMet()) { MongoException writeConcernBasedError = ProtocolHelper.createSpecialException( result, connectionDescription.getServerAddress(), "errMsg", operationContext.getTimeoutContext()); if (writeConcernBasedError != null) { assertNotNull(result); - addRetryableWriteErrorLabel(writeConcernBasedError, connectionDescription.getMaxWireVersion()); + addRetryableWriteErrorLabelIfNeeded(writeConcernBasedError, connectionDescription.getMaxWireVersion()); addErrorLabelsToWriteConcern(result.getDocument("writeConcernError"), writeConcernBasedError.getErrorLabels()); throw new MongoWriteConcernWithResponseException(writeConcernBasedError, result); } diff --git a/driver-core/src/main/com/mongodb/internal/operation/OperationHelper.java b/driver-core/src/main/com/mongodb/internal/operation/OperationHelper.java index 1b183019848..a8eb4a17439 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/OperationHelper.java +++ b/driver-core/src/main/com/mongodb/internal/operation/OperationHelper.java @@ -144,9 +144,9 @@ private static void checkBypassDocumentValidationIsSupported(@Nullable final Boo } } - static boolean isRetryableWrite(final boolean retryWrites, final WriteConcern writeConcern, + static boolean isNonCommandWriteRetryRequirementsMet(final boolean retryWritesSetting, final WriteConcern writeConcern, final ConnectionDescription connectionDescription, final SessionContext sessionContext) { - if (!retryWrites) { + if (!retryWritesSetting) { return false; } else if (!writeConcern.isAcknowledged()) { LOGGER.debug("retryWrites set to true but the writeConcern is unacknowledged."); @@ -155,11 +155,11 @@ static boolean isRetryableWrite(final boolean retryWrites, final WriteConcern wr LOGGER.debug("retryWrites set to true but in an active transaction."); return false; } else { - return canRetryWrite(connectionDescription); + return isServerWriteRetryRequirementsMet(connectionDescription); } } - static boolean canRetryWrite(final ConnectionDescription connectionDescription) { + static boolean isServerWriteRetryRequirementsMet(final ConnectionDescription connectionDescription) { if (connectionDescription.getLogicalSessionTimeoutMinutes() == null) { LOGGER.debug("retryWrites set to true but the server does not support sessions."); return false; @@ -170,7 +170,10 @@ static boolean canRetryWrite(final ConnectionDescription connectionDescription) return true; } - static boolean canRetryRead(final OperationContext operationContext) { + static boolean isReadRetryRequirementsMet(final boolean retryReadsSetting, final OperationContext operationContext) { + if (!retryReadsSetting) { + return false; + } if (operationContext.getSessionContext().hasActiveTransaction()) { LOGGER.debug("retryReads set to true but in an active transaction."); return false; diff --git a/driver-core/src/main/com/mongodb/internal/operation/SpecRetryPolicy.java b/driver-core/src/main/com/mongodb/internal/operation/SpecRetryPolicy.java new file mode 100644 index 00000000000..1615123cf53 --- /dev/null +++ b/driver-core/src/main/com/mongodb/internal/operation/SpecRetryPolicy.java @@ -0,0 +1,382 @@ +/* + * 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.operation; + +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoConnectionPoolClearedException; +import com.mongodb.MongoException; +import com.mongodb.MongoOperationTimeoutException; +import com.mongodb.MongoSecurityException; +import com.mongodb.MongoServerException; +import com.mongodb.MongoSocketException; +import com.mongodb.assertions.Assertions; +import com.mongodb.connection.ConnectionDescription; +import com.mongodb.internal.async.function.RetryContext; +import com.mongodb.internal.async.function.RetryPolicy; +import com.mongodb.internal.async.function.RetryPolicy.Decision.RetryAttemptInfo; +import com.mongodb.internal.connection.OperationContext; +import com.mongodb.internal.connection.OperationContext.ServerDeprioritization; +import com.mongodb.lang.Nullable; + +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.Set; +import java.util.function.Supplier; + +import static com.mongodb.assertions.Assertions.assertFalse; +import static com.mongodb.assertions.Assertions.assertNotNull; +import static com.mongodb.assertions.Assertions.assertNull; +import static com.mongodb.assertions.Assertions.assertTrue; +import static com.mongodb.assertions.Assertions.fail; +import static com.mongodb.internal.TimeoutContext.createMongoTimeoutException; +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 com.mongodb.internal.operation.CommandOperationHelper.addRetryableWriteErrorLabelIfNeeded; +import static com.mongodb.internal.operation.CommandOperationHelper.isRetryableException; +import static com.mongodb.internal.operation.OperationHelper.LOGGER; +import static com.mongodb.internal.operation.SpecRetryPolicy.Descriptor.READ; +import static com.mongodb.internal.operation.SpecRetryPolicy.Descriptor.WRITE; +import static com.mongodb.internal.operation.SpecRetryPolicy.Descriptor.assertNoConflicts; +import static com.mongodb.internal.operation.SpecRetryPolicy.ExplicitMaxRetries.NO_RETRIES; +import static java.lang.Boolean.TRUE; +import static java.lang.String.format; + +/** + * Implements all specification retry policies. + */ +final class SpecRetryPolicy implements RetryPolicy { + private static final int INFINITE_ATTEMPTS = Integer.MAX_VALUE; + + private final Set descriptors; + private final boolean effectiveRetrySetting; + private final boolean retryRequirementsMaybeMet; + private final ExplicitMaxRetries explicitMaxRetries; + private final int maxAttempts; + private final ServerDeprioritization serverDeprioritization; + private Supplier commandDescriptionSupplier; + @Nullable + private Boolean writeRetryRequirementsMet; + @Nullable + private Integer maxWireVersion; + + /** + * @param descriptors A set of descriptors of the applicable specification retry policies. + * @param effectiveRetrySetting See {@link MongoClientSettings#getRetryWrites()}, {@link MongoClientSettings#getRetryReads()}. + * Note that for some commands, like {@code commitTransaction}/{@code abortTransaction}, + * retries are deemed to be enabled regardless of the settings; this argument must be {@code true} for them. + * @param retryRequirementsMaybeMet {@code false} iff the retry requirements are known not to be met. + * For example, if {@code effectiveRetrySetting} is {@code false}, then {@code retryRequirementsMaybeMet} must be {@code false}. + *

    + * For {@link Descriptor#READ}, this parameter specifies whether the read retry requirements are met; + * for {@link Descriptor#WRITE}, see {@link #onWriteRetryRequirements(boolean, ConnectionDescription)}. + */ + SpecRetryPolicy( + final Set descriptors, + final boolean effectiveRetrySetting, + final boolean retryRequirementsMaybeMet, + final ExplicitMaxRetries explicitMaxRetries, + final ServerDeprioritization serverDeprioritization) { + this.descriptors = assertNoConflicts(descriptors); + assertTrue(effectiveRetrySetting || !retryRequirementsMaybeMet); + this.effectiveRetrySetting = effectiveRetrySetting; + this.retryRequirementsMaybeMet = retryRequirementsMaybeMet; + if (!retryRequirementsMaybeMet) { + assertTrue(explicitMaxRetries == NO_RETRIES); + } + this.explicitMaxRetries = explicitMaxRetries; + this.maxAttempts = explicitMaxRetries.maxAttempts(descriptors); + this.serverDeprioritization = serverDeprioritization; + commandDescriptionSupplier = () -> null; + writeRetryRequirementsMet = null; + maxWireVersion = null; + } + + void onAttemptStart(final RetryContext retryContext, final OperationContext operationContext) { + if (LOGGER.isDebugEnabled() && !retryContext.isFirstAttempt()) { + String commandDescription = commandDescriptionSupplier.get(); + long operationId = operationContext.getId(); + Throwable prospectiveFailedResult = retryContext.getProspectiveFailedResult().orElseThrow(Assertions::fail); + int oneBasedAttempt = retryContext.attempt() + 1; + LOGGER.debug(commandDescription == null + ? format("Retrying a command within the operation with operation ID %s due to the error \"%s\". Retry attempt number: #%d", + operationId, prospectiveFailedResult, oneBasedAttempt) + : format("Retrying the command '%s' within the operation with operation ID %s due to the error \"%s\". Retry attempt number: #%d", + commandDescription, operationId, prospectiveFailedResult, oneBasedAttempt)); + } + } + + /** + * @return {@code this}. + */ + SpecRetryPolicy onCommand(final Supplier commandDescriptionSupplier) { + this.commandDescriptionSupplier = commandDescriptionSupplier; + return this; + } + + /** + * The information gathered via this method is reset after each invocation of {@link #onAttemptFailure(RetryContext, Throwable)}. + * + * @param remainingWriteRequirementsMet This argument, combined with + * {@link SpecRetryPolicy#SpecRetryPolicy(Set, boolean, boolean, ExplicitMaxRetries, ServerDeprioritization) retryRequirementsMaybeMet}, + * specifies whether the write retry requirements are met. + *

    + * Specifying {@code false}, or not calling this method at all, does not completely prevent retrying, + * but affects logging and which failed results may be eligible for retry. + * For example, {@link MongoConnectionPoolClearedException} may be eligible for retry regardless of this flag. + * @return {@code this}. + */ + SpecRetryPolicy onWriteRetryRequirements(final boolean remainingWriteRequirementsMet, final ConnectionDescription connectionDescription) { + assertNull(writeRetryRequirementsMet); + assertTrue(descriptors.contains(WRITE)); + writeRetryRequirementsMet = retryRequirementsMaybeMet && remainingWriteRequirementsMet; + maxWireVersion = connectionDescription.getMaxWireVersion(); + return this; + } + + private void resetWriteRetryRequirementsInfo() { + maxWireVersion = null; + writeRetryRequirementsMet = null; + } + + @Override + public Decision onAttemptFailure(final RetryContext retryContext, final Throwable attemptFailedResult) { + serverDeprioritization.onAttemptFailure(attemptFailedResult); + int attempt = retryContext.attempt(); + assertTrue(attempt < INFINITE_ATTEMPTS); + assertTrue(attempt < maxAttempts); + boolean retryableError; + if (descriptors.contains(WRITE)) { + retryableError = decideRetryableAndAddRetryableWriteErrorLabelIfNeeded(attemptFailedResult); + } else if (descriptors.contains(READ)) { + retryableError = isRetryableReadError(attemptFailedResult); + } else { + throw fail(descriptors.toString()); + } + boolean maxAttemptsReached = attempt == maxAttempts - 1; + if (retryRequirementsMaybeMet) { + if (retryableError && maxAttemptsReached) { + logUnableToRetryMaxAttemptsReached(); + } else if (!retryableError) { + logUnableToRetryError(attemptFailedResult); + } + } + boolean retry = retryableError && !maxAttemptsReached; + Decision decision = new Decision( + decideProspectiveFailedResult(retryContext.getProspectiveFailedResult().orElse(null), attemptFailedResult), + retry ? new RetryAttemptInfo() : null); + resetWriteRetryRequirementsInfo(); + return decision; + } + + /** + * Returns {@code true} iff another attempt must be executed; + * in this case, also adds the {@value CommandOperationHelper#RETRYABLE_WRITE_ERROR_LABEL} label if needed. + */ + private boolean decideRetryableAndAddRetryableWriteErrorLabelIfNeeded(final Throwable maybeInternalException) { + Throwable exception = stripResourceSupplierInternalException(maybeInternalException); + if (!(exception instanceof MongoException)) { + return false; + } + MongoException mongoException = (MongoException) exception; + boolean connectionPoolClearedException = mongoException instanceof MongoConnectionPoolClearedException; + if (connectionPoolClearedException && effectiveRetrySetting) { + // We would have retried regardless of the settings, + // but we add `RETRYABLE_WRITE_ERROR_LABEL` only if retries are enabled via settings. + mongoException.addLabel(RETRYABLE_WRITE_ERROR_LABEL); + } + boolean retryRegardlessOfRequirementsHavingBeenMet = connectionPoolClearedException || isRetryableMongoSecurityException(mongoException); + boolean retry; + if (TRUE.equals(writeRetryRequirementsMet)) { + addRetryableWriteErrorLabelIfNeeded(mongoException, assertNotNull(maxWireVersion)); + retry = mongoException.hasErrorLabel(RETRYABLE_WRITE_ERROR_LABEL); + } else { + retry = retryRegardlessOfRequirementsHavingBeenMet; + } + return retry; + } + + private static Throwable stripResourceSupplierInternalException(final Throwable maybeInternal) { + Throwable external; + if (maybeInternal instanceof OperationHelper.ResourceSupplierInternalException) { + external = maybeInternal.getCause(); + } else { + external = maybeInternal; + } + return external; + } + + private static boolean isRetryableMongoSecurityException(final MongoException exception) { + return exception instanceof MongoSecurityException + && exception.getCause() != null && isRetryableException(exception.getCause()); + } + + private static boolean isRetryableReadError(final Throwable exception) { + assertFalse(exception instanceof OperationHelper.ResourceSupplierInternalException); + if (!(exception instanceof MongoException)) { + return false; + } + MongoException mongoException = (MongoException) exception; + return isRetryableMongoSecurityException(mongoException) || isRetryableException(mongoException); + } + + private void logUnableToRetryMaxAttemptsReached() { + if (LOGGER.isDebugEnabled()) { + String commandDescription = commandDescriptionSupplier.get(); + int maxRetries = ExplicitMaxRetries.maxRetries(maxAttempts); + LOGGER.debug(commandDescription == null + ? format("Unable to retry a command due to reaching the max retry attempts limit of %d", maxRetries) + : format("Unable to retry the command '%s' due to reaching the max retry attempts limit of %d", commandDescription, maxRetries)); + } + } + + private void logUnableToRetryError(final Throwable maybeInternalException) { + if (LOGGER.isDebugEnabled()) { + Throwable exception = stripResourceSupplierInternalException(maybeInternalException); + String commandDescription = commandDescriptionSupplier.get(); + LOGGER.debug(commandDescription == null + ? format("Unable to retry a command due to the error \"%s\"", exception) + : format("Unable to retry the command '%s' due to the error \"%s\"", commandDescription, exception)); + } + } + + private Throwable decideProspectiveFailedResult( + @Nullable final Throwable currentProspectiveFailedResult, final Throwable mostRecentAttemptFailedResult) { + Throwable newProspectiveFailedResult; + if (descriptors.contains(WRITE)) { + newProspectiveFailedResult = decideWriteProspectiveFailedResult(currentProspectiveFailedResult, mostRecentAttemptFailedResult); + } else if (descriptors.contains(READ)) { + newProspectiveFailedResult = decideReadProspectiveFailedResult(currentProspectiveFailedResult, mostRecentAttemptFailedResult); + } else { + throw fail(descriptors.toString()); + } + if (mostRecentAttemptFailedResult instanceof MongoOperationTimeoutException) { + newProspectiveFailedResult = createMongoTimeoutException(newProspectiveFailedResult); + } + return newProspectiveFailedResult; + } + + private static Throwable decideWriteProspectiveFailedResult( + @Nullable final Throwable currentProspectiveFailedResult, final Throwable mostRecentAttemptFailedResult) { + if (currentProspectiveFailedResult == null) { + return stripResourceSupplierInternalException(mostRecentAttemptFailedResult); + } else if (mostRecentAttemptFailedResult instanceof OperationHelper.ResourceSupplierInternalException + || (mostRecentAttemptFailedResult instanceof MongoException + && ((MongoException) mostRecentAttemptFailedResult).hasErrorLabel(NO_WRITES_PERFORMED_ERROR_LABEL))) { + return currentProspectiveFailedResult; + } else { + return mostRecentAttemptFailedResult; + } + } + + private static Throwable decideReadProspectiveFailedResult( + @Nullable final Throwable currentProspectiveFailedResult, final Throwable mostRecentAttemptFailedResult) { + assertFalse(mostRecentAttemptFailedResult instanceof OperationHelper.ResourceSupplierInternalException); + if (currentProspectiveFailedResult == null + || mostRecentAttemptFailedResult instanceof MongoSocketException + || mostRecentAttemptFailedResult instanceof MongoServerException) { + return mostRecentAttemptFailedResult; + } else { + return currentProspectiveFailedResult; + } + } + + @Override + public String toString() { + return "SpecRetryPolicy{" + + "descriptors=" + descriptors + + ", effectiveRetrySetting=" + effectiveRetrySetting + + ", retryRequirementsMaybeMet=" + retryRequirementsMaybeMet + + ", explicitMaxRetries=" + explicitMaxRetries + + ", maxAttempts=" + maxAttempts + + ", serverDeprioritization=" + serverDeprioritization + + ", commandDescription=" + commandDescriptionSupplier.get() + + ", writeRetryRequirementsMet=" + writeRetryRequirementsMet + + ", maxWireVersion=" + maxWireVersion + + '}'; + } + + enum Descriptor { + /** + * See Retryable Writes. + */ + WRITE, + /** + * See Retryable Reads. + */ + READ; + + private static final EnumMap> CONFLICTS; + + static { + CONFLICTS = new EnumMap<>(Descriptor.class); + CONFLICTS.put(WRITE, EnumSet.of(READ)); + CONFLICTS.put(READ, EnumSet.of(WRITE)); + } + + static Set assertNoConflicts(final Set descriptors) { + for (Descriptor descriptor : descriptors) { + for (Descriptor conflictingDescriptor : CONFLICTS.get(descriptor)) { + assertFalse(descriptors.contains(conflictingDescriptor)); + } + } + return descriptors; + } + } + + enum ExplicitMaxRetries { + NO_RETRIES_LIMIT, + /** + * See {@link Descriptor}. + */ + RETRIES_LIMITED_BY_DESCRIPTORS, + NO_RETRIES; + + private int maxAttempts(final Set descriptors) { + switch (this) { + case NO_RETRIES_LIMIT: { + return INFINITE_ATTEMPTS; + } + case RETRIES_LIMITED_BY_DESCRIPTORS: { + return maxAttempts(maxRetries(descriptors)); + } + case NO_RETRIES: { + return maxAttempts(0); + } + default: { + throw fail(this.toString()); + } + } + } + + private static int maxRetries(final Set descriptors) { + if (descriptors.contains(WRITE) || descriptors.contains(READ)) { + return 1; + } else { + throw fail(descriptors.toString()); + } + } + + private static int maxAttempts(final int maxRetries) { + assertTrue(maxRetries < INFINITE_ATTEMPTS - 1); + return maxRetries + 1; + } + + static int maxRetries(final int maxAttempts) { + assertTrue(maxAttempts > 0); + return maxAttempts - 1; + } + } +} diff --git a/driver-core/src/main/com/mongodb/internal/operation/SyncOperationHelper.java b/driver-core/src/main/com/mongodb/internal/operation/SyncOperationHelper.java index e6368e0f4a4..f41c286628e 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/SyncOperationHelper.java +++ b/driver-core/src/main/com/mongodb/internal/operation/SyncOperationHelper.java @@ -19,8 +19,10 @@ import com.mongodb.MongoException; import com.mongodb.ReadPreference; import com.mongodb.client.cursor.TimeoutMode; +import com.mongodb.connection.ConnectionDescription; import com.mongodb.internal.TimeoutContext; import com.mongodb.internal.VisibleForTesting; +import com.mongodb.internal.async.MutableValue; import com.mongodb.internal.async.function.RetryControl; import com.mongodb.internal.async.function.RetryingSyncSupplier; import com.mongodb.internal.binding.ConnectionSource; @@ -29,7 +31,6 @@ import com.mongodb.internal.binding.WriteBinding; import com.mongodb.internal.connection.Connection; import com.mongodb.internal.connection.OperationContext; -import com.mongodb.internal.operation.retry.AttachmentKeys; import com.mongodb.internal.session.SessionContext; import com.mongodb.internal.validator.NoOpFieldNameValidator; import com.mongodb.lang.Nullable; @@ -39,6 +40,7 @@ import org.bson.codecs.BsonDocumentCodec; import org.bson.codecs.Decoder; +import java.util.EnumSet; import java.util.function.Function; import java.util.function.Supplier; @@ -48,13 +50,14 @@ import static com.mongodb.assertions.Assertions.notNull; import static com.mongodb.internal.VisibleForTesting.AccessModifier.PRIVATE; import static com.mongodb.internal.operation.CommandOperationHelper.CommandCreator; -import static com.mongodb.internal.operation.CommandOperationHelper.isRetryableWriteCommand; -import static com.mongodb.internal.operation.CommandOperationHelper.logRetryCommand; -import static com.mongodb.internal.operation.CommandOperationHelper.onRetryableReadAttemptFailure; -import static com.mongodb.internal.operation.CommandOperationHelper.onRetryableWriteAttemptFailure; +import static com.mongodb.internal.operation.CommandOperationHelper.createSpecRetryControl; +import static com.mongodb.internal.operation.CommandOperationHelper.isWriteRetryRequirementsMet; +import static com.mongodb.internal.operation.CommandOperationHelper.transformWriteException; import static com.mongodb.internal.operation.OperationHelper.ResourceSupplierInternalException; -import static com.mongodb.internal.operation.OperationHelper.canRetryRead; -import static com.mongodb.internal.operation.OperationHelper.canRetryWrite; +import static com.mongodb.internal.operation.OperationHelper.isReadRetryRequirementsMet; +import static com.mongodb.internal.operation.OperationHelper.isServerWriteRetryRequirementsMet; +import static com.mongodb.internal.operation.SpecRetryPolicy.Descriptor.READ; +import static com.mongodb.internal.operation.SpecRetryPolicy.Descriptor.WRITE; import static com.mongodb.internal.operation.WriteConcernHelper.throwOnWriteConcernError; final class SyncOperationHelper { @@ -188,9 +191,9 @@ static T executeRetryableRead( final CommandCreator commandCreator, final Decoder decoder, final CommandReadTransformer transformer, - final boolean retryReads) { + final boolean retryReadsSetting) { return executeRetryableRead(operationContext, binding::getReadConnectionSource, database, commandCreator, - decoder, transformer, retryReads); + decoder, transformer, retryReadsSetting); } static T executeRetryableRead( @@ -200,12 +203,11 @@ static T executeRetryableRead( final CommandCreator commandCreator, final Decoder decoder, final CommandReadTransformer transformer, - final boolean retryReads) { - RetryControl retryControl = CommandOperationHelper.initialRetryState(retryReads, operationContext.getTimeoutContext()); + final boolean retryReadsSetting) { + RetryControl retryControl = createSpecRetryControl(EnumSet.of(READ), retryReadsSetting, isReadRetryRequirementsMet(retryReadsSetting, operationContext), operationContext); - Supplier read = decorateReadWithRetries(retryControl, operationContext, () -> + Supplier read = decorateWithRetries(retryControl, operationContext, () -> withSourceAndConnection(readConnectionSourceSupplier, false, operationContext, (source, connection, operationContextWithMinRtt) -> { - retryControl.breakAndThrowIfRetryAnd(() -> !canRetryRead(operationContextWithMinRtt)); return createReadCommandAndExecute(retryControl, operationContextWithMinRtt, source, database, commandCreator, decoder, transformer, connection); }) @@ -249,6 +251,9 @@ static T executeCommand(final WriteBinding binding, final OperationContext o connection); } + /** + * @param effectiveRetryWritesSetting See {@link SpecRetryPolicy}. + */ static R executeRetryableWrite( final WriteBinding binding, final OperationContext operationContext, @@ -258,50 +263,43 @@ static R executeRetryableWrite( final Decoder commandResultDecoder, final CommandCreator commandCreator, final CommandWriteTransformer transformer, - final com.mongodb.Function retryCommandModifier) { - RetryControl retryControl = CommandOperationHelper.initialRetryState(true, operationContext.getTimeoutContext()); - Supplier retryingWrite = decorateWriteWithRetries(retryControl, operationContext, () -> { + final com.mongodb.Function retryCommandModifier, + final boolean effectiveRetryWritesSetting) { + MutableValue command = new MutableValue<>(); + RetryControl retryControl = createSpecRetryControl(EnumSet.of(WRITE), effectiveRetryWritesSetting, effectiveRetryWritesSetting, operationContext); + Supplier retryingWrite = decorateWithRetries(retryControl, operationContext, () -> { boolean firstAttempt = retryControl.isFirstAttempt(); SessionContext sessionContext = operationContext.getSessionContext(); if (!firstAttempt && sessionContext.hasActiveTransaction()) { sessionContext.clearTransactionContext(); } return withSourceAndConnection(binding::getWriteConnectionSource, true, operationContext, (source, connection, operationContextWithMinRtt) -> { - int maxWireVersion = connection.getDescription().getMaxWireVersion(); - try { - retryControl.breakAndThrowIfRetryAnd(() -> !canRetryWrite(connection.getDescription())); - BsonDocument command = retryControl.attachment(AttachmentKeys.command()) - .map(previousAttemptCommand -> { - assertFalse(firstAttempt); - return retryCommandModifier.apply(previousAttemptCommand); - }).orElseGet(() -> commandCreator.create(operationContextWithMinRtt, source.getServerDescription(), - connection.getDescription())); - // attach `maxWireVersion`, `retryableWriteCommandFlag` ASAP because they are used to check whether we should retry - retryControl.attach(AttachmentKeys.maxWireVersion(), maxWireVersion, true) - .attach(AttachmentKeys.retryableWriteCommandFlag(), isRetryableWriteCommand(command), true) - .attach(AttachmentKeys.commandDescriptionSupplier(), command::getFirstKey, false) - .attach(AttachmentKeys.command(), command, false); - return transformer.apply(assertNotNull(connection.command(database, command, fieldNameValidator, readPreference, - commandResultDecoder, operationContextWithMinRtt)), - connection); - } catch (MongoException e) { - if (!firstAttempt) { - CommandOperationHelper.addRetryableWriteErrorLabel(e, maxWireVersion); + ConnectionDescription connectionDescription = connection.getDescription(); + retryControl.breakAndThrowIfRetryAnd(() -> !isServerWriteRetryRequirementsMet(connectionDescription)); + if (command.getNullable() == null) { + command.set(commandCreator.create(operationContextWithMinRtt, source.getServerDescription(), connectionDescription)); + } else { + assertFalse(firstAttempt); + command.set(retryCommandModifier.apply(command.get())); } - throw e; - } + retryControl.getPolicy() + .onCommand(() -> command.get().getFirstKey()) + .onWriteRetryRequirements(isWriteRetryRequirementsMet(command.get()), connectionDescription); + T result = connection.command(database, command.get(), fieldNameValidator, readPreference, + commandResultDecoder, operationContextWithMinRtt); + return transformer.apply(assertNotNull(result), connection); }); }); try { return retryingWrite.get(); } catch (MongoException e) { - throw CommandOperationHelper.transformWriteException(e); + throw transformWriteException(e); } } @Nullable static T createReadCommandAndExecute( - final RetryControl retryControl, + final RetryControl retryControl, final OperationContext operationContext, final ConnectionSource source, final String database, @@ -311,7 +309,7 @@ static T createReadCommandAndExecute( final Connection connection) { BsonDocument command = commandCreator.create(operationContext, source.getServerDescription(), connection.getDescription()); - retryControl.attach(AttachmentKeys.commandDescriptionSupplier(), command::getFirstKey, false); + retryControl.getPolicy().onCommand(command::getFirstKey); D result = assertNotNull(connection.command(database, command, NoOpFieldNameValidator.INSTANCE, source.getReadPreference(), decoder, operationContext)); @@ -319,22 +317,13 @@ static T createReadCommandAndExecute( return transformer.apply(result, source, connection, operationContext); } - - static Supplier decorateWriteWithRetries(final RetryControl retryControl, - final OperationContext operationContext, final Supplier writeFunction) { - return new RetryingSyncSupplier<>(retryControl, onRetryableWriteAttemptFailure(operationContext.getServerDeprioritization()), - CommandOperationHelper::loggingShouldAttemptToRetryWriteAndAddRetryableLabel, () -> { - logRetryCommand(retryControl, operationContext); - return writeFunction.get(); - }); - } - - static Supplier decorateReadWithRetries(final RetryControl retryControl, final OperationContext operationContext, - final Supplier readFunction) { - return new RetryingSyncSupplier<>(retryControl, onRetryableReadAttemptFailure(operationContext.getServerDeprioritization()), - CommandOperationHelper::loggingShouldAttemptToRetryRead, () -> { - logRetryCommand(retryControl, operationContext); - return readFunction.get(); + static Supplier decorateWithRetries( + final RetryControl retryControl, + final OperationContext operationContext, + final Supplier supplier) { + return new RetryingSyncSupplier<>(retryControl, () -> { + retryControl.getPolicy().onAttemptStart(retryControl, operationContext); + return supplier.get(); }); } diff --git a/driver-core/src/main/com/mongodb/internal/operation/TransactionOperation.java b/driver-core/src/main/com/mongodb/internal/operation/TransactionOperation.java index 703d440fb04..6ee79ab6a17 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/TransactionOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/TransactionOperation.java @@ -60,7 +60,7 @@ public Void execute(final WriteBinding binding, final OperationContext operation TimeoutContext timeoutContext = operationContext.getTimeoutContext(); return executeRetryableWrite(binding, operationContext, "admin", null, NoOpFieldNameValidator.INSTANCE, new BsonDocumentCodec(), getCommandCreator(), - writeConcernErrorTransformer(timeoutContext), getRetryCommandModifier(timeoutContext)); + writeConcernErrorTransformer(timeoutContext), getRetryCommandModifier(timeoutContext), true); } @Override @@ -70,6 +70,7 @@ public void executeAsync(final AsyncWriteBinding binding, final OperationContext executeRetryableWriteAsync(binding, operationContext, "admin", null, NoOpFieldNameValidator.INSTANCE, new BsonDocumentCodec(), getCommandCreator(), writeConcernErrorTransformerAsync(timeoutContext), getRetryCommandModifier(timeoutContext), + true, errorHandlingCallback(callback, LOGGER)); } diff --git a/driver-core/src/main/com/mongodb/internal/operation/WriteConcernHelper.java b/driver-core/src/main/com/mongodb/internal/operation/WriteConcernHelper.java index 10b02eda4fe..602206bcca1 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/WriteConcernHelper.java +++ b/driver-core/src/main/com/mongodb/internal/operation/WriteConcernHelper.java @@ -32,7 +32,7 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static com.mongodb.internal.operation.CommandOperationHelper.addRetryableWriteErrorLabel; +import static com.mongodb.internal.operation.CommandOperationHelper.addRetryableWriteErrorLabelIfNeeded; /** * This class is NOT part of the public API. It may change at any time without notification. @@ -67,7 +67,7 @@ public static void throwOnWriteConcernError(final BsonDocument result, final Ser if (exception == null) { exception = createWriteConcernException(result, serverAddress); } - addRetryableWriteErrorLabel(exception, maxWireVersion); + addRetryableWriteErrorLabelIfNeeded(exception, maxWireVersion); throw exception; } } diff --git a/driver-core/src/main/com/mongodb/internal/operation/retry/AttachmentKeys.java b/driver-core/src/main/com/mongodb/internal/operation/retry/AttachmentKeys.java deleted file mode 100644 index d3c5cc32346..00000000000 --- a/driver-core/src/main/com/mongodb/internal/operation/retry/AttachmentKeys.java +++ /dev/null @@ -1,108 +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.operation.retry; - -import com.mongodb.MongoConnectionPoolClearedException; -import com.mongodb.annotations.Immutable; -import com.mongodb.internal.async.function.LoopControl.AttachmentKey; -import org.bson.BsonDocument; - -import java.util.HashSet; -import java.util.Set; -import java.util.function.Supplier; - -import static com.mongodb.assertions.Assertions.assertTrue; -import static com.mongodb.assertions.Assertions.fail; - -/** - * A class with {@code static} methods providing access to {@link AttachmentKey}s relevant when implementing retryable operations. - * - *

    This class is not part of the public API and may be removed or changed at any time

    - * - * @see AttachmentKey - */ -public final class AttachmentKeys { - private static final AttachmentKey MAX_WIRE_VERSION = DefaultAttachmentKey.of("maxWireVersion"); - private static final AttachmentKey COMMAND = DefaultAttachmentKey.of("command"); - private static final AttachmentKey RETRYABLE_WRITE_COMMAND_FLAG = DefaultAttachmentKey.of("retryableWriteCommandFlag"); - private static final AttachmentKey> COMMAND_DESCRIPTION_SUPPLIER = DefaultAttachmentKey.of("commandDescriptionSupplier"); - - public static AttachmentKey maxWireVersion() { - return MAX_WIRE_VERSION; - } - - public static AttachmentKey command() { - return COMMAND; - } - - /** - * Setting this flag to {@code false}, or leaving it unset, does not completely disable retrying, - * but does change which failed results may be eligible for retry. - * For example, {@link MongoConnectionPoolClearedException} may be eligible for retry regardless of this flag. - */ - public static AttachmentKey retryableWriteCommandFlag() { - return RETRYABLE_WRITE_COMMAND_FLAG; - } - - public static AttachmentKey> commandDescriptionSupplier() { - return COMMAND_DESCRIPTION_SUPPLIER; - } - - private AttachmentKeys() { - fail(); - } - - /** - * A value-based class. - */ - @Immutable - private static final class DefaultAttachmentKey implements AttachmentKey { - private static final Set AVOID_KEY_DUPLICATION = new HashSet<>(); - - private final String key; - - private DefaultAttachmentKey(final String key) { - assertTrue(AVOID_KEY_DUPLICATION.add(key)); - this.key = key; - } - - static DefaultAttachmentKey of(final String key) { - return new DefaultAttachmentKey<>(key); - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - DefaultAttachmentKey that = (DefaultAttachmentKey) o; - return key.equals(that.key); - } - - @Override - public int hashCode() { - return key.hashCode(); - } - - @Override - public String toString() { - return key; - } - } -} diff --git a/driver-core/src/main/com/mongodb/internal/operation/retry/package-info.java b/driver-core/src/main/com/mongodb/internal/operation/retry/package-info.java deleted file mode 100644 index 29c27a47914..00000000000 --- a/driver-core/src/main/com/mongodb/internal/operation/retry/package-info.java +++ /dev/null @@ -1,25 +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. - */ - -/** - * This package contains internal functionality that may change at any time. - */ -@Internal -@NonNullApi -package com.mongodb.internal.operation.retry; - -import com.mongodb.annotations.Internal; -import com.mongodb.lang.NonNullApi; diff --git a/driver-core/src/test/functional/com/mongodb/internal/operation/MixedBulkWriteOperationSpecification.groovy b/driver-core/src/test/functional/com/mongodb/internal/operation/MixedBulkWriteOperationSpecification.groovy index 13fc8ea33c1..a0a30842630 100644 --- a/driver-core/src/test/functional/com/mongodb/internal/operation/MixedBulkWriteOperationSpecification.groovy +++ b/driver-core/src/test/functional/com/mongodb/internal/operation/MixedBulkWriteOperationSpecification.groovy @@ -1118,7 +1118,7 @@ class MixedBulkWriteOperationSpecification extends OperationFunctionalSpecificat when: testRetryableOperationThrowsOriginalError(operation, [[3, 6, 0], [3, 6, 0]], - [REPLICA_SET_PRIMARY, REPLICA_SET_PRIMARY, STANDALONE], originalException, async, 4) + [REPLICA_SET_PRIMARY, STANDALONE], originalException, async, 4) then: Exception commandException = thrown() @@ -1126,7 +1126,7 @@ class MixedBulkWriteOperationSpecification extends OperationFunctionalSpecificat when: testRetryableOperationThrowsOriginalError(operation, [[3, 6, 0]], - [REPLICA_SET_PRIMARY, REPLICA_SET_PRIMARY], originalException, async, 2) + [REPLICA_SET_PRIMARY], originalException, async, 2) then: commandException = thrown() diff --git a/driver-core/src/test/unit/com/mongodb/internal/async/function/LoopControlTest.java b/driver-core/src/test/unit/com/mongodb/internal/async/function/LoopControlTest.java index 84e07d48d5b..66615087a5b 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/async/function/LoopControlTest.java +++ b/driver-core/src/test/unit/com/mongodb/internal/async/function/LoopControlTest.java @@ -16,13 +16,12 @@ package com.mongodb.internal.async.function; import com.mongodb.client.syncadapter.SupplyingCallback; -import com.mongodb.internal.async.function.LoopControl.AttachmentKey; -import com.mongodb.internal.operation.retry.AttachmentKeys; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -49,7 +48,7 @@ void iterationsAndAdvance() { } @Test - void maskAsLastIteration() { + void markAsLastIteration() { LoopControl loopControl = new LoopControl(); loopControl.markAsLastIteration(); assertTrue(loopControl.isLastIteration()); @@ -76,27 +75,10 @@ void breakAndCompleteIfTrue() { void breakAndCompleteIfPredicateThrows() { LoopControl loopControl = new LoopControl(); SupplyingCallback callback = new SupplyingCallback<>(); - RuntimeException e = new RuntimeException() { - }; + RuntimeException e = new RuntimeException(); assertTrue(loopControl.breakAndCompleteIf(() -> { throw e; }, callback)); - assertThrows(e.getClass(), callback::get); - } - - @Test - void attachAndAttachment() { - LoopControl loopControl = new LoopControl(); - AttachmentKey attachmentKey = AttachmentKeys.maxWireVersion(); - int attachmentValue = 1; - assertFalse(loopControl.attachment(attachmentKey).isPresent()); - loopControl.attach(attachmentKey, attachmentValue, false); - assertEquals(attachmentValue, loopControl.attachment(attachmentKey).get()); - loopControl.advance(); - assertEquals(attachmentValue, loopControl.attachment(attachmentKey).get()); - loopControl.attach(attachmentKey, attachmentValue, true); - assertEquals(attachmentValue, loopControl.attachment(attachmentKey).get()); - loopControl.advance(); - assertFalse(loopControl.attachment(attachmentKey).isPresent()); + assertSame(e, assertThrows(e.getClass(), callback::get)); } } diff --git a/driver-core/src/test/unit/com/mongodb/internal/async/function/RetryControlTest.java b/driver-core/src/test/unit/com/mongodb/internal/async/function/RetryControlTest.java index d5a450ac26b..edf0f900401 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/async/function/RetryControlTest.java +++ b/driver-core/src/test/unit/com/mongodb/internal/async/function/RetryControlTest.java @@ -15,408 +15,179 @@ */ package com.mongodb.internal.async.function; -import com.mongodb.MongoOperationTimeoutException; -import com.mongodb.client.syncadapter.SupplyingCallback; -import com.mongodb.internal.TimeoutContext; -import com.mongodb.internal.async.function.LoopControl.AttachmentKey; -import com.mongodb.internal.operation.retry.AttachmentKeys; -import org.junit.jupiter.api.Assertions; +import com.mongodb.internal.async.function.RetryPolicy.Decision; +import com.mongodb.internal.async.function.RetryPolicy.Decision.RetryAttemptInfo; +import com.mongodb.internal.mockito.MongoMockito; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; -import java.util.function.BiPredicate; -import java.util.function.BinaryOperator; -import java.util.stream.Stream; +import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; -import static org.junit.jupiter.api.Named.named; -import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; final class RetryControlTest { - private static final String EXPECTED_TIMEOUT_MESSAGE = "Retry attempt exceeded the timeout limit."; - - private static Stream atMostTwoRetriesAndUnlimitedRetries() { - return Stream.of( - arguments(named("at most two retries", new RetryControl(2))), - arguments(named("unlimited retries", new RetryControl()))); + @Test + void isFirstAttempt() { + RetryControl retryControl = new RetryControl<>((retryContext, attemptFailedResult) -> + new Decision(attemptFailedResult, new RetryAttemptInfo())); + assertTrue(retryControl.isFirstAttempt()); + retryControl.advanceOrThrow(new RuntimeException()); + assertFalse(retryControl.isFirstAttempt()); } - private static Stream noRetries() { - return Stream.of( - arguments(named("no retries", new RetryControl(0)))); + @Test + void attempt() { + RetryControl retryControl = new RetryControl<>((retryContext, attemptFailedResult) -> + new Decision(attemptFailedResult, new RetryAttemptInfo())); + assertEquals(0, retryControl.attempt()); + retryControl.advanceOrThrow(new RuntimeException()); + assertEquals(1, retryControl.attempt()); + assertThrows(Throwable.class, () -> retryControl.breakAndThrowIfRetryAnd(() -> true)); + assertEquals(1, retryControl.attempt()); } @Test - void unlimitedAttemptsAndAdvance() { - final RetryControl retryControl = new RetryControl(); - RuntimeException attemptException = new RuntimeException(); - assertAll( - () -> assertTrue(retryControl.isFirstAttempt()), - () -> assertEquals(0, retryControl.attempt()) - ); - retryControl.advanceOrThrow(attemptException, (e1, e2) -> e2, (rs, e) -> true); - assertAll( - () -> assertFalse(retryControl.isFirstAttempt()), - () -> assertEquals(1, retryControl.attempt()) - ); + void getPolicy() { + RetryPolicy retryPolicy = (retryContext, attemptFailedResult) -> new Decision(attemptFailedResult, new RetryAttemptInfo()); + RetryControl retryControl = new RetryControl<>(retryPolicy); + assertSame(retryPolicy, retryControl.getPolicy()); } @Test - void limitedAttemptsAndAdvance() { - RetryControl retryControl = new RetryControl(0); - RuntimeException attemptException = new RuntimeException(); + void advanceOrThrowPassesCorrectArgumentsToAttemptFailure() { + RetryPolicy retryPolicy = MongoMockito.mock(RetryPolicy.class, retryPolicyMock -> { + when(retryPolicyMock.onAttemptFailure(any(), any())).thenReturn(new Decision(new RuntimeException(), new RetryAttemptInfo())); + }); + RetryControl retryControl = new RetryControl<>(retryPolicy); + RuntimeException attemptFailedResult = new RuntimeException(); + retryControl.advanceOrThrow(attemptFailedResult); + @SuppressWarnings("unchecked") + ArgumentCaptor> retryControlArgumentCaptor = ArgumentCaptor.forClass(RetryControl.class); + ArgumentCaptor attemptFailedResultArgumentCaptor = ArgumentCaptor.forClass(Throwable.class); + verify(retryPolicy).onAttemptFailure(retryControlArgumentCaptor.capture(), attemptFailedResultArgumentCaptor.capture()); assertAll( - () -> assertTrue(retryControl.isFirstAttempt()), - () -> assertEquals(0, retryControl.attempt()), - () -> assertAdvanceOrThrowThrows(attemptException, retryControl, attemptException), - // when there is only one attempt, it is both the first and the last one - () -> assertTrue(retryControl.isFirstAttempt()), - () -> assertEquals(0, retryControl.attempt()) + () -> assertSame(retryControl, retryControlArgumentCaptor.getValue()), + () -> assertSame(attemptFailedResult, attemptFailedResultArgumentCaptor.getValue()) ); } - @ParameterizedTest - @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void breakAndThrowIfRetryAndFirstAttempt(final RetryControl retryControl) { - retryControl.breakAndThrowIfRetryAnd(Assertions::fail); - assertAdvanceOrThrowDoesNotThrow(retryControl, new RuntimeException()); - } - - @ParameterizedTest - @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void breakAndThrowIfRetryAndFalse(final RetryControl retryControl) { - advance(retryControl); - retryControl.breakAndThrowIfRetryAnd(() -> false); - assertAdvanceOrThrowDoesNotThrow(retryControl, new RuntimeException()); - } - - @ParameterizedTest - @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void breakAndThrowIfRetryAndTrue(final RetryControl retryControl) { - advance(retryControl); - assertThrows(RuntimeException.class, () -> retryControl.breakAndThrowIfRetryAnd(() -> true)); - RuntimeException attemptException = new RuntimeException(); - assertAdvanceOrThrowThrows(attemptException, retryControl, attemptException); - } - - @ParameterizedTest - @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void breakAndThrowIfRetryIfPredicateThrows(final RetryControl retryControl) { - advance(retryControl); - RuntimeException exception = new RuntimeException(); - assertSame( - exception, - assertThrows(exception.getClass(), () -> retryControl.breakAndThrowIfRetryAnd(() -> { - throw exception; - }))); - assertAdvanceOrThrowDoesNotThrow(retryControl, exception); - } - - @ParameterizedTest - @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void breakAndCompleteIfRetryAndFirstAttempt(final RetryControl retryControl) { - SupplyingCallback callback = new SupplyingCallback<>(); - assertFalse(retryControl.breakAndCompleteIfRetryAnd(Assertions::fail, callback)); - assertFalse(callback.completed()); - assertAdvanceOrThrowDoesNotThrow(retryControl, new RuntimeException()); - } - - @ParameterizedTest - @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void breakAndCompleteIfRetryAndFalse(final RetryControl retryControl) { - advance(retryControl); - SupplyingCallback callback = new SupplyingCallback<>(); - assertFalse(retryControl.breakAndCompleteIfRetryAnd(() -> false, callback)); - assertFalse(callback.completed()); - assertAdvanceOrThrowDoesNotThrow(retryControl, new RuntimeException()); - } - - @ParameterizedTest - @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void breakAndCompleteIfRetryAndTrue(final RetryControl retryControl) { - advance(retryControl); - SupplyingCallback callback = new SupplyingCallback<>(); - assertTrue(retryControl.breakAndCompleteIfRetryAnd(() -> true, callback)); - assertThrows(RuntimeException.class, callback::get); - RuntimeException attemptException = new RuntimeException(); - assertAdvanceOrThrowThrows(attemptException, retryControl, attemptException); - } - - @ParameterizedTest - @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void breakAndCompleteIfRetryAndPredicateThrows(final RetryControl retryControl) { - advance(retryControl); - Error exception = new Error(); - SupplyingCallback callback = new SupplyingCallback<>(); - assertTrue(retryControl.breakAndCompleteIfRetryAnd(() -> { - throw exception; - }, callback)); - assertSame( - exception, - assertThrows(exception.getClass(), callback::get)); - assertAdvanceOrThrowDoesNotThrow(retryControl, exception); + @Test + void advanceOrThrowReturnsIfAnotherAttempt() { + RetryAttemptInfo immediateNextAttemptInfo = new RetryAttemptInfo(); + RetryPolicy retryPolicy = (retryContext, attemptFailedResult) -> new Decision(attemptFailedResult, immediateNextAttemptInfo); + RetryControl retryControl = new RetryControl<>(retryPolicy); + RetryAttemptInfo actualImmediateNextAttemptInfo = retryControl.advanceOrThrow(new RuntimeException()); + assertSame(immediateNextAttemptInfo, actualImmediateNextAttemptInfo); } - @ParameterizedTest - @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void advanceOrThrowPredicateFalse(final RetryControl retryControl) { - RuntimeException attemptException = new RuntimeException(); - assertAdvanceOrThrowThrows(attemptException, retryControl, attemptException, (rs, e) -> false); + @Test + void advanceOrThrowThrowsIfNoMoreAttempts() { + RuntimeException prospectiveFailedResult = new RuntimeException(); + RetryPolicy retryPolicy = (retryContext, attemptFailedResult) -> new Decision(prospectiveFailedResult, null); + RetryControl retryControl = new RetryControl<>(retryPolicy); + assertSame(prospectiveFailedResult, + assertThrows(prospectiveFailedResult.getClass(), () -> retryControl.advanceOrThrow(new RuntimeException()))); } - @ParameterizedTest - @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - @DisplayName("should rethrow detected timeout exception") - void advanceReThrowDetectedTimeoutException(final RetryControl retryControl) { - MongoOperationTimeoutException expectedTimeoutException = TimeoutContext.createMongoTimeoutException("Server selection failed"); - assertAdvanceOrThrowThrows(expectedTimeoutException, retryControl, expectedTimeoutException, - (e1, e2) -> expectedTimeoutException, - (rs, e) -> false); + @Test + void advanceOrThrowThrowsIfLastAttempt() { + RuntimeException prospectiveFailedResult = new RuntimeException(); + RetryPolicy retryPolicy = (retryContext, attemptFailedResult) -> new Decision(prospectiveFailedResult, new RetryAttemptInfo()); + RetryControl retryControl = new RetryControl<>(retryPolicy); + retryControl.advanceOrThrow(new RuntimeException()); + assertThrows(prospectiveFailedResult.getClass(), () -> retryControl.breakAndThrowIfRetryAnd(() -> true)); + assertSame(prospectiveFailedResult, + assertThrows(prospectiveFailedResult.getClass(), () -> retryControl.advanceOrThrow(new RuntimeException()))); } @Test - @DisplayName("should throw timeout exception from retry, when transformer swallows original timeout exception") - void advanceThrowTimeoutExceptionWhenTransformerSwallowOriginalTimeoutException() { - RetryControl retryControl = new RetryControl(); - RuntimeException previousAttemptException = new RuntimeException(); - MongoOperationTimeoutException latestAttemptException = TimeoutContext.createMongoTimeoutException("Server selection failed"); - - retryControl.advanceOrThrow(previousAttemptException, - (e1, e2) -> previousAttemptException, - (rs, e) -> true); - - MongoOperationTimeoutException actualTimeoutException = - assertThrows(MongoOperationTimeoutException.class, () -> retryControl.advanceOrThrow(latestAttemptException, - (e1, e2) -> previousAttemptException, - (rs, e) -> false)); - - assertNotEquals(latestAttemptException, actualTimeoutException); - assertEquals(EXPECTED_TIMEOUT_MESSAGE, actualTimeoutException.getMessage()); - assertSame(previousAttemptException, actualTimeoutException.getCause(), - "Retry timeout exception should have a cause if transformer returned non-timeout exception."); + void advanceOrThrowStoresProspectiveFailedResult() { + RuntimeException prospectiveFailedResult = new RuntimeException(); + RetryPolicy retryPolicy = (retryContext, attemptFailedResult) -> new Decision(prospectiveFailedResult, new RetryAttemptInfo()); + RetryControl retryControl = new RetryControl<>(retryPolicy); + retryControl.advanceOrThrow(new RuntimeException()); + Optional actualProspectiveFailedResult = retryControl.getProspectiveFailedResult(); + if (actualProspectiveFailedResult.isPresent()) { + assertSame(prospectiveFailedResult, actualProspectiveFailedResult.get()); + } else { + fail(); + } } - @Test - @DisplayName("should throw original timeout exception from retry, when transformer returns original timeout exception") - void advanceThrowOriginalTimeoutExceptionWhenTransformerReturnsOriginalTimeoutException() { - RetryControl retryControl = new RetryControl(); - RuntimeException previousAttemptException = new RuntimeException(); - MongoOperationTimeoutException expectedTimeoutException = TimeoutContext - .createMongoTimeoutException("Server selection failed"); - - retryControl.advanceOrThrow(previousAttemptException, - (e1, e2) -> previousAttemptException, - (rs, e) -> true); - - assertAdvanceOrThrowThrows(expectedTimeoutException, retryControl, expectedTimeoutException, - (e1, e2) -> expectedTimeoutException, - (rs, e) -> false); + void advanceOrThrowOverwritesProspectiveFailedResult() { + RetryPolicy retryPolicy = (retryContext, attemptFailedResult) -> new Decision(attemptFailedResult, new RetryAttemptInfo()); + RetryControl retryControl = new RetryControl<>(retryPolicy); + retryControl.advanceOrThrow(new RuntimeException()); + RuntimeException prospectiveFailedResult = new RuntimeException(); + retryControl.advanceOrThrow(prospectiveFailedResult); + Optional actualProspectiveFailedResult = retryControl.getProspectiveFailedResult(); + if (actualProspectiveFailedResult.isPresent()) { + assertSame(prospectiveFailedResult, actualProspectiveFailedResult.get()); + } else { + fail(); + } } @Test - void advanceOrThrowPredicateTrueAndLastAttempt() { - RetryControl retryControl = new RetryControl(0); - Error attemptException = new Error(); - assertAdvanceOrThrowThrows(attemptException, retryControl, attemptException); + @DisplayName("breakAndThrowIfRetryAnd does nothing if first attempt") + void breakAndThrowIfRetryAndDoesNothingIfFirstAttempt() { + RetryControl retryControl = new RetryControl<>((retryContext, attemptFailedResult) -> + new Decision(attemptFailedResult, new RetryAttemptInfo())); + assertDoesNotThrow(() -> retryControl.breakAndThrowIfRetryAnd(() -> true)); } - @ParameterizedTest - @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void advanceOrThrowPredicateThrowsAfterFirstAttempt(final RetryControl retryControl) { - RuntimeException predicateException = new RuntimeException(); - RuntimeException attemptException = new RuntimeException(); - assertAdvanceOrThrowThrows(predicateException, retryControl, attemptException, - (e1, e2) -> e2, - (rs, e) -> { - assertTrue(rs.isFirstAttempt()); - assertSame(attemptException, e); - throw predicateException; - }); + @Test + @DisplayName("breakAndThrowIfRetryAnd throws if not first attempt") + void breakAndThrowIfRetryAndThrowsIfNotFirstAttempt() { + RuntimeException prospectiveFailedResult = new RuntimeException(); + RetryPolicy retryPolicy = (retryContext, attemptFailedResult) -> new Decision(prospectiveFailedResult, new RetryAttemptInfo()); + RetryControl retryControl = new RetryControl<>(retryPolicy); + retryControl.advanceOrThrow(new RuntimeException()); + assertSame(prospectiveFailedResult, + assertThrows(prospectiveFailedResult.getClass(), () -> retryControl.breakAndThrowIfRetryAnd(() -> true))); } @Test - void advanceOrThrowPredicateThrowsTimeoutAfterFirstAttempt() { - RetryControl retryControl = new RetryControl(); + @DisplayName("breakAndThrowIfRetryAnd propagates if predicate throws") + void breakAndThrowIfRetryAndPropagatesIfPredicateThrows() { + RetryControl retryControl = new RetryControl<>((retryContext, attemptFailedResult) -> + new Decision(attemptFailedResult, new RetryAttemptInfo())); + retryControl.advanceOrThrow(new RuntimeException()); RuntimeException predicateException = new RuntimeException(); - RuntimeException attemptException = new MongoOperationTimeoutException(EXPECTED_TIMEOUT_MESSAGE); - MongoOperationTimeoutException mongoOperationTimeoutException = assertThrows(MongoOperationTimeoutException.class, - () -> retryControl.advanceOrThrow(attemptException, (e1, e2) -> e2, (rs, e) -> { - assertTrue(rs.isFirstAttempt()); - assertSame(attemptException, e); - throw predicateException; - })); - - assertEquals(EXPECTED_TIMEOUT_MESSAGE, mongoOperationTimeoutException.getMessage()); - assertNull(mongoOperationTimeoutException.getCause()); + assertSame(predicateException, + assertThrows(predicateException.getClass(), + () -> retryControl.breakAndThrowIfRetryAnd(() -> { + throw predicateException; + }))); } - @ParameterizedTest - @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void advanceOrThrowPredicateThrows(final RetryControl retryControl) { - RuntimeException firstAttemptException = new RuntimeException(); - retryControl.advanceOrThrow(firstAttemptException, (e1, e2) -> e2, (rs, e) -> true); - RuntimeException secondAttemptException = new RuntimeException(); + @Test + @DisplayName("breakAndThrowIfRetryAnd adds suppressed prospective failed result if predicate throws") + void breakAndThrowIfRetryAndAddsSuppressedProspectiveFailedResultIfPredicateThrows() { + RuntimeException prospectiveFailedResult = new RuntimeException(); + RetryControl retryControl = new RetryControl<>((retryContext, attemptFailedResult) -> + new Decision(prospectiveFailedResult, new RetryAttemptInfo())); + retryControl.advanceOrThrow(new RuntimeException()); RuntimeException predicateException = new RuntimeException(); - assertAdvanceOrThrowThrows(predicateException, retryControl, secondAttemptException, - (e1, e2) -> e2, - (rs, e) -> { - assertEquals(1, rs.attempt()); - assertSame(secondAttemptException, e); + Throwable[] suppressed = assertThrows(predicateException.getClass(), + () -> retryControl.breakAndThrowIfRetryAnd(() -> { throw predicateException; - }); - } - - @ParameterizedTest - @MethodSource({"noRetries", "atMostTwoRetriesAndUnlimitedRetries"}) - void advanceOrThrowTransformerThrowsAfterFirstAttempt(final RetryControl retryControl) { - RuntimeException transformerException = new RuntimeException(); - assertAdvanceOrThrowThrows(transformerException, retryControl, new AssertionError(), - (e1, e2) -> { - throw transformerException; - }, - (rs, e) -> fail()); - } - - @ParameterizedTest - @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void advanceOrThrowTransformerThrows(final RetryControl retryControl) throws Throwable { - Error firstAttemptException = new Error(); - retryControl.advanceOrThrow(firstAttemptException, (e1, e2) -> e2, (rs, e) -> true); - RuntimeException transformerException = new RuntimeException(); - assertAdvanceOrThrowThrows(transformerException, retryControl, new AssertionError(), - (e1, e2) -> { - throw transformerException; - }, - (rs, e) -> fail()); - } - - @ParameterizedTest - @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void advanceOrThrowTransformAfterFirstAttempt(final RetryControl retryControl) { - RuntimeException attemptException = new RuntimeException(); - RuntimeException transformerResult = new RuntimeException(); - assertAdvanceOrThrowThrows(transformerResult, retryControl, attemptException, - (e1, e2) -> { - assertNull(e1); - assertSame(attemptException, e2); - return transformerResult; - }, - (rs, e) -> { - assertSame(attemptException, e); - return false; - }); - } - - @Test - void advanceOrThrowTransformThrowsTimeoutExceptionAfterFirstAttempt() { - RetryControl retryControl = new RetryControl(); - - RuntimeException attemptException = new MongoOperationTimeoutException(EXPECTED_TIMEOUT_MESSAGE); - RuntimeException transformerResult = new RuntimeException(); - - MongoOperationTimeoutException mongoOperationTimeoutException = - assertThrows(MongoOperationTimeoutException.class, () -> retryControl.advanceOrThrow(attemptException, - (e1, e2) -> { - assertNull(e1); - assertSame(attemptException, e2); - return transformerResult; - }, - (rs, e) -> { - assertSame(attemptException, e); - return false; - })); - - assertEquals(EXPECTED_TIMEOUT_MESSAGE, mongoOperationTimeoutException.getMessage()); - assertSame(transformerResult, mongoOperationTimeoutException.getCause()); - } - - @ParameterizedTest - @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void advanceOrThrowTransform(final RetryControl retryControl) { - RuntimeException firstAttemptException = new RuntimeException(); - retryControl.advanceOrThrow(firstAttemptException, (e1, e2) -> e2, (rs, e) -> true); - RuntimeException secondAttemptException = new RuntimeException(); - RuntimeException transformerResult = new RuntimeException(); - assertAdvanceOrThrowThrows(transformerResult, retryControl, secondAttemptException, - (e1, e2) -> { - assertSame(firstAttemptException, e1); - assertSame(secondAttemptException, e2); - return transformerResult; - }, - (rs, e) -> { - assertSame(secondAttemptException, e); - return false; - }); - } - - @ParameterizedTest - @MethodSource({"atMostTwoRetriesAndUnlimitedRetries"}) - void attachAndAttachment(final RetryControl retryControl) { - AttachmentKey attachmentKey = AttachmentKeys.maxWireVersion(); - int attachmentValue = 1; - assertFalse(retryControl.attachment(attachmentKey).isPresent()); - retryControl.attach(attachmentKey, attachmentValue, false); - assertEquals(attachmentValue, retryControl.attachment(attachmentKey).get()); - advance(retryControl); - assertEquals(attachmentValue, retryControl.attachment(attachmentKey).get()); - retryControl.attach(attachmentKey, attachmentValue, true); - assertEquals(attachmentValue, retryControl.attachment(attachmentKey).get()); - advance(retryControl); - assertFalse(retryControl.attachment(attachmentKey).isPresent()); - } - - private static void advance(final RetryControl retryControl) { - retryControl.advanceOrThrow(new RuntimeException(), (e1, e2) -> e2, (rs, e) -> true); - } - - private static void assertAdvanceOrThrowDoesNotThrow( - final RetryControl retryControl, - final Throwable attemptException) { - assertDoesNotThrow(() -> retryControl.advanceOrThrow(attemptException, (e1, e2) -> e2, (rs, e) -> true)); - } - - private static void assertAdvanceOrThrowThrows( - final Throwable expectedException, - final RetryControl retryControl, - final Throwable attemptException) { - assertAdvanceOrThrowThrows( - com.mongodb.assertions.Assertions.assertNotNull(expectedException), - retryControl, attemptException, (rs, e) -> true); - } - - private static void assertAdvanceOrThrowThrows( - final Throwable expectedException, - final RetryControl retryControl, - final Throwable attemptException, - final BiPredicate retryPredicate) { - assertAdvanceOrThrowThrows( - com.mongodb.assertions.Assertions.assertNotNull(expectedException), - retryControl, attemptException, (e1, e2) -> e2, retryPredicate); - } - - private static void assertAdvanceOrThrowThrows( - final Throwable expectedException, - final RetryControl retryControl, - final Throwable attemptException, - final BinaryOperator onAttemptFailureOperator, - final BiPredicate retryPredicate) { - com.mongodb.assertions.Assertions.assertNotNull(expectedException); - assertSame( - expectedException, - assertThrows(expectedException.getClass(), () -> - retryControl.advanceOrThrow(attemptException, onAttemptFailureOperator, retryPredicate))); + })).getSuppressed(); + assertAll( + () -> assertEquals(1, suppressed.length), + () -> assertSame(suppressed[0], prospectiveFailedResult) + ); } } diff --git a/driver-core/src/test/unit/com/mongodb/internal/async/function/RetryingAsyncCallbackSupplierTest.java b/driver-core/src/test/unit/com/mongodb/internal/async/function/RetryingAsyncCallbackSupplierTest.java new file mode 100644 index 00000000000..9a9ecad1bcf --- /dev/null +++ b/driver-core/src/test/unit/com/mongodb/internal/async/function/RetryingAsyncCallbackSupplierTest.java @@ -0,0 +1,102 @@ +/* + * 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.async.function; + +import com.mongodb.internal.async.function.RetryingSyncSupplierTest.AssertingUnusedRetryPolicy; +import org.junit.jupiter.api.Test; + +import static com.mongodb.internal.async.AsyncRunnable.beginAsync; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class RetryingAsyncCallbackSupplierTest { + @Test + void doWhileDisabledThrowsAtFirstAttempt() { + RetryControl retryControl = new RetryControl<>(new AssertingUnusedRetryPolicy(false)); + RuntimeException exception = new RuntimeException(); + RetryingAsyncCallbackSupplier retryingSupplier = new RetryingAsyncCallbackSupplier<>( + retryControl, + callback -> { + retryControl.doWhileDisabledAsync(actionCallback -> { + actionCallback.completeExceptionally(exception); + }, callback); + }); + retryingSupplier.get((r, t) -> assertSame(exception, t)); + assertTrue(retryControl.isFirstAttempt()); + } + + @Test + void doWhileDisabledThrowsAtSecondAttempt() { + RetryControl retryControl = new RetryControl<>(new AssertingUnusedRetryPolicy(true)); + RuntimeException exception = new RuntimeException(); + RetryingAsyncCallbackSupplier retryingSupplier = new RetryingAsyncCallbackSupplier<>( + retryControl, + callback -> { + if (retryControl.isFirstAttempt()) { + callback.completeExceptionally(new RuntimeException()); + return; + } + retryControl.doWhileDisabledAsync(actionCallback -> { + actionCallback.completeExceptionally(exception); + }, callback); + }); + retryingSupplier.get((r, t) -> assertSame(exception, t)); + assertEquals(1, retryControl.attempt()); + } + + @Test + void doWhileDisabledCompletesNormally() { + RetryControl retryControl = new RetryControl<>(new AssertingUnusedRetryPolicy(true)); + Object result = new Object(); + RetryingAsyncCallbackSupplier retryingSupplier = new RetryingAsyncCallbackSupplier<>( + retryControl, + callback -> { + beginAsync().thenSupply(c -> { + retryControl.doWhileDisabledAsync(actionCallback -> actionCallback.complete(result), c); + }).thenApply((doWhileDisabledResult, c) -> { + if (retryControl.isFirstAttempt()) { + c.completeExceptionally(new RuntimeException()); + return; + } + c.complete(doWhileDisabledResult); + }).finish(callback); + }); + retryingSupplier.get((r, t) -> assertSame(result, r)); + assertEquals(1, retryControl.attempt()); + } + + @Test + void doWhileDisabledNestedThrowsAtFirstAttempt() { + RetryControl retryControl = new RetryControl<>(new AssertingUnusedRetryPolicy(false)); + RuntimeException exception = new RuntimeException(); + RetryingAsyncCallbackSupplier retryingSupplier = new RetryingAsyncCallbackSupplier<>( + retryControl, + callback -> { + retryControl.doWhileDisabledAsync(actionCallback -> { + beginAsync().thenSupply(c -> { + retryControl.doWhileDisabledAsync(nestedActionCallback -> { + nestedActionCallback.completeExceptionally(exception); + }, c); + }).thenConsume((doWhileDisabledResult, c) -> { + c.complete(c); + }).finish(actionCallback); + }, callback); + }); + retryingSupplier.get((r, t) -> assertSame(exception, t)); + assertTrue(retryControl.isFirstAttempt()); + } +} diff --git a/driver-core/src/test/unit/com/mongodb/internal/async/function/RetryingSyncSupplierTest.java b/driver-core/src/test/unit/com/mongodb/internal/async/function/RetryingSyncSupplierTest.java new file mode 100644 index 00000000000..f1ae3452171 --- /dev/null +++ b/driver-core/src/test/unit/com/mongodb/internal/async/function/RetryingSyncSupplierTest.java @@ -0,0 +1,114 @@ +/* + * 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.async.function; + +import com.mongodb.internal.async.function.RetryPolicy.Decision.RetryAttemptInfo; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +final class RetryingSyncSupplierTest { + @Test + void doWhileDisabledThrowsAtFirstAttempt() { + RetryControl retryControl = new RetryControl<>(new AssertingUnusedRetryPolicy(false)); + RuntimeException exception = new RuntimeException(); + RetryingSyncSupplier retryingSupplier = new RetryingSyncSupplier<>( + retryControl, + () -> { + retryControl.doWhileDisabled(() -> { + throw exception; + }); + return null; + }); + assertSame(exception, assertThrows(exception.getClass(), () -> retryingSupplier.get())); + assertTrue(retryControl.isFirstAttempt()); + } + + @Test + void doWhileDisabledThrowsAtSecondAttempt() { + RetryControl retryControl = new RetryControl<>(new AssertingUnusedRetryPolicy(true)); + RuntimeException exception = new RuntimeException(); + RetryingSyncSupplier retryingSupplier = new RetryingSyncSupplier<>( + retryControl, + () -> { + if (retryControl.isFirstAttempt()) { + throw new RuntimeException(); + } + retryControl.doWhileDisabled(() -> { + throw exception; + }); + return null; + }); + assertSame(exception, assertThrows(exception.getClass(), () -> retryingSupplier.get())); + assertEquals(1, retryControl.attempt()); + } + + @Test + void doWhileDisabledCompletesNormally() { + RetryControl retryControl = new RetryControl<>(new AssertingUnusedRetryPolicy(true)); + Object result = new Object(); + RetryingSyncSupplier retryingSupplier = new RetryingSyncSupplier<>( + retryControl, + () -> { + Object doWhileDisabledResult = retryControl.doWhileDisabled(() -> result); + if (retryControl.isFirstAttempt()) { + throw new RuntimeException(); + } + return doWhileDisabledResult; + }); + assertSame(result, retryingSupplier.get()); + assertEquals(1, retryControl.attempt()); + } + + @Test + void doWhileDisabledNestedThrowsAtFirstAttempt() { + RetryControl retryControl = new RetryControl<>(new AssertingUnusedRetryPolicy(false)); + RuntimeException exception = new RuntimeException(); + RetryingSyncSupplier retryingSupplier = new RetryingSyncSupplier<>( + retryControl, + () -> { + retryControl.doWhileDisabled(() -> { + retryControl.doWhileDisabled(() -> { + throw exception; + }); + return null; + }); + return null; + }); + assertSame(exception, assertThrows(exception.getClass(), () -> retryingSupplier.get())); + assertTrue(retryControl.isFirstAttempt()); + } + + static final class AssertingUnusedRetryPolicy implements RetryPolicy { + private final boolean skipFailingOnFirstAttempt; + + AssertingUnusedRetryPolicy(final boolean skipFailingOnFirstAttempt) { + this.skipFailingOnFirstAttempt = skipFailingOnFirstAttempt; + } + + @Override + public Decision onAttemptFailure(final RetryContext retryContext, final Throwable attemptFailedResult) { + if (skipFailingOnFirstAttempt && retryContext.isFirstAttempt()) { + return new Decision(attemptFailedResult, new RetryAttemptInfo()); + } + return fail(); + } + } +} diff --git a/driver-core/src/test/unit/com/mongodb/internal/operation/AsyncOperationHelperSpecification.groovy b/driver-core/src/test/unit/com/mongodb/internal/operation/AsyncOperationHelperSpecification.groovy index 6080f8bb727..0e43d16de43 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/operation/AsyncOperationHelperSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/internal/operation/AsyncOperationHelperSpecification.groovy @@ -91,7 +91,7 @@ class AsyncOperationHelperSpecification extends Specification { when: executeRetryableWriteAsync(asyncWriteBinding, operationContext, dbName, primary(), NoOpFieldNameValidator.INSTANCE, decoder, commandCreator, FindAndModifyHelper.asyncTransformer(), - { cmd -> cmd }, callback) + { cmd -> cmd }, true, callback) then: 2 * connection.commandAsync(dbName, command, _, primary(), decoder, *_) >> { it.last().onResult(results.poll(), null) } diff --git a/driver-core/src/test/unit/com/mongodb/internal/operation/BulkWriteBatchSpecification.groovy b/driver-core/src/test/unit/com/mongodb/internal/operation/BulkWriteBatchSpecification.groovy index 2ccd3513cf7..a7eaacda36c 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/operation/BulkWriteBatchSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/internal/operation/BulkWriteBatchSpecification.groovy @@ -309,7 +309,7 @@ class BulkWriteBatchSpecification extends Specification { [new DeleteRequest(new BsonDocument()).multi(true), new InsertRequest(new BsonDocument())], operationContext, null, null) then: - !bulkWriteBatch.getRetryWrites() + !bulkWriteBatch.isWriteRetryRequirementsMet() } def 'should handle operation responses'() { diff --git a/driver-core/src/test/unit/com/mongodb/internal/operation/OperationHelperSpecification.groovy b/driver-core/src/test/unit/com/mongodb/internal/operation/OperationHelperSpecification.groovy index e61e7d5e12b..6dcb6caf34f 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/operation/OperationHelperSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/internal/operation/OperationHelperSpecification.groovy @@ -36,8 +36,8 @@ import static com.mongodb.WriteConcern.ACKNOWLEDGED import static com.mongodb.WriteConcern.UNACKNOWLEDGED import static com.mongodb.connection.ServerType.REPLICA_SET_PRIMARY import static com.mongodb.connection.ServerType.STANDALONE -import static com.mongodb.internal.operation.OperationHelper.canRetryRead -import static com.mongodb.internal.operation.OperationHelper.isRetryableWrite +import static com.mongodb.internal.operation.OperationHelper.isReadRetryRequirementsMet +import static com.mongodb.internal.operation.OperationHelper.isNonCommandWriteRetryRequirementsMet import static com.mongodb.internal.operation.OperationHelper.validateWriteRequests class OperationHelperSpecification extends Specification { @@ -81,8 +81,8 @@ class OperationHelperSpecification extends Specification { } expect: - isRetryableWrite(retryWrites, writeConcern, connectionDescription, noTransactionSessionContext) == expected - !isRetryableWrite(retryWrites, writeConcern, connectionDescription, activeTransactionSessionContext) + isNonCommandWriteRetryRequirementsMet(retryWrites, writeConcern, connectionDescription, noTransactionSessionContext) == expected + !isNonCommandWriteRetryRequirementsMet(retryWrites, writeConcern, connectionDescription, activeTransactionSessionContext) where: retryWrites | writeConcern | connectionDescription | expected @@ -106,8 +106,8 @@ class OperationHelperSpecification extends Specification { } expect: - canRetryRead(createOperationContext().withSessionContext(noTransactionSessionContext)) - !canRetryRead(createOperationContext().withSessionContext(activeTransactionSessionContext)) + isReadRetryRequirementsMet(true, createOperationContext().withSessionContext(noTransactionSessionContext)) + !isReadRetryRequirementsMet(true, createOperationContext().withSessionContext(activeTransactionSessionContext)) } diff --git a/driver-core/src/test/unit/com/mongodb/internal/operation/SyncOperationHelperSpecification.groovy b/driver-core/src/test/unit/com/mongodb/internal/operation/SyncOperationHelperSpecification.groovy index 8b66947c026..8674faf8a80 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/operation/SyncOperationHelperSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/internal/operation/SyncOperationHelperSpecification.groovy @@ -103,8 +103,8 @@ class SyncOperationHelperSpecification extends Specification { when: executeRetryableWrite(writeBinding, operationContext, dbName, primary(), - NoOpFieldNameValidator.INSTANCE, decoder, commandCreator, FindAndModifyHelper.transformer()) - { cmd -> cmd } + NoOpFieldNameValidator.INSTANCE, decoder, commandCreator, FindAndModifyHelper.transformer(), + { cmd -> cmd }, true) then: 2 * connection.command(dbName, command, _, primary(), decoder, _) >> { results.poll() } diff --git a/driver-sync/src/test/functional/com/mongodb/client/CrudProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/CrudProseTest.java index 9a002b94137..0e6e751bd61 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/CrudProseTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/CrudProseTest.java @@ -21,6 +21,7 @@ import com.mongodb.Function; import com.mongodb.MongoBulkWriteException; import com.mongodb.MongoClientSettings; +import com.mongodb.MongoException; import com.mongodb.MongoNamespace; import com.mongodb.MongoWriteConcernException; import com.mongodb.MongoWriteException; @@ -36,7 +37,9 @@ import com.mongodb.client.model.bulk.ClientNamespacedWriteModel; import com.mongodb.client.test.CollectionHelper; import com.mongodb.event.CommandStartedEvent; +import com.mongodb.event.CommandSucceededEvent; import com.mongodb.internal.connection.TestCommandListener; +import com.mongodb.internal.event.ConfigureFailPointCommandListener; import org.bson.BsonArray; import org.bson.BsonDocument; import org.bson.BsonDocumentWrapper; @@ -74,6 +77,7 @@ import static com.mongodb.client.model.bulk.ClientBulkWriteOptions.clientBulkWriteOptions; import static com.mongodb.client.model.bulk.ClientNamespacedWriteModel.insertOne; import static com.mongodb.client.model.bulk.ClientUpdateOneOptions.clientUpdateOneOptions; +import static com.mongodb.internal.operation.CommandOperationHelper.RETRYABLE_WRITE_ERROR_LABEL; import static java.lang.String.join; import static java.util.Arrays.asList; import static java.util.Collections.nCopies; @@ -278,36 +282,72 @@ void testBulkWriteHandlesCursorRequiringGetMoreWithinTransaction() { assertBulkWriteHandlesCursorRequiringGetMore(true); } + /** + * This test is not from the specification. + */ + @DisplayName("MongoClient.bulkWrite must not retry the bulkWrite command when the corresponding getMore command fails with an error" + + " eligible for retry under the write retry policy") + @Test + void testBulkWriteCommandNotRetriedWhenGetMoreFails() throws Exception { + assumeTrue(serverVersionAtLeast(8, 0)); + TestCommandListener commandListener = new TestCommandListener(); + BsonDocument configureFailPointFromListener = BsonDocument.parse( + "{\n" + + " configureFailPoint: \"failCommand\",\n" + + " mode: { times: 1 },\n" + + " data: {\n" + + " failCommands: ['getMore'],\n" + + " errorCode: 6,\n" + + " errorLabels: ['" + RETRYABLE_WRITE_ERROR_LABEL + "']\n" + + " }\n" + + "}\n"); + try (ConfigureFailPointCommandListener failGetMoreAfterBulkWrite = + new ConfigureFailPointCommandListener(configureFailPointFromListener, getPrimary(), commandEvent -> + (commandEvent instanceof CommandSucceededEvent) && commandEvent.getCommandName().equals("bulkWrite")); + MongoClient client = createMongoClient(getMongoClientSettingsBuilder() + .retryWrites(true) + .addCommandListener(commandListener) + .addCommandListener(failGetMoreAfterBulkWrite))) { + assertThrows(MongoException.class, () -> clientBulkWriteWithGetMore(client, false)); + } finally { + assertEquals(1, commandListener.getCommandStartedEvents("bulkWrite").size()); + assertEquals(1, commandListener.getCommandStartedEvents("getMore").size()); + assertEquals(1, commandListener.getCommandFailedEvents("getMore").size()); + } + } + private void assertBulkWriteHandlesCursorRequiringGetMore(final boolean transaction) { TestCommandListener commandListener = new TestCommandListener(); try (MongoClient client = createMongoClient(getMongoClientSettingsBuilder() .retryWrites(false) .addCommandListener(commandListener))) { - int maxBsonObjectSize = droppedDatabase(client).runCommand(new Document("hello", 1)).getInteger("maxBsonObjectSize"); - try (ClientSession session = transaction ? client.startSession() : null) { - BiFunction, ClientBulkWriteOptions, ClientBulkWriteResult> bulkWrite = - (models, options) -> session == null - ? client.bulkWrite(models, options) - : client.bulkWrite(session, models, options); - Supplier action = () -> bulkWrite.apply(asList( - ClientNamespacedWriteModel.updateOne( - NAMESPACE, - Filters.eq(join("", nCopies(maxBsonObjectSize / 2, "a"))), - Updates.set("x", 1), - clientUpdateOneOptions().upsert(true)), - ClientNamespacedWriteModel.updateOne( - NAMESPACE, - Filters.eq(join("", nCopies(maxBsonObjectSize / 2, "b"))), - Updates.set("x", 1), - clientUpdateOneOptions().upsert(true))), - clientBulkWriteOptions().verboseResults(true) - ); - - ClientBulkWriteResult result = transaction ? runInTransaction(session, action) : action.get(); - assertEquals(2, result.getUpsertedCount()); - assertEquals(2, result.getVerboseResults().orElseThrow(Assertions::fail).getUpdateResults().size()); - assertEquals(1, commandListener.getCommandStartedEvents("bulkWrite").size()); - } + ClientBulkWriteResult result = clientBulkWriteWithGetMore(client, transaction); + assertEquals(2, result.getUpsertedCount()); + assertEquals(2, result.getVerboseResults().orElseThrow(Assertions::fail).getUpdateResults().size()); + assertEquals(1, commandListener.getCommandStartedEvents("getMore").size()); + } + } + + private static ClientBulkWriteResult clientBulkWriteWithGetMore(final MongoClient client, final boolean transaction) { + int maxBsonObjectSize = droppedDatabase(client).runCommand(new Document("hello", 1)).getInteger("maxBsonObjectSize"); + try (ClientSession session = transaction ? client.startSession() : null) { + BiFunction, ClientBulkWriteOptions, ClientBulkWriteResult> bulkWrite = + (models, options) -> session == null + ? client.bulkWrite(models, options) + : runInTransaction(session, () -> client.bulkWrite(session, models, options)); + return bulkWrite.apply(asList( + ClientNamespacedWriteModel.updateOne( + NAMESPACE, + Filters.eq(join("", nCopies(maxBsonObjectSize / 2, "a"))), + Updates.set("x", 1), + clientUpdateOneOptions().upsert(true)), + ClientNamespacedWriteModel.updateOne( + NAMESPACE, + Filters.eq(join("", nCopies(maxBsonObjectSize / 2, "b"))), + Updates.set("x", 1), + clientUpdateOneOptions().upsert(true))), + clientBulkWriteOptions().verboseResults(true) + ); } }