Skip to content

Commit 1f30837

Browse files
authored
Fix race condition in retry logic. (#1377)
JAVA-5451
1 parent 2fdae3b commit 1f30837

File tree

2 files changed

+71
-4
lines changed

2 files changed

+71
-4
lines changed

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,16 +186,20 @@ private void doAdvanceOrThrow(final Throwable attemptException,
186186
final boolean onlyRuntimeExceptions) throws Throwable {
187187
assertTrue(attempt() < attempts);
188188
assertNotNull(attemptException);
189-
if (attemptException instanceof MongoOperationTimeoutException) {
190-
throw attemptException;
191-
}
192189
if (onlyRuntimeExceptions) {
193190
assertTrue(isRuntime(attemptException));
194191
}
195192
assertTrue(!isFirstAttempt() || previouslyChosenException == null);
196193
Throwable newlyChosenException = transformException(previouslyChosenException, attemptException, onlyRuntimeExceptions, exceptionTransformer);
197194

198-
if (isLastAttempt()) {
195+
/*
196+
* A MongoOperationTimeoutException indicates that the operation timed out, either during command execution or server selection.
197+
* The timeout for server selection is determined by the computedServerSelectionMS = min(serverSelectionTimeoutMS, timeoutMS).
198+
*
199+
* The isLastAttempt() method checks if the timeoutMS has expired, which could be greater than the computedServerSelectionMS.
200+
* Therefore, it's important to check if the exception is an instance of MongoOperationTimeoutException to detect a timeout.
201+
*/
202+
if (isLastAttempt() || attemptException instanceof MongoOperationTimeoutException) {
199203
previouslyChosenException = newlyChosenException;
200204
/*
201205
* The function of isLastIteration() is to indicate if retrying has been explicitly halted. Such a stop is not interpreted as

driver-core/src/test/unit/com/mongodb/internal/async/function/RetryStateTest.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.mongodb.internal.async.function.LoopState.AttachmentKey;
2323
import com.mongodb.internal.operation.retry.AttachmentKeys;
2424
import org.junit.jupiter.api.Assertions;
25+
import org.junit.jupiter.api.DisplayName;
2526
import org.junit.jupiter.api.Test;
2627
import org.junit.jupiter.params.ParameterizedTest;
2728
import org.junit.jupiter.params.provider.Arguments;
@@ -268,6 +269,68 @@ void advanceOrThrowPredicateFalse(final TimeoutContext timeoutContext) {
268269
assertThrows(attemptException.getClass(), () -> retryState.advanceOrThrow(attemptException, (e1, e2) -> e2, (rs, e) -> false));
269270
}
270271

272+
@ParameterizedTest
273+
@MethodSource({"infiniteTimeout"})
274+
@DisplayName("should rethrow detected timeout exception even if timeout in retry state is not expired")
275+
void advanceReThrowDetectedTimeoutExceptionEvenIfTimeoutInRetryStateIsNotExpired(final TimeoutContext timeoutContext) {
276+
RetryState retryState = new RetryState(timeoutContext);
277+
278+
MongoOperationTimeoutException expectedTimeoutException = TimeoutContext.createMongoTimeoutException("Server selection failed");
279+
MongoOperationTimeoutException actualTimeoutException =
280+
assertThrows(expectedTimeoutException.getClass(), () -> retryState.advanceOrThrow(expectedTimeoutException,
281+
(e1, e2) -> expectedTimeoutException,
282+
(rs, e) -> false));
283+
284+
Assertions.assertEquals(actualTimeoutException, expectedTimeoutException);
285+
}
286+
287+
@Test
288+
@DisplayName("should throw timeout exception from retry, when transformer swallows original timeout exception")
289+
void advanceThrowTimeoutExceptionWhenTransformerSwallowOriginalTimeoutException() {
290+
RetryState retryState = new RetryState(TIMEOUT_CONTEXT_INFINITE_GLOBAL_TIMEOUT);
291+
RuntimeException previousAttemptException = new RuntimeException() {
292+
};
293+
MongoOperationTimeoutException expectedTimeoutException = TimeoutContext.createMongoTimeoutException("Server selection failed");
294+
295+
retryState.advanceOrThrow(previousAttemptException,
296+
(e1, e2) -> previousAttemptException,
297+
(rs, e) -> true);
298+
299+
MongoOperationTimeoutException actualTimeoutException =
300+
assertThrows(expectedTimeoutException.getClass(), () -> retryState.advanceOrThrow(expectedTimeoutException,
301+
(e1, e2) -> previousAttemptException,
302+
(rs, e) -> false));
303+
304+
Assertions.assertNotEquals(actualTimeoutException, expectedTimeoutException);
305+
Assertions.assertEquals("Retry attempt timed out.", actualTimeoutException.getMessage());
306+
Assertions.assertEquals(previousAttemptException, actualTimeoutException.getCause(),
307+
"Retry timeout exception should have a cause if transformer returned non-timeout exception.");
308+
}
309+
310+
311+
@Test
312+
@DisplayName("should throw original timeout exception from retry, when transformer returns original timeout exception")
313+
void advanceThrowOriginalTimeoutExceptionWhenTransformerReturnsOriginalTimeoutException() {
314+
RetryState retryState = new RetryState(TIMEOUT_CONTEXT_INFINITE_GLOBAL_TIMEOUT);
315+
RuntimeException previousAttemptException = new RuntimeException() {
316+
};
317+
MongoOperationTimeoutException expectedTimeoutException = TimeoutContext
318+
.createMongoTimeoutException("Server selection failed");
319+
320+
retryState.advanceOrThrow(previousAttemptException,
321+
(e1, e2) -> previousAttemptException,
322+
(rs, e) -> true);
323+
324+
MongoOperationTimeoutException actualTimeoutException =
325+
assertThrows(expectedTimeoutException.getClass(), () -> retryState.advanceOrThrow(expectedTimeoutException,
326+
(e1, e2) -> expectedTimeoutException,
327+
(rs, e) -> false));
328+
329+
Assertions.assertEquals(actualTimeoutException, expectedTimeoutException);
330+
Assertions.assertNull(actualTimeoutException.getCause(),
331+
"Original timeout exception should not have a cause if transformer already returned timeout exception.");
332+
}
333+
271334
@Test
272335
void advanceOrThrowPredicateTrueAndLastAttempt() {
273336
RetryState retryState = RetryState.withNonRetryableState();

0 commit comments

Comments
 (0)