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/AsyncRunnable.java b/driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java
index 9468e787dcb..f5fb6358b87 100644
--- a/driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java
+++ b/driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java
@@ -17,8 +17,8 @@
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.RetryState;
+import com.mongodb.internal.async.function.LoopControl;
+import com.mongodb.internal.async.function.RetryControl;
import com.mongodb.internal.async.function.RetryingAsyncCallbackSupplier;
import java.util.function.BooleanSupplier;
@@ -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 RetryState(),
- (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
@@ -255,10 +253,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 +282,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/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/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..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 looping via {@link LoopState}.
- * Looping continues until either of the following happens:
+ * 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
+ *
+ * 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 be deemed as initial for the purpose of 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/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/LoopControl.java b/driver-core/src/main/com/mongodb/internal/async/function/LoopControl.java
new file mode 100644
index 00000000000..5b89ce55434
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/internal/async/function/LoopControl.java
@@ -0,0 +1,140 @@
+/*
+ * 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.internal.async.MutableValue;
+import com.mongodb.internal.async.SingleResultCallback;
+
+import java.util.function.Supplier;
+
+import static com.mongodb.assertions.Assertions.assertFalse;
+
+/**
+ * A stateful controller of a loop that can be used to control it, for example,
+ * to {@linkplain #breakAndCompleteIf(Supplier, SingleResultCallback) break} it.
+ * {@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
+ */
+@NotThreadSafe
+public final class LoopControl {
+ private int iteration;
+ private boolean lastIteration;
+
+ public LoopControl() {
+ iteration = 0;
+ }
+
+ /**
+ * 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 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++;
+ return true;
+ }
+ }
+
+ /**
+ * Returns {@code true} iff the current iteration is the first one.
+ *
+ * @see #iteration()
+ */
+ boolean isFirstIteration() {
+ return iteration == 0;
+ }
+
+ /**
+ * Returns {@code true} iff {@link #breakAndCompleteIf(Supplier, SingleResultCallback)} / {@link #markAsLastIteration()} was called.
+ */
+ boolean isLastIteration() {
+ return lastIteration;
+ }
+
+ /**
+ * A 0-based iteration number.
+ */
+ int iteration() {
+ return iteration;
+ }
+
+ /**
+ * 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 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;
+ * - this method broke the loop.
+ *
+ *
+ * @see #isLastIteration()
+ */
+ public boolean breakAndCompleteIf(final Supplier predicate, final SingleResultCallback> callback) {
+ assertFalse(lastIteration);
+ try {
+ lastIteration = predicate.get();
+ } catch (Throwable predicateException) {
+ callback.onResult(null, predicateException);
+ return true;
+ }
+ if (lastIteration) {
+ callback.onResult(null, null);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * This method is similar to {@link #breakAndCompleteIf(Supplier, SingleResultCallback)}.
+ * The difference is that it allows the current iteration to continue, yet no more iterations will happen.
+ *
+ * @see #isLastIteration()
+ */
+ void markAsLastIteration() {
+ assertFalse(lastIteration);
+ lastIteration = true;
+ }
+
+ @Override
+ public String toString() {
+ return "LoopControl{"
+ + "iteration=" + iteration
+ + ", lastIteration=" + lastIteration
+ + '}';
+ }
+}
diff --git a/driver-core/src/main/com/mongodb/internal/async/function/LoopState.java b/driver-core/src/main/com/mongodb/internal/async/function/LoopState.java
deleted file mode 100644
index f3b19fecde7..00000000000
--- a/driver-core/src/main/com/mongodb/internal/async/function/LoopState.java
+++ /dev/null
@@ -1,215 +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.annotations.Immutable;
-import com.mongodb.annotations.NotThreadSafe;
-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.,
- * 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
- *
- * @see AsyncCallbackLoop
- */
-@NotThreadSafe
-public final class LoopState {
- private int iteration;
- private boolean lastIteration;
- @Nullable
- private Map, AttachmentValueContainer> attachments;
-
- public LoopState() {
- iteration = 0;
- }
-
- /**
- * Advances this {@link LoopState} 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}.
- */
- boolean advance() {
- if (lastIteration) {
- return false;
- } else {
- iteration++;
- removeAutoRemovableAttachments();
- return true;
- }
- }
-
- /**
- * Returns {@code true} iff the current iteration is the first one.
- *
- * @see #iteration()
- */
- public boolean isFirstIteration() {
- return iteration == 0;
- }
-
- /**
- * Returns {@code true} iff {@link #breakAndCompleteIf(Supplier, SingleResultCallback)} / {@link #markAsLastIteration()} was called.
- */
- boolean isLastIteration() {
- return lastIteration;
- }
-
- /**
- * A 0-based iteration number.
- */
- public int iteration() {
- return iteration;
- }
-
- /**
- * This method emulates executing the
- * {@code break} statement. Must not be called more than once per {@link LoopState}.
- *
- * @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:
- *
- * - the {@code predicate} completed abruptly, in which case the exception thrown is relayed to the {@code callback};
- * - this method broke the associated 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);
- return true;
- }
- if (lastIteration) {
- callback.onResult(null, null);
- return true;
- } else {
- return false;
- }
- }
-
- /**
- * This method is similar to {@link #breakAndCompleteIf(Supplier, SingleResultCallback)}.
- * The difference is that it allows the current iteration to continue, yet no more iterations will happen.
- *
- * @see #isLastIteration()
- */
- void markAsLastIteration() {
- assertFalse(lastIteration);
- 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 LoopState 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 "LoopState{"
- + "iteration=" + iteration
- + ", attachments=" + attachments
- + '}';
- }
-
- /**
- * 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
new file mode 100644
index 00000000000..d192ebbe3ee
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/internal/async/function/RetryControl.java
@@ -0,0 +1,234 @@
+/*
+ * 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.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.RetryPolicy.Decision;
+import com.mongodb.internal.async.function.RetryPolicy.Decision.RetryAttemptInfo;
+import com.mongodb.lang.Nullable;
+
+import java.util.Optional;
+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.async.AsyncRunnable.beginAsync;
+
+/**
+ * A stateful controller of a retryable activity that can be used to control it, for example,
+ * to {@linkplain #breakAndThrowIfRetryAnd(Supplier) break} it.
+ * 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
implements RetryContext {
+ private final LoopControl loopControl;
+ private boolean disabled;
+ @Nullable
+ private Decision mostRecentDecision;
+ private final P policy;
+
+ public RetryControl(final P policy) {
+ loopControl = new LoopControl();
+ this.policy = policy;
+ disabled = false;
+ mostRecentDecision = null;
+ }
+
+ @Override
+ public boolean isFirstAttempt() {
+ return loopControl.isFirstIteration();
+ }
+
+ @Override
+ public int attempt() {
+ return loopControl.iteration();
+ }
+
+ public P getPolicy() {
+ return policy;
+ }
+
+ /**
+ * 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.
+ */
+ RetryAttemptInfo advanceOrThrow(final Throwable attemptFailedResult) throws RuntimeException {
+ assertNotNull(attemptFailedResult);
+ try {
+ if (disabled) {
+ throw attemptFailedResult;
+ }
+ // 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 {
+ assertTrue(loopControl.advance());
+ return decision.getImmediateNextAttemptInfo().orElseThrow(Assertions::fail);
+ }
+ } catch (RuntimeException | Error uncheckedFailedResult) {
+ throw uncheckedFailedResult;
+ } catch (Throwable checkedFailedResult) {
+ throw new RuntimeException(checkedFailedResult);
+ }
+ }
+
+ private static
Decision onAttemptFailure(
+ final P policy,
+ final RetryContext retryContext,
+ @Nullable final Throwable prospectiveFailedResult,
+ final Throwable attemptFailedResult) throws RuntimeException {
+ Decision decision;
+ try {
+ decision = assertNotNull(policy.onAttemptFailure(retryContext, attemptFailedResult));
+ } catch (Throwable onAttemptFailureException) {
+ if (prospectiveFailedResult != null && prospectiveFailedResult != onAttemptFailureException) {
+ onAttemptFailureException.addSuppressed(prospectiveFailedResult);
+ }
+ if (attemptFailedResult != onAttemptFailureException) {
+ onAttemptFailureException.addSuppressed(attemptFailedResult);
+ }
+ throw onAttemptFailureException;
+ }
+ return decision;
+ }
+
+ @Override
+ public Optional getProspectiveFailedResult() {
+ assertTrue(isFirstAttempt() ^ mostRecentDecision != null);
+ return Optional.ofNullable(prospectiveFailedResult());
+ }
+
+ @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.
+ * 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 retryable activity detects that a retry attempt should not happen
+ * despite having been started.
+ *
+ * Must not be called after breaking the retry loop.
+ *
+ * @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 the retry loop.
+ *
+ */
+ public void breakAndThrowIfRetryAnd(final Supplier predicate) throws RuntimeException {
+ assertFalse(loopControl.isLastIteration());
+ if (isFirstAttempt()) {
+ return;
+ }
+ Throwable prospectiveFailedResult = assertNotNull(prospectiveFailedResult());
+ try {
+ if (predicate.get()) {
+ loopControl.markAsLastIteration();
+ }
+ } catch (Throwable predicateException) {
+ if (prospectiveFailedResult != predicateException) {
+ predicateException.addSuppressed(prospectiveFailedResult);
+ }
+ throw predicateException;
+ }
+ if (loopControl.isLastIteration()) {
+ try {
+ throw prospectiveFailedResult;
+ } catch (RuntimeException | Error unchecked) {
+ throw unchecked;
+ } catch (Throwable checked) {
+ throw new RuntimeException(checked);
+ }
+ }
+ }
+
+ /**
+ * 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 #doWhileDisabledAsync(AsyncSupplier, SingleResultCallback)
+ */
+ 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;
+ }
+
+ /**
+ * 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 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
+ + ", 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/RetryState.java b/driver-core/src/main/com/mongodb/internal/async/function/RetryState.java
deleted file mode 100644
index 9742116f239..00000000000
--- a/driver-core/src/main/com/mongodb/internal/async/function/RetryState.java
+++ /dev/null
@@ -1,369 +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.MongoOperationTimeoutException;
-import com.mongodb.annotations.NotThreadSafe;
-import com.mongodb.internal.async.SingleResultCallback;
-import com.mongodb.internal.async.function.LoopState.AttachmentKey;
-import com.mongodb.lang.NonNull;
-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;
-
-/**
- * Represents both the state associated with a retryable activity and a handle that can be used to affect retrying, e.g.,
- * 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
- *
- * @see RetryingSyncSupplier
- * @see RetryingAsyncCallbackSupplier
- */
-@NotThreadSafe
-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 int attempts;
- @Nullable
- private Throwable previouslyChosenException;
-
- /**
- * Creates a {@link RetryState} 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() {
- 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}.
- */
- public RetryState(final int retries) {
- assertTrue(retries >= 0);
- loopState = new LoopState();
- attempts = retries == INFINITE_RETRIES ? INFINITE_RETRIES : retries + 1;
- }
-
- /**
- * Advances this {@link RetryState} 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 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
- * 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);
- }
- }
-
- /**
- * 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);
- }
-
- /**
- * @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.
- * @param onAttemptFailureOperator See {@link #advanceOrThrow(RuntimeException, BinaryOperator, BiPredicate)}.
- */
- 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);
- }
- throw previouslyChosenException;
- } else {
- // note that we must not update the state, e.g, `previouslyChosenException`, `loopState`, before calling `retryPredicate`
- boolean retry = shouldRetry(this, attemptException, newlyChosenException, onlyRuntimeExceptions, retryPredicate);
- previouslyChosenException = newlyChosenException;
- if (retry) {
- assertTrue(loopState.advance());
- } else {
- throw previouslyChosenException;
- }
- }
- }
-
- /**
- * @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;
- try {
- result = assertNotNull(onAttemptFailureOperator.apply(previouslyChosenException, attemptException));
- if (onlyRuntimeExceptions) {
- assertTrue(isRuntime(result));
- }
- } catch (Throwable onAttemptFailureOperatorException) {
- if (onlyRuntimeExceptions && !isRuntime(onAttemptFailureOperatorException)) {
- throw onAttemptFailureOperatorException;
- }
- if (previouslyChosenException != null) {
- onAttemptFailureOperatorException.addSuppressed(previouslyChosenException);
- }
- onAttemptFailureOperatorException.addSuppressed(attemptException);
- throw onAttemptFailureOperatorException;
- }
- return result;
- }
-
- /**
- * @param readOnlyRetryState 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) {
- try {
- return retryPredicate.test(readOnlyRetryState, attemptException);
- } catch (Throwable retryPredicateException) {
- if (onlyRuntimeExceptions && !isRuntime(retryPredicateException)) {
- throw retryPredicateException;
- }
- retryPredicateException.addSuppressed(newlyChosenException);
- throw retryPredicateException;
- }
- }
-
- private static boolean isRuntime(@Nullable final Throwable exception) {
- return exception instanceof RuntimeException;
- }
-
- /**
- * 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.
- * 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}.
- *
- * 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.
- *
- * If this method is called from
- * {@linkplain RetryingSyncSupplier#RetryingSyncSupplier(RetryState, BinaryOperator, BiPredicate, Supplier)
- * retry predicate / failed result transformer}, the behavior is unspecified.
- *
- * @param predicate {@code true} iff retrying needs to be broken.
- * The {@code predicate} is not called during the {@linkplain #isFirstAttempt() first attempt}.
- * @throws RuntimeException Iff any of the following is true:
- *
- * - the {@code predicate} completed abruptly;
- * - this method broke retrying.
- *
- * 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(loopState.isLastIteration());
- if (!isFirstAttempt()) {
- assertNotNull(previouslyChosenException);
- assertTrue(previouslyChosenException instanceof RuntimeException);
- RuntimeException localException = (RuntimeException) previouslyChosenException;
- try {
- if (predicate.get()) {
- loopState.markAsLastIteration();
- }
- } catch (Exception predicateException) {
- predicateException.addSuppressed(localException);
- throw predicateException;
- }
- if (loopState.isLastIteration()) {
- throw localException;
- }
- }
- }
-
- /**
- * 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(RetryState, 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;
- }
- }
-
- /**
- * Returns {@code true} iff the current attempt is the first one, i.e., no retry attempts have been made.
- *
- * @see #attempt()
- */
- public boolean isFirstAttempt() {
- return loopState.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 loopState.isLastIteration() || operationTimeout || attemptLimit;
- }
-
- /**
- * A 0-based attempt number.
- *
- * @see #isFirstAttempt()
- */
- public int attempt() {
- return loopState.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 LoopState#attach(AttachmentKey, Object, boolean)
- */
- public RetryState attach(final AttachmentKey key, final V value, final boolean autoRemove) {
- loopState.attach(key, value, autoRemove);
- return this;
- }
-
- /**
- * @see LoopState#attachment(AttachmentKey)
- */
- public Optional attachment(final AttachmentKey key) {
- return loopState.attachment(key);
- }
-
- @Override
- public String toString() {
- return "RetryState{"
- + "loopState=" + loopState
- + ", 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 8d12e82e19e..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,80 +17,39 @@
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 java.util.function.Supplier;
+import static com.mongodb.assertions.Assertions.assertNotNull;
/**
* 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 RetryControl}.
+ *
+ * This class is not part of the public API and may be removed or changed at any time.
*
* @see RetryingSyncSupplier
*/
@NotThreadSafe
public final class RetryingAsyncCallbackSupplier implements AsyncCallbackSupplier {
- private final RetryState state;
- private final BiPredicate retryPredicate;
- private final BinaryOperator onAttemptFailureOperator;
+ private final RetryControl> control;
private final AsyncCallbackSupplier asyncFunction;
/**
- * @param state The {@link RetryState} to be deemed as initial for the purpose of 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 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
- * after (in the happens-before order) testing the {@code retryPredicate}, and only if the predicate completes normally.
+ * @param control The {@link RetryControl} to control the new {@link RetryingAsyncCallbackSupplier}.
* @param asyncFunction The retryable {@link AsyncCallbackSupplier} to be decorated.
*/
- public RetryingAsyncCallbackSupplier(
- final RetryState state,
- final BinaryOperator onAttemptFailureOperator,
- final BiPredicate retryPredicate,
- final AsyncCallbackSupplier asyncFunction) {
- this.state = state;
- this.retryPredicate = retryPredicate;
- this.onAttemptFailureOperator = onAttemptFailureOperator;
+ public RetryingAsyncCallbackSupplier(final RetryControl> control, final AsyncCallbackSupplier asyncFunction) {
+ this.control = control;
this.asyncFunction = asyncFunction;
}
@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));
}
@@ -106,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 {
- state.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 ad3e4b2b807..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,46 +17,30 @@
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.
*
- * 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 RetryControl}.
+ *
+ * This class is not part of the public API and may be removed or changed at any time.
*
* @see RetryingAsyncCallbackSupplier
*/
@NotThreadSafe
public final class RetryingSyncSupplier implements Supplier {
- private final RetryState state;
- private final BiPredicate retryPredicate;
- private final BinaryOperator onAttemptFailureOperator;
+ private final RetryControl> control;
private final Supplier syncFunction;
/**
- * See {@link RetryingAsyncCallbackSupplier#RetryingAsyncCallbackSupplier(RetryState, 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 RetryState state,
- final BinaryOperator onAttemptFailureOperator,
- final BiPredicate retryPredicate,
- final Supplier syncFunction) {
- this.state = state;
- this.retryPredicate = retryPredicate;
- this.onAttemptFailureOperator = onAttemptFailureOperator;
+ public RetryingSyncSupplier(final RetryControl> control, final Supplier syncFunction) {
+ this.control = control;
this.syncFunction = syncFunction;
}
@@ -65,11 +49,10 @@ public R get() {
while (true) {
try {
return syncFunction.get();
- } catch (RuntimeException attemptException) {
- state.advanceOrThrow(attemptException, onAttemptFailureOperator, retryPredicate);
- } catch (Exception attemptException) {
- // wrap potential sneaky / Kotlin exceptions
- state.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/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/connection/OperationContext.java b/driver-core/src/main/com/mongodb/internal/connection/OperationContext.java
index 06c2c9b9358..80cc143230f 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/OperationContext.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/OperationContext.java
@@ -30,6 +30,7 @@
import com.mongodb.internal.TimeoutSettings;
import com.mongodb.internal.observability.micrometer.Span;
import com.mongodb.internal.observability.micrometer.TracingManager;
+import com.mongodb.internal.operation.OperationHelper;
import com.mongodb.internal.session.SessionContext;
import com.mongodb.lang.Nullable;
import com.mongodb.selector.ServerSelector;
@@ -41,6 +42,7 @@
import java.util.concurrent.atomic.AtomicLong;
import static com.mongodb.MongoException.SYSTEM_OVERLOADED_ERROR_LABEL;
+import static com.mongodb.assertions.Assertions.assertFalse;
import static java.util.stream.Collectors.toList;
/**
@@ -264,16 +266,17 @@ 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) {
+ assertFalse(attemptFailedResult instanceof OperationHelper.ResourceSupplierInternalException);
+ 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 90c013dd293..52211611215 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/AsyncOperationHelper.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/AsyncOperationHelper.java
@@ -21,14 +21,16 @@
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;
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;
@@ -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;
@@ -48,16 +50,15 @@
import java.util.Collections;
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.isServerWriteRetryRequirementsMet;
import static com.mongodb.internal.operation.WriteConcernHelper.throwOnWriteConcernError;
final class AsyncOperationHelper {
@@ -174,10 +175,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,19 +189,17 @@ static void executeRetryableReadAsync(
final CommandCreator commandCreator,
final Decoder decoder,
final CommandReadTransformerAsync transformer,
- final boolean retryReads,
+ final boolean retryReadsSetting,
final SingleResultCallback callback) {
- RetryState retryState = initialRetryState(retryReads, operationContext.getTimeoutContext());
+ RetryControl retryControl = createSpecRetryControl(
+ new SpecRetryPolicy.IndividualPolicies(retryReadsSetting).includeRead(operationContext),
+ operationContext);
binding.retain();
- AsyncCallbackSupplier asyncRead = decorateReadWithRetriesAsync(retryState, operationContext,
+ AsyncCallbackSupplier asyncRead = decorateWithRetriesAsync(retryControl, operationContext,
(AsyncCallbackSupplier) funcCallback ->
withAsyncSourceAndConnection(sourceAsyncFunction, false, operationContext, funcCallback,
(source, connection, operationContextWithMinRtt, releasingCallback) -> {
- if (retryState.breakAndCompleteIfRetryAnd(
- () -> !OperationHelper.canRetryRead(operationContextWithMinRtt), releasingCallback)) {
- return;
- }
- createReadCommandAndExecuteAsync(retryState, operationContextWithMinRtt, source, database,
+ createReadCommandAndExecuteAsync(retryControl, operationContextWithMinRtt, source, database,
commandCreator, decoder, transformer, connection, releasingCallback);
})
).whenComplete(binding::release);
@@ -240,12 +239,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 +256,53 @@ static void executeRetryableWriteAsync(
final CommandCreator commandCreator,
final CommandWriteTransformerAsync transformer,
final Function retryCommandModifier,
+ final boolean effectiveRetryWritesSetting,
final SingleResultCallback callback) {
-
- RetryState retryState = initialRetryState(true, operationContext.getTimeoutContext());
- binding.retain();
-
- AsyncCallbackSupplier asyncWrite = decorateWriteWithRetriesAsync(retryState, operationContext,
- (AsyncCallbackSupplier) funcCallback -> {
- boolean firstAttempt = retryState.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 (retryState.breakAndCompleteIfRetryAnd(() ->
- !OperationHelper.canRetryWrite(connection.getDescription()), addingRetryableLabelCallback)) {
- return;
- }
- BsonDocument command;
- try {
- command = retryState.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
- retryState.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(
+ new SpecRetryPolicy.IndividualPolicies(effectiveRetryWritesSetting).includeWrite(),
+ 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 RetryState retryState,
+ 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());
- retryState.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 RetryState retryState, final OperationContext operationContext,
- final AsyncCallbackSupplier asyncReadFunction) {
- return new RetryingAsyncCallbackSupplier<>(retryState, onRetryableReadAttemptFailure(operationContext.getServerDeprioritization()),
- CommandOperationHelper::loggingShouldAttemptToRetryRead, callback -> {
- logRetryCommand(retryState, operationContext);
- asyncReadFunction.get(callback);
- });
- }
-
- static AsyncCallbackSupplier decorateWriteWithRetriesAsync(final RetryState retryState, final OperationContext operationContext,
- final AsyncCallbackSupplier asyncWriteFunction) {
- return new RetryingAsyncCallbackSupplier<>(retryState, onRetryableWriteAttemptFailure(operationContext.getServerDeprioritization()),
- CommandOperationHelper::loggingShouldAttemptToRetryWriteAndAddRetryableLabel, callback -> {
- logRetryCommand(retryState, 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..c0607ee4c69 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;
@@ -83,7 +83,7 @@ public final class BulkWriteBatch {
private final boolean ordered;
private final WriteConcern writeConcern;
private final Boolean bypassDocumentValidation;
- private final boolean retryWrites;
+ private final boolean writeRetryRequirementsMet;
private final BulkWriteBatchCombiner bulkWriteBatchCombiner;
private final IndexMap indexMap;
private final WriteRequest.Type batchType;
@@ -97,30 +97,29 @@ public final class BulkWriteBatch {
static BulkWriteBatch createBulkWriteBatch(final MongoNamespace namespace,
final ConnectionDescription connectionDescription,
final boolean ordered, final WriteConcern writeConcern,
- final Boolean bypassDocumentValidation, final boolean retryWrites,
+ final Boolean bypassDocumentValidation, final boolean retryWritesSetting,
final List extends WriteRequest> writeRequests,
final OperationContext operationContext,
@Nullable final BsonValue comment, @Nullable final BsonDocument variables) {
- boolean canRetryWrites = isRetryableWrite(retryWrites, writeConcern, connectionDescription, operationContext.getSessionContext());
+ boolean nonCommandWriteRetryRequirementsMet = isNonCommandWriteRetryRequirementsMet(retryWritesSetting, 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);
}
private BulkWriteBatch(final MongoNamespace namespace, final ConnectionDescription connectionDescription,
final boolean ordered, final WriteConcern writeConcern, @Nullable final Boolean bypassDocumentValidation,
- final boolean retryWrites, final BulkWriteBatchCombiner bulkWriteBatchCombiner,
+ final boolean writeRetryRequirementsMet, final BulkWriteBatchCombiner bulkWriteBatchCombiner,
final List writeRequestsWithIndices, final OperationContext operationContext,
@Nullable final BsonValue comment, @Nullable final BsonDocument variables) {
this.namespace = namespace;
@@ -130,7 +129,7 @@ private BulkWriteBatch(final MongoNamespace namespace, final ConnectionDescripti
this.bypassDocumentValidation = bypassDocumentValidation;
this.bulkWriteBatchCombiner = bulkWriteBatchCombiner;
this.batchType = writeRequestsWithIndices.isEmpty() ? INSERT : writeRequestsWithIndices.get(0).getType();
- this.retryWrites = retryWrites;
+ this.writeRetryRequirementsMet = writeRetryRequirementsMet;
List payloadItems = new ArrayList<>();
List unprocessedItems = new ArrayList<>();
@@ -171,7 +170,7 @@ private BulkWriteBatch(final MongoNamespace namespace, final ConnectionDescripti
}
putIfNotNull(command, "comment", comment);
putIfNotNull(command, "let", variables);
- if (retryWrites) {
+ if (writeRetryRequirementsMet) {
command.put("txnNumber", new BsonInt64(sessionContext.advanceTransactionNumber()));
}
}
@@ -179,7 +178,7 @@ private BulkWriteBatch(final MongoNamespace namespace, final ConnectionDescripti
private BulkWriteBatch(final MongoNamespace namespace, final ConnectionDescription connectionDescription,
final boolean ordered, final WriteConcern writeConcern, final Boolean bypassDocumentValidation,
- final boolean retryWrites, final BulkWriteBatchCombiner bulkWriteBatchCombiner, final IndexMap indexMap,
+ final boolean writeRetryRequirementsMet, final BulkWriteBatchCombiner bulkWriteBatchCombiner, final IndexMap indexMap,
final WriteRequest.Type batchType, final BsonDocument command, final SplittablePayload payload,
final List unprocessed, final OperationContext operationContext,
@Nullable final BsonValue comment, @Nullable final BsonDocument variables) {
@@ -193,11 +192,11 @@ private BulkWriteBatch(final MongoNamespace namespace, final ConnectionDescripti
this.batchType = batchType;
this.payload = payload;
this.unprocessed = unprocessed;
- this.retryWrites = retryWrites;
+ this.writeRetryRequirementsMet = writeRetryRequirementsMet;
this.operationContext = operationContext;
this.comment = comment;
this.variables = variables;
- if (retryWrites) {
+ if (writeRetryRequirementsMet) {
command.put("txnNumber", new BsonInt64(operationContext.getSessionContext().advanceTransactionNumber()));
}
this.command = command;
@@ -214,8 +213,8 @@ void addResult(@Nullable final BsonDocument result) {
}
}
- boolean getRetryWrites() {
- return retryWrites;
+ boolean isWriteRetryRequirementsMet() {
+ return writeRetryRequirementsMet;
}
BsonDocument getCommand() {
@@ -261,11 +260,11 @@ BulkWriteBatch getNextBatch() {
}
- return new BulkWriteBatch(namespace, connectionDescription, ordered, writeConcern, bypassDocumentValidation, retryWrites,
+ return new BulkWriteBatch(namespace, connectionDescription, ordered, writeConcern, bypassDocumentValidation, writeRetryRequirementsMet,
bulkWriteBatchCombiner, nextIndexMap, batchType, command, payload.getNextSplit(), unprocessed, operationContext,
comment, variables);
} else {
- return new BulkWriteBatch(namespace, connectionDescription, ordered, writeConcern, bypassDocumentValidation, retryWrites,
+ return new BulkWriteBatch(namespace, connectionDescription, ordered, writeConcern, bypassDocumentValidation, writeRetryRequirementsMet,
bulkWriteBatchCombiner, unprocessed, operationContext, comment, variables);
}
}
@@ -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 ff078b5b09c..346aacd53aa 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.RetryState;
-import com.mongodb.internal.async.function.RetryingSyncSupplier;
+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;
@@ -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;
@@ -111,6 +107,7 @@
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 +122,16 @@
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.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;
@@ -282,30 +279,28 @@ private Integer executeBatch(
List extends ClientNamespacedWriteModel> unexecutedModels = models.subList(batchStartModelIndex, models.size());
assertFalse(unexecutedModels.isEmpty());
SessionContext sessionContext = operationContext.getSessionContext();
- TimeoutContext timeoutContext = operationContext.getTimeoutContext();
- RetryState retryState = initialRetryState(retryWritesSetting, timeoutContext);
+ RetryControl retryControl = createSpecRetryControl(
+ new SpecRetryPolicy.IndividualPolicies(retryWritesSetting).includeWrite(),
+ operationContext);
BatchEncoder batchEncoder = new BatchEncoder();
- Supplier retryingBatchExecutor = decorateWriteWithRetries(
- retryState, operationContext,
+ 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.
// If connection pinning is required, `binding` handles that,
// 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);
- retryState.breakAndThrowIfRetryAnd(() -> !effectiveRetryWrites);
+ retryControl.breakAndThrowIfRetryAnd(() -> !isServerWriteRetryRequirementsMet(connectionDescription));
resultAccumulator.onNewServerAddress(connectionDescription.getServerAddress());
- retryState.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, connectionDescription, effectiveWriteConcern, sessionContext, unexecutedModels, batchEncoder,
+ () -> retryPolicy.onWriteRetryRequirements(true, connectionDescription));
return executeBulkWriteCommandAndExhaustOkResponse(
- retryState, connectionSource, connection, bulkWriteCommand, effectiveWriteConcern, operationContextWithMinRtt);
+ retryControl, connectionSource, connection, bulkWriteCommand, effectiveWriteConcern, operationContextWithMinRtt);
})
);
@@ -321,12 +316,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(retryState, mongoException);
- }
throw mongoException;
}
}
@@ -345,12 +334,13 @@ private void executeBatchAsync(
List extends ClientNamespacedWriteModel> unexecutedModels = models.subList(batchStartModelIndex, models.size());
assertFalse(unexecutedModels.isEmpty());
SessionContext sessionContext = operationContext.getSessionContext();
- TimeoutContext timeoutContext = operationContext.getTimeoutContext();
- RetryState retryState = initialRetryState(retryWritesSetting, timeoutContext);
+ RetryControl retryControl = createSpecRetryControl(
+ new SpecRetryPolicy.IndividualPolicies(retryWritesSetting).includeWrite(),
+ operationContext);
BatchEncoder batchEncoder = new BatchEncoder();
- AsyncCallbackSupplier retryingBatchExecutor = decorateWriteWithRetriesAsync(
- retryState, operationContext,
+ 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.
// If connection pinning is required, `binding` handles that,
@@ -358,18 +348,15 @@ 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);
- retryState.breakAndThrowIfRetryAnd(() -> !effectiveRetryWrites);
+ retryControl.breakAndThrowIfRetryAnd(() -> !isServerWriteRetryRequirementsMet(connectionDescription));
resultAccumulator.onNewServerAddress(connectionDescription.getServerAddress());
- retryState.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, connectionDescription, effectiveWriteConcern, sessionContext, unexecutedModels, batchEncoder,
+ () -> retryPolicy.onWriteRetryRequirements(true, connectionDescription));
executeBulkWriteCommandAndExhaustOkResponseAsync(
- retryState, connectionSource, connection, bulkWriteCommand, effectiveWriteConcern, operationContextWithMinRtt, executeAndExhaustCallback);
+ retryControl, connectionSource, connection, bulkWriteCommand, effectiveWriteConcern, operationContextWithMinRtt, executeAndExhaustCallback);
}).finish(functionCallback);
})
);
@@ -391,12 +378,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(retryState, mongoException);
- }
throw mongoException;
} else {
onErrorCallback.completeExceptionally(t);
@@ -414,7 +395,7 @@ private void executeBatchAsync(
*/
@Nullable
private ExhaustiveClientBulkWriteCommandOkResponse executeBulkWriteCommandAndExhaustOkResponse(
- final RetryState retryState,
+ final RetryControl retryControl,
final ConnectionSource connectionSource,
final Connection connection,
final ClientBulkWriteCommand bulkWriteCommand,
@@ -433,7 +414,7 @@ private ExhaustiveClientBulkWriteCommandOkResponse executeBulkWriteCommandAndExh
return null;
}
ClientBulkWriteCommandOkResponse response = new ClientBulkWriteCommandOkResponse(okResponseDocument);
- List> cursorExhaustBatches = doWithRetriesDisabled(retryState, () ->
+ List> cursorExhaustBatches = retryControl.doWhileDisabled(() ->
exhaustBulkWriteCommandOkResponseCursor(connectionSource, operationContext, connection, response));
return createExhaustiveClientBulkWriteCommandOkResponse(
response,
@@ -442,10 +423,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,
@@ -469,7 +450,7 @@ private void executeBulkWriteCommandAndExhaustOkResponseAsync(
}
ClientBulkWriteCommandOkResponse response = new ClientBulkWriteCommandOkResponse(okResponseDocument);
beginAsync().>>thenSupply(exhaustCallback -> {
- doWithRetriesDisabledAsync(retryState, (actionCallback) -> {
+ retryControl.doWhileDisabledAsync((actionCallback) -> {
exhaustBulkWriteCommandOkResponseCursorAsync(connectionSource, connection, response, operationContext, actionCallback);
}, exhaustCallback);
}).thenApply((cursorExhaustBatches, transformExhaustionResultCallback) -> {
@@ -482,7 +463,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,
@@ -501,45 +482,6 @@ private static ExhaustiveClientBulkWriteCommandOkResponse createExhaustiveClient
return exhaustiveResponse;
}
- /**
- * This method disables retries on {@code outerRetryState} 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 Supplier action) {
- // TODO-JAVA-5956 The current implementation incorrectly uses `retryableWriteCommandFlag` to achieve the behavior needed.
- Optional originalRetryableWriteCommandFlag = outerRetryState.attachment(AttachmentKeys.retryableWriteCommandFlag());
-
- try {
- outerRetryState.attach(AttachmentKeys.retryableWriteCommandFlag(), false, true);
- return action.get();
- } finally {
- originalRetryableWriteCommandFlag.ifPresent(value -> outerRetryState.attach(AttachmentKeys.retryableWriteCommandFlag(), value, true));
- }
- }
-
- /**
- * @see #doWithRetriesDisabled(RetryState, Supplier)
- */
- private void doWithRetriesDisabledAsync(
- final RetryState retryState,
- 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());
-
- beginAsync().thenSupply(actionCallback -> {
- retryState.attach(AttachmentKeys.retryableWriteCommandFlag(), false, true);
- action.finish(actionCallback);
- }).thenAlwaysRunAndFinish(() -> {
- originalRetryableWriteCommandFlag.ifPresent(value -> retryState.attach(AttachmentKeys.retryableWriteCommandFlag(), value, true));
- }, c);
- }).finish(callback);
- }
-
private List> exhaustBulkWriteCommandOkResponseCursor(
final ConnectionSource connectionSource,
final OperationContext operationContext,
@@ -585,13 +527,13 @@ private void exhaustBulkWriteCommandOkResponseCursorAsync(
}
private ClientBulkWriteCommand createBulkWriteCommand(
- final RetryState retryState,
- final boolean effectiveRetryWrites,
+ final RetryControl retryControl,
+ final ConnectionDescription connectionDescription,
final WriteConcern effectiveWriteConcern,
final SessionContext sessionContext,
final List extends ClientNamespacedWriteModel> 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()));
@@ -606,12 +548,13 @@ private ClientBulkWriteCommand createBulkWriteCommand(
return new ClientBulkWriteCommand(
commandDocument,
new ClientBulkWriteCommand.OpsAndNsInfo(
- effectiveRetryWrites, unexecutedModels,
+ isNonCommandWriteRetryRequirementsMet(retryWritesSetting, effectiveWriteConcern, connectionDescription, sessionContext),
+ unexecutedModels,
batchEncoder,
options,
() -> {
- retriesEnabler.run();
- return retryState.isFirstAttempt()
+ onWriteRetryRequirementsMet.run();
+ return retryControl.isFirstAttempt()
? sessionContext.advanceTransactionNumber()
: sessionContext.getTransactionNumber();
}));
@@ -975,25 +918,32 @@ OpsAndNsInfo getOpsAndNsInfo() {
}
public static final class OpsAndNsInfo extends DualMessageSequences {
- private final boolean effectiveRetryWrites;
+ private final boolean nonCommandWriteRetryRequirementsMet;
private final List extends ClientNamespacedWriteModel> 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 extends ClientNamespacedWriteModel> 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
@@ -1004,7 +954,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);
@@ -1026,8 +976,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();
}
}
@@ -1035,13 +985,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);
}
/**
@@ -1170,6 +1120,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 a0acea83485..6efe10b9e8c 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,22 @@
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.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;
-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 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.operation.OperationHelper.LOGGER;
-import static java.lang.String.format;
+import static com.mongodb.internal.operation.SpecRetryPolicy.ExplicitMaxRetries.RETRIES_LIMITED_BY_INDIVIDUAL_POLICIES;
+import static com.mongodb.internal.operation.SpecRetryPolicy.ExplicitMaxRetries.NO_RETRIES_LIMIT;
import static java.util.Arrays.asList;
@SuppressWarnings("overloads")
@@ -78,55 +67,18 @@ 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 RetryState initialRetryState(final boolean retry, final TimeoutContext timeoutContext) {
- if (retry) {
- return timeoutContext.hasTimeoutMS() ? new RetryState() : new RetryState(MAX_RETRIES);
- }
- return new RetryState(0);
+ static RetryControl createSpecRetryControl(
+ final SpecRetryPolicy.IndividualPolicies policies,
+ final OperationContext operationContext) {
+ ExplicitMaxRetries explicitMaxRetries = operationContext.getTimeoutContext().hasTimeoutMS()
+ ? NO_RETRIES_LIMIT
+ : RETRIES_LIMITED_BY_INDIVIDUAL_POLICIES;
+ return new RetryControl<>(new SpecRetryPolicy(
+ policies,
+ 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 +117,7 @@ static boolean isNamespaceError(final Throwable t) {
}
}
- static boolean loggingShouldAttemptToRetryRead(final RetryState retryState, 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);
- }
- return decision;
- }
-
- static boolean loggingShouldAttemptToRetryWriteAndAddRetryableLabel(final RetryState retryState, final Throwable attemptFailure) {
- Throwable attemptFailureNotToBeRetried = addRetryableLabelOrGetWriteAttemptFailureNotToBeRetried(retryState, attemptFailure);
- boolean decision = attemptFailureNotToBeRetried == null;
- if (!decision && retryState.attachment(AttachmentKeys.retryableWriteCommandFlag()).orElse(false)) {
- logUnableToRetryCommand(retryState, 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 RetryState retryState, 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 (retryState.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())
- .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 +132,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 +140,6 @@ 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;
- 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 RetryState retryState, final Throwable originalError) {
- if (LOGGER.isDebugEnabled()) {
- String commandDescription = retryState.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 958a0dc5c62..bad874ac02c 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;
@@ -46,20 +46,19 @@
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.OperationReadConcernHelper.appendReadConcernToCommand;
import static com.mongodb.internal.operation.ServerVersionHelper.UNKNOWN_WIRE_VERSION;
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 +298,15 @@ 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 = createSpecRetryControl(
+ new SpecRetryPolicy.IndividualPolicies(retryReads).includeRead(findOperationContext),
+ findOperationContext);
+ Supplier> read = decorateWithRetries(retryControl, findOperationContext, () ->
withSourceAndConnection(binding::getReadConnectionSource, false, findOperationContext,
(source, connection, commandOperationContext) -> {
- retryState.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 +326,16 @@ public void executeAsync(final AsyncReadBinding binding, final OperationContext
}
OperationContext findOperationContext = getFindOperationContext(operationContext);
- RetryState retryState = initialRetryState(retryReads, findOperationContext.getTimeoutContext());
+ RetryControl retryControl = createSpecRetryControl(
+ new SpecRetryPolicy.IndividualPolicies(retryReads).includeRead(findOperationContext),
+ findOperationContext);
binding.retain();
- AsyncCallbackSupplier> asyncRead = decorateReadWithRetriesAsync(
- retryState, operationContext, (AsyncCallbackSupplier>) funcCallback ->
+ AsyncCallbackSupplier> asyncRead = decorateWithRetriesAsync(
+ retryControl, operationContext, (AsyncCallbackSupplier>) funcCallback ->
withAsyncSourceAndConnection(binding::getReadConnectionSource, false, findOperationContext, funcCallback,
- (source, connection, operationContextWithMinRTT, releasingCallback) -> {
- if (retryState.breakAndCompleteIfRetryAnd(() -> !canRetryRead(findOperationContext), releasingCallback)) {
- return;
- }
+ (source, connection, operationContextWithMinRTT, releasingCallback) -> {
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..e597e86a04e 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;
@@ -43,11 +43,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 +55,11 @@
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.SingleBatchCursor.createEmptySingleBatchCursor;
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,12 +174,13 @@ 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 = createSpecRetryControl(
+ new SpecRetryPolicy.IndividualPolicies(retryReads).includeRead(listCollectionsOperationContext),
+ listCollectionsOperationContext);
+ Supplier> read = decorateWithRetries(retryControl, listCollectionsOperationContext, () ->
withSourceAndConnection(binding::getReadConnectionSource, false, listCollectionsOperationContext, (source, connection, operationContextWithMinRTT) -> {
- retryState.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,15 @@ public void executeAsync(final AsyncReadBinding binding, final OperationContext
final SingleResultCallback> callback) {
OperationContext listCollectionsOperationContext = applyTimeoutModeToOperationContext(timeoutMode, operationContext);
- RetryState retryState = initialRetryState(retryReads, listCollectionsOperationContext.getTimeoutContext());
+ RetryControl retryControl = createSpecRetryControl(
+ new SpecRetryPolicy.IndividualPolicies(retryReads).includeRead(listCollectionsOperationContext),
+ listCollectionsOperationContext);
binding.retain();
- AsyncCallbackSupplier> asyncRead = decorateReadWithRetriesAsync(
- retryState, listCollectionsOperationContext, (AsyncCallbackSupplier>) funcCallback ->
+ AsyncCallbackSupplier> asyncRead = decorateWithRetriesAsync(
+ retryControl, listCollectionsOperationContext, (AsyncCallbackSupplier>) funcCallback ->
withAsyncSourceAndConnection(binding::getReadConnectionSource, false, listCollectionsOperationContext, funcCallback,
(source, connection, operationContextWithMinRtt, releasingCallback) -> {
- if (retryState.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..c340c1c2f72 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;
@@ -40,23 +40,22 @@
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.SingleBatchCursor.createEmptySingleBatchCursor;
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,12 +131,13 @@ 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 = createSpecRetryControl(
+ new SpecRetryPolicy.IndividualPolicies(retryReads).includeRead(listIndexesOperationContext),
+ listIndexesOperationContext);
+ Supplier> read = decorateWithRetries(retryControl, listIndexesOperationContext, () ->
withSourceAndConnection(binding::getReadConnectionSource, false, listIndexesOperationContext, (source, connection, operationContextWithMinRTT) -> {
- retryState.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,15 @@ 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 = createSpecRetryControl(
+ new SpecRetryPolicy.IndividualPolicies(retryReads).includeRead(listIndexesOperationContext),
+ listIndexesOperationContext);
binding.retain();
- AsyncCallbackSupplier> asyncRead = decorateReadWithRetriesAsync(
- retryState, listIndexesOperationContext, (AsyncCallbackSupplier>) funcCallback ->
+ AsyncCallbackSupplier> asyncRead = decorateWithRetriesAsync(
+ retryControl, listIndexesOperationContext, (AsyncCallbackSupplier>) funcCallback ->
withAsyncSourceAndConnection(binding::getReadConnectionSource, false, listIndexesOperationContext, funcCallback,
(source, connection, operationContextWithMinRtt, releasingCallback) -> {
- if (retryState.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 f43007677ca..80ab345fec7 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;
@@ -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;
@@ -59,16 +57,15 @@
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.SyncOperationHelper.decorateWithRetries;
import static com.mongodb.internal.operation.SyncOperationHelper.withSourceAndConnection;
/**
@@ -240,23 +237,23 @@ private BatchWithSourceAndConnection executeBatchReusingCon
final OperationContext operationContext) {
MutableValue batch = new MutableValue<>(maybeBatch);
MutableValue sourceAndConnection = new MutableValue<>(maybeSourceAndConnection);
- RetryState retryState = initialRetryState(
- retryWrites, operationContext.getTimeoutContext());
- Supplier> retryingBatchExecutor = decorateWriteWithRetries(
- retryState,
+ RetryControl retryControl = createSpecRetryControl(
+ new SpecRetryPolicy.IndividualPolicies(retryWrites).includeWrite(),
+ operationContext);
+ Supplier> retryingBatchExecutor = decorateWithRetries(
+ 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);
batch.set(batch.getNullable() != null
? batch.get()
: createFirstBatch(connectionDescription, reusedOrNewSourceAndConnection.getOperationContext(), effectiveWriteConcern));
- onBatch(batch.get(), retryState);
+ onBatch(batch.get(), retryControl, connectionDescription);
executeBatch(batch.get(), reusedOrNewSourceAndConnection, effectiveWriteConcern);
return new BatchWithSourceAndConnection<>(batch.get().getNextBatch(), reusedOrNewSourceAndConnection);
} catch (Throwable e) {
@@ -275,12 +272,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(retryState, e);
- }
throw e;
}
}
@@ -299,25 +290,25 @@ private void executeBatchReusingConnectionAsync(
beginAsync().>thenSupply(c -> {
MutableValue batch = new MutableValue<>(maybeBatch);
MutableValue sourceAndConnection = new MutableValue<>(maybeSourceAndConnection);
- RetryState retryState = initialRetryState(
- retryWrites, operationContext.getTimeoutContext());
- AsyncCallbackSupplier> retryingBatchExecutor = decorateWriteWithRetriesAsync(
- retryState,
+ RetryControl retryControl = createSpecRetryControl(
+ new SpecRetryPolicy.IndividualPolicies(retryWrites).includeWrite(),
+ operationContext);
+ AsyncCallbackSupplier> retryingBatchExecutor = decorateWithRetriesAsync(
+ 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);
batch.set(batch.getNullable() != null
? batch.get()
: createFirstBatch(connectionDescription, reusedOrNewSourceAndConnection.getOperationContext(), effectiveWriteConcern));
- onBatch(batch.get(), retryState);
+ onBatch(batch.get(), retryControl, connectionDescription);
executeBatchAsync(batch.get(), reusedOrNewSourceAndConnection, effectiveWriteConcern, executeBatchCallback);
}).>thenSupply(createNextBatchCallback -> {
createNextBatchCallback.complete(new BatchWithSourceAndConnection<>(batch.get().getNextBatch(), reusedOrNewSourceAndConnection));
@@ -339,12 +330,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(retryState, e);
- }
onErrorCallback.completeExceptionally(e);
}).finish(c);
}).finish(callback);
@@ -355,14 +340,11 @@ 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);
+ onNewConnection(newSourceAndConnection.getConnection().getDescription(), effectiveWriteConcern, retryControl);
} catch (Throwable e) {
newSourceAndConnection.close();
throw e;
@@ -378,7 +360,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()) {
@@ -386,10 +368,7 @@ private void reuseOrSelectServerAndCheckoutConnectionIfClosedAsync(
selectServerAndCheckoutConnectionAsync(binding, operationContext, selectServerAndCheckoutConnectionCallback);
}).thenApply((newSourceAndConnection, onNewConnectionCallback) -> {
try {
- onNewConnection(
- newSourceAndConnection.getConnection().getDescription(),
- newSourceAndConnection.getOperationContext().getSessionContext(),
- effectiveWriteConcern, retryState);
+ onNewConnection(newSourceAndConnection.getConnection().getDescription(), effectiveWriteConcern, retryControl);
} catch (Throwable e) {
newSourceAndConnection.close();
throw e;
@@ -430,12 +409,9 @@ private static void selectServerAndCheckoutConnectionAsync(
private void onNewConnection(
final ConnectionDescription connectionDescription,
- final SessionContext sessionContext,
final WriteConcern effectiveWriteConcern,
- final RetryState retryState) {
- boolean effectiveRetryWrites = isRetryableWrite(
- retryWrites, effectiveWriteConcern, connectionDescription, sessionContext);
- retryState.breakAndThrowIfRetryAnd(() -> !effectiveRetryWrites);
+ final RetryControl retryControl) {
+ retryControl.breakAndThrowIfRetryAnd(() -> !isServerWriteRetryRequirementsMet(connectionDescription));
validateWriteRequests(connectionDescription, bypassDocumentValidation, writeRequests, effectiveWriteConcern);
}
@@ -449,11 +425,15 @@ 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,
+ final ConnectionDescription connectionDescription) {
commandName = batch.getCommand().getFirstKey();
String commandDescriptionToCapture = commandName;
- retryState.attach(AttachmentKeys.retryableWriteCommandFlag(), batch.getRetryWrites(), false)
- .attach(AttachmentKeys.commandDescriptionSupplier(), () -> commandDescriptionToCapture, false);
+ retryControl.getPolicy()
+ .onCommand(() -> commandDescriptionToCapture)
+ .onWriteRetryRequirements(batch.isWriteRetryRequirementsMet(), connectionDescription);
}
private void executeBatch(
@@ -500,12 +480,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..30b63969611
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/internal/operation/SpecRetryPolicy.java
@@ -0,0 +1,491 @@
+/*
+ * 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.Optional;
+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.OperationHelper.isReadRetryRequirementsMet;
+import static java.lang.Boolean.TRUE;
+import static java.lang.String.format;
+import static java.util.Arrays.asList;
+
+/**
+ * Implements all {@linkplain IndividualPolicies individual specification retry policies}.
+ */
+final class SpecRetryPolicy implements RetryPolicy {
+ private static final int INFINITE_ATTEMPTS = Integer.MAX_VALUE;
+
+ private final IndividualPolicies policies;
+ private final int maxAttempts;
+ private final ServerDeprioritization serverDeprioritization;
+ private Supplier commandDescriptionSupplier;
+
+ /**
+ * @param policies The included individual specification retry policies.
+ */
+ SpecRetryPolicy(
+ final IndividualPolicies policies,
+ final ExplicitMaxRetries explicitMaxRetries,
+ final ServerDeprioritization serverDeprioritization) {
+ this.policies = policies.assertValid();
+ this.maxAttempts = explicitMaxRetries.maxAttempts(policies);
+ this.serverDeprioritization = serverDeprioritization;
+ commandDescriptionSupplier = () -> 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)}.
+ *
+ * This method may be called only if the {@linkplain IndividualPolicies#includeWrite() write retry policy is incluided}.
+ *
+ * @param remainingWriteRequirementsMet This argument, combined with the information passed to
+ * {@link IndividualPolicies#IndividualPolicies(boolean)} and {@link IndividualPolicies#includeWrite()}, 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) {
+ return policies.write().map(state -> {
+ state.onRequirements(remainingWriteRequirementsMet, connectionDescription);
+ return this;
+ }).orElseThrow(() -> fail());
+ }
+
+ @Override
+ public Decision onAttemptFailure(final RetryContext retryContext, final Throwable maybeInternalAttemptFailedResult) {
+ Throwable attemptFailedResult = stripResourceSupplierInternalException(maybeInternalAttemptFailedResult);
+ serverDeprioritization.onAttemptFailure(attemptFailedResult);
+ int attempt = retryContext.attempt();
+ assertTrue(attempt < INFINITE_ATTEMPTS);
+ assertTrue(attempt < maxAttempts);
+ boolean retryableError = false;
+ retryableError |= policies.write().map(state ->
+ decideRetryableAndAddRetryableWriteErrorLabelIfNeeded(state, attemptFailedResult)).orElse(false);
+ retryableError |= policies.read().map(state ->
+ decideRetryable(state, attemptFailedResult)).orElse(false);
+ boolean maxAttemptsReached = attempt >= maxAttempts - 1;
+ if (policies.isRetrySettingEffectivelyTrue() && !retryableError) {
+ logUnableToRetryError(attemptFailedResult);
+ }
+ boolean retry = retryableError && !maxAttemptsReached;
+ Decision decision = new Decision(
+ decideProspectiveFailedResult(retryContext.getProspectiveFailedResult().orElse(null), maybeInternalAttemptFailedResult),
+ retry ? new RetryAttemptInfo() : null);
+ policies.write().ifPresent(IndividualPolicies.State.Write::resetRequirementsInfo);
+ return decision;
+ }
+
+ private static Throwable stripResourceSupplierInternalException(final Throwable maybeInternal) {
+ Throwable external;
+ if (maybeInternal instanceof OperationHelper.ResourceSupplierInternalException) {
+ external = maybeInternal.getCause();
+ } else {
+ external = maybeInternal;
+ }
+ return external;
+ }
+
+ /**
+ * Returns {@code true} iff another attempt must be executed provided that {@link #maxAttempts} has not been reached;
+ * in this case, also adds the {@value CommandOperationHelper#RETRYABLE_WRITE_ERROR_LABEL} label if needed.
+ */
+ private boolean decideRetryableAndAddRetryableWriteErrorLabelIfNeeded(final IndividualPolicies.State.Write state, final Throwable exception) {
+ assertFalse(exception instanceof OperationHelper.ResourceSupplierInternalException);
+ if (!(exception instanceof MongoException)) {
+ return false;
+ }
+ MongoException mongoException = (MongoException) exception;
+ boolean connectionPoolClearedException = mongoException instanceof MongoConnectionPoolClearedException;
+ if (connectionPoolClearedException && policies.isRetrySettingEffectivelyTrue()) {
+ // 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 (state.isRequirementsMet()) {
+ addRetryableWriteErrorLabelIfNeeded(mongoException, state.getMaxWireVersion());
+ retry = mongoException.hasErrorLabel(RETRYABLE_WRITE_ERROR_LABEL);
+ } else {
+ retry = retryRegardlessOfRequirementsHavingBeenMet;
+ }
+ return retry;
+ }
+
+ private static boolean isRetryableMongoSecurityException(final MongoException exception) {
+ return exception instanceof MongoSecurityException
+ && exception.getCause() != null && isRetryableException(exception.getCause());
+ }
+
+ /**
+ * Returns {@code true} iff another attempt must be executed provided that {@link #maxAttempts} has not been reached.
+ */
+ private static boolean decideRetryable(final IndividualPolicies.State.Read state, final Throwable exception) {
+ assertFalse(exception instanceof OperationHelper.ResourceSupplierInternalException);
+ if (!state.isRequirementsMet() || !(exception instanceof MongoException)) {
+ return false;
+ }
+ MongoException mongoException = (MongoException) exception;
+ return isRetryableMongoSecurityException(mongoException) || isRetryableException(mongoException);
+ }
+
+ private void logUnableToRetryError(final Throwable exception) {
+ assertFalse(exception instanceof OperationHelper.ResourceSupplierInternalException);
+ if (LOGGER.isDebugEnabled()) {
+ 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 (policies.write().isPresent()) {
+ newProspectiveFailedResult = decideWriteProspectiveFailedResult(currentProspectiveFailedResult, mostRecentAttemptFailedResult);
+ } else if (policies.read().isPresent()) {
+ newProspectiveFailedResult = decideReadProspectiveFailedResult(currentProspectiveFailedResult, mostRecentAttemptFailedResult);
+ } else {
+ throw fail(toString());
+ }
+ if (mostRecentAttemptFailedResult instanceof MongoOperationTimeoutException) {
+ newProspectiveFailedResult = createMongoTimeoutException(newProspectiveFailedResult);
+ }
+ return newProspectiveFailedResult;
+ }
+
+ private static Throwable decideWriteProspectiveFailedResult(
+ @Nullable final Throwable currentProspectiveFailedResult, final Throwable maybeInternalMostRecentAttemptFailedResult) {
+ if (currentProspectiveFailedResult == null) {
+ return stripResourceSupplierInternalException(maybeInternalMostRecentAttemptFailedResult);
+ } else if (maybeInternalMostRecentAttemptFailedResult instanceof OperationHelper.ResourceSupplierInternalException
+ || (maybeInternalMostRecentAttemptFailedResult instanceof MongoException
+ && ((MongoException) maybeInternalMostRecentAttemptFailedResult).hasErrorLabel(NO_WRITES_PERFORMED_ERROR_LABEL))) {
+ return currentProspectiveFailedResult;
+ } else {
+ return maybeInternalMostRecentAttemptFailedResult;
+ }
+ }
+
+ 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;
+ }
+ }
+
+ private static int maxAttempts(final int maxRetries) {
+ assertTrue(maxRetries < INFINITE_ATTEMPTS - 1);
+ return maxRetries + 1;
+ }
+
+ @Override
+ public String toString() {
+ return "SpecRetryPolicy{"
+ + "policies=" + policies
+ + ", maxAttempts=" + maxAttempts
+ + ", serverDeprioritization=" + serverDeprioritization
+ + ", commandDescription=" + commandDescriptionSupplier.get()
+ + '}';
+ }
+
+ static final class IndividualPolicies {
+ private static final EnumMap> CONFLICTS;
+
+ private final boolean effectiveRetrySetting;
+ private final EnumMap policies;
+
+ static {
+ CONFLICTS = new EnumMap<>(Descriptor.class);
+ CONFLICTS.put(Descriptor.WRITE, EnumSet.of(Descriptor.READ));
+ CONFLICTS.put(Descriptor.READ, EnumSet.of(Descriptor.WRITE));
+ assertTrue(CONFLICTS.keySet().containsAll(asList(Descriptor.values())));
+ }
+
+ /**
+ * @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.
+ */
+ IndividualPolicies(final boolean effectiveRetrySetting) {
+ this.effectiveRetrySetting = effectiveRetrySetting;
+ this.policies = new EnumMap<>(Descriptor.class);
+ }
+
+ private IndividualPolicies assertValid() {
+ assertFalse(policies.isEmpty());
+ assertNoConflicts(policies.keySet());
+ return this;
+ }
+
+ private static void assertNoConflicts(final Set descriptors) {
+ for (Descriptor descriptor : descriptors) {
+ for (Descriptor conflictingDescriptor : assertNotNull(CONFLICTS.get(descriptor))) {
+ assertFalse(descriptors.contains(conflictingDescriptor));
+ }
+ }
+ }
+
+ private boolean isRetrySettingEffectivelyTrue() {
+ return effectiveRetrySetting;
+ }
+
+ /**
+ * See
+ * Retryable Writes,
+ * which specifies the write retry policy.
+ */
+ IndividualPolicies includeWrite() {
+ include(Descriptor.WRITE, new State.Write(effectiveRetrySetting));
+ return this;
+ }
+
+ /**
+ * See
+ * Retryable Reads,
+ * which specifies the read retry policy.
+ */
+ IndividualPolicies includeRead(final OperationContext operationContext) {
+ include(Descriptor.READ, new State.Read(effectiveRetrySetting, operationContext));
+ return this;
+ }
+
+ private void include(final Descriptor descriptor, final State state) {
+ State previous = policies.put(descriptor, state);
+ assertNull(previous);
+ }
+
+ private int getMaxAttempts() {
+ int maxRetries;
+ if (policies.containsKey(Descriptor.WRITE)) {
+ maxRetries = Descriptor.WRITE.maxRetries;
+ } else if (policies.containsKey(Descriptor.READ)) {
+ maxRetries = Descriptor.READ.maxRetries;
+ } else {
+ throw fail(toString());
+ }
+ return maxAttempts(maxRetries);
+ }
+
+ private Optional write() {
+ return Optional.ofNullable((State.Write) policies.get(Descriptor.WRITE));
+ }
+
+ private Optional read() {
+ return Optional.ofNullable((State.Read) policies.get(Descriptor.READ));
+ }
+
+ @Override
+ public String toString() {
+ return "IndividualPolicies{"
+ + "effectiveRetrySetting=" + effectiveRetrySetting
+ + ", policies=" + policies
+ + '}';
+ }
+
+ private enum Descriptor {
+ /**
+ * See {@link #includeWrite()}.
+ */
+ WRITE(1),
+ /**
+ * See {@link #includeRead(OperationContext)}.
+ */
+ READ(1);
+
+ private final int maxRetries;
+
+ Descriptor(final int maxRetries) {
+ this.maxRetries = maxRetries;
+ }
+ }
+
+ /**
+ * The state specific to an individual retry policy.
+ */
+ private abstract static class State {
+ private State() {
+ }
+
+ static final class Write extends State {
+ private final boolean requirementsMaybeMet;
+ @Nullable
+ private Boolean requirementsMet;
+ @Nullable
+ private Integer maxWireVersion;
+
+ Write(final boolean effectiveRetryWritesSetting) {
+ this.requirementsMaybeMet = effectiveRetryWritesSetting;
+ }
+
+ /**
+ * @see #isRequirementsMet()
+ * @see #resetRequirementsInfo()
+ * @see SpecRetryPolicy#onWriteRetryRequirements(boolean, ConnectionDescription)
+ */
+ void onRequirements(
+ final boolean remainingRequirementsMet,
+ final ConnectionDescription connectionDescription) {
+ assertNull(requirementsMet);
+ requirementsMet = requirementsMaybeMet && remainingRequirementsMet;
+ if (requirementsMet) {
+ maxWireVersion = connectionDescription.getMaxWireVersion();
+ }
+ }
+
+ /**
+ * @see #onRequirements(boolean, ConnectionDescription)
+ * @see SpecRetryPolicy#onWriteRetryRequirements(boolean, ConnectionDescription)
+ */
+ void resetRequirementsInfo() {
+ maxWireVersion = null;
+ requirementsMet = null;
+ }
+
+ /**
+ * See {@link Read#isRequirementsMet()}.
+ *
+ * @see #onRequirements(boolean, ConnectionDescription)
+ */
+ boolean isRequirementsMet() {
+ return TRUE.equals(requirementsMet);
+ }
+
+ /**
+ * May be called only if {@link #isRequirementsMet()}.
+ */
+ int getMaxWireVersion() {
+ return assertNotNull(maxWireVersion);
+ }
+
+ @Override
+ public String toString() {
+ return "Write{"
+ + "requirementsMaybeMet=" + requirementsMaybeMet
+ + ", requirementsMet=" + requirementsMet
+ + ", maxWireVersion=" + maxWireVersion
+ + '}';
+ }
+ }
+
+ static final class Read extends State {
+ private final boolean requirementsMet;
+
+ Read(final boolean effectiveRetryReadsSetting, final OperationContext operationContext) {
+ requirementsMet = isReadRetryRequirementsMet(effectiveRetryReadsSetting, operationContext);
+ }
+
+ /**
+ * The requirements in question include settings requirements, server requirements, command requirements, etc.,
+ * but exclude error requirements, {@link #maxAttempts} requirements.
+ */
+ boolean isRequirementsMet() {
+ return requirementsMet;
+ }
+
+ @Override
+ public String toString() {
+ return "Read{"
+ + "requirementsMet=" + requirementsMet
+ + '}';
+ }
+ }
+ }
+ }
+
+ enum ExplicitMaxRetries {
+ NO_RETRIES_LIMIT,
+ /**
+ * See {@link IndividualPolicies}.
+ */
+ RETRIES_LIMITED_BY_INDIVIDUAL_POLICIES;
+
+ private int maxAttempts(final IndividualPolicies policies) {
+ switch (this) {
+ case NO_RETRIES_LIMIT: {
+ return INFINITE_ATTEMPTS;
+ }
+ case RETRIES_LIMITED_BY_INDIVIDUAL_POLICIES: {
+ return policies.getMaxAttempts();
+ }
+ default: {
+ throw fail(this.toString());
+ }
+ }
+ }
+ }
+}
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..8fa8b0db1de 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/SyncOperationHelper.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/SyncOperationHelper.java
@@ -19,9 +19,11 @@
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.function.RetryState;
+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;
import com.mongodb.internal.binding.ReadBinding;
@@ -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;
@@ -48,13 +49,11 @@
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.isServerWriteRetryRequirementsMet;
import static com.mongodb.internal.operation.WriteConcernHelper.throwOnWriteConcernError;
final class SyncOperationHelper {
@@ -188,9 +187,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,13 +199,14 @@ static T executeRetryableRead(
final CommandCreator commandCreator,
final Decoder decoder,
final CommandReadTransformer transformer,
- final boolean retryReads) {
- RetryState retryState = CommandOperationHelper.initialRetryState(retryReads, operationContext.getTimeoutContext());
+ final boolean retryReadsSetting) {
+ RetryControl retryControl = createSpecRetryControl(
+ new SpecRetryPolicy.IndividualPolicies(retryReadsSetting).includeRead(operationContext),
+ operationContext);
- Supplier read = decorateReadWithRetries(retryState, operationContext, () ->
+ Supplier read = decorateWithRetries(retryControl, operationContext, () ->
withSourceAndConnection(readConnectionSourceSupplier, false, operationContext, (source, connection, operationContextWithMinRtt) -> {
- retryState.breakAndThrowIfRetryAnd(() -> !canRetryRead(operationContextWithMinRtt));
- return createReadCommandAndExecute(retryState, operationContextWithMinRtt, source, database,
+ return createReadCommandAndExecute(retryControl, operationContextWithMinRtt, source, database,
commandCreator, decoder, transformer, connection);
})
);
@@ -249,6 +249,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 +261,45 @@ static R executeRetryableWrite(
final Decoder commandResultDecoder,
final CommandCreator commandCreator,
final CommandWriteTransformer transformer,
- final com.mongodb.Function retryCommandModifier) {
- RetryState retryState = CommandOperationHelper.initialRetryState(true, operationContext.getTimeoutContext());
- Supplier