Skip to content

Commit 8fa85b1

Browse files
authored
Introduce the RetryPolicy abstraction (#2004)
JAVA-6229
1 parent cc087b0 commit 8fa85b1

54 files changed

Lines changed: 2056 additions & 2009 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

driver-core/src/main/com/mongodb/MongoClientSettings.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,8 @@ public Builder retryReads(final boolean retryReads) {
542542
* The attempts failed due to retryable overload errors are retried only if
543543
* {@link #retryWrites(boolean)} is {@code true} and {@link #retryReads(boolean)} is {@code true}.
544544
* <p>
545-
* The command kind is unknown when a command is executed via the {@code MongoDatabase.runCommand} operation.
545+
* The command kind is unknown if and only if a command is executed via
546+
* the {@code MongoDatabase.runCommand} operation.
546547
* </td>
547548
* </tr>
548549
* </tbody>
@@ -772,14 +773,11 @@ public Builder autoEncryptionSettings(@Nullable final AutoEncryptionSettings aut
772773

773774
/**
774775
* Sets the context provider
775-
*
776776
* <p>
777777
* When used with the synchronous driver, this must be an instance of {@code com.mongodb.client.SynchronousContextProvider}.
778778
* When used with the reactive streams driver, this must be an instance of
779779
* {@code com.mongodb.reactivestreams.client.ReactiveContextProvider}.
780780
*
781-
* </p>
782-
*
783781
* @param contextProvider the context provider
784782
* @return this
785783
* @since 4.4

driver-core/src/main/com/mongodb/internal/TimeoutContext.java

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
* <p>The context for handling timeouts in relation to the Client Side Operation Timeout specification.</p>
4141
*/
4242
public class TimeoutContext {
43+
public static final String DEFAULT_TIMEOUT_MESSAGE = "Operation exceeded the timeout limit.";
4344
private static final int NO_ROUND_TRIP_TIME_MS = 0;
4445
private final TimeoutSettings timeoutSettings;
4546
/**
@@ -64,11 +65,11 @@ public static <T> T throwMongoTimeoutException(final String message) {
6465
throw new MongoOperationTimeoutException(message);
6566
}
6667
public static <T> T throwMongoTimeoutException() {
67-
throw new MongoOperationTimeoutException("The operation exceeded the timeout limit.");
68+
throw new MongoOperationTimeoutException(DEFAULT_TIMEOUT_MESSAGE);
6869
}
6970

7071
public static MongoOperationTimeoutException createMongoTimeoutException(final Throwable cause) {
71-
return createMongoTimeoutException("Operation exceeded the timeout limit: " + cause.getMessage(), cause);
72+
return createMongoTimeoutException(DEFAULT_TIMEOUT_MESSAGE, cause);
7273
}
7374

7475
public static MongoOperationTimeoutException createMongoTimeoutException(final String message, @Nullable final Throwable cause) {
@@ -185,7 +186,7 @@ public long timeoutOrAlternative(final long alternativeTimeoutMS) {
185186
return timeout.call(MILLISECONDS,
186187
() -> 0L,
187188
(ms) -> ms,
188-
() -> throwMongoTimeoutException("The operation exceeded the timeout limit."));
189+
() -> throwMongoTimeoutException());
189190
}
190191
}
191192

@@ -229,7 +230,7 @@ public int getConnectTimeoutMs() {
229230
return Math.toIntExact(Timeout.nullAsInfinite(timeout).call(MILLISECONDS,
230231
() -> connectTimeoutMS,
231232
(ms) -> connectTimeoutMS == 0 ? ms : Math.min(ms, connectTimeoutMS),
232-
() -> throwMongoTimeoutException("The operation exceeded the timeout limit.")));
233+
() -> throwMongoTimeoutException()));
233234
}
234235

235236
/**
@@ -250,13 +251,11 @@ public TimeoutContext withMaxTimeAsMaxAwaitTimeOverride() {
250251
* The override will be provided as the remaining value in
251252
* {@link #runMaxTimeMS}, where 0 is ignored. This is useful for setting timeout
252253
* in {@link CommandMessage} as an extra element before we send it to the server.
253-
*
254254
* <p>
255-
* NOTE: Suitable for static user-defined values only (i.e MaxAwaitTimeMS),
255+
* Suitable for static user-defined values only (i.e. {@code MaxAwaitTimeMS}),
256256
* not for running timeouts that adjust dynamically (CSOT).
257-
*
257+
* <p>
258258
* If remaining CSOT timeout is less than this static timeout, then CSOT timeout will be used.
259-
*
260259
*/
261260
public TimeoutContext withMaxTimeOverride(final long maxTimeMS) {
262261
return new TimeoutContext(
@@ -480,10 +479,10 @@ private void runMinTimeout(final LongConsumer onRemaining, final long fixedMs) {
480479
timeout.run(MILLISECONDS, () -> {
481480
onRemaining.accept(fixedMs);
482481
},
483-
(renamingMs) -> {
484-
onRemaining.accept(Math.min(renamingMs, fixedMs));
482+
(remainingMs) -> {
483+
onRemaining.accept(Math.min(remainingMs, fixedMs));
485484
}, () -> {
486-
throwMongoTimeoutException("The operation exceeded the timeout limit.");
485+
throwMongoTimeoutException();
487486
});
488487
} else {
489488
onRemaining.accept(fixedMs);

driver-core/src/main/com/mongodb/internal/async/AsyncRunnable.java

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
package com.mongodb.internal.async;
1818

1919
import com.mongodb.internal.async.function.AsyncCallbackLoop;
20-
import com.mongodb.internal.async.function.LoopState;
21-
import com.mongodb.internal.async.function.RetryState;
20+
import com.mongodb.internal.async.function.LoopControl;
21+
import com.mongodb.internal.async.function.RetryControl;
2222
import com.mongodb.internal.async.function.RetryingAsyncCallbackSupplier;
2323

2424
import java.util.function.BooleanSupplier;
@@ -233,9 +233,7 @@ default <R> AsyncSupplier<R> thenSupply(final AsyncSupplier<R> supplier) {
233233
default AsyncRunnable thenRunRetryingWhile(final AsyncRunnable runnable, final Predicate<Throwable> shouldRetry) {
234234
return thenRun(callback -> {
235235
new RetryingAsyncCallbackSupplier<Void>(
236-
new RetryState(),
237-
(previouslyChosenFailure, lastAttemptFailure) -> lastAttemptFailure,
238-
(rs, lastAttemptFailure) -> shouldRetry.test(lastAttemptFailure),
236+
new RetryControl<>(new SimpleRetryPolicy(shouldRetry)),
239237
// `finish` is required here instead of `unsafeFinish`
240238
// because only `finish` meets the contract of
241239
// `AsyncCallbackSupplier.get`, which we implement here
@@ -255,10 +253,10 @@ default AsyncRunnable thenRunRetryingWhile(final AsyncRunnable runnable, final P
255253
*/
256254
default AsyncRunnable thenRunWhileLoop(final BooleanSupplier whileCheck, final AsyncRunnable loopBodyRunnable) {
257255
return thenRun(finalCallback -> {
258-
LoopState loopState = new LoopState();
259-
new AsyncCallbackLoop(loopState, iterationCallback -> {
256+
LoopControl loopControl = new LoopControl();
257+
new AsyncCallbackLoop(loopControl, iterationCallback -> {
260258

261-
if (loopState.breakAndCompleteIf(() -> !whileCheck.getAsBoolean(), iterationCallback)) {
259+
if (loopControl.breakAndCompleteIf(() -> !whileCheck.getAsBoolean(), iterationCallback)) {
262260
return;
263261
}
264262
loopBodyRunnable.finish((result, t) -> {
@@ -284,15 +282,15 @@ default AsyncRunnable thenRunWhileLoop(final BooleanSupplier whileCheck, final A
284282
*/
285283
default AsyncRunnable thenRunDoWhileLoop(final AsyncRunnable loopBodyRunnable, final BooleanSupplier whileCheck) {
286284
return thenRun(finalCallback -> {
287-
LoopState loopState = new LoopState();
288-
new AsyncCallbackLoop(loopState, iterationCallback -> {
285+
LoopControl loopControl = new LoopControl();
286+
new AsyncCallbackLoop(loopControl, iterationCallback -> {
289287

290288
loopBodyRunnable.finish((result, t) -> {
291289
if (t != null) {
292290
iterationCallback.completeExceptionally(t);
293291
return;
294292
}
295-
if (loopState.breakAndCompleteIf(() -> !whileCheck.getAsBoolean(), iterationCallback)) {
293+
if (loopControl.breakAndCompleteIf(() -> !whileCheck.getAsBoolean(), iterationCallback)) {
296294
return;
297295
}
298296
iterationCallback.complete(iterationCallback);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2008-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.mongodb.internal.async;
17+
18+
import com.mongodb.internal.async.function.RetryContext;
19+
import com.mongodb.internal.async.function.RetryPolicy;
20+
import com.mongodb.internal.async.function.RetryPolicy.Decision.RetryAttemptInfo;
21+
22+
import java.util.function.Predicate;
23+
24+
final class SimpleRetryPolicy implements RetryPolicy {
25+
private final Predicate<Throwable> shouldRetry;
26+
27+
SimpleRetryPolicy(final Predicate<Throwable> shouldRetry) {
28+
this.shouldRetry = shouldRetry;
29+
}
30+
31+
@Override
32+
public Decision onAttemptFailure(final RetryContext retryContext, final Throwable attemptFailedResult) {
33+
return new Decision(attemptFailedResult, shouldRetry.test(attemptFailedResult) ? new RetryAttemptInfo() : null);
34+
}
35+
}

driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackFunction.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@
3535
* <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-14.html#jls-14.1">"normal" and "abrupt completion"</a>
3636
* are used as they defined by the Java Language Specification, while the terms "successful" and "failed completion" are used to refer to a
3737
* situation when the function produces either a successful or a failed result respectively.
38-
*
39-
* <p>This class is not part of the public API and may be removed or changed at any time</p>
38+
* <p>
39+
* This class is not part of the public API and may be removed or changed at any time.
4040
*
4141
* @param <P> The type of the first parameter to the function.
4242
* @param <R> The type of successful result. A failed result is of the {@link Throwable} type

driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackLoop.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,26 +29,26 @@
2929
* This class emulates the <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-14.html#jls-14.12">{@code while(true)}</a>
3030
* statement.
3131
* <p>
32-
* The original function may additionally observe or control looping via {@link LoopState}.
33-
* Looping continues until either of the following happens:
32+
* The original function may additionally observe or control the loop via {@link LoopControl}.
33+
* The loop continues until either of the following happens:
3434
* <ul>
3535
* <li>the original function fails as specified by {@link AsyncCallbackFunction};</li>
36-
* <li>the original function calls {@link LoopState#breakAndCompleteIf(Supplier, SingleResultCallback)}.</li>
36+
* <li>the original function calls {@link LoopControl#breakAndCompleteIf(Supplier, SingleResultCallback)}.</li>
3737
* </ul>
38-
*
39-
* <p>This class is not part of the public API and may be removed or changed at any time</p>
38+
* <p>
39+
* This class is not part of the public API and may be removed or changed at any time.
4040
*/
4141
@NotThreadSafe
4242
public final class AsyncCallbackLoop implements AsyncCallbackRunnable {
43-
private final LoopState state;
43+
private final LoopControl control;
4444
private final AsyncCallbackRunnable body;
4545

4646
/**
47-
* @param state The {@link LoopState} to be deemed as initial for the purpose of the new {@link AsyncCallbackLoop}.
47+
* @param control The {@link LoopControl} to control the new {@link AsyncCallbackLoop}.
4848
* @param body The body of the loop.
4949
*/
50-
public AsyncCallbackLoop(final LoopState state, final AsyncCallbackRunnable body) {
51-
this.state = state;
50+
public AsyncCallbackLoop(final LoopControl control, final AsyncCallbackRunnable body) {
51+
this.control = control;
5252
this.body = body;
5353
}
5454

@@ -77,7 +77,7 @@ public void onResult(@Nullable final Void result, @Nullable final Throwable t) {
7777
} else {
7878
boolean continueLooping;
7979
try {
80-
continueLooping = state.advance();
80+
continueLooping = control.advance();
8181
} catch (Throwable e) {
8282
wrapped.onResult(null, e);
8383
return;

driver-core/src/main/com/mongodb/internal/async/function/AsyncCallbackRunnable.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
/**
2121
* An {@linkplain AsyncCallbackFunction asynchronous callback-based function} of no parameters and no successful result.
2222
* This class is a callback-based counterpart of {@link Runnable}.
23-
*
24-
* <p>This class is not part of the public API and may be removed or changed at any time</p>
23+
* <p>
24+
* This class is not part of the public API and may be removed or changed at any time.
2525
*
2626
* @see AsyncCallbackFunction
2727
*/
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*
2+
* Copyright 2008-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.mongodb.internal.async.function;
17+
18+
import com.mongodb.annotations.NotThreadSafe;
19+
import com.mongodb.internal.async.MutableValue;
20+
import com.mongodb.internal.async.SingleResultCallback;
21+
22+
import java.util.function.Supplier;
23+
24+
import static com.mongodb.assertions.Assertions.assertFalse;
25+
26+
/**
27+
* A stateful controller of a loop that can be used to control it, for example,
28+
* to {@linkplain #breakAndCompleteIf(Supplier, SingleResultCallback) break} it.
29+
* {@linkplain MutableValue} may be used by the loop to preserve state between iterations.
30+
* <p>
31+
* This class is not part of the public API and may be removed or changed at any time.
32+
*
33+
* @see AsyncCallbackLoop
34+
*/
35+
@NotThreadSafe
36+
public final class LoopControl {
37+
private int iteration;
38+
private boolean lastIteration;
39+
40+
public LoopControl() {
41+
iteration = 0;
42+
}
43+
44+
/**
45+
* Advances this {@link LoopControl} such that it represents the state of the immediate next iteration, if any.
46+
* Must not be called before the {@linkplain #isFirstIteration() first iteration}, must be called before each subsequent iteration.
47+
*
48+
* @return {@code true} if another iteration must be executed;
49+
* otherwise the loop was {@link #isLastIteration() broken} and {@code false} is returned.
50+
*/
51+
boolean advance() {
52+
if (lastIteration) {
53+
return false;
54+
} else {
55+
iteration++;
56+
return true;
57+
}
58+
}
59+
60+
/**
61+
* Returns {@code true} iff the current iteration is the first one.
62+
*
63+
* @see #iteration()
64+
*/
65+
boolean isFirstIteration() {
66+
return iteration == 0;
67+
}
68+
69+
/**
70+
* Returns {@code true} iff {@link #breakAndCompleteIf(Supplier, SingleResultCallback)} / {@link #markAsLastIteration()} was called.
71+
*/
72+
boolean isLastIteration() {
73+
return lastIteration;
74+
}
75+
76+
/**
77+
* A 0-based iteration number.
78+
*/
79+
int iteration() {
80+
return iteration;
81+
}
82+
83+
/**
84+
* This method emulates executing the <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-14.html#jls-14.15">{@code break}</a> statement
85+
* in callback-based code. If {@code true} is returned, the caller must complete the current attempt.
86+
* <p>
87+
* Must not be called after breaking the loop.
88+
*
89+
* @param predicate {@code true} iff the loop needs to be broken.
90+
* <ul>
91+
* <li>
92+
* If the {@code predicate} completes abruptly, this method completes the {@code callback} with the same exception but does not break the loop;</li>
93+
* <li>
94+
* if the {@code predicate} is {@code true}, then this method breaks the retry loop;</li>
95+
* <li>
96+
* if the {@code predicate} is {@code false}, then this method does nothing.
97+
* </ul>
98+
* @return {@code true} iff the {@code callback} was completed, which happens iff any of the following is true:
99+
* <ul>
100+
* <li>the {@code predicate} completed abruptly;</li>
101+
* <li>this method broke the loop.</li>
102+
* </ul>
103+
*
104+
* @see #isLastIteration()
105+
*/
106+
public boolean breakAndCompleteIf(final Supplier<Boolean> predicate, final SingleResultCallback<?> callback) {
107+
assertFalse(lastIteration);
108+
try {
109+
lastIteration = predicate.get();
110+
} catch (Throwable predicateException) {
111+
callback.onResult(null, predicateException);
112+
return true;
113+
}
114+
if (lastIteration) {
115+
callback.onResult(null, null);
116+
return true;
117+
} else {
118+
return false;
119+
}
120+
}
121+
122+
/**
123+
* This method is similar to {@link #breakAndCompleteIf(Supplier, SingleResultCallback)}.
124+
* The difference is that it allows the current iteration to continue, yet no more iterations will happen.
125+
*
126+
* @see #isLastIteration()
127+
*/
128+
void markAsLastIteration() {
129+
assertFalse(lastIteration);
130+
lastIteration = true;
131+
}
132+
133+
@Override
134+
public String toString() {
135+
return "LoopControl{"
136+
+ "iteration=" + iteration
137+
+ ", lastIteration=" + lastIteration
138+
+ '}';
139+
}
140+
}

0 commit comments

Comments
 (0)