Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -153,19 +153,25 @@ public TimedAttemptSettings createNextAttempt(
Throwable previousThrowable,
ResponseT previousResponse,
TimedAttemptSettings previousSettings) {
// a small optimization that avoids calling relatively heavy methods
// like timedAlgorithm.createNextAttempt(), when it is not necessary.
if (!shouldRetryBasedOnResult(context, previousThrowable, previousResponse)) {
return null;
if (shouldRetryBasedOnResult(context, previousThrowable, previousResponse)) {
TimedAttemptSettings newSettings =
createNextAttemptBasedOnResult(
context, previousThrowable, previousResponse, previousSettings);
if (newSettings == null) {
newSettings = createNextAttemptBasedOnTiming(context, previousSettings);
}
return newSettings;
}

TimedAttemptSettings newSettings =
createNextAttemptBasedOnResult(
context, previousThrowable, previousResponse, previousSettings);
if (newSettings == null) {
newSettings = createNextAttemptBasedOnTiming(context, previousSettings);
// If we shouldn't retry based on result and the last attempt failed with an
// exception, defer to any exception thrown by shouldRetryBasedOnTiming.
// This lets a timeout-related exception get propagated when justified
// rather than e.g. exceptions caused by very short RPC deadlines on a
// final polling attempt.
Comment on lines +166 to +169

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qq, I'm not sure I'm following this comment. I might not be understanding this fully.

What do you mean by defer to any exception thrown by shouldRetryBasedOnTiming`? Is this referring to an exception thrown by the ExponentialRetryAlgorithm?

IIRC, short RPC deadlines that can't be processed in time will result in a DEADLINE_EXCEEDED status code and that may or may not be retried as a retryable status code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Is this referring to an exception thrown by the ExponentialRetryAlgorithm?" Not by ExponentialRetryAlgorithm itself, but by its subclass OperationTimedPollAlgorithm used for LRO polling, which throws a CancellationException when the cumulative timeout for all polls is exhausted.

With the current structure of RetryAlgorithm.createNextAttempt() the CancellationException-throwing codepath doesn't get reached if the final LRO polling RPC fails. That results in an ExecutionException getting propagated instead, which is misleading when that failure is triggered by an overly-truncated deadline. The mismatch of expectations is IMO likely at the root of the test flakes from #12373 and #3277.

if (previousThrowable != null) {
TimedAttemptSettings nextSettings = createNextAttemptBasedOnTiming(context, previousSettings);
shouldRetryBasedOnTiming(context, nextSettings);
}
return newSettings;
return null;
}

private TimedAttemptSettings createNextAttemptBasedOnResult(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,14 @@
package com.google.api.gax.retrying;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.util.concurrent.CancellationException;
import org.junit.jupiter.api.Test;

@SuppressWarnings({"unchecked", "deprecation"})
Expand Down Expand Up @@ -113,6 +117,45 @@ void testNextAttemptWithContext() {
verify(resultAlgorithm).shouldRetry(context, previousThrowable, previousResult);
}

@Test
void testCreateNextAttempt_exceptionAndTimeout_defersToTimingException() {
ResultRetryAlgorithm<Object> resultAlgorithm = mock(ResultRetryAlgorithm.class);
TimedRetryAlgorithm timedAlgorithm = mock(TimedRetryAlgorithm.class);
RetryAlgorithm<Object> algorithm = new RetryAlgorithm<>(resultAlgorithm, timedAlgorithm);
Throwable throwable = new Throwable();
Object result = new Object();
TimedAttemptSettings previousSettings = mock(TimedAttemptSettings.class);
TimedAttemptSettings nextSettings = mock(TimedAttemptSettings.class);

when(resultAlgorithm.shouldRetry(throwable, result)).thenReturn(false);
when(timedAlgorithm.createNextAttempt(previousSettings)).thenReturn(nextSettings);
when(timedAlgorithm.shouldRetry(nextSettings)).thenThrow(new CancellationException());

assertThrows(
CancellationException.class,
() -> algorithm.createNextAttempt(throwable, result, previousSettings));
}

@Test
void testCreateNextAttempt_exceptionAndNoTimeout_returnsNull() {
ResultRetryAlgorithm<Object> resultAlgorithm = mock(ResultRetryAlgorithm.class);
TimedRetryAlgorithm timedAlgorithm = mock(TimedRetryAlgorithm.class);
RetryAlgorithm<Object> algorithm = new RetryAlgorithm<>(resultAlgorithm, timedAlgorithm);
Throwable throwable = new Throwable();
Object result = new Object();
TimedAttemptSettings previousSettings = mock(TimedAttemptSettings.class);
TimedAttemptSettings nextSettings = mock(TimedAttemptSettings.class);

when(resultAlgorithm.shouldRetry(throwable, result)).thenReturn(false);
when(timedAlgorithm.createNextAttempt(previousSettings)).thenReturn(nextSettings);
when(timedAlgorithm.shouldRetry(nextSettings)).thenReturn(true);

TimedAttemptSettings nextAttemptSettings =
algorithm.createNextAttempt(throwable, result, previousSettings);

assertNull(nextAttemptSettings);
}

@Test
void testShouldRetry() {
ResultRetryAlgorithm<Object> resultAlgorithm = mock(ResultRetryAlgorithm.class);
Expand Down Expand Up @@ -201,4 +244,19 @@ void testShouldRetryWithContext_noPreviousSettings() {

assertFalse(algorithm.shouldRetry(context, previousThrowable, previousResult, null));
}

@Test
void testShouldRetry_resultFalseAndThrowableNull_shortCircuits() {
ResultRetryAlgorithm<Object> resultAlgorithm = mock(ResultRetryAlgorithm.class);
TimedRetryAlgorithm timedAlgorithm = mock(TimedRetryAlgorithm.class);
RetryAlgorithm<Object> algorithm = new RetryAlgorithm<>(resultAlgorithm, timedAlgorithm);
Object previousResult = new Object();
TimedAttemptSettings previousSettings = mock(TimedAttemptSettings.class);
when(resultAlgorithm.shouldRetry(null, previousResult)).thenReturn(false);

boolean shouldRetry = algorithm.shouldRetry(null, previousResult, previousSettings);

assertFalse(shouldRetry);
verify(timedAlgorithm, never()).shouldRetry(previousSettings);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,17 @@ void testNextAttemptReturnsNullWhenShouldNotRetry() {
StreamingRetryAlgorithm<String> algorithm =
new StreamingRetryAlgorithm<>(resultAlgorithm, timedAlgorithm);

TimedAttemptSettings previousSettings = mock(TimedAttemptSettings.class);
when(previousSettings.getGlobalSettings()).thenReturn(DEFAULT_RETRY_SETTINGS);
when(previousSettings.getRpcTimeoutDuration())
.thenReturn(DEFAULT_RETRY_SETTINGS.getInitialRpcTimeoutDuration());

TimedAttemptSettings attempt =
algorithm.createNextAttempt(context, exception, null, mock(TimedAttemptSettings.class));
algorithm.createNextAttempt(context, exception, null, previousSettings);
assertThat(attempt).isNull();

TimedAttemptSettings attemptWithoutContext =
algorithm.createNextAttempt(exception, null, mock(TimedAttemptSettings.class));
algorithm.createNextAttempt(exception, null, previousSettings);
assertThat(attemptWithoutContext).isNull();
}

Expand Down
Loading