Skip to content

Commit c7d11d4

Browse files
authored
chore: Fix testCancelGetAttempt flaky test (#3592)
Fixes #2851
1 parent 81e21f2 commit c7d11d4

File tree

2 files changed

+112
-109
lines changed

2 files changed

+112
-109
lines changed

gax-java/gax/src/test/java/com/google/api/gax/retrying/AbstractRetryingExecutorTest.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,14 @@
5252
import com.google.common.base.Stopwatch;
5353
import java.util.concurrent.CancellationException;
5454
import java.util.concurrent.ExecutionException;
55+
import java.util.concurrent.Executors;
56+
import java.util.concurrent.ScheduledExecutorService;
5557
import java.util.concurrent.TimeUnit;
5658
import java.util.concurrent.TimeoutException;
5759
import java.util.concurrent.atomic.AtomicInteger;
5860
import java.util.stream.Stream;
61+
import org.junit.jupiter.api.AfterEach;
62+
import org.junit.jupiter.api.BeforeEach;
5963
import org.junit.jupiter.api.extension.ExtendWith;
6064
import org.junit.jupiter.params.ParameterizedTest;
6165
import org.junit.jupiter.params.provider.Arguments;
@@ -66,6 +70,23 @@
6670
@ExtendWith(MockitoExtension.class)
6771
public abstract class AbstractRetryingExecutorTest {
6872

73+
private static final int DEFAULT_AWAIT_TERMINATION_SEC = 10;
74+
75+
// This is the default executor service for RetryingExecutor tests unless
76+
// a local executor is specifically used
77+
ScheduledExecutorService scheduledExecutorService;
78+
79+
@BeforeEach
80+
void setup() {
81+
scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
82+
}
83+
84+
@AfterEach
85+
void cleanup() throws InterruptedException {
86+
scheduledExecutorService.shutdownNow();
87+
scheduledExecutorService.awaitTermination(DEFAULT_AWAIT_TERMINATION_SEC, TimeUnit.SECONDS);
88+
}
89+
6990
public static Stream<Arguments> data() {
7091
return Stream.of(Arguments.of(false), Arguments.of(true));
7192
}

gax-java/gax/src/test/java/com/google/api/gax/retrying/ScheduledRetryingExecutorTest.java

Lines changed: 91 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -47,22 +47,17 @@
4747
import java.util.concurrent.Future;
4848
import java.util.concurrent.RejectedExecutionException;
4949
import java.util.concurrent.ScheduledExecutorService;
50-
import java.util.concurrent.TimeUnit;
51-
import org.junit.jupiter.api.AfterEach;
5250
import org.junit.jupiter.api.Test;
53-
import org.junit.jupiter.params.ParameterizedTest;
54-
import org.junit.jupiter.params.provider.MethodSource;
5551
import org.mockito.Mockito;
5652

5753
class ScheduledRetryingExecutorTest extends AbstractRetryingExecutorTest {
58-
private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
5954

6055
// Number of test runs, essential for multithreaded tests.
6156
private static final int EXECUTIONS_COUNT = 5;
6257

6358
@Override
6459
protected RetryingExecutorWithContext<String> getExecutor(RetryAlgorithm<String> retryAlgorithm) {
65-
return getRetryingExecutor(retryAlgorithm, scheduler);
60+
return getRetryingExecutor(retryAlgorithm, scheduledExecutorService);
6661
}
6762

6863
@Override
@@ -78,30 +73,24 @@ private RetryingExecutorWithContext<String> getRetryingExecutor(
7873
return new ScheduledRetryingExecutor<>(retryAlgorithm, scheduler);
7974
}
8075

81-
@AfterEach
82-
void after() {
83-
scheduler.shutdownNow();
84-
}
85-
8676
@Test
8777
void testSuccessWithFailuresPeekAttempt() throws Exception {
78+
RetrySettings retrySettings =
79+
FAST_RETRY_SETTINGS
80+
.toBuilder()
81+
.setTotalTimeoutDuration(java.time.Duration.ofMillis(1000L))
82+
.setMaxAttempts(100)
83+
.build();
8884
for (int executionsCount = 0; executionsCount < EXECUTIONS_COUNT; executionsCount++) {
89-
final int maxRetries = 100;
9085

91-
ScheduledExecutorService localExecutor = Executors.newSingleThreadScheduledExecutor();
9286
FailingCallable callable = new FailingCallable(15, "request", "SUCCESS", tracer);
9387

94-
RetrySettings retrySettings =
95-
FAST_RETRY_SETTINGS
96-
.toBuilder()
97-
.setTotalTimeoutDuration(java.time.Duration.ofMillis(1000L))
98-
.setMaxAttempts(maxRetries)
99-
.build();
100-
10188
RetryingExecutorWithContext<String> executor =
102-
getRetryingExecutor(getAlgorithm(retrySettings, 0, null), localExecutor);
89+
getRetryingExecutor(getAlgorithm(retrySettings, 0, null), scheduledExecutorService);
10390
RetryingFuture<String> future =
104-
executor.createFuture(callable, FakeCallContext.createDefault().withTracer(tracer));
91+
executor.createFuture(
92+
callable,
93+
FakeCallContext.createDefault().withTracer(tracer).withRetrySettings(retrySettings));
10594
callable.setExternalFuture(future);
10695

10796
assertNull(future.peekAttemptResult());
@@ -131,28 +120,28 @@ void testSuccessWithFailuresPeekAttempt() throws Exception {
131120
assertFutureSuccess(future);
132121
assertEquals(15, future.getAttemptSettings().getAttemptCount());
133122
assertTrue(failedAttempts > 0);
134-
localExecutor.shutdownNow();
135123
}
136124
}
137125

138126
@Test
139127
void testSuccessWithFailuresGetAttempt() throws Exception {
128+
int maxRetries = 100;
129+
RetrySettings retrySettings =
130+
FAST_RETRY_SETTINGS
131+
.toBuilder()
132+
.setTotalTimeoutDuration(java.time.Duration.ofMillis(1000L))
133+
.setMaxAttempts(maxRetries)
134+
.build();
140135
for (int executionsCount = 0; executionsCount < EXECUTIONS_COUNT; executionsCount++) {
141-
final int maxRetries = 100;
142136

143-
ScheduledExecutorService localExecutor = Executors.newSingleThreadScheduledExecutor();
144137
FailingCallable callable = new FailingCallable(15, "request", "SUCCESS", tracer);
145-
RetrySettings retrySettings =
146-
FAST_RETRY_SETTINGS
147-
.toBuilder()
148-
.setTotalTimeoutDuration(java.time.Duration.ofMillis(1000L))
149-
.setMaxAttempts(maxRetries)
150-
.build();
151138

152139
RetryingExecutorWithContext<String> executor =
153-
getRetryingExecutor(getAlgorithm(retrySettings, 0, null), localExecutor);
140+
getRetryingExecutor(getAlgorithm(retrySettings, 0, null), scheduledExecutorService);
154141
RetryingFuture<String> future =
155-
executor.createFuture(callable, FakeCallContext.createDefault().withTracer(tracer));
142+
executor.createFuture(
143+
callable,
144+
FakeCallContext.createDefault().withTracer(tracer).withRetrySettings(retrySettings));
156145
callable.setExternalFuture(future);
157146

158147
assertNull(future.peekAttemptResult());
@@ -185,75 +174,64 @@ void testSuccessWithFailuresGetAttempt() throws Exception {
185174
assertFutureSuccess(future);
186175
assertEquals(15, future.getAttemptSettings().getAttemptCount());
187176
assertTrue(checks > 1 && checks <= maxRetries, "checks is equal to " + checks);
188-
localExecutor.shutdownNow();
189177
}
190178
}
191179

192-
@ParameterizedTest
193-
@MethodSource("data")
194-
void testCancelGetAttempt(boolean withCustomRetrySettings) throws Exception {
195-
setUp(withCustomRetrySettings);
196-
for (int executionsCount = 0; executionsCount < EXECUTIONS_COUNT; executionsCount++) {
197-
ScheduledExecutorService localExecutor = Executors.newSingleThreadScheduledExecutor();
198-
final int maxRetries = 20;
199-
200-
FailingCallable callable = new FailingCallable(maxRetries - 1, "request", "SUCCESS", tracer);
201-
RetrySettings retrySettings =
202-
FAST_RETRY_SETTINGS
203-
.toBuilder()
204-
.setTotalTimeoutDuration(java.time.Duration.ofMillis(5000L))
205-
.setMaxAttempts(maxRetries)
206-
.build();
207-
208-
RetryingExecutorWithContext<String> executor =
209-
getRetryingExecutor(getAlgorithm(retrySettings, 0, null), localExecutor);
210-
RetryingFuture<String> future = executor.createFuture(callable, retryingContext);
211-
callable.setExternalFuture(future);
212-
213-
assertNull(future.peekAttemptResult());
214-
assertSame(future.getAttemptResult(), future.getAttemptResult());
215-
assertFalse(future.getAttemptResult().isDone());
216-
assertFalse(future.getAttemptResult().isCancelled());
217-
218-
future.setAttemptFuture(executor.submit(future));
219-
220-
CustomException exception;
221-
CancellationException cancellationException = null;
222-
int checks = 0;
223-
int failedCancelations = 0;
224-
do {
225-
exception = null;
226-
checks++;
227-
Future<String> attemptResult = future.getAttemptResult();
228-
try {
229-
attemptResult.get();
230-
assertNotNull(future.peekAttemptResult());
231-
} catch (CancellationException e) {
232-
cancellationException = e;
233-
} catch (ExecutionException e) {
234-
exception = (CustomException) e.getCause();
235-
}
236-
assertTrue(attemptResult.isDone());
237-
if (!future.cancel(true)) {
238-
failedCancelations++;
239-
}
240-
} while (exception != null && checks < maxRetries);
241-
242-
assertTrue(future.isDone());
243-
assertNotNull(cancellationException);
244-
// future.cancel(true) may return false sometimes, which is ok. Also, the every cancellation
245-
// of
246-
// an already cancelled future should return false (this is what -1 means here)
247-
assertEquals(2, checks - (failedCancelations - 1));
248-
assertTrue(future.getAttemptSettings().getAttemptCount() > 0);
249-
assertFutureCancel(future);
250-
localExecutor.shutdownNow();
251-
}
180+
@Test
181+
void testCancelGetAttempt() throws Exception {
182+
int maxRetries = 100;
183+
RetrySettings retrySettings =
184+
FAST_RETRY_SETTINGS
185+
.toBuilder()
186+
.setInitialRpcTimeoutDuration(java.time.Duration.ofMillis(50L))
187+
.setMaxRpcTimeoutDuration(java.time.Duration.ofMillis(50L))
188+
.setTotalTimeoutDuration(java.time.Duration.ofMillis(5000L))
189+
.setMaxAttempts(maxRetries)
190+
.build();
191+
FailingCallable callable = new FailingCallable(maxRetries - 1, "request", "SUCCESS", tracer);
192+
193+
RetryingExecutorWithContext<String> executor =
194+
getRetryingExecutor(getAlgorithm(retrySettings, 0, null), scheduledExecutorService);
195+
RetryingFuture<String> future =
196+
executor.createFuture(
197+
callable,
198+
FakeCallContext.createDefault().withTracer(tracer).withRetrySettings(retrySettings));
199+
callable.setExternalFuture(future);
200+
201+
assertNull(future.peekAttemptResult());
202+
assertSame(future.getAttemptResult(), future.getAttemptResult());
203+
assertFalse(future.getAttemptResult().isDone());
204+
assertFalse(future.getAttemptResult().isCancelled());
205+
206+
future.setAttemptFuture(executor.submit(future));
207+
208+
CustomException exception;
209+
CancellationException cancellationException = null;
210+
int checks = 0;
211+
do {
212+
exception = null;
213+
checks++;
214+
Future<String> attemptResult = future.getAttemptResult();
215+
try {
216+
attemptResult.get();
217+
assertNotNull(future.peekAttemptResult());
218+
} catch (CancellationException e) {
219+
cancellationException = e;
220+
} catch (ExecutionException e) {
221+
exception = (CustomException) e.getCause();
222+
}
223+
future.cancel(true);
224+
assertTrue(attemptResult.isDone());
225+
} while (exception != null && checks < maxRetries);
226+
227+
assertTrue(future.isDone());
228+
assertNotNull(cancellationException);
229+
assertTrue(future.getAttemptSettings().getAttemptCount() > 0);
230+
assertFutureCancel(future);
252231
}
253232

254233
@Test
255234
void testCancelOuterFutureAfterStart() throws Exception {
256-
ScheduledExecutorService localExecutor = Executors.newSingleThreadScheduledExecutor();
257235
RetrySettings retrySettings =
258236
FAST_RETRY_SETTINGS
259237
.toBuilder()
@@ -278,9 +256,11 @@ void testCancelOuterFutureAfterStart() throws Exception {
278256
for (int executionsCount = 0; executionsCount < EXECUTIONS_COUNT; executionsCount++) {
279257
FailingCallable callable = new FailingCallable(4, "request", "SUCCESS", tracer);
280258
RetryingExecutorWithContext<String> executor =
281-
getRetryingExecutor(getAlgorithm(retrySettings, 0, null), localExecutor);
259+
getRetryingExecutor(getAlgorithm(retrySettings, 0, null), scheduledExecutorService);
282260
RetryingFuture<String> future =
283-
executor.createFuture(callable, FakeCallContext.createDefault().withTracer(tracer));
261+
executor.createFuture(
262+
callable,
263+
FakeCallContext.createDefault().withTracer(tracer).withRetrySettings(retrySettings));
284264
callable.setExternalFuture(future);
285265
future.setAttemptFuture(executor.submit(future));
286266

@@ -301,27 +281,29 @@ void testCancelOuterFutureAfterStart() throws Exception {
301281
assertTrue(future.getAttemptSettings().getAttemptCount() > 0);
302282
assertTrue(future.getAttemptSettings().getAttemptCount() < 4);
303283
}
304-
localExecutor.shutdown();
305-
localExecutor.awaitTermination(10, TimeUnit.SECONDS);
306284
}
307285

308286
@Test
309287
void testCancelProxiedFutureAfterStart() throws Exception {
288+
RetrySettings retrySettings =
289+
FAST_RETRY_SETTINGS
290+
.toBuilder()
291+
.setInitialRetryDelayDuration(java.time.Duration.ofMillis(1_000L))
292+
.setMaxRetryDelayDuration(java.time.Duration.ofMillis(1_000L))
293+
.setTotalTimeoutDuration(java.time.Duration.ofMillis(10_0000L))
294+
.build();
310295
// this is a heavy test, which takes a lot of time, so only few executions.
311296
for (int executionsCount = 0; executionsCount < 2; executionsCount++) {
297+
// Use a test local executor for this test case due to the reasons listed below
312298
ScheduledExecutorService localExecutor = Executors.newSingleThreadScheduledExecutor();
313299
FailingCallable callable = new FailingCallable(5, "request", "SUCCESS", tracer);
314-
RetrySettings retrySettings =
315-
FAST_RETRY_SETTINGS
316-
.toBuilder()
317-
.setInitialRetryDelayDuration(java.time.Duration.ofMillis(1_000L))
318-
.setMaxRetryDelayDuration(java.time.Duration.ofMillis(1_000L))
319-
.setTotalTimeoutDuration(java.time.Duration.ofMillis(10_0000L))
320-
.build();
300+
321301
RetryingExecutorWithContext<String> executor =
322302
getRetryingExecutor(getAlgorithm(retrySettings, 0, null), localExecutor);
323303
RetryingFuture<String> future =
324-
executor.createFuture(callable, FakeCallContext.createDefault().withTracer(tracer));
304+
executor.createFuture(
305+
callable,
306+
FakeCallContext.createDefault().withTracer(tracer).withRetrySettings(retrySettings));
325307
callable.setExternalFuture(future);
326308
future.setAttemptFuture(executor.submit(future));
327309

@@ -330,8 +312,8 @@ void testCancelProxiedFutureAfterStart() throws Exception {
330312
// Note that shutdownNow() will not cancel internal FutureTasks automatically, which
331313
// may potentially cause another thread handing on RetryingFuture#get() call forever.
332314
// Canceling the tasks returned by shutdownNow() also does not help, because of missing
333-
// feature
334-
// in guava's ListenableScheduledFuture, which does not cancel itself, when its delegate is
315+
// feature in guava's ListenableScheduledFuture, which does not cancel itself, when its
316+
// delegate is
335317
// canceled.
336318
// So only the graceful shutdown() is supported properly.
337319
localExecutor.shutdown();

0 commit comments

Comments
 (0)