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) {
@@ -113,10 +114,6 @@ public TimeoutContext(final TimeoutSettings timeoutSettings) {
this(false, timeoutSettings, startTimeout(timeoutSettings.getTimeoutMS()));
}
- private TimeoutContext(final TimeoutSettings timeoutSettings, @Nullable final Timeout timeout) {
- this(false, timeoutSettings, timeout);
- }
-
private TimeoutContext(final boolean isMaintenanceContext,
final TimeoutSettings timeoutSettings,
@Nullable final Timeout timeout) {
@@ -181,6 +178,7 @@ public Timeout timeoutIncludingRoundTrip() {
* @param alternativeTimeoutMS the alternative timeout.
* @return timeout to use.
*/
+ @VisibleForTesting(otherwise = PRIVATE)
public long timeoutOrAlternative(final long alternativeTimeoutMS) {
if (timeout == null) {
return alternativeTimeoutMS;
@@ -188,7 +186,7 @@ public long timeoutOrAlternative(final long alternativeTimeoutMS) {
return timeout.call(MILLISECONDS,
() -> 0L,
(ms) -> ms,
- () -> throwMongoTimeoutException("The operation exceeded the timeout limit."));
+ () -> throwMongoTimeoutException());
}
}
@@ -232,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()));
}
/**
@@ -253,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(
@@ -389,11 +385,6 @@ public TimeoutContext withAdditionalReadTimeout(final int additionalReadTimeout)
return new TimeoutContext(timeoutSettings.withReadTimeoutMS(newReadTimeout > 0 ? newReadTimeout : Long.MAX_VALUE));
}
- // Creates a copy of the timeout context that can be reset without resetting the original.
- public TimeoutContext copyTimeoutContext() {
- return new TimeoutContext(getTimeoutSettings(), getTimeout());
- }
-
@Override
public String toString() {
return "TimeoutContext{"
@@ -488,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 deeef6239c4..37b55afe6af 100644
--- a/driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java
+++ b/driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java
@@ -17,9 +17,10 @@
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 com.mongodb.internal.thread.AsyncClientExecutor;
import java.util.function.BooleanSupplier;
import java.util.function.Predicate;
@@ -111,9 +112,11 @@
*
Is every c.complete followed by a return, to end execution?
* Have all sync method calls been converted to async, where needed?
*
- *
- * This class is not part of the public API and may be removed or changed
- * at any time
+ *
+ * If, when writing a lambda expression, you need to have an effectively {@code final} variable
+ * whose value may be mutated, use {@link MutableValue}.
+ *
+ * This class is not part of the public API and may be removed or changed at any time.
*/
@FunctionalInterface
public interface AsyncRunnable extends AsyncSupplier, AsyncConsumer {
@@ -231,9 +234,9 @@ 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),
+ // `AsyncClientExecutor` is not needed, given the contract of `SimpleRetryPolicy`, `RetryingAsyncCallbackSupplier`
+ AsyncClientExecutor.NO_OP,
+ 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
@@ -253,10 +256,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) -> {
@@ -282,15 +285,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..3854c77a891
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/internal/async/SimpleRetryPolicy.java
@@ -0,0 +1,39 @@
+/*
+ * 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.time.Duration;
+import java.util.function.Predicate;
+
+/**
+ * {@link RetryAttemptInfo#getBackoff()} is always {@link Duration#isZero() zero}.
+ */
+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(Duration.ZERO) : null);
+ }
+}
diff --git a/driver-core/src/main/com/mongodb/internal/async/SingleResultCallback.java b/driver-core/src/main/com/mongodb/internal/async/SingleResultCallback.java
index 11da1c97f75..cefab04f312 100644
--- a/driver-core/src/main/com/mongodb/internal/async/SingleResultCallback.java
+++ b/driver-core/src/main/com/mongodb/internal/async/SingleResultCallback.java
@@ -37,7 +37,7 @@ public interface SingleResultCallback {
* @param result the result, which may be null. Always null if e is not null.
* @param t the throwable, or null if the operation completed normally
* @throws RuntimeException Never.
- * @throws Error Never, on the best effort basis.
+ * @throws Error Never, on the best-effort basis.
*/
void onResult(@Nullable T result, @Nullable Throwable t);
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..3efe1fa97bf 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
@@ -51,7 +51,7 @@ public interface AsyncCallbackFunction {
* @param callback A consumer of a result, {@link SingleResultCallback#onResult(Object, Throwable) completed} after
* (in the happens-before order) the asynchronous function completes.
* @throws RuntimeException Never. Exceptions must be relayed to the {@code callback}.
- * @throws Error Never, on the best effort basis. Errors should be relayed to the {@code callback}.
+ * @throws Error Never, on the best-effort basis. Errors should be relayed to the {@code callback}.
*/
void apply(P a, SingleResultCallback callback);
}
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..a84145b73e0
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/internal/async/function/RetryPolicy.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2008-present MongoDB, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.mongodb.internal.async.function;
+
+import com.mongodb.annotations.NotThreadSafe;
+import com.mongodb.lang.Nullable;
+
+import java.time.Duration;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+import static com.mongodb.assertions.Assertions.assertFalse;
+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 {
+ private final Duration backoff;
+
+ public RetryAttemptInfo(final Duration backoff) {
+ assertFalse(backoff.isNegative());
+ this.backoff = backoff;
+ }
+
+ /**
+ * A non-{@linkplain Duration#isNegative() negative} backoff.
+ */
+ public Duration getBackoff() {
+ return backoff;
+ }
+
+ @Override
+ public String toString() {
+ return "RetryAttemptInfo{"
+ + "backoff=" + backoff
+ + '}';
+ }
+ }
+ }
+}
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 b209035d7a2..00000000000
--- a/driver-core/src/main/com/mongodb/internal/async/function/RetryState.java
+++ /dev/null
@@ -1,378 +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 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;
- }
- }
-
- /**
- * This method is similar to
- * {@link RetryState#breakAndThrowIfRetryAnd(Supplier)} / {@link RetryState#breakAndCompleteIfRetryAnd(Supplier, SingleResultCallback)}.
- * The difference is that it allows the current attempt to continue, yet no more attempts will happen. Also, unlike the aforementioned
- * methods, this method has effect even if called during the {@linkplain #isFirstAttempt() first attempt}.
- */
- public void markAsLastAttempt() {
- loopState.markAsLastIteration();
- }
-
- /**
- * 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)} / {@link #markAsLastAttempt()} 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..3b229ed20da 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
@@ -16,108 +16,69 @@
package com.mongodb.internal.async.function;
import com.mongodb.annotations.NotThreadSafe;
+import com.mongodb.internal.async.MutableValue;
import com.mongodb.internal.async.SingleResultCallback;
-import com.mongodb.lang.NonNull;
-import com.mongodb.lang.Nullable;
+import com.mongodb.internal.async.function.RetryPolicy.Decision.RetryAttemptInfo;
+import com.mongodb.internal.thread.AsyncClientExecutor;
+import com.mongodb.internal.thread.ThreadUtil;
-import java.util.function.BiPredicate;
-import java.util.function.BinaryOperator;
-import java.util.function.Supplier;
+import java.time.Duration;
+
+import static com.mongodb.internal.async.AsyncRunnable.beginAsync;
+import static com.mongodb.internal.thread.ThreadUtil.sleepAsync;
/**
* 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 AsyncClientExecutor clientExecutor;
+ 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 clientExecutor For {@linkplain ThreadUtil#sleepAsync(Duration, AsyncClientExecutor, SingleResultCallback) delaying} attempts
+ * according to {@link RetryAttemptInfo#getBackoff()}.
+ * @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 AsyncClientExecutor clientExecutor,
+ final RetryControl> control,
final AsyncCallbackSupplier asyncFunction) {
- this.state = state;
- this.retryPredicate = retryPredicate;
- this.onAttemptFailureOperator = onAttemptFailureOperator;
+ this.clientExecutor = clientExecutor;
+ 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.get(new RetryingCallback(callback));
- }
-
- /**
- * This callback is allowed to be completed more than once.
- */
- @NotThreadSafe
- private class RetryingCallback implements SingleResultCallback {
- private final SingleResultCallback wrapped;
-
- RetryingCallback(final SingleResultCallback callback) {
- wrapped = callback;
- }
-
- @Override
- public void onResult(@Nullable final R result, @Nullable final Throwable t) {
- if (t != null) {
- try {
- state.advanceOrThrow(t, onAttemptFailureOperator, retryPredicate);
- } catch (Throwable failedResult) {
- wrapped.onResult(null, failedResult);
- return;
+ MutableValue> asyncFunctionSuccessfulResult = new MutableValue<>();
+ beginAsync().thenRunWhileLoop(() -> asyncFunctionSuccessfulResult.getNullable() == null, iterationCallback -> {
+ beginAsync().thenSupply(asyncFunctionCallback -> {
+ asyncFunction.get(asyncFunctionCallback);
+ }).thenConsume((attemptSuccessfulResult, onAttemptSuccessCallback) -> {
+ // `attemptSuccessfulResult` may be `null`, so we have to wrap it in `MutableValue` for the while check to notice it
+ asyncFunctionSuccessfulResult.set(new MutableValue<>(attemptSuccessfulResult));
+ onAttemptSuccessCallback.complete(onAttemptSuccessCallback);
+ }).onErrorIf(e -> true, (attemptFailedResult, onAttemptFailureCallback) -> {
+ if (attemptFailedResult instanceof Error) {
+ onAttemptFailureCallback.completeExceptionally(attemptFailedResult);
+ } else {
+ RetryAttemptInfo retryAttemptInfo = control.advanceOrThrow(attemptFailedResult);
+ sleepAsync(retryAttemptInfo.getBackoff(), clientExecutor, onAttemptFailureCallback);
}
- asyncFunction.get(this);
- } else {
- wrapped.onResult(result, null);
- }
- }
+ }).finish(iterationCallback);
+ }).thenSupply(c -> {
+ c.complete(asyncFunctionSuccessfulResult.get().getNullable());
+ }).finish(callback);
}
}
diff --git a/driver-core/src/main/com/mongodb/internal/async/function/RetryingSyncSupplier.java b/driver-core/src/main/com/mongodb/internal/async/function/RetryingSyncSupplier.java
index ad3e4b2b807..89cb2d0c8b8 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
@@ -16,60 +16,48 @@
package com.mongodb.internal.async.function;
import com.mongodb.annotations.NotThreadSafe;
+import com.mongodb.internal.async.function.RetryPolicy.Decision.RetryAttemptInfo;
+import com.mongodb.internal.thread.AsyncClientExecutor;
+import com.mongodb.lang.Nullable;
-import java.util.function.BiPredicate;
-import java.util.function.BinaryOperator;
import java.util.function.Supplier;
+import static com.mongodb.internal.thread.ThreadUtil.sleep;
+
/**
* 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(AsyncClientExecutor, 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;
}
@Override
+ @Nullable
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) {
+ RetryAttemptInfo retryAttemptInfo = control.advanceOrThrow(attemptFailedResult);
+ sleep(retryAttemptInfo.getBackoff());
}
}
}
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/AsynchronousSocketChannelStream.java b/driver-core/src/main/com/mongodb/internal/connection/AsynchronousSocketChannelStream.java
index c60981c115e..f9939ae66d3 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/AsynchronousSocketChannelStream.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/AsynchronousSocketChannelStream.java
@@ -19,8 +19,10 @@
import com.mongodb.MongoSocketException;
import com.mongodb.MongoSocketOpenException;
import com.mongodb.ServerAddress;
+import com.mongodb.annotations.ThreadSafe;
import com.mongodb.connection.AsyncCompletionHandler;
import com.mongodb.connection.SocketSettings;
+import com.mongodb.internal.VisibleForTesting;
import com.mongodb.lang.Nullable;
import com.mongodb.spi.dns.InetAddressResolver;
@@ -38,6 +40,7 @@
import java.util.concurrent.atomic.AtomicReference;
import static com.mongodb.assertions.Assertions.isTrue;
+import static com.mongodb.internal.VisibleForTesting.AccessModifier.PRIVATE;
import static com.mongodb.internal.connection.ServerAddressHelper.getSocketAddresses;
/**
@@ -47,24 +50,24 @@ public final class AsynchronousSocketChannelStream extends AsynchronousChannelSt
private final ServerAddress serverAddress;
private final InetAddressResolver inetAddressResolver;
private final SocketSettings settings;
- @Nullable
- private final AsynchronousChannelGroup group;
+ private final AsynchronousSocketChannelOpener channelOpener;
+ @VisibleForTesting(otherwise = PRIVATE)
AsynchronousSocketChannelStream(
final ServerAddress serverAddress, final InetAddressResolver inetAddressResolver,
final SocketSettings settings, final PowerOfTwoBufferPool bufferProvider) {
- this(serverAddress, inetAddressResolver, settings, bufferProvider, null);
+ this(serverAddress, inetAddressResolver, settings, bufferProvider, AsynchronousSocketChannel::open);
}
- public AsynchronousSocketChannelStream(
+ AsynchronousSocketChannelStream(
final ServerAddress serverAddress, final InetAddressResolver inetAddressResolver,
final SocketSettings settings, final PowerOfTwoBufferPool bufferProvider,
- @Nullable final AsynchronousChannelGroup group) {
+ final AsynchronousSocketChannelOpener channelOpener) {
super(serverAddress, settings, bufferProvider);
this.serverAddress = serverAddress;
this.inetAddressResolver = inetAddressResolver;
this.settings = settings;
- this.group = group;
+ this.channelOpener = channelOpener;
}
@Override
@@ -89,10 +92,7 @@ private void initializeSocketChannel(final AsyncCompletionHandler handler,
SocketAddress socketAddress = socketAddressQueue.poll();
try {
- AsynchronousSocketChannel attemptConnectionChannel;
- attemptConnectionChannel = group == null
- ? AsynchronousSocketChannel.open()
- : AsynchronousSocketChannel.open(group);
+ AsynchronousSocketChannel attemptConnectionChannel = channelOpener.open();
attemptConnectionChannel.setOption(StandardSocketOptions.TCP_NODELAY, true);
attemptConnectionChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);
if (settings.getReceiveBufferSize() > 0) {
@@ -207,4 +207,15 @@ public void close() throws IOException {
channel.close();
}
}
+
+ /**
+ * An implementation must be thread-safe.
+ */
+ @ThreadSafe
+ interface AsynchronousSocketChannelOpener {
+ /**
+ * See {@link AsynchronousSocketChannel#open(AsynchronousChannelGroup)}.
+ */
+ AsynchronousSocketChannel open() throws IOException;
+ }
}
diff --git a/driver-core/src/main/com/mongodb/internal/connection/AsynchronousSocketChannelStreamFactory.java b/driver-core/src/main/com/mongodb/internal/connection/AsynchronousSocketChannelStreamFactory.java
index 1ea15abe59d..7dfbe2097cc 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/AsynchronousSocketChannelStreamFactory.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/AsynchronousSocketChannelStreamFactory.java
@@ -19,13 +19,15 @@
import com.mongodb.ServerAddress;
import com.mongodb.connection.SocketSettings;
import com.mongodb.connection.SslSettings;
-import com.mongodb.lang.Nullable;
+import com.mongodb.internal.VisibleForTesting;
+import com.mongodb.internal.connection.AsynchronousSocketChannelStream.AsynchronousSocketChannelOpener;
import com.mongodb.spi.dns.InetAddressResolver;
-import java.nio.channels.AsynchronousChannelGroup;
+import java.nio.channels.AsynchronousSocketChannel;
import static com.mongodb.assertions.Assertions.assertFalse;
import static com.mongodb.assertions.Assertions.notNull;
+import static com.mongodb.internal.VisibleForTesting.AccessModifier.PRIVATE;
/**
* Factory to create a Stream that's an AsynchronousSocketChannelStream. Throws an exception if SSL is enabled.
@@ -34,34 +36,28 @@ public class AsynchronousSocketChannelStreamFactory implements StreamFactory {
private final PowerOfTwoBufferPool bufferProvider = PowerOfTwoBufferPool.DEFAULT;
private final SocketSettings settings;
private final InetAddressResolver inetAddressResolver;
- @Nullable
- private final AsynchronousChannelGroup group;
+ private final AsynchronousSocketChannelOpener channelOpener;
- /**
- * Create a new factory with the default {@code BufferProvider} and {@code AsynchronousChannelGroup}.
- *
- * @param settings the settings for the connection to a MongoDB server
- * @param sslSettings the settings for connecting via SSL
- */
+ @VisibleForTesting(otherwise = PRIVATE)
public AsynchronousSocketChannelStreamFactory(
final InetAddressResolver inetAddressResolver, final SocketSettings settings,
final SslSettings sslSettings) {
- this(inetAddressResolver, settings, sslSettings, null);
+ this(inetAddressResolver, settings, sslSettings, AsynchronousSocketChannel::open);
}
AsynchronousSocketChannelStreamFactory(
final InetAddressResolver inetAddressResolver, final SocketSettings settings,
- final SslSettings sslSettings, @Nullable final AsynchronousChannelGroup group) {
+ final SslSettings sslSettings, final AsynchronousSocketChannelOpener channelOpener) {
assertFalse(sslSettings.isEnabled());
this.inetAddressResolver = inetAddressResolver;
this.settings = notNull("settings", settings);
- this.group = group;
+ this.channelOpener = channelOpener;
}
@Override
public Stream create(final ServerAddress serverAddress) {
return new AsynchronousSocketChannelStream(
- serverAddress, inetAddressResolver, settings, bufferProvider, group);
+ serverAddress, inetAddressResolver, settings, bufferProvider, channelOpener);
}
}
diff --git a/driver-core/src/main/com/mongodb/internal/connection/AsynchronousSocketChannelStreamFactoryFactory.java b/driver-core/src/main/com/mongodb/internal/connection/AsynchronousSocketChannelStreamFactoryFactory.java
index 8c5a8f654c5..e63492e5dbe 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/AsynchronousSocketChannelStreamFactoryFactory.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/AsynchronousSocketChannelStreamFactoryFactory.java
@@ -16,12 +16,25 @@
package com.mongodb.internal.connection;
+import com.mongodb.MongoClientException;
+import com.mongodb.connection.AsyncTransportSettings;
import com.mongodb.connection.SocketSettings;
import com.mongodb.connection.SslSettings;
+import com.mongodb.internal.VisibleForTesting;
+import com.mongodb.internal.thread.DaemonThreadFactory;
+import com.mongodb.internal.thread.MongoThreadPoolExecutor;
import com.mongodb.lang.Nullable;
import com.mongodb.spi.dns.InetAddressResolver;
+import java.io.IOException;
import java.nio.channels.AsynchronousChannelGroup;
+import java.nio.channels.AsynchronousSocketChannel;
+import java.time.Duration;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.SynchronousQueue;
+
+import static com.mongodb.internal.VisibleForTesting.AccessModifier.PRIVATE;
/**
* A {@code StreamFactoryFactory} implementation for AsynchronousSocketChannel-based streams.
@@ -30,30 +43,59 @@
*/
public final class AsynchronousSocketChannelStreamFactoryFactory implements StreamFactoryFactory {
private final InetAddressResolver inetAddressResolver;
- @Nullable
private final AsynchronousChannelGroup group;
+ private final ExecutorService ownedExecutorService;
- public AsynchronousSocketChannelStreamFactoryFactory(final InetAddressResolver inetAddressResolver) {
+ @VisibleForTesting(otherwise = PRIVATE)
+ AsynchronousSocketChannelStreamFactoryFactory(final InetAddressResolver inetAddressResolver) {
this(inetAddressResolver, null);
}
+ /**
+ * @param applicationSuppliedOwnedExecutorService Owned {@link ExecutorService} from {@link AsyncTransportSettings#getExecutorService()}.
+ */
AsynchronousSocketChannelStreamFactoryFactory(
final InetAddressResolver inetAddressResolver,
- @Nullable final AsynchronousChannelGroup group) {
+ @Nullable final ExecutorService applicationSuppliedOwnedExecutorService) {
this.inetAddressResolver = inetAddressResolver;
- this.group = group;
+ try {
+ if (applicationSuppliedOwnedExecutorService == null) {
+ // We try to create a group similarly to how
+ // the system-wide default `AsynchronousChannelGroup` is created.
+ // This means:
+ // - creating the executor similarly to `Executors.newCachedThreadPool`;
+ // - creating the group via `withCachedThreadPool`;
+ // - requesting the implementation specific default by passing negative `initialSize`.
+ ownedExecutorService = new MongoThreadPoolExecutor(
+ 0, Integer.MAX_VALUE, Duration.ofSeconds(60), new SynchronousQueue<>(), new DaemonThreadFactory("IOExecutor"));
+ group = AsynchronousChannelGroup.withCachedThreadPool(ownedExecutorService, -1);
+ } else {
+ ownedExecutorService = applicationSuppliedOwnedExecutorService;
+ group = AsynchronousChannelGroup.withThreadPool(ownedExecutorService);
+ }
+ } catch (IOException e) {
+ throw new MongoClientException("Unable to create an asynchronous channel group", e);
+ }
}
@Override
public StreamFactory create(final SocketSettings socketSettings, final SslSettings sslSettings) {
return new AsynchronousSocketChannelStreamFactory(
- inetAddressResolver, socketSettings, sslSettings, group);
+ inetAddressResolver, socketSettings, sslSettings, () -> AsynchronousSocketChannel.open(group));
+ }
+
+ /**
+ * @return The {@link ExecutorService} used by the {@link StreamFactory} created via {@link #create(SocketSettings, SslSettings)}.
+ * It may be provided by an application via {@link AsyncTransportSettings#getExecutorService()}.
+ */
+ @Override
+ public Executor getExecutor() {
+ return ownedExecutorService;
}
@Override
public void close() {
- if (group != null) {
- group.shutdown();
- }
+ // termination of the `group` results in the orderly shutdown of the `ownedExecutorService`
+ group.shutdown();
}
}
diff --git a/driver-core/src/main/com/mongodb/internal/connection/BackpressureErrorLabeler.java b/driver-core/src/main/com/mongodb/internal/connection/BackpressureErrorLabeler.java
new file mode 100644
index 00000000000..ec14c941896
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/internal/connection/BackpressureErrorLabeler.java
@@ -0,0 +1,179 @@
+/*
+ * 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.connection;
+
+import com.mongodb.MongoException;
+import com.mongodb.MongoSocketException;
+import com.mongodb.MongoSocksProxyException;
+
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLProtocolException;
+import java.net.UnknownHostException;
+import java.security.cert.CertPathBuilderException;
+import java.security.cert.CertPathValidatorException;
+import java.security.cert.CertificateException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * Attaches {@link MongoException#SYSTEM_OVERLOADED_ERROR_LABEL} and
+ * {@link MongoException#RETRYABLE_ERROR_LABEL} to network errors encountered during connection
+ * establishment or the hello message, per the CMAP specification.
+ *
+ * This is topology-agnostic: it must be invoked from the connection-establishment path so that
+ * both default SDAM and load-balanced modes are covered.
+ */
+final class BackpressureErrorLabeler {
+
+ /**
+ * BouncyCastle TLS fatal-alert exception type names.
+ */
+ private static final Set BOUNCY_CASTLE_TLS_FATAL_TYPE_NAMES = Collections.unmodifiableSet(
+ new HashSet<>(Arrays.asList(
+ "org.bouncycastle.tls.TlsFatalAlert",
+ "org.bouncycastle.tls.TlsFatalAlertReceived",
+ "org.bouncycastle.tls.crypto.TlsCryptoException")));
+
+ /**
+ * RFC 5246 / RFC 8446 alert descriptions that surface in BouncyCastle TLS exception messages.
+ * See AlertDescription.java.
+ * See (TLS) Protocol Version 1.2 - Alert Protocol.
+ */
+ private static final Set BOUNCY_CASTLE_TLS_ALERT_DESCRIPTIONS = Collections.unmodifiableSet(
+ new HashSet<>(Arrays.asList(
+ "close_notify", "unexpected_message", "bad_record_mac", "decryption_failed",
+ "record_overflow", "decompression_failure", "handshake_failure", "no_certificate",
+ "bad_certificate", "unsupported_certificate", "certificate_revoked", "certificate_expired",
+ "certificate_unknown", "illegal_parameter", "unknown_ca", "access_denied",
+ "decode_error", "decrypt_error", "export_restriction", "protocol_version",
+ "insufficient_security", "internal_error", "no_renegotiation", "unsupported_extension",
+ "certificate_unobtainable", "unrecognized_name", "bad_certificate_status_response",
+ "bad_certificate_hash_value", "unknown_psk_identity", "no_application_protocol",
+ "inappropriate_fallback", "missing_extension", "certificate_required")));
+
+ private BackpressureErrorLabeler() {
+ }
+
+ static void applyLabelsIfEligible(final Throwable t) {
+ if (!(t instanceof MongoSocketException)) {
+ return;
+ }
+ MongoSocketException socketException = (MongoSocketException) t;
+ if (isSocksFailureNotEligibleForLabeling(socketException)) {
+ return;
+ }
+ if (isDnsLookupFailure(socketException)) {
+ return;
+ }
+ if (isTlsConfigurationError(socketException)) {
+ return;
+ }
+ socketException.addLabel(MongoException.SYSTEM_OVERLOADED_ERROR_LABEL);
+ socketException.addLabel(MongoException.RETRYABLE_ERROR_LABEL);
+ }
+
+ private static boolean isSocksFailureNotEligibleForLabeling(final MongoSocketException t) {
+ if (!(t instanceof MongoSocksProxyException)) {
+ return false;
+ }
+ Integer replyCode = ((MongoSocksProxyException) t).getProxyReplyCode();
+ if (replyCode == null) {
+ return true;
+ }
+ return replyCode != SocksSocket.ServerReply.NET_UNREACHABLE.getReplyNumber()
+ && replyCode != SocksSocket.ServerReply.HOST_UNREACHABLE.getReplyNumber()
+ && replyCode != SocksSocket.ServerReply.CONN_REFUSED.getReplyNumber();
+ }
+
+ private static boolean isDnsLookupFailure(final MongoSocketException t) {
+ Throwable cause = t.getCause();
+ while (cause != null) {
+ if (cause instanceof UnknownHostException) {
+ return true;
+ }
+ cause = cause.getCause();
+ }
+ return false;
+ }
+
+ private static boolean isTlsConfigurationError(final MongoSocketException t) {
+ Throwable cause = t.getCause();
+ while (cause != null) {
+ if (cause instanceof CertificateException
+ || cause instanceof CertPathBuilderException
+ || cause instanceof CertPathValidatorException
+ || cause instanceof SSLPeerUnverifiedException
+ || cause instanceof SSLProtocolException) {
+ return true;
+ }
+ if (cause instanceof SSLHandshakeException) {
+ String message = cause.getMessage();
+ if (message != null) {
+ String lowerMessage = message.toLowerCase(Locale.ROOT);
+ if (lowerMessage.contains("verify")
+ || lowerMessage.contains("protocol")
+ || lowerMessage.contains("cipher")
+ || lowerMessage.contains("received fatal alert")) {
+ return true;
+ }
+ }
+ }
+ if (isBouncyCastleTlsError(cause)) {
+ return true;
+ }
+
+ cause = cause.getCause();
+ }
+ return false;
+ }
+
+ private static boolean isBouncyCastleTlsError(final Throwable cause) {
+ if (!isBouncyCastleTlsFatalType(cause.getClass())) {
+ return false;
+ }
+ String message = cause.getMessage();
+ if (message == null) {
+ return false;
+ }
+ String description = message.toLowerCase(Locale.ROOT);
+ for (String alertName : BOUNCY_CASTLE_TLS_ALERT_DESCRIPTIONS) {
+ if (description.contains(alertName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Walks the class hierarchy comparing fully qualified names so a subclass of a known BC type
+ * still matches.
+ */
+ private static boolean isBouncyCastleTlsFatalType(final Class> exceptionClass) {
+ Class> cls = exceptionClass;
+ while (cls != null) {
+ if (BOUNCY_CASTLE_TLS_FATAL_TYPE_NAMES.contains(cls.getName())) {
+ return true;
+ }
+ cls = cls.getSuperclass();
+ }
+ return false;
+ }
+}
diff --git a/driver-core/src/main/com/mongodb/internal/connection/BaseCluster.java b/driver-core/src/main/com/mongodb/internal/connection/BaseCluster.java
index 4146d06c22e..2f0fcaa6379 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/BaseCluster.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/BaseCluster.java
@@ -113,9 +113,9 @@ abstract class BaseCluster implements Cluster {
private volatile ClusterDescription description;
BaseCluster(final ClusterId clusterId,
- final ClusterSettings settings,
- final ClusterableServerFactory serverFactory,
- final ClientMetadata clientMetadata) {
+ final ClusterSettings settings,
+ final ClusterableServerFactory serverFactory,
+ final ClientMetadata clientMetadata) {
this.clusterId = notNull("clusterId", clusterId);
this.settings = notNull("settings", settings);
this.serverFactory = notNull("serverFactory", serverFactory);
@@ -159,7 +159,7 @@ public ServerTuple selectServer(final ServerSelector serverSelector, final Opera
if (serverTuple != null) {
ServerAddress serverAddress = serverTuple.getServerDescription().getAddress();
logServerSelectionSucceeded(operationContext, clusterId, serverAddress, serverSelector, currentDescription);
- serverDeprioritization.updateCandidate(serverAddress);
+ serverDeprioritization.updateCandidate(serverAddress, currentDescription.getType());
return serverTuple;
}
computedServerSelectionTimeout.onExpired(() ->
@@ -302,7 +302,7 @@ private boolean handleServerSelectionRequest(
if (serverTuple != null) {
ServerAddress serverAddress = serverTuple.getServerDescription().getAddress();
logServerSelectionSucceeded(operationContext, clusterId, serverAddress, request.originalSelector, description);
- serverDeprioritization.updateCandidate(serverAddress);
+ serverDeprioritization.updateCandidate(serverAddress, description.getType());
request.onResult(serverTuple, null);
return true;
}
@@ -361,8 +361,7 @@ private static ServerSelector getCompleteServerSelector(
final ClusterSettings settings) {
List selectors = Stream.of(
getRaceConditionPreFilteringSelector(serversSnapshot),
- serverSelector,
- serverDeprioritization.getServerSelector(),
+ serverDeprioritization.apply(serverSelector),
settings.getServerSelector(), // may be null
new LatencyMinimizingServerSelector(settings.getLocalThreshold(MILLISECONDS), MILLISECONDS),
AtMostTwoRandomServerSelector.instance(),
diff --git a/driver-core/src/main/com/mongodb/internal/connection/DefaultClusterFactory.java b/driver-core/src/main/com/mongodb/internal/connection/DefaultClusterFactory.java
index ac853cb002e..668af7119c3 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/DefaultClusterFactory.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/DefaultClusterFactory.java
@@ -35,6 +35,7 @@
import com.mongodb.internal.VisibleForTesting;
import com.mongodb.internal.diagnostics.logging.Logger;
import com.mongodb.internal.diagnostics.logging.Loggers;
+import com.mongodb.internal.thread.AsyncClientExecutor;
import com.mongodb.lang.Nullable;
import com.mongodb.spi.dns.DnsClient;
@@ -65,6 +66,7 @@ public Cluster createCluster(final ClusterSettings originalClusterSettings, fina
final StreamFactory streamFactory,
final TimeoutSettings heartbeatTimeoutSettings,
final StreamFactory heartbeatStreamFactory,
+ final AsyncClientExecutor clientExecutor,
@Nullable final MongoCredential credential,
final LoggerSettings loggerSettings,
@Nullable final CommandListener commandListener,
@@ -103,9 +105,9 @@ public Cluster createCluster(final ClusterSettings originalClusterSettings, fina
DnsSrvRecordMonitorFactory dnsSrvRecordMonitorFactory = new DefaultDnsSrvRecordMonitorFactory(clusterId, serverSettings, dnsClient);
InternalOperationContextFactory clusterOperationContextFactory =
- new InternalOperationContextFactory(clusterTimeoutSettings, serverApi);
+ new InternalOperationContextFactory(clusterTimeoutSettings, serverApi, clientExecutor);
InternalOperationContextFactory heartBeatOperationContextFactory =
- new InternalOperationContextFactory(heartbeatTimeoutSettings, serverApi);
+ new InternalOperationContextFactory(heartbeatTimeoutSettings, serverApi, clientExecutor);
ClientMetadata clientMetadata = new ClientMetadata(
applicationName,
diff --git a/driver-core/src/main/com/mongodb/internal/connection/DefaultConnectionPool.java b/driver-core/src/main/com/mongodb/internal/connection/DefaultConnectionPool.java
index 2339cf18b86..a0a8bbe6e2d 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/DefaultConnectionPool.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/DefaultConnectionPool.java
@@ -894,7 +894,7 @@ public boolean shouldPrune(final UsageTrackingInternalConnection usageTrackingCo
/**
* Package-access methods are thread-safe,
- * and only they should be called outside of the {@link OpenConcurrencyLimiter}'s code.
+ * and only they should be called outside the {@link OpenConcurrencyLimiter}'s code.
*/
@ThreadSafe
private final class OpenConcurrencyLimiter {
@@ -1334,7 +1334,6 @@ private boolean initUnlessClosed() {
* {@linkplain Task#failAsClosed() fail} asynchronously.
*/
@Override
- @SuppressWarnings("try")
public void close() {
withLock(lock, () -> {
if (state != State.CLOSED) {
diff --git a/driver-core/src/main/com/mongodb/internal/connection/DefaultSdamServerDescriptionManager.java b/driver-core/src/main/com/mongodb/internal/connection/DefaultSdamServerDescriptionManager.java
index af4acd8c031..371e709d807 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/DefaultSdamServerDescriptionManager.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/DefaultSdamServerDescriptionManager.java
@@ -137,6 +137,9 @@ private void handleException(final SdamIssue sdamIssue, final boolean beforeHand
serverMonitor.connect();
} else if (sdamIssue.relatedToNetworkNotTimeout()
|| (beforeHandshake && (sdamIssue.relatedToNetworkTimeout() || sdamIssue.relatedToAuth()))) {
+ if (sdamIssue.hasSystemOverloadedLabel()) {
+ return;
+ }
updateDescription(sdamIssue.serverDescription());
connectionPool.invalidate(sdamIssue.exception().orElse(null));
serverMonitor.cancelCurrentCheck();
diff --git a/driver-core/src/main/com/mongodb/internal/connection/InternalOperationContextFactory.java b/driver-core/src/main/com/mongodb/internal/connection/InternalOperationContextFactory.java
index 4653c90050b..851ca5a38ca 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/InternalOperationContextFactory.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/InternalOperationContextFactory.java
@@ -18,6 +18,7 @@
import com.mongodb.ServerApi;
import com.mongodb.internal.TimeoutContext;
import com.mongodb.internal.TimeoutSettings;
+import com.mongodb.internal.thread.AsyncClientExecutor;
import com.mongodb.lang.Nullable;
import static com.mongodb.internal.connection.OperationContext.simpleOperationContext;
@@ -27,17 +28,22 @@ public final class InternalOperationContextFactory {
private final TimeoutSettings timeoutSettings;
@Nullable
private final ServerApi serverApi;
+ private final AsyncClientExecutor clientExecutor;
- public InternalOperationContextFactory(final TimeoutSettings timeoutSettings, @Nullable final ServerApi serverApi) {
+ public InternalOperationContextFactory(
+ final TimeoutSettings timeoutSettings,
+ @Nullable final ServerApi serverApi,
+ final AsyncClientExecutor clientExecutor) {
this.timeoutSettings = timeoutSettings;
this.serverApi = serverApi;
+ this.clientExecutor = clientExecutor;
}
/**
* @return a simple operation context without timeoutMS
*/
OperationContext create() {
- return simpleOperationContext(timeoutSettings.connectionOnly(), serverApi);
+ return simpleOperationContext(timeoutSettings.connectionOnly(), serverApi, clientExecutor);
}
/**
diff --git a/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java b/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java
index 27de4840633..24c685fcbca 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnection.java
@@ -231,9 +231,11 @@ public void open(final OperationContext originalOperationContext) {
isTrue("Open already called", stream == null);
stream = streamFactory.create(serverId.getAddress());
OperationContext operationContext = originalOperationContext;
+ boolean beforeHandshake = true;
try {
stream.open(operationContext);
InternalConnectionInitializationDescription initializationDescription = connectionInitializer.startHandshake(this, operationContext);
+ beforeHandshake = false;
operationContext = operationContext.withOverride(TimeoutContext::withNewlyStartedMaintenanceTimeout);
initAfterHandshakeStart(initializationDescription);
@@ -242,6 +244,9 @@ public void open(final OperationContext originalOperationContext) {
initAfterHandshakeFinish(initializationDescription);
} catch (Throwable t) {
close();
+ if (beforeHandshake) {
+ BackpressureErrorLabeler.applyLabelsIfEligible(t);
+ }
if (t instanceof MongoException) {
throw (MongoException) t;
} else {
@@ -264,6 +269,7 @@ public void completed(@Nullable final Void aVoid) {
(initialResult, initialException) -> {
if (initialException != null) {
close();
+ BackpressureErrorLabeler.applyLabelsIfEligible(initialException);
callback.onResult(null, initialException);
} else {
assertNotNull(initialResult);
@@ -279,11 +285,13 @@ public void completed(@Nullable final Void aVoid) {
@Override
public void failed(final Throwable t) {
close();
+ BackpressureErrorLabeler.applyLabelsIfEligible(t);
callback.onResult(null, t);
}
});
} catch (Throwable t) {
close();
+ BackpressureErrorLabeler.applyLabelsIfEligible(t);
callback.onResult(null, t);
}
}
diff --git a/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnectionInitializer.java b/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnectionInitializer.java
index 574a85669d0..36f6688cb0e 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnectionInitializer.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/InternalStreamConnectionInitializer.java
@@ -172,7 +172,8 @@ private InternalConnectionInitializationDescription createInitializationDescript
private BsonDocument createHelloCommand(final Authenticator authenticator, final InternalConnection connection) {
BsonDocument helloCommandDocument = new BsonDocument(getHandshakeCommandName(), new BsonInt32(1))
- .append("helloOk", BsonBoolean.TRUE);
+ .append("helloOk", BsonBoolean.TRUE)
+ .append("backpressure", BsonBoolean.TRUE);
if (clientMetadataDocument != null) {
helloCommandDocument.append("client", clientMetadataDocument);
}
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 3793d5b7a5b..5920bbc8756 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java
@@ -135,7 +135,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 f23d5e5226b..cb8d0830dd1 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/OperationContext.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/OperationContext.java
@@ -17,6 +17,7 @@
import com.mongodb.Function;
import com.mongodb.MongoConnectionPoolClearedException;
+import com.mongodb.MongoException;
import com.mongodb.ReadConcern;
import com.mongodb.RequestContext;
import com.mongodb.ServerAddress;
@@ -30,7 +31,9 @@
import com.mongodb.internal.VisibleForTesting;
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.internal.thread.AsyncClientExecutor;
import com.mongodb.lang.Nullable;
import com.mongodb.selector.ServerSelector;
@@ -40,6 +43,9 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
+import static com.mongodb.MongoException.SYSTEM_OVERLOADED_ERROR_LABEL;
+import static com.mongodb.assertions.Assertions.assertFalse;
+import static com.mongodb.internal.VisibleForTesting.AccessModifier.PRIVATE;
import static java.util.stream.Collectors.toList;
/**
@@ -57,19 +63,37 @@ public class OperationContext {
private final ServerApi serverApi;
@Nullable
private final String operationName;
+ private final AsyncClientExecutor clientExecutor;
@Nullable
private Span tracingSpan;
+ @VisibleForTesting(otherwise = PRIVATE)
public OperationContext(final RequestContext requestContext, final SessionContext sessionContext, final TimeoutContext timeoutContext,
@Nullable final ServerApi serverApi) {
- this(requestContext, sessionContext, timeoutContext, TracingManager.NO_OP, serverApi, null);
+ this(requestContext, sessionContext, timeoutContext, AsyncClientExecutor.NO_OP, TracingManager.NO_OP, serverApi, null);
}
public OperationContext(final RequestContext requestContext, final SessionContext sessionContext, final TimeoutContext timeoutContext,
+ final AsyncClientExecutor clientExecutor,
final TracingManager tracingManager,
@Nullable final ServerApi serverApi,
@Nullable final String operationName) {
this(NEXT_ID.incrementAndGet(), requestContext, sessionContext, timeoutContext, new ServerDeprioritization(),
+ clientExecutor,
+ tracingManager,
+ serverApi,
+ operationName,
+ null);
+ }
+
+ public OperationContext(final RequestContext requestContext, final SessionContext sessionContext, final TimeoutContext timeoutContext,
+ final AsyncClientExecutor clientExecutor,
+ final TracingManager tracingManager,
+ @Nullable final ServerApi serverApi,
+ @Nullable final String operationName,
+ final ServerDeprioritization serverDeprioritization) {
+ this(NEXT_ID.incrementAndGet(), requestContext, sessionContext, timeoutContext, serverDeprioritization,
+ clientExecutor,
tracingManager,
serverApi,
operationName,
@@ -77,40 +101,45 @@ public OperationContext(final RequestContext requestContext, final SessionContex
}
public static OperationContext simpleOperationContext(
- final TimeoutSettings timeoutSettings, @Nullable final ServerApi serverApi) {
+ final TimeoutSettings timeoutSettings, @Nullable final ServerApi serverApi, final AsyncClientExecutor clientExecutor) {
return new OperationContext(
IgnorableRequestContext.INSTANCE,
NoOpSessionContext.INSTANCE,
new TimeoutContext(timeoutSettings),
+ clientExecutor,
TracingManager.NO_OP,
serverApi,
- null
- );
+ null);
}
- public static OperationContext simpleOperationContext(final TimeoutContext timeoutContext) {
- return new OperationContext(
- IgnorableRequestContext.INSTANCE,
- NoOpSessionContext.INSTANCE,
- timeoutContext,
- TracingManager.NO_OP,
- null,
- null);
+ @VisibleForTesting(otherwise = PRIVATE)
+ static OperationContext simpleOperationContext(final TimeoutSettings timeoutSettings) {
+ return simpleOperationContext(timeoutSettings, null, AsyncClientExecutor.NO_OP);
}
public OperationContext withSessionContext(final SessionContext sessionContext) {
- return new OperationContext(id, requestContext, sessionContext, timeoutContext, serverDeprioritization, tracingManager, serverApi,
- operationName, tracingSpan);
+ return new OperationContext(id, requestContext, sessionContext, timeoutContext, serverDeprioritization, clientExecutor,
+ tracingManager, serverApi, operationName, tracingSpan);
}
public OperationContext withTimeoutContext(final TimeoutContext timeoutContext) {
- return new OperationContext(id, requestContext, sessionContext, timeoutContext, serverDeprioritization, tracingManager, serverApi,
- operationName, tracingSpan);
+ return new OperationContext(id, requestContext, sessionContext, timeoutContext, serverDeprioritization, clientExecutor,
+ tracingManager, serverApi, operationName, tracingSpan);
}
public OperationContext withOperationName(final String operationName) {
- return new OperationContext(id, requestContext, sessionContext, timeoutContext, serverDeprioritization, tracingManager, serverApi,
- operationName, tracingSpan);
+ return new OperationContext(id, requestContext, sessionContext, timeoutContext, serverDeprioritization, clientExecutor,
+ tracingManager, serverApi, operationName, tracingSpan);
+ }
+
+ /**
+ * TODO-JAVA-6058: This method enables overriding the ServerDeprioritization state.
+ * It is a temporary solution to handle cases where deprioritization state persists across operations.
+ */
+ public OperationContext withNewServerDeprioritization() {
+ return new OperationContext(id, requestContext, sessionContext, timeoutContext,
+ new ServerDeprioritization(serverDeprioritization.enableOverloadRetargeting), clientExecutor,
+ tracingManager, serverApi, operationName, tracingSpan);
}
public long getId() {
@@ -143,6 +172,10 @@ public String getOperationName() {
return operationName;
}
+ public AsyncClientExecutor getClientExecutor() {
+ return clientExecutor;
+ }
+
@Nullable
public Span getTracingSpan() {
return tracingSpan;
@@ -152,12 +185,12 @@ public void setTracingSpan(final Span tracingSpan) {
this.tracingSpan = tracingSpan;
}
- @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE)
- public OperationContext(final long id,
+ private OperationContext(final long id,
final RequestContext requestContext,
final SessionContext sessionContext,
final TimeoutContext timeoutContext,
final ServerDeprioritization serverDeprioritization,
+ final AsyncClientExecutor clientExecutor,
final TracingManager tracingManager,
@Nullable final ServerApi serverApi,
@Nullable final String operationName,
@@ -168,32 +201,13 @@ public OperationContext(final long id,
this.requestContext = requestContext;
this.sessionContext = sessionContext;
this.timeoutContext = timeoutContext;
+ this.clientExecutor = clientExecutor;
this.tracingManager = tracingManager;
this.serverApi = serverApi;
this.operationName = operationName;
this.tracingSpan = tracingSpan;
}
- @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE)
- public OperationContext(final long id,
- final RequestContext requestContext,
- final SessionContext sessionContext,
- final TimeoutContext timeoutContext,
- final TracingManager tracingManager,
- @Nullable final ServerApi serverApi,
- @Nullable final String operationName) {
- this.id = id;
- this.serverDeprioritization = new ServerDeprioritization();
- this.requestContext = requestContext;
- this.sessionContext = sessionContext;
- this.timeoutContext = timeoutContext;
- this.tracingManager = tracingManager;
- this.serverApi = serverApi;
- this.operationName = operationName;
- this.tracingSpan = null;
- }
-
-
/**
* @return The same {@link ServerDeprioritization} if called on the same {@link OperationContext}.
*/
@@ -217,7 +231,8 @@ public OperationContext withConnectionEstablishmentSessionContext() {
}
public OperationContext withMinRoundTripTime(final ServerDescription serverDescription) {
- return withTimeoutContext(timeoutContext.withMinRoundTripTime(TimeUnit.NANOSECONDS.toMillis(serverDescription.getMinRoundTripTimeNanos())));
+ return withTimeoutContext(
+ timeoutContext.withMinRoundTripTime(TimeUnit.NANOSECONDS.toMillis(serverDescription.getMinRoundTripTimeNanos())));
}
public OperationContext withOverride(final TimeoutContextOverride timeoutContextOverrideFunction) {
@@ -227,34 +242,55 @@ public OperationContext withOverride(final TimeoutContextOverride timeoutContext
public static final class ServerDeprioritization {
@Nullable
private ServerAddress candidate;
+ @Nullable
+ private ClusterType clusterType;
private final Set deprioritized;
- private final DeprioritizingSelector selector;
+ private final boolean enableOverloadRetargeting;
- private ServerDeprioritization() {
- candidate = null;
- deprioritized = new HashSet<>();
- selector = new DeprioritizingSelector();
+ public ServerDeprioritization() {
+ this(false);
+ }
+
+ public ServerDeprioritization(final boolean enableOverloadRetargeting) {
+ this.enableOverloadRetargeting = enableOverloadRetargeting;
+ this.candidate = null;
+ this.deprioritized = new HashSet<>();
+ this.clusterType = null;
}
/**
- * The returned {@link ServerSelector} tries to {@linkplain ServerSelector#select(ClusterDescription) select}
- * only the {@link ServerDescription}s that do not have deprioritized {@link ServerAddress}es.
- * If no such {@link ServerDescription} can be selected, then it selects {@link ClusterDescription#getServerDescriptions()}.
+ * The returned {@link ServerSelector} wraps the provided selector and attempts
+ * {@linkplain ServerSelector#select(ClusterDescription) server selection} in two passes:
+ *
+ * - First pass: selects using the wrapped selector with only non-deprioritized {@link ServerDescription}s.
+ * - Second pass: if the first pass selects no {@link ServerDescription}s,
+ * selects using the wrapped selector again with all {@link ServerDescription}s, including deprioritized ones.
+ *
*/
- ServerSelector getServerSelector() {
- return selector;
+ ServerSelector apply(final ServerSelector wrappedSelector) {
+ return new DeprioritizingSelector(wrappedSelector);
}
- void updateCandidate(final ServerAddress serverAddress) {
- candidate = serverAddress;
+ void updateCandidate(final ServerAddress serverAddress, final ClusterType clusterType) {
+ this.candidate = serverAddress;
+ 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;
}
- deprioritized.add(candidate);
+
+ // As per spec: sharded clusters deprioritize on any error,
+ // other topologies deprioritize on overload only when retargeting is enabled.
+ boolean isSystemOverloadedError = attemptFailedResult instanceof MongoException
+ && ((MongoException) attemptFailedResult).hasErrorLabel(SYSTEM_OVERLOADED_ERROR_LABEL);
+
+ if (clusterType == ClusterType.SHARDED || (isSystemOverloadedError && enableOverloadRetargeting)) {
+ deprioritized.add(candidate);
+ }
}
/**
@@ -263,28 +299,46 @@ public void onAttemptFailure(final Throwable failure) {
* which indeed may be used concurrently. {@link DeprioritizingSelector} does not need to be thread-safe.
*/
private final class DeprioritizingSelector implements ServerSelector {
- private DeprioritizingSelector() {
+ private final ServerSelector wrappedSelector;
+
+ private DeprioritizingSelector(final ServerSelector wrappedSelector) {
+ this.wrappedSelector = wrappedSelector;
}
@Override
public List select(final ClusterDescription clusterDescription) {
List serverDescriptions = clusterDescription.getServerDescriptions();
- if (!isEnabled(clusterDescription.getType())) {
- return serverDescriptions;
+
+ // TODO-JAVA-5908: Evaluate whether using the early-return optimization has a meaningful performance impact on server selection.
+ if (serverDescriptions.size() == 1 || deprioritized.isEmpty()) {
+ return wrappedSelector.select(clusterDescription);
}
+
+ // TODO-JAVA-5908: Evaluate whether using a loop instead of Stream has a meaningful performance impact on server selection.
List nonDeprioritizedServerDescriptions = serverDescriptions
.stream()
.filter(serverDescription -> !deprioritized.contains(serverDescription.getAddress()))
.collect(toList());
- return nonDeprioritizedServerDescriptions.isEmpty() ? serverDescriptions : nonDeprioritizedServerDescriptions;
- }
- private boolean isEnabled(final ClusterType clusterType) {
- return clusterType == ClusterType.SHARDED;
+ // TODO-JAVA-5908: Evaluate whether using the early-return optimization has a meaningful performance impact on server selection.
+ if (nonDeprioritizedServerDescriptions.isEmpty()) {
+ return wrappedSelector.select(clusterDescription);
+ }
+
+ List selected = wrappedSelector.select(
+ new ClusterDescription(
+ clusterDescription.getConnectionMode(),
+ clusterDescription.getType(),
+ clusterDescription.getSrvResolutionException(),
+ nonDeprioritizedServerDescriptions,
+ clusterDescription.getClusterSettings(),
+ clusterDescription.getServerSettings()));
+ return selected.isEmpty() ? wrappedSelector.select(clusterDescription) : selected;
}
}
}
- public interface TimeoutContextOverride extends Function {}
+ public interface TimeoutContextOverride extends Function {
+ }
}
diff --git a/driver-core/src/main/com/mongodb/internal/connection/SdamServerDescriptionManager.java b/driver-core/src/main/com/mongodb/internal/connection/SdamServerDescriptionManager.java
index 7f014d7ede6..4b989193c0d 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/SdamServerDescriptionManager.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/SdamServerDescriptionManager.java
@@ -17,6 +17,7 @@
package com.mongodb.internal.connection;
import com.mongodb.MongoCommandException;
+import com.mongodb.MongoException;
import com.mongodb.MongoNodeIsRecoveringException;
import com.mongodb.MongoNotPrimaryException;
import com.mongodb.MongoSecurityException;
@@ -162,6 +163,11 @@ boolean relatedToWriteConcern() {
return exception instanceof MongoWriteConcernWithResponseException;
}
+ boolean hasSystemOverloadedLabel() {
+ return exception instanceof MongoException
+ && ((MongoException) exception).hasErrorLabel(MongoException.SYSTEM_OVERLOADED_ERROR_LABEL);
+ }
+
private static boolean stale(@Nullable final Throwable t, final ServerDescription currentServerDescription) {
return TopologyVersionHelper.topologyVersion(t)
.map(candidateTopologyVersion -> TopologyVersionHelper.newerOrEqual(
diff --git a/driver-core/src/main/com/mongodb/internal/connection/SocketStream.java b/driver-core/src/main/com/mongodb/internal/connection/SocketStream.java
index a1c3ed0d914..764f41034f2 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/SocketStream.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/SocketStream.java
@@ -16,14 +16,18 @@
package com.mongodb.internal.connection;
+import com.mongodb.MongoException;
+import com.mongodb.MongoInterruptedException;
import com.mongodb.MongoSocketException;
import com.mongodb.MongoSocketOpenException;
import com.mongodb.MongoSocketReadException;
+import com.mongodb.MongoSocksProxyException;
import com.mongodb.ServerAddress;
import com.mongodb.connection.AsyncCompletionHandler;
import com.mongodb.connection.ProxySettings;
import com.mongodb.connection.SocketSettings;
import com.mongodb.connection.SslSettings;
+import com.mongodb.lang.Nullable;
import com.mongodb.spi.dns.InetAddressResolver;
import org.bson.ByteBuf;
@@ -38,6 +42,7 @@
import java.net.SocketTimeoutException;
import java.util.Iterator;
import java.util.List;
+import java.util.Optional;
import static com.mongodb.assertions.Assertions.assertTrue;
import static com.mongodb.assertions.Assertions.notNull;
@@ -79,13 +84,26 @@ public void open(final OperationContext operationContext) {
socket = initializeSocket(operationContext);
outputStream = socket.getOutputStream();
inputStream = socket.getInputStream();
+ } catch (MongoSocksProxyException e) {
+ close();
+ throw translateOr(e.getCause(), e);
} catch (IOException e) {
close();
- throw translateInterruptedException(e, "Interrupted while connecting")
- .orElseThrow(() -> new MongoSocketOpenException("Exception opening socket", getAddress(), e));
+ throw translateOr(e, new MongoSocketOpenException("Exception opening socket", getAddress(), e));
}
}
+ /**
+ * If {@code interruptCandidate} represents a thread interruption, returns the corresponding
+ * {@link MongoInterruptedException}; otherwise returns {@code fallback}.
+ */
+ private static MongoException translateOr(@Nullable final Throwable interruptCandidate,
+ final MongoException fallback) {
+ Optional translated =
+ translateInterruptedException(interruptCandidate, "Interrupted while connecting");
+ return translated.isPresent() ? translated.get() : fallback;
+ }
+
protected Socket initializeSocket(final OperationContext operationContext) throws IOException {
ProxySettings proxySettings = settings.getProxySettings();
if (proxySettings.isProxyEnabled()) {
@@ -119,15 +137,28 @@ private SSLSocket initializeSslSocketOverSocksProxy(final OperationContext opera
final int serverPort = address.getPort();
SocksSocket socksProxy = new SocksSocket(settings.getProxySettings());
- configureSocket(socksProxy, operationContext, settings);
- InetSocketAddress inetSocketAddress = toSocketAddress(serverHost, serverPort);
- socksProxy.connect(inetSocketAddress, operationContext.getTimeoutContext().getConnectTimeoutMs());
-
- SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socksProxy, serverHost, serverPort, true);
- //Even though Socks proxy connection is already established, TLS handshake has not been performed yet.
- //So it is possible to set SSL parameters before handshake is done.
- configureSslSocket(sslSocket, sslSettings, inetSocketAddress);
- return sslSocket;
+ // Track the outermost socket layer to close on failure. Initially this is socksProxy;
+ // once we wrap it into an SSLSocket, that becomes the outermost layer and closing it
+ // tears down the underlying socksProxy as well.
+ Socket toClose = socksProxy;
+ try {
+ configureSocket(socksProxy, operationContext, settings);
+ InetSocketAddress inetSocketAddress = toSocketAddress(serverHost, serverPort);
+ socksProxy.connect(inetSocketAddress, operationContext.getTimeoutContext().getConnectTimeoutMs());
+ SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socksProxy, serverHost, serverPort, true);
+ toClose = sslSocket;
+ //Even though Socks proxy connection is already established, TLS handshake has not been performed yet.
+ //So it is possible to set SSL parameters before handshake is done.
+ configureSslSocket(sslSocket, sslSettings, inetSocketAddress);
+ return sslSocket;
+ } catch (IOException | RuntimeException e) {
+ try {
+ toClose.close();
+ } catch (IOException closeException) {
+ e.addSuppressed(closeException);
+ }
+ throw e;
+ }
}
@@ -141,17 +172,27 @@ private static InetSocketAddress toSocketAddress(final String serverHost, final
private Socket initializeSocketOverSocksProxy(final OperationContext operationContext) throws IOException {
Socket createdSocket = socketFactory.createSocket();
- configureSocket(createdSocket, operationContext, settings);
- /*
- Wrap the configured socket with SocksSocket to add extra functionality.
- Reason for separate steps: We can't directly extend Java 11 methods within 'SocksSocket'
- to configure itself.
- */
- SocksSocket socksProxy = new SocksSocket(createdSocket, settings.getProxySettings());
-
- socksProxy.connect(toSocketAddress(address.getHost(), address.getPort()),
- operationContext.getTimeoutContext().getConnectTimeoutMs());
- return socksProxy;
+ try {
+ configureSocket(createdSocket, operationContext, settings);
+ /*
+ Wrap the configured socket with SocksSocket to add extra functionality.
+ Reason for separate steps: We can't directly extend Java 11 methods within 'SocksSocket'
+ to configure itself.
+ */
+ SocksSocket socksProxy = new SocksSocket(createdSocket, settings.getProxySettings());
+ socksProxy.connect(toSocketAddress(address.getHost(), address.getPort()),
+ operationContext.getTimeoutContext().getConnectTimeoutMs());
+ return socksProxy;
+ } catch (IOException | RuntimeException e) {
+ // SocksSocket.connect() closes itself on failure, but createdSocket may not yet
+ // be owned by a SocksSocket (e.g. configureSocket threw). Close defensively;
+ try {
+ createdSocket.close();
+ } catch (IOException closeException) {
+ e.addSuppressed(closeException);
+ }
+ throw e;
+ }
}
@Override
diff --git a/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java b/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java
index 2619a3c2c10..9a1a42511e1 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/SocksSocket.java
@@ -15,6 +15,8 @@
*/
package com.mongodb.internal.connection;
+import com.mongodb.MongoSocksProxyException;
+import com.mongodb.ServerAddress;
import com.mongodb.connection.ProxySettings;
import com.mongodb.internal.time.Timeout;
import com.mongodb.lang.Nullable;
@@ -81,7 +83,7 @@ public SocksSocket(@Nullable final Socket socket, final ProxySettings proxySetti
}
@Override
- public void connect(final SocketAddress endpoint, final int connectTimeoutMs) throws IOException {
+ public void connect(final SocketAddress endpoint, final int connectTimeoutMs) {
// `Socket` requires `IllegalArgumentException`
isTrueArgument("connectTimeoutMs", connectTimeoutMs >= 0);
try {
@@ -97,22 +99,53 @@ public void connect(final SocketAddress endpoint, final int connectTimeoutMs) th
(ms) -> socketConnect(proxyAddress, Math.toIntExact(ms)),
() -> throwSocketConnectionTimeout());
- SocksAuthenticationMethod authenticationMethod = performNegotiation(timeout);
- authenticate(authenticationMethod, timeout);
- sendConnect(timeout);
- } catch (SocketException socketException) {
- /*
- * The 'close()' call here has two purposes:
- *
- * 1. Enforces self-closing under RFC 1928 if METHOD is X'FF'.
- * 2. Handles all other errors during connection, distinct from external closures.
- */
+ SocksAuthenticationMethod authenticationMethod;
+ try {
+ authenticationMethod = performNegotiation(timeout);
+ } catch (IOException e) {
+ throw new MongoSocksProxyException("Negotiation failed: " + e.getMessage(),
+ targetServerAddress(), e);
+ }
+
+ try {
+ authenticate(authenticationMethod, timeout);
+ } catch (IOException e) {
+ throw new MongoSocksProxyException("Authentication failed: " + e.getMessage(),
+ targetServerAddress(), e);
+ }
+
+ try {
+ sendConnect(timeout);
+ } catch (IOException e) {
+ throw new MongoSocksProxyException("CONNECT relay failed: " + e.getMessage(),
+ targetServerAddress(), e);
+ }
+ } catch (MongoSocksProxyException e) {
+ // Reached for any SOCKS5 protocol failure (negotiation / authentication / CONNECT-relay,
+ // including RFC 1928 X'FF' "no acceptable method" self-close). The proxy TCP socket is
+ // already connected at this point. MongoSocksProxyException is a RuntimeException and is
+ // not caught below, so close the socket here to avoid leaking the FD.
try {
close();
} catch (Exception closeException) {
- socketException.addSuppressed(closeException);
+ e.addSuppressed(closeException);
}
- throw socketException;
+ throw e;
+ } catch (IOException ioException) {
+ // Reached only when the initial proxy TCP connect fails
+ // before any SOCKS5 handshake byte goes on the wire. Inner-phase IOExceptions are
+ // converted to MongoSocksProxyException by the per-phase wrappers and caught above.
+ // Close the partially-initialised proxy socket so we don't leak a FD.
+ try {
+ close();
+ } catch (Exception closeException) {
+ ioException.addSuppressed(closeException);
+ }
+ throw new MongoSocksProxyException(
+ "Exception connecting to SOCKS5 proxy ("
+ + proxySettings.getHost() + ":" + proxySettings.getPort() + "): "
+ + ioException.getMessage(),
+ targetServerAddress(), ioException);
}
}
@@ -223,7 +256,9 @@ private void checkServerReply(final Timeout timeout) throws IOException {
}
return;
}
- throw new ConnectException(reply.getMessage());
+ throw new MongoSocksProxyException(
+ "CONNECT reply: " + reply.message + " (code " + reply.replyNumber + ")",
+ targetServerAddress(), reply.replyNumber);
}
private void authenticate(final SocksAuthenticationMethod authenticationMethod, final Timeout timeout) throws IOException {
@@ -249,7 +284,9 @@ private void authenticate(final SocksAuthenticationMethod authenticationMethod,
byte authStatus = authResult[1];
if (authStatus != AUTHENTICATION_SUCCEEDED_STATUS) {
- throw new ConnectException("Authentication failed. Proxy server returned status: " + authStatus);
+ throw new MongoSocksProxyException(
+ "Authentication failed. Proxy server returned status: " + authStatus,
+ targetServerAddress());
}
}
}
@@ -273,13 +310,16 @@ private SocksAuthenticationMethod performNegotiation(final Timeout timeout) thro
byte[] handshakeReply = readSocksReply(2, timeout);
if (handshakeReply[0] != SOCKS_VERSION) {
- throw new ConnectException("Remote server doesn't support socks version 5"
- + " Received version: " + handshakeReply[0]);
+ throw new MongoSocksProxyException("Remote server doesn't support socks version 5"
+ + " Received version: " + handshakeReply[0],
+ targetServerAddress());
}
byte authMethodNumber = handshakeReply[1];
if (authMethodNumber == (byte) 0xFF) {
- throw new ConnectException("None of the authentication methods listed are acceptable. Attempted methods: "
- + Arrays.toString(authenticationMethods));
+ throw new MongoSocksProxyException(
+ "None of the authentication methods listed are acceptable. Attempted methods: "
+ + Arrays.toString(authenticationMethods),
+ targetServerAddress());
}
if (authMethodNumber == SocksAuthenticationMethod.NO_AUTH.getMethodNumber()) {
return SocksAuthenticationMethod.NO_AUTH;
@@ -287,7 +327,12 @@ private SocksAuthenticationMethod performNegotiation(final Timeout timeout) thro
return SocksAuthenticationMethod.USERNAME_PASSWORD;
}
- throw new ConnectException("Proxy returned unsupported authentication method: " + authMethodNumber);
+ throw new MongoSocksProxyException("Proxy returned unsupported authentication method: " + authMethodNumber,
+ targetServerAddress());
+ }
+
+ private ServerAddress targetServerAddress() {
+ return new ServerAddress(remoteAddress.getHostString(), remoteAddress.getPort());
}
private SocksAuthenticationMethod[] getSocksAuthenticationMethods() {
@@ -435,6 +480,10 @@ static ServerReply of(final byte byteStatus) throws ConnectException {
public String getMessage() {
return message;
}
+
+ public int getReplyNumber() {
+ return replyNumber;
+ }
}
@Override
diff --git a/driver-core/src/main/com/mongodb/internal/connection/StreamFactoryFactory.java b/driver-core/src/main/com/mongodb/internal/connection/StreamFactoryFactory.java
index 6cbe620fd43..589a20bb8e1 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/StreamFactoryFactory.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/StreamFactoryFactory.java
@@ -18,6 +18,10 @@
import com.mongodb.connection.SocketSettings;
import com.mongodb.connection.SslSettings;
+import com.mongodb.connection.TransportSettings;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
/**
* A factory of {@code StreamFactory} instances.
@@ -29,10 +33,18 @@ public interface StreamFactoryFactory extends AutoCloseable {
*
* @param socketSettings the socket settings
* @param sslSettings the SSL settings
- * @return a stream factory that will apply the given settins
+ * @return a stream factory that will apply the given settings
*/
StreamFactory create(SocketSettings socketSettings, SslSettings sslSettings);
+ /**
+ * The {@link Executor} that the {@link StreamFactoryFactory} uses for I/O.
+ * It is usually {@link ExecutorService#shutdown() shut down} by {@link #close()},
+ * but the behavior may be different if an application provides an executor via {@link TransportSettings}.
+ * See the documentation of the corresponding API for information about the lifecycle of an application-provided executor.
+ */
+ Executor getExecutor();
+
@Override
void close();
}
diff --git a/driver-core/src/main/com/mongodb/internal/connection/StreamFactoryHelper.java b/driver-core/src/main/com/mongodb/internal/connection/StreamFactoryHelper.java
index 7aeb65720b0..b8b14e317ef 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/StreamFactoryHelper.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/StreamFactoryHelper.java
@@ -27,10 +27,11 @@
import com.mongodb.lang.Nullable;
import com.mongodb.spi.dns.InetAddressResolver;
-import java.io.IOException;
-import java.nio.channels.AsynchronousChannelGroup;
+import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
+import static com.mongodb.assertions.Assertions.fail;
+
/**
* This class is not part of the public API and may be removed or changed at any time
*/
@@ -47,6 +48,11 @@ public StreamFactory create(final SocketSettings socketSettings, final SslSettin
return new SocketStreamFactory(inetAddressResolver, socketSettings, sslSettings);
}
+ @Override
+ public Executor getExecutor() {
+ throw fail();
+ }
+
@Override
public void close() {
//NOP
@@ -71,15 +77,7 @@ public static StreamFactoryFactory getAsyncStreamFactoryFactory(final MongoClien
if (settings.getSslSettings().isEnabled()) {
return new TlsChannelStreamFactoryFactory(inetAddressResolver, executorService);
}
- AsynchronousChannelGroup group = null;
- if (executorService != null) {
- try {
- group = AsynchronousChannelGroup.withThreadPool(executorService);
- } catch (IOException e) {
- throw new MongoClientException("Unable to create an asynchronous channel group", e);
- }
- }
- return new AsynchronousSocketChannelStreamFactoryFactory(inetAddressResolver, group);
+ return new AsynchronousSocketChannelStreamFactoryFactory(inetAddressResolver, executorService);
} else if (transportSettings instanceof NettyTransportSettings) {
return getNettyStreamFactoryFactory(inetAddressResolver, (NettyTransportSettings) transportSettings);
} else {
diff --git a/driver-core/src/main/com/mongodb/internal/connection/Time.java b/driver-core/src/main/com/mongodb/internal/connection/Time.java
index e3940adf1de..9b7f935e631 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/Time.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/Time.java
@@ -20,7 +20,11 @@
* To enable unit testing of classes that rely on System.nanoTime
*
* This class is not part of the public API and may be removed or changed at any time
+ *
+ * @deprecated Use {@link com.mongodb.internal.time.SystemNanoTime} in production code,
+ * and {@code Mockito.mockStatic} in test code to tamper with it.
*/
+@Deprecated
public final class Time {
static final long CONSTANT_TIME = 42;
diff --git a/driver-core/src/main/com/mongodb/internal/connection/TlsChannelStreamFactoryFactory.java b/driver-core/src/main/com/mongodb/internal/connection/TlsChannelStreamFactoryFactory.java
index b0fae1d044d..4296cb01392 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/TlsChannelStreamFactoryFactory.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/TlsChannelStreamFactoryFactory.java
@@ -20,6 +20,7 @@
import com.mongodb.MongoSocketOpenException;
import com.mongodb.ServerAddress;
import com.mongodb.connection.AsyncCompletionHandler;
+import com.mongodb.connection.AsyncTransportSettings;
import com.mongodb.connection.SocketSettings;
import com.mongodb.connection.SslSettings;
import com.mongodb.internal.connection.tlschannel.BufferAllocator;
@@ -47,6 +48,7 @@
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
@@ -93,10 +95,22 @@ public StreamFactory create(final SocketSettings socketSettings, final SslSettin
selectorMonitor);
}
+ /**
+ * @return The {@link Executor} used by the {@link StreamFactory} created via {@link #create(SocketSettings, SslSettings)}.
+ * It may be backed by the {@link ExecutorService} provided by an application via {@link AsyncTransportSettings#getExecutorService()}.
+ */
+ @Override
+ public Executor getExecutor() {
+ return group;
+ }
+
@Override
public void close() {
- selectorMonitor.close();
- group.shutdown();
+ try {
+ selectorMonitor.close();
+ } finally {
+ group.shutdown();
+ }
}
/**
diff --git a/driver-core/src/main/com/mongodb/internal/connection/netty/NettyStreamFactoryFactory.java b/driver-core/src/main/com/mongodb/internal/connection/netty/NettyStreamFactoryFactory.java
index 7fe54defaa2..a939092f952 100644
--- a/driver-core/src/main/com/mongodb/internal/connection/netty/NettyStreamFactoryFactory.java
+++ b/driver-core/src/main/com/mongodb/internal/connection/netty/NettyStreamFactoryFactory.java
@@ -36,6 +36,7 @@
import java.security.Security;
import java.util.Objects;
+import java.util.concurrent.Executor;
import static com.mongodb.assertions.Assertions.isTrueArgument;
import static com.mongodb.assertions.Assertions.notNull;
@@ -151,7 +152,7 @@ public Builder eventLoopGroup(final EventLoopGroup eventLoopGroup) {
/**
* Sets a {@linkplain SslContextBuilder#forClient() client-side} {@link SslContext io.netty.handler.ssl.SslContext},
* which overrides the standard {@link SslSettings#getContext()}.
- * By default it is {@code null} and {@link SslSettings#getContext()} is at play.
+ * By default, it is {@code null} and {@link SslSettings#getContext()} is at play.
*
* This option may be used as a convenient way to utilize
* OpenSSL as an alternative to the TLS/SSL protocol implementation in a JDK.
@@ -203,13 +204,22 @@ public StreamFactory create(final SocketSettings socketSettings, final SslSettin
sslContext);
}
+ /**
+ * @return The {@link EventLoopGroup} used by the {@link StreamFactory} created via {@link #create(SocketSettings, SslSettings)}.
+ * It may be provided by an application via {@link NettyTransportSettings#getEventLoopGroup()}.
+ */
+ @Override
+ public Executor getExecutor() {
+ return eventLoopGroup;
+ }
+
@Override
public void close() {
- if (ownsEventLoopGroup) {
- // ignore the returned Future. This is in line with MongoClient behavior to not block waiting for connections to be returned
- // to the pool
- eventLoopGroup.shutdownGracefully();
- }
+ if (ownsEventLoopGroup) {
+ // ignore the returned Future. This is in line with MongoClient behavior to not block waiting for connections to be returned
+ // to the pool
+ eventLoopGroup.shutdownGracefully();
+ }
}
@Override
diff --git a/driver-core/src/main/com/mongodb/internal/diagnostics/logging/Logger.java b/driver-core/src/main/com/mongodb/internal/diagnostics/logging/Logger.java
index e9907bb3953..4c1eda53e11 100644
--- a/driver-core/src/main/com/mongodb/internal/diagnostics/logging/Logger.java
+++ b/driver-core/src/main/com/mongodb/internal/diagnostics/logging/Logger.java
@@ -17,8 +17,7 @@
package com.mongodb.internal.diagnostics.logging;
/**
- * This class is not part of the public API. It 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.
*/
public interface Logger {
/**
diff --git a/driver-core/src/main/com/mongodb/internal/event/CommandListenerMulticaster.java b/driver-core/src/main/com/mongodb/internal/event/CommandListenerMulticaster.java
index e18318f8cde..8f9a9f51e24 100644
--- a/driver-core/src/main/com/mongodb/internal/event/CommandListenerMulticaster.java
+++ b/driver-core/src/main/com/mongodb/internal/event/CommandListenerMulticaster.java
@@ -16,6 +16,7 @@
package com.mongodb.internal.event;
+import com.mongodb.annotations.ThreadSafe;
import com.mongodb.event.CommandFailedEvent;
import com.mongodb.event.CommandListener;
import com.mongodb.event.CommandStartedEvent;
@@ -29,7 +30,11 @@
import static com.mongodb.assertions.Assertions.isTrue;
import static java.lang.String.format;
-
+/**
+ * This {@link CommandListener} is {@linkplain ThreadSafe thread-safe},
+ * provided that the recipient listeners passed to {@link #CommandListenerMulticaster(List)} are.
+ */
+@ThreadSafe
final class CommandListenerMulticaster implements CommandListener {
private static final Logger LOGGER = Loggers.getLogger("protocol.event");
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/AsyncChangeStreamBatchCursor.java b/driver-core/src/main/com/mongodb/internal/operation/AsyncChangeStreamBatchCursor.java
index ce7127e0dc3..7c32dc41404 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/AsyncChangeStreamBatchCursor.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/AsyncChangeStreamBatchCursor.java
@@ -73,7 +73,9 @@ final class AsyncChangeStreamBatchCursor implements AsyncAggregateResponseBat
this.wrapped = new AtomicReference<>(assertNotNull(wrapped));
this.binding = binding;
binding.retain();
- this.initialOperationContext = operationContext.withOverride(TimeoutContext::withMaxTimeAsMaxAwaitTimeOverride);
+ this.initialOperationContext = operationContext
+ .withOverride(TimeoutContext::withMaxTimeAsMaxAwaitTimeOverride)
+ .withNewServerDeprioritization();
this.resumeToken = resumeToken;
this.maxWireVersion = maxWireVersion;
isClosed = new AtomicBoolean();
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 91e52ca1baf..d327ad663c9 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 {
@@ -110,8 +111,7 @@ static void withAsyncSourceAndConnection(
final boolean wrapConnectionSourceException,
final OperationContext operationContext,
final SingleResultCallback callback,
- final AsyncCallbackTriFunction asyncFunction)
- throws OperationHelper.ResourceSupplierInternalException {
+ final AsyncCallbackTriFunction asyncFunction) {
SingleResultCallback errorHandlingCallback = errorHandlingCallback(callback, OperationHelper.LOGGER);
OperationContext serverSelectionOperationContext =
@@ -140,8 +140,7 @@ static void withAsyncSuppliedResource(final Asyn
final boolean wrapSourceConnectionException,
final OperationContext operationContext,
final SingleResultCallback callback,
- final AsyncCallbackFunction function)
- throws OperationHelper.ResourceSupplierInternalException {
+ final AsyncCallbackFunction function) {
SingleResultCallback errorHandlingCallback = errorHandlingCallback(callback, OperationHelper.LOGGER);
resourceSupplier.apply(operationContext, (resource, supplierException) -> {
if (supplierException != null) {
@@ -176,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(
@@ -190,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);
@@ -242,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,
@@ -258,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,
@@ -320,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;
@@ -329,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),
- 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),
- CommandOperationHelper::loggingShouldAttemptToRetryWriteAndAddRetryableLabel, callback -> {
- logRetryCommand(retryState, operationContext);
- asyncWriteFunction.get(callback);
+ static AsyncCallbackSupplier decorateWithRetriesAsync(
+ final RetryControl retryControl,
+ final OperationContext operationContext,
+ final AsyncCallbackSupplier supplier) {
+ return new RetryingAsyncCallbackSupplier<>(operationContext.getClientExecutor(), retryControl, callback -> {
+ retryControl.getPolicy().onAttemptStart(retryControl, operationContext);
+ supplier.get(callback);
});
}
@@ -379,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) -> {
@@ -485,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/ChangeStreamBatchCursor.java b/driver-core/src/main/com/mongodb/internal/operation/ChangeStreamBatchCursor.java
index cf9f1dcf6c4..226deafa9fa 100644
--- a/driver-core/src/main/com/mongodb/internal/operation/ChangeStreamBatchCursor.java
+++ b/driver-core/src/main/com/mongodb/internal/operation/ChangeStreamBatchCursor.java
@@ -85,7 +85,9 @@ final class ChangeStreamBatchCursor implements AggregateResponseBatchCursor callback) {
- WriteConcern effectiveWriteConcern = validateAndGetEffectiveWriteConcern(operationContext.getSessionContext());
- ResultAccumulator resultAccumulator = new ResultAccumulator();
- MutableValue transformedTopLevelError = new MutableValue<>();
-
- beginAsync().thenSupply(c -> {
- executeAllBatchesAsync(effectiveWriteConcern, binding, operationContext, resultAccumulator, c);
- }).onErrorIf(topLevelError -> topLevelError instanceof MongoException, (topLevelError, c) -> {
- transformedTopLevelError.set(transformWriteException((MongoException) topLevelError));
- c.complete(c);
- }).thenApply((ignored, c) -> {
- c.complete(resultAccumulator.build(transformedTopLevelError.getNullable(), effectiveWriteConcern));
+ beginAsync().thenSupply(c -> {
+ WriteConcern effectiveWriteConcern = validateAndGetEffectiveWriteConcern(operationContext.getSessionContext());
+ ResultAccumulator resultAccumulator = new ResultAccumulator();
+ MutableValue transformedTopLevelError = new MutableValue<>();
+
+ beginAsync().thenRun(executeAllBatchesCallback -> {
+ executeAllBatchesAsync(effectiveWriteConcern, binding, operationContext, resultAccumulator, executeAllBatchesCallback);
+ }).onErrorIf(topLevelError -> topLevelError instanceof MongoException, (topLevelError, onErrorCallback) -> {
+ transformedTopLevelError.set(transformWriteException((MongoException) topLevelError));
+ onErrorCallback.complete(onErrorCallback);
+ }).thenApply((ignored, buildResultCallback) -> {
+ buildResultCallback.complete(resultAccumulator.build(transformedTopLevelError.getNullable(), effectiveWriteConcern));
+ }).finish(c);
}).finish(callback);
}
@@ -253,16 +250,18 @@ private void executeAllBatchesAsync(
final OperationContext operationContext,
final ResultAccumulator resultAccumulator,
final SingleResultCallback callback) {
- MutableValue nextBatchStartModelIndex = new MutableValue<>(INITIAL_BATCH_MODEL_START_INDEX);
-
- beginAsync().thenRunDoWhileLoop(iterationCallback -> {
- beginAsync().thenSupply(c -> {
- executeBatchAsync(nextBatchStartModelIndex.get(), effectiveWriteConcern, binding, operationContext, resultAccumulator, c);
- }).thenApply((nextBatchStartModelIdx, c) -> {
- nextBatchStartModelIndex.set(nextBatchStartModelIdx);
- c.complete(c);
- }).finish(iterationCallback);
- }, () -> nextBatchStartModelIndex.getNullable() != null).finish(callback);
+ beginAsync().thenRun(c -> {
+ MutableValue nextBatchStartModelIndex = new MutableValue<>(INITIAL_BATCH_MODEL_START_INDEX);
+
+ beginAsync().thenRunDoWhileLoop(iterationCallback -> {
+ beginAsync().thenSupply(executeBatchCallback -> {
+ executeBatchAsync(nextBatchStartModelIndex.get(), effectiveWriteConcern, binding, operationContext, resultAccumulator, executeBatchCallback);
+ }).thenConsume((nextBatchStartModelIdx, setNextBatchStartModelIndexCallback) -> {
+ nextBatchStartModelIndex.set(nextBatchStartModelIdx);
+ setNextBatchStartModelIndexCallback.complete(setNextBatchStartModelIndexCallback);
+ }).finish(iterationCallback);
+ }, () -> nextBatchStartModelIndex.getNullable() != null).finish(c);
+ }).finish(callback);
}
/**
@@ -280,31 +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,
+ () -> withSourceAndConnection(binding::getWriteConnectionSource, true, operationContext,
(connectionSource, connection, operationContextWithMinRtt) -> {
+ 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);
return executeBulkWriteCommandAndExhaustOkResponse(
- retryState, connectionSource, connection, bulkWriteCommand, effectiveWriteConcern, operationContextWithMinRtt);
- }, operationContext)
+ retryControl, connectionSource, connection, bulkWriteCommand, effectiveWriteConcern, operationContextWithMinRtt);
+ })
);
try {
@@ -318,10 +314,6 @@ private Integer executeBatch(
resultAccumulator.onBulkWriteCommandErrorResponse(bulkWriteCommandException);
throw bulkWriteCommandException;
} catch (MongoException mongoException) {
- // The server does not have a chance to add "RetryableWriteError" label to `e`,
- // and if it is the last attempt failure, `RetryingSyncSupplier` also may not have a chance
- // to add the label. So we do that explicitly.
- getWriteAttemptFailureNotToBeRetriedOrAddRetryableLabel(retryState, mongoException);
resultAccumulator.onBulkWriteCommandErrorWithoutResponse(mongoException);
throw mongoException;
}
@@ -337,61 +329,58 @@ private void executeBatchAsync(
final OperationContext operationContext,
final ResultAccumulator resultAccumulator,
final SingleResultCallback callback) {
- 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);
- BatchEncoder batchEncoder = new BatchEncoder();
-
- AsyncCallbackSupplier retryingBatchExecutor = decorateWriteWithRetriesAsync(
- retryState, 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.
- funcCallback -> withAsyncSourceAndConnection(binding::getWriteConnectionSource, true, operationContext, funcCallback,
- (connectionSource, connection, operationContextWithMinRtt, resultCallback) -> {
- ConnectionDescription connectionDescription = connection.getDescription();
- boolean effectiveRetryWrites = isRetryableWrite(
- retryWritesSetting, effectiveWriteConcern, connectionDescription, sessionContext);
- retryState.breakAndThrowIfRetryAnd(() -> !effectiveRetryWrites);
- 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));
- executeBulkWriteCommandAndExhaustOkResponseAsync(
- retryState, connectionSource, connection, bulkWriteCommand, effectiveWriteConcern, operationContextWithMinRtt, resultCallback);
- })
- );
-
- beginAsync().thenSupply(c -> {
- retryingBatchExecutor.get(c);
- }).thenApply((response, c) -> {
- c.complete(resultAccumulator.onBulkWriteCommandOkResponseOrNoResponse(
- batchStartModelIndex, response, batchEncoder.intoEncodedBatchInfo()));
- }).onErrorIf(throwable -> true, (t, c) -> {
- if (t instanceof MongoWriteConcernWithResponseException) {
- MongoWriteConcernWithResponseException mongoWriteConcernWithOkResponseException = (MongoWriteConcernWithResponseException) t;
- c.complete(resultAccumulator.onBulkWriteCommandOkResponseWithWriteConcernError(
- batchStartModelIndex, mongoWriteConcernWithOkResponseException, batchEncoder.intoEncodedBatchInfo()));
- } else if (t instanceof MongoCommandException) {
- MongoCommandException bulkWriteCommandException = (MongoCommandException) t;
- resultAccumulator.onBulkWriteCommandErrorResponse(bulkWriteCommandException);
- c.completeExceptionally(t);
- } else if (t instanceof MongoException) {
- MongoException mongoException = (MongoException) t;
- // The server does not have a chance to add "RetryableWriteError" label to `e`,
- // and if it is the last attempt failure, `RetryingSyncSupplier` also may not have a chance
- // to add the label. So we do that explicitly.
- getWriteAttemptFailureNotToBeRetriedOrAddRetryableLabel(retryState, mongoException);
- resultAccumulator.onBulkWriteCommandErrorWithoutResponse(mongoException);
- c.completeExceptionally(mongoException);
- } else {
- c.completeExceptionally(t);
- }
+ beginAsync().thenSupply(c -> {
+ List extends ClientNamespacedWriteModel> unexecutedModels = models.subList(batchStartModelIndex, models.size());
+ assertFalse(unexecutedModels.isEmpty());
+ SessionContext sessionContext = operationContext.getSessionContext();
+ RetryControl retryControl = createSpecRetryControl(
+ new SpecRetryPolicy.IndividualPolicies(retryWritesSetting).includeWrite(),
+ operationContext);
+ BatchEncoder batchEncoder = new BatchEncoder();
+
+ 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,
+ // and `ClientSession`, `TransactionContext` are aware of that.
+ supplierCallback -> withAsyncSourceAndConnection(binding::getWriteConnectionSource, true, operationContext, supplierCallback,
+ (connectionSource, connection, operationContextWithMinRtt, functionCallback) -> {
+ beginAsync().thenSupply(executeAndExhaustCallback -> {
+ retryControl.getPolicy().onCommand(() -> BULK_WRITE_COMMAND_NAME);
+ ConnectionDescription connectionDescription = connection.getDescription();
+ retryControl.breakAndThrowIfRetryAnd(() -> !isServerWriteRetryRequirementsMet(connectionDescription));
+ resultAccumulator.onNewServerAddress(connectionDescription.getServerAddress());
+ ClientBulkWriteCommand bulkWriteCommand = createBulkWriteCommand(
+ retryControl, connectionDescription, effectiveWriteConcern, sessionContext, unexecutedModels, batchEncoder);
+ executeBulkWriteCommandAndExhaustOkResponseAsync(
+ retryControl, connectionSource, connection, bulkWriteCommand, effectiveWriteConcern, operationContextWithMinRtt, executeAndExhaustCallback);
+ }).finish(functionCallback);
+ })
+ );
+
+ beginAsync().thenSupply(executorCallback -> {
+ retryingBatchExecutor.get(executorCallback);
+ }).thenApply((response, transformResponseCallback) -> {
+ transformResponseCallback.complete(resultAccumulator.onBulkWriteCommandOkResponseOrNoResponse(
+ batchStartModelIndex, response, batchEncoder.intoEncodedBatchInfo()));
+ }).onErrorIf(throwable -> true, (t, onErrorCallback) -> {
+ if (t instanceof MongoWriteConcernWithResponseException) {
+ MongoWriteConcernWithResponseException mongoWriteConcernWithOkResponseException = (MongoWriteConcernWithResponseException) t;
+ onErrorCallback.complete(resultAccumulator.onBulkWriteCommandOkResponseWithWriteConcernError(
+ batchStartModelIndex, mongoWriteConcernWithOkResponseException, batchEncoder.intoEncodedBatchInfo()));
+ } else if (t instanceof MongoCommandException) {
+ MongoCommandException bulkWriteCommandException = (MongoCommandException) t;
+ resultAccumulator.onBulkWriteCommandErrorResponse(bulkWriteCommandException);
+ throw bulkWriteCommandException;
+ } else if (t instanceof MongoException) {
+ MongoException mongoException = (MongoException) t;
+ resultAccumulator.onBulkWriteCommandErrorWithoutResponse(mongoException);
+ throw mongoException;
+ } else {
+ onErrorCallback.completeExceptionally(t);
+ }
+ }).finish(c);
}).finish(callback);
}
@@ -404,7 +393,7 @@ private void executeBatchAsync(
*/
@Nullable
private ExhaustiveClientBulkWriteCommandOkResponse executeBulkWriteCommandAndExhaustOkResponse(
- final RetryState retryState,
+ final RetryControl retryControl,
final ConnectionSource connectionSource,
final Connection connection,
final ClientBulkWriteCommand bulkWriteCommand,
@@ -423,7 +412,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,
@@ -432,10 +421,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,
@@ -459,11 +448,11 @@ 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, exhaustCallback) -> {
- exhaustCallback.complete(createExhaustiveClientBulkWriteCommandOkResponse(
+ }).thenApply((cursorExhaustBatches, transformExhaustionResultCallback) -> {
+ transformExhaustionResultCallback.complete(createExhaustiveClientBulkWriteCommandOkResponse(
response,
cursorExhaustBatches,
connection.getDescription()));
@@ -471,14 +460,18 @@ private void executeBulkWriteCommandAndExhaustOkResponseAsync(
}).finish(callback);
}
+ /**
+ * @see #executeBulkWriteCommandAndExhaustOkResponse(RetryControl, ConnectionSource, Connection, ClientBulkWriteCommand, WriteConcern, OperationContext)
+ */
private static ExhaustiveClientBulkWriteCommandOkResponse createExhaustiveClientBulkWriteCommandOkResponse(
final ClientBulkWriteCommandOkResponse response,
final List> cursorExhaustBatches,
- final ConnectionDescription connectionDescription) {
+ final ConnectionDescription connectionDescription) throws MongoWriteConcernWithResponseException {
ExhaustiveClientBulkWriteCommandOkResponse exhaustiveResponse = new ExhaustiveClientBulkWriteCommandOkResponse(
response, cursorExhaustBatches);
- // `Connection.command` does not throw `MongoWriteConcernException`, so we have to construct it ourselves
+ // Given that the response is OK, `Connection.command` does not throw an exception when the write concern is violated,
+ // so we have to construct such an exception ourselves.
MongoWriteConcernException writeConcernException = Exceptions.createWriteConcernException(
response, connectionDescription.getServerAddress());
if (writeConcernException != null) {
@@ -487,43 +480,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) {
- // TODO-JAVA-5956 The current implementation incorrectly uses `retryableWriteCommandFlag` to achieve the behavior needed.
- Optional originalRetryableWriteCommandFlag = retryState.attachment(AttachmentKeys.retryableWriteCommandFlag());
-
- beginAsync().thenSupply(c -> {
- retryState.attach(AttachmentKeys.retryableWriteCommandFlag(), false, true);
- action.finish(c);
- }).thenAlwaysRunAndFinish(() -> {
- originalRetryableWriteCommandFlag.ifPresent(value -> retryState.attach(AttachmentKeys.retryableWriteCommandFlag(), value, true));
- }, callback);
- }
-
private List> exhaustBulkWriteCommandOkResponseCursor(
final ConnectionSource connectionSource,
final OperationContext operationContext,
@@ -549,31 +505,32 @@ private void exhaustBulkWriteCommandOkResponseCursorAsync(
final ClientBulkWriteCommandOkResponse response,
final OperationContext operationContext,
final SingleResultCallback>> callback) {
- AsyncBatchCursor cursor = cursorDocumentToAsyncBatchCursor(
- TimeoutMode.CURSOR_LIFETIME,
- response.getDocument(),
- SERVER_DEFAULT_CURSOR_BATCH_SIZE,
- codecRegistry.get(BsonDocument.class),
- options.getComment().orElse(null),
- connectionSource,
- connection,
- operationContext);
-
beginAsync().>>thenSupply(c -> {
- cursor.exhaust(c);
- }).thenAlwaysRunAndFinish(() -> {
- cursor.close();
- }, callback);
+ AsyncBatchCursor cursor = cursorDocumentToAsyncBatchCursor(
+ TimeoutMode.CURSOR_LIFETIME,
+ response.getDocument(),
+ SERVER_DEFAULT_CURSOR_BATCH_SIZE,
+ codecRegistry.get(BsonDocument.class),
+ options.getComment().orElse(null),
+ connectionSource,
+ connection,
+ operationContext);
+
+ beginAsync().>>thenSupply(exhaustCallback -> {
+ cursor.exhaust(exhaustCallback);
+ }).thenAlwaysRunAndFinish(() -> {
+ cursor.close();
+ }, c);
+ }).finish(callback);
}
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 BatchEncoder batchEncoder) {
BsonDocument commandDocument = new BsonDocument(BULK_WRITE_COMMAND_NAME, new BsonInt32(1))
.append("errorsOnly", BsonBoolean.valueOf(!options.isVerboseResults()))
.append("ordered", BsonBoolean.valueOf(options.isOrdered()));
@@ -588,12 +545,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()
+ retryControl.getPolicy().onWriteRetryRequirements(true, connectionDescription);
+ return retryControl.isFirstAttempt()
? sessionContext.advanceTransactionNumber()
: sessionContext.getTransactionNumber();
}));
@@ -647,12 +605,9 @@ private static MongoWriteConcernException createWriteConcernException(
if (!responseDocument.containsKey(writeConcernErrorFieldName)) {
return null;
}
- BsonDocument writeConcernErrorDocument = responseDocument.getDocument(writeConcernErrorFieldName);
- WriteConcernError writeConcernError = WriteConcernHelper.createWriteConcernError(writeConcernErrorDocument);
- Set errorLabels = responseDocument.getArray("errorLabels", new BsonArray()).stream()
- .map(i -> i.asString().getValue())
- .collect(toSet());
- return new MongoWriteConcernException(writeConcernError, null, serverAddress, errorLabels);
+ // We do not use `MongoWriteConcernException.getWriteResult`,
+ // so we do not care what result `WriteConcernHelper.createWriteConcernException` puts there.
+ return WriteConcernHelper.createWriteConcernException(responseDocument, serverAddress);
}
}
@@ -960,25 +915,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 markRetryRequirementsMetAndAdvanceGetTxnNumber;
@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 markRetryRequirementsMetAndAdvanceGetTxnNumber) {
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.markRetryRequirementsMetAndAdvanceGetTxnNumber = new MemoizingLongSupplier(markRetryRequirementsMetAndAdvanceGetTxnNumber);
}
@Override
@@ -989,7 +951,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);
@@ -1011,8 +973,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();
}
}
@@ -1020,13 +982,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(markRetryRequirementsMetAndAdvanceGetTxnNumber.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);
}
/**
@@ -1155,6 +1117,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 b39cddd6544..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,36 +22,26 @@
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.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")
-final class CommandOperationHelper {
+public final class CommandOperationHelper {
static WriteConcern validateAndGetEffectiveWriteConcern(final WriteConcern writeConcernSetting, final SessionContext sessionContext)
throws MongoClientException {
boolean activeTransaction = sessionContext.hasActiveTransaction();
@@ -77,55 +67,18 @@ BsonDocument create(
ConnectionDescription connectionDescription);
}
- static BinaryOperator onRetryableReadAttemptFailure(final OperationContext operationContext) {
- return (@Nullable Throwable previouslyChosenException, Throwable mostRecentAttemptException) -> {
- operationContext.getServerDeprioritization().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 OperationContext operationContext) {
- return (@Nullable Throwable previouslyChosenException, Throwable mostRecentAttemptException) -> {
- operationContext.getServerDeprioritization().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);
@@ -164,57 +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 = getWriteAttemptFailureNotToBeRetriedOrAddRetryableLabel(retryState, attemptFailure);
- boolean decision = attemptFailureNotToBeRetried == null;
- if (!decision && retryState.attachment(AttachmentKeys.retryableWriteCommandFlag()).orElse(false)) {
- logUnableToRetryCommand(retryState, assertNotNull(attemptFailureNotToBeRetried));
- }
- return decision;
- }
-
- /**
- * @return {@code null} if the decision is {@code true}. Otherwise, returns the {@link Throwable} that must not be retried.
- */
- @Nullable
- static Throwable getWriteAttemptFailureNotToBeRetriedOrAddRetryableLabel(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
@@ -226,21 +129,10 @@ static boolean isRetryableWriteCommand(final BsonDocument command) {
|| command.getFirstKey().equals("commitTransaction") || command.getFirstKey().equals("abortTransaction"));
}
- static final String RETRYABLE_WRITE_ERROR_LABEL = "RetryableWriteError";
- private static final String NO_WRITES_PERFORMED_ERROR_LABEL = "NoWritesPerformed";
+ public static final String RETRYABLE_WRITE_ERROR_LABEL = "RetryableWriteError";
+ public static final String NO_WRITES_PERFORMED_ERROR_LABEL = "NoWritesPerformed";
- private static boolean decideRetryableAndAddRetryableWriteErrorLabel(final Throwable t, @Nullable final Integer maxWireVersion) {
- if (!(t instanceof MongoException)) {
- 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)) {
@@ -248,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 de02983f012..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,19 +298,21 @@ public BatchCursor execute(final ReadBinding binding, final OperationContext
}
OperationContext findOperationContext = getFindOperationContext(operationContext);
- RetryState retryState = initialRetryState(retryReads, findOperationContext.getTimeoutContext());
- Supplier> read = decorateReadWithRetries(retryState, findOperationContext, () ->
- withSourceAndConnection(binding::getReadConnectionSource, false,
+ 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) {
throw new MongoQueryException(e.getResponse(), e.getServerAddress());
}
- }, findOperationContext)
+ })
);
return read.get();
}
@@ -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 2340d3880e9..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,18 +174,19 @@ 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, () ->
- withSourceAndConnection(binding::getReadConnectionSource, false, (source, connection, operationContextWithMinRTT) -> {
- retryState.breakAndThrowIfRetryAnd(() -> !canRetryRead(operationContextWithMinRTT));
+ RetryControl retryControl = createSpecRetryControl(
+ new SpecRetryPolicy.IndividualPolicies(retryReads).includeRead(listCollectionsOperationContext),
+ listCollectionsOperationContext);
+ Supplier> read = decorateWithRetries(retryControl, listCollectionsOperationContext, () ->
+ withSourceAndConnection(binding::getReadConnectionSource, false, listCollectionsOperationContext, (source, connection, operationContextWithMinRTT) -> {
try {
- return createReadCommandAndExecute(retryState, operationContextWithMinRTT, source, databaseName,
+ return createReadCommandAndExecute(retryControl, operationContextWithMinRTT, source, databaseName,
getCommandCreator(), createCommandDecoder(), transformer(), connection);
} catch (MongoCommandException e) {
return rethrowIfNotNamespaceError(e,
createEmptySingleBatchCursor(source.getServerDescription().getAddress(), batchSize));
}
- }, listCollectionsOperationContext)
+ })
);
return read.get();
}
@@ -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