/***************************************************************************** * ------------------------------------------------------------------------- * * 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.google.mu.util; import static com.google.common.truth.Truth.assertThat; import static com.google.mu.util.FutureAssertions.assertCancelled; import static com.google.mu.util.FutureAssertions.assertCauseOf; import static com.google.mu.util.FutureAssertions.assertCompleted; import static com.google.mu.util.FutureAssertions.assertPending; import static java.util.Arrays.asList; import static java.util.Objects.requireNonNull; import static java.util.concurrent.CompletableFuture.completedFuture; import static java.util.stream.Collectors.toCollection; import static java.util.stream.Collectors.toList; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import java.io.IOException; import java.lang.reflect.Proxy; import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutionException; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.jupiter.api.function.Executable; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Matchers; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.Spy; import com.google.common.testing.EqualsTester; import com.google.common.testing.NullPointerTester; import com.google.common.truth.ThrowableSubject; import com.google.mu.function.CheckedSupplier; import com.google.mu.util.Retryer.Delay; @RunWith(JUnit4.class) public class RetryerTest { @Spy private FakeClock clock; @Spy private FakeScheduledExecutorService executor; @Mock private Action action; private final List<ScheduledFuture<?>> scheduledFutures = new ArrayList<>(); private Retryer retryer = new Retryer(); @Before public void setUpMocks() { MockitoAnnotations.initMocks(this); } @After public void noMoreInteractions() { Mockito.verifyNoMoreInteractions(action); } @Test public void cannotRetryOnInterruptedException() { assertThrows(IllegalArgumentException.class, () -> upon(InterruptedException.class, asList())); assertThrows( IllegalArgumentException.class, () -> retryer.upon(InterruptedException.class, e -> false, asList())); } @Test public void cannotRetryOnSubtypeOfInterruptedException() { @SuppressWarnings("serial") class MyInterruptedException extends InterruptedException {} assertThrows( IllegalArgumentException.class, () -> upon(MyInterruptedException.class, asList())); assertThrows( IllegalArgumentException.class, () -> retryer.upon(MyInterruptedException.class, e -> false, asList())); } @Test public void expectedReturnValueFirstTime() throws Exception { Delay<String> delay = spy(ofSeconds(1)); when(action.run()).thenReturn("good"); Retryer.ForReturnValue<String> forReturnValue = retryer.uponReturn("bad", asList(delay)); assertCompleted(forReturnValue.retry(action::run, executor)).isEqualTo("good"); verify(action).run(); verify(delay, never()).beforeDelay(any()); verify(delay, never()).afterDelay(any()); } @Test public void nullReturnValueIsGood() throws Exception { Delay<String> delay = spy(ofSeconds(1)); when(action.run()).thenReturn(null); Retryer.ForReturnValue<String> forReturnValue = retryer.uponReturn("bad", asList(delay)); assertCompleted(forReturnValue.retry(action::run, executor)).isNull(); verify(action).run(); verify(delay, never()).beforeDelay(any()); verify(delay, never()).afterDelay(any()); } @Test public void nullReturnValueRetried() throws Exception { Delay<String> delay = spy(ofSeconds(1)); Retryer.ForReturnValue<String> forReturnValue = retryer.uponReturn(null, asList(delay)); when(action.run()).thenReturn(null).thenReturn("fixed"); CompletionStage<String> stage = forReturnValue.retry(action::run, executor); assertPending(stage); elapse(Duration.ofSeconds(1)); assertCompleted(stage).isEqualTo("fixed"); verify(action, times(2)).run(); verify(delay).beforeDelay(null); verify(delay).afterDelay(null); } @Test public void errorPropagatedDuringReturnValueRetry() throws Exception { Error error = new Error("test"); Delay<String> delay = spy(ofSeconds(1)); Retryer.ForReturnValue<String> forReturnValue = retryer.uponReturn("bad", asList(delay)); when(action.run()).thenThrow(error); assertException(Error.class, () -> forReturnValue.retry(action::run, executor)) .isSameAs(error); assertThat(error.getSuppressed()).isEmpty(); verify(action).run(); verify(delay, never()).beforeDelay(any()); verify(delay, never()).afterDelay(any()); } @Test public void uncheckedExceptionPropagatedDuringReturnValueRetry() throws Exception { RuntimeException error = new RuntimeException("test"); Delay<String> delay = spy(ofSeconds(1)); Retryer.ForReturnValue<String> forReturnValue = retryer.uponReturn("bad", asList(delay)); when(action.run()).thenThrow(error); assertException(RuntimeException.class, () -> forReturnValue.retry(action::run, executor)) .isSameAs(error); assertThat(error.getSuppressed()).isEmpty(); verify(action).run(); verify(delay, never()).beforeDelay(any()); verify(delay, never()).afterDelay(any()); } @Test public void exceptionFromBeforeDelayReportedDuringReturnValueRetry() throws Exception { Delay<String> delay = spy(ofSeconds(1)); Retryer.ForReturnValue<String> forReturnValue = retryer.uponReturn("bad", asList(delay)); when(action.run()).thenReturn("bad"); RuntimeException unexpected = new RuntimeException(); Mockito.doThrow(unexpected).when(delay).beforeDelay("bad"); assertException(RuntimeException.class, () -> forReturnValue.retry(action::run, executor)) .isSameAs(unexpected); assertThat(unexpected.getSuppressed()).isEmpty(); verify(action).run(); verify(delay).beforeDelay("bad"); verify(delay, never()).afterDelay("bad"); } @Test public void exceptionFromAfterDelayReportedDuringReturnValueRetry() throws Exception { Delay<String> delay = spy(ofSeconds(1)); Retryer.ForReturnValue<String> forReturnValue = retryer.uponReturn("bad", asList(delay)); when(action.run()).thenReturn("bad"); RuntimeException unexpected = new RuntimeException(); Mockito.doThrow(unexpected).when(delay).afterDelay("bad"); CompletionStage<String> stage = forReturnValue.retry(action::run, executor); assertPending(stage); elapse(Duration.ofSeconds(1)); assertCauseOf(ExecutionException.class, stage).isSameAs(unexpected); assertThat(unexpected.getSuppressed()).isEmpty(); verify(action).run(); verify(delay).beforeDelay("bad"); verify(delay).afterDelay("bad"); } @Test public void exceptionFromExecutorReportedDuringReturnValueRetry() throws Exception { Delay<String> delay = spy(ofSeconds(1)); Retryer.ForReturnValue<String> forReturnValue = retryer.uponReturn("bad", asList(delay)); when(action.run()).thenReturn("bad"); RejectedExecutionException unexpected = new RejectedExecutionException(); Mockito.doThrow(unexpected) .when(executor).schedule(any(Runnable.class), any(long.class), any(TimeUnit.class)); assertException( RejectedExecutionException.class, () -> forReturnValue.retry(action::run, executor)) .isSameAs(unexpected); assertThat(unexpected.getSuppressed()).isEmpty(); verify(action).run(); verify(delay).beforeDelay("bad"); verify(delay, never()).afterDelay("bad"); } @Test public void returnValueScheduledForRetry() throws Exception { Delay<String> delay = spy(ofSeconds(1)); Retryer.ForReturnValue<String> forReturnValue = retryer.uponReturn("bad", asList(delay)); when(action.run()).thenReturn("bad"); CompletionStage<String> stage = forReturnValue.retry(action::run, executor); assertPending(stage); elapse(Duration.ofMillis(999)); assertPending(stage); verify(action).run(); verify(delay).beforeDelay("bad"); verify(delay, never()).afterDelay(any()); } @Test public void returnValueRetriedButCancelled() throws Exception { Delay<String> delay = spy(ofSeconds(0)); Retryer.ForReturnValue<String> forReturnValue = retryer.uponReturn("bad", asList(delay)); when(action.run()).thenReturn("bad").thenReturn("fixed"); CompletionStage<String> stage = forReturnValue.retry(action::run, executor); assertPending(stage); stage.toCompletableFuture().cancel(true); assertThat(scheduledFutures).hasSize(1); verify(scheduledFutures.get(0)).cancel(true); CancellationException cancelled = assertCancelled(stage); assertThat(cancelled.getSuppressed()).isEmpty(); verify(action).run(); verify(delay).beforeDelay("bad"); // Cancelled so no more retry. elapse(Duration.ofSeconds(100)); verifyNoMoreInteractions(action); } @Test public void returnValueRetried() throws Exception { Delay<String> delay = spy(ofSeconds(1)); Retryer.ForReturnValue<String> forReturnValue = retryer.uponReturn("bad", asList(delay)); when(action.run()).thenReturn("bad").thenReturn("fixed"); CompletionStage<String> stage = forReturnValue.retry(action::run, executor); assertPending(stage); elapse(Duration.ofSeconds(1)); assertCompleted(stage).isEqualTo("fixed"); verify(action, times(2)).run(); verify(delay).beforeDelay("bad"); verify(delay).afterDelay("bad"); } @Test public void returnValueRetriedToNoAvail() throws Exception { Delay<String> delay = spy(ofSeconds(1)); Retryer.ForReturnValue<String> forReturnValue = retryer.uponReturn("bad", asList(delay, delay)); when(action.run()).thenReturn("bad"); CompletionStage<String> stage = forReturnValue.retry(action::run, executor); assertPending(stage); elapse(Duration.ofSeconds(1)); assertPending(stage); elapse(Duration.ofSeconds(1)); assertCompleted(stage).isEqualTo("bad"); verify(action, times(3)).run(); verify(delay, times(2)).beforeDelay("bad"); verify(delay, times(2)).afterDelay("bad"); } @Test public void returnValueRetrialExceedsTime() throws Exception { Retryer.ForReturnValue<String> forReturnValue = retryer.uponReturn( "bad", ofSeconds(4).timed(Collections.nCopies(100, ofSeconds(1)), clock)); when(action.run()).thenReturn("bad").thenReturn("bad").thenReturn("bad").thenReturn("good"); CompletionStage<String> stage = forReturnValue.retry(action::run, executor); assertPending(stage); elapse(Duration.ofSeconds(2)); assertPending(stage); elapse(Duration.ofSeconds(1)); // exceeds deadline assertCompleted(stage).isEqualTo("bad"); verify(action, times(3)).run(); // Retry twice. } @Test public void returnValueAsyncRetriedToSuccess() throws Exception { Retryer.ForReturnValue<String> forReturnValue = retryer.uponReturn( "bad", ofSeconds(1).exponentialBackoff(2, 1)); when(action.runAsync()) .thenReturn(completedFuture("bad")) .thenReturn(completedFuture("fixed")); CompletionStage<String> stage = forReturnValue.retryAsync(action::runAsync, executor); assertPending(stage); elapse(Duration.ofSeconds(1)); assertCompleted(stage).isEqualTo("fixed"); verify(action, times(2)).runAsync(); } @Test public void returnValueAsyncFailedAfterRetry() throws Exception { Delay<String> delay = spy(ofSeconds(1)); Retryer.ForReturnValue<String> forReturnValue = retryer.ifReturns((String s) -> s.startsWith("bad"), asList(delay)); when(action.runAsync()) .thenReturn(completedFuture("bad")) .thenReturn(completedFuture("bad2")); CompletionStage<String> stage = forReturnValue.retryAsync(action::runAsync, executor); assertPending(stage); elapse(Duration.ofSeconds(1)); assertCompleted(stage).isEqualTo("bad2"); verify(action, times(2)).runAsync(); verify(delay).beforeDelay("bad"); verify(delay).afterDelay("bad"); } @Test public void testCustomDelayForReturnValueRetry() throws Exception { TestDelay<String> delay = new TestDelay<String>() { @Override public Duration duration() { return Duration.ofMillis(1); } }; Retryer.ForReturnValue<String> forReturnValue = retryer.ifReturns(s -> s.startsWith("bad"), asList(delay).stream()); when(action.run()).thenReturn("bad").thenReturn("fixed"); CompletionStage<String> stage = forReturnValue.retry(action::run, executor); elapse(Duration.ofMillis(1)); assertCompleted(stage).isEqualTo("fixed"); verify(action, times(2)).run(); assertThat(delay.before).isEqualTo("bad"); assertThat(delay.after).isEqualTo("bad"); } @Test public void actionSucceedsFirstTime() throws Exception { when(action.run()).thenReturn("good"); assertCompleted(retry(action::run)).isEqualTo("good"); verify(action).run(); } @Test public void errorPropagated() throws Exception { Error error = new Error("test"); Delay<Throwable> delay = spy(ofSeconds(1)); upon(IOException.class, asList(delay)); when(action.run()).thenThrow(error); assertException(Error.class, () -> retry(action::run)).isSameAs(error); assertThat(error.getSuppressed()).isEmpty(); verify(action).run(); verify(delay, never()).beforeDelay(Matchers.<Throwable>any()); verify(delay, never()).afterDelay(Matchers.<Throwable>any()); } @Test public void uncheckedExceptionPropagated() throws Exception { RuntimeException error = new RuntimeException("test"); Delay<Throwable> delay = spy(ofSeconds(1)); upon(IOException.class, asList(delay)); when(action.run()).thenThrow(error); assertException(RuntimeException.class, () -> retry(action::run)).isSameAs(error); assertThat(error.getSuppressed()).isEmpty(); verify(action).run(); verify(delay, never()).beforeDelay(Matchers.<Throwable>any()); verify(delay, never()).afterDelay(Matchers.<Throwable>any()); } @Test public void actionFailedButNoRetry() throws Exception { IOException exception = new IOException("bad"); when(action.run()).thenThrow(exception); assertCauseOf(ExecutionException.class, retry(action::run)).isSameAs(exception); assertThat(exception.getSuppressed()).isEmpty(); verify(action).run(); } @Test public void exceptionFromBeforeDelayPropagated() throws Exception { Delay<Throwable> delay = spy(ofSeconds(1)); upon(IOException.class, asList(delay)); IOException exception = new IOException(); when(action.run()).thenThrow(exception); RuntimeException unexpected = new RuntimeException(); Mockito.doThrow(unexpected).when(delay).beforeDelay(Matchers.<Throwable>any()); assertException(RuntimeException.class, () -> retry(action::run)) .isSameAs(unexpected); assertThat(unexpected.getSuppressed()).asList().containsExactly(exception); verify(action).run(); verify(delay).beforeDelay(exception); verify(delay, never()).afterDelay(exception); } @Test public void exceptionFromAfterDelayResultsInExecutionException() throws Exception { Delay<Throwable> delay = spy(ofSeconds(1)); upon(IOException.class, asList(delay)); IOException exception = new IOException(); when(action.run()).thenThrow(exception); RuntimeException unexpected = new RuntimeException(); Mockito.doThrow(unexpected).when(delay).afterDelay(Matchers.<Throwable>any()); CompletionStage<String> stage = retry(action::run); assertPending(stage); elapse(Duration.ofSeconds(1)); assertCauseOf(ExecutionException.class, stage).isSameAs(unexpected); assertThat(unexpected.getSuppressed()).asList().containsExactly(exception); verify(action).run(); verify(delay).beforeDelay(exception); verify(delay).afterDelay(exception); } @Test public void exceptionFromExecutorPropagated() throws Exception { Delay<Throwable> delay = spy(ofSeconds(1)); upon(IOException.class, asList(delay)); IOException exception = new IOException(); when(action.run()).thenThrow(exception); RejectedExecutionException unexpected = new RejectedExecutionException(); Mockito.doThrow(unexpected) .when(executor).schedule(any(Runnable.class), any(long.class), any(TimeUnit.class)); assertException(RejectedExecutionException.class, () -> retry(action::run)) .isSameAs(unexpected); assertThat(unexpected.getSuppressed()).asList().containsExactly(exception); verify(action).run(); verify(delay).beforeDelay(exception); verify(delay, never()).afterDelay(exception); } @Test public void actionFailedAndScheduledForRetry() throws Exception { Delay<Throwable> delay = spy(ofSeconds(1)); upon(IOException.class, asList(delay)); IOException exception = new IOException(); when(action.run()).thenThrow(exception); CompletionStage<String> stage = retry(action::run); assertPending(stage); elapse(Duration.ofMillis(999)); assertPending(stage); verify(action).run(); verify(delay).beforeDelay(exception); verify(delay, never()).afterDelay(Matchers.<Throwable>any()); } @Test public void actionNotScheduledForRetryDueToCancellation() throws Exception { Delay<Throwable> delay = spy(ofSeconds(1)); upon(IOException.class, asList(delay)); CompletableFuture<String> result = new CompletableFuture<>(); when(action.runAsync()).thenReturn(result); CompletionStage<String> stage = retryAsync(action::runAsync); assertPending(stage); stage.toCompletableFuture().cancel(false); IOException exception = new IOException(); result.completeExceptionally(exception); CancellationException cancelled = assertCancelled(stage); assertThat(cancelled.getSuppressed()).asList().containsExactly(exception); verify(action).runAsync(); verify(executor, never()).schedule(any(Runnable.class), any(long.class), any(TimeUnit.class)); verify(delay, never()).beforeDelay(exception); verify(delay, never()).afterDelay(exception); } @Test public void actionRetriedButCancelled() throws Exception { Delay<Throwable> delay = spy(ofSeconds(1)); upon(IOException.class, asList(delay)); IOException exception = new IOException(); when(action.run()).thenThrow(exception).thenReturn("fixed"); CompletionStage<String> stage = retry(action::run); assertPending(stage); stage.toCompletableFuture().cancel(false); assertThat(scheduledFutures).hasSize(1); verify(scheduledFutures.get(0)).cancel(true); CancellationException cancelled = assertCancelled(stage); assertThat(cancelled.getSuppressed()).asList().containsExactly(exception); verify(action).run(); verify(delay).beforeDelay(exception); // Cancelled so no more retry. elapse(Duration.ofSeconds(100)); verifyNoMoreInteractions(action); } @Test public void actionFailedAndRetriedToSuccess() throws Exception { Delay<Throwable> delay = spy(ofSeconds(1)); upon(IOException.class, asList(delay)); IOException exception = new IOException(); when(action.run()).thenThrow(exception).thenReturn("fixed"); CompletionStage<String> stage = retry(action::run); assertPending(stage); elapse(Duration.ofSeconds(1)); assertCompleted(stage).isEqualTo("fixed"); verify(action, times(2)).run(); verify(delay).beforeDelay(exception); verify(delay).afterDelay(exception); } @Test public void errorRetried() throws Exception { Delay<Throwable> delay = spy(ofSeconds(1)); upon(MyError.class, asList(delay)); MyError error = new MyError("test"); when(action.run()).thenThrow(error).thenReturn("fixed"); CompletionStage<String> stage = retry(action::run); assertPending(stage); elapse(Duration.ofSeconds(1)); assertCompleted(stage).isEqualTo("fixed"); verify(action, times(2)).run(); verify(delay).beforeDelay(error); verify(delay).afterDelay(error); } @Test public void uncheckedExceptionRetried() throws Exception { Delay<Throwable> delay = spy(ofSeconds(1)); upon(RuntimeException.class, asList(delay)); RuntimeException exception = new RuntimeException("test"); when(action.run()).thenThrow(exception).thenReturn("fixed"); CompletionStage<String> stage = retry(action::run); assertPending(stage); elapse(Duration.ofSeconds(1)); assertCompleted(stage).isEqualTo("fixed"); verify(action, times(2)).run(); verify(delay).beforeDelay(exception); verify(delay).afterDelay(exception); } @Test public void actionFailedAfterRetry() throws Exception { Delay<Throwable> delay = spy(ofSeconds(1)); upon(IOException.class, asList(delay)); IOException firstException = new IOException(); IOException exception = new IOException("hopeless"); when(action.run()).thenThrow(firstException).thenThrow(exception); CompletionStage<String> stage = retry(action::run); assertPending(stage); elapse(Duration.ofSeconds(1)); assertCauseOf(ExecutionException.class, stage).isSameAs(exception); assertThat(exception.getSuppressed()).asList().containsExactly(firstException); verify(action, times(2)).run(); verify(delay).beforeDelay(firstException); verify(delay).afterDelay(firstException); } @Test public void retrialExceedsTime() throws Exception { upon( IOException.class, ofSeconds(4).timed(Collections.nCopies(100, ofSeconds(1)), clock)); IOException exception1 = new IOException(); IOException exception = new IOException("hopeless"); when(action.run()) .thenThrow(exception1).thenThrow(exception).thenThrow(exception).thenReturn("good"); CompletionStage<String> stage = retry(action::run); assertPending(stage); elapse(Duration.ofSeconds(2)); assertPending(stage); elapse(Duration.ofSeconds(1)); // exceeds time assertCauseOf(ExecutionException.class, stage).isSameAs(exception); assertThat(exception.getSuppressed()).asList().containsExactly(exception1); verify(action, times(3)).run(); // Retry twice. } @Test public void asyncExceptionRetriedToSuccess() throws Exception { upon(IOException.class, ofSeconds(1).exponentialBackoff(2, 1)); when(action.runAsync()) .thenReturn(exceptionally(new IOException())) .thenReturn(completedFuture("fixed")); CompletionStage<String> stage = retryAsync(action::runAsync); assertPending(stage); elapse(Duration.ofSeconds(1)); assertCompleted(stage).isEqualTo("fixed"); verify(action, times(2)).runAsync(); } @Test public void asyncFailedAfterRetry() throws Exception { Delay<Throwable> delay = spy(ofSeconds(1)); upon(IOException.class, asList(delay)); IOException firstException = new IOException(); IOException exception = new IOException("hopeless"); when(action.runAsync()) .thenReturn(exceptionally(firstException)) .thenReturn(exceptionally(exception)); CompletionStage<String> stage = retryAsync(action::runAsync); assertPending(stage); elapse(Duration.ofSeconds(1)); assertCauseOf(ExecutionException.class, stage).isSameAs(exception); verify(action, times(2)).runAsync(); verify(delay).beforeDelay(firstException); verify(delay).afterDelay(firstException); } @Test public void twoDifferentExceptionRulesRetriedToSuccess() throws Exception { Delay<Throwable> delay = spy(ofSeconds(1)); upon(IOException.class, asList(delay, delay)); upon(MyError.class, asList(delay)); IOException exception = new IOException(); MyError error = new MyError("test"); when(action.run()).thenThrow(exception).thenThrow(error).thenThrow(exception).thenReturn("fixed"); CompletionStage<String> stage = retry(action::run); assertPending(stage); elapse(4, Duration.ofSeconds(1)); assertCompleted(stage).isEqualTo("fixed"); verify(action, times(4)).run(); verify(delay, times(2)).beforeDelay(exception); verify(delay, times(2)).afterDelay(exception); verify(delay).beforeDelay(error); verify(delay).afterDelay(error); } @Test public void twoDifferentExceptionRulesRetriedAndFailed() throws Exception { Delay<Throwable> delay = spy(ofSeconds(1)); upon(IOException.class, asList(delay, delay)); upon(MyError.class, asList(delay)); IOException exception1 = new IOException(); MyError error2 = new MyError("test"); IOException exception3 = new IOException(); MyError error4 = new MyError("test"); when(action.run()).thenThrow(exception1).thenThrow(error2).thenThrow(exception3) .thenThrow(error4); CompletionStage<String> stage = retry(action::run); assertPending(stage); elapse(4, Duration.ofSeconds(1)); assertCauseOf(ExecutionException.class, stage).isSameAs(error4); assertThat(error4.getSuppressed()).asList().containsExactly(exception1, error2, exception3); assertThat(error4.getCause()).isNull(); assertThat(exception3.getSuppressed()).isEmpty(); assertThat(error2.getSuppressed()).isEmpty(); assertThat(exception1.getSuppressed()).isEmpty(); verify(action, times(4)).run(); verify(delay).beforeDelay(exception1); verify(delay).afterDelay(exception1); verify(delay).beforeDelay(error2); verify(delay).afterDelay(error2); verify(delay).beforeDelay(exception3); verify(delay).afterDelay(exception3); } @Test public void returnValueAndExceptionRetryToSuccess() throws Exception { Delay<Throwable> exceptionDelay = spy(ofSeconds(1)); Delay<String> returnValueDelay = spy(ofSeconds(1)); Retryer.ForReturnValue<String> forReturnValue = retryer .upon(IOException.class, asList(exceptionDelay)) .uponReturn("bad", asList(returnValueDelay, returnValueDelay)); IOException exception = new IOException(); when(action.run()) .thenReturn("bad").thenThrow(exception).thenReturn("bad").thenReturn("fixed"); CompletionStage<String> stage = forReturnValue.retry(action::run, executor); assertPending(stage); elapse(4, Duration.ofSeconds(1)); assertCompleted(stage).isEqualTo("fixed"); verify(action, times(4)).run(); verify(returnValueDelay, times(2)).beforeDelay("bad"); verify(returnValueDelay, times(2)).afterDelay("bad"); verify(exceptionDelay).beforeDelay(exception); verify(exceptionDelay).afterDelay(exception); } @Test public void returnValueAndExceptionRetriedButStillReturnBad() throws Exception { Delay<Throwable> exceptionDelay = spy(ofSeconds(1)); Delay<String> returnValueDelay = spy(ofSeconds(1)); Retryer.ForReturnValue<String> forReturnValue = retryer .upon(IOException.class, asList(exceptionDelay)) .uponReturn("bad", asList(returnValueDelay, returnValueDelay)); IOException exception = new IOException(); when(action.run()) .thenReturn("bad").thenThrow(exception).thenReturn("bad").thenReturn("bad") .thenReturn("fixed"); CompletionStage<String> stage = forReturnValue.retry(action::run, executor); assertPending(stage); elapse(4, Duration.ofSeconds(1)); assertCompleted(stage).isEqualTo("bad"); verify(action, times(4)).run(); verify(returnValueDelay, times(2)).beforeDelay("bad"); verify(returnValueDelay, times(2)).afterDelay("bad"); verify(exceptionDelay).beforeDelay(exception); verify(exceptionDelay).afterDelay(exception); } @Test public void returnValueAndExceptionRetriedButStillThrows() throws Exception { Delay<Throwable> exceptionDelay = spy(ofSeconds(1)); Delay<String> returnValueDelay = spy(ofSeconds(1)); Retryer.ForReturnValue<String> forReturnValue = retryer .upon(IOException.class, asList(exceptionDelay)) .uponReturn("bad", asList(returnValueDelay, returnValueDelay)); IOException exception1 = new IOException(); IOException exception = new IOException(); when(action.run()) .thenReturn("bad").thenThrow(exception1).thenReturn("bad").thenThrow(exception) .thenReturn("fixed"); CompletionStage<String> stage = forReturnValue.retry(action::run, executor); assertPending(stage); elapse(4, Duration.ofSeconds(1)); assertCauseOf(ExecutionException.class, stage).isSameAs(exception); assertThat(exception.getSuppressed()).asList().containsExactly(exception1); verify(action, times(4)).run(); verify(returnValueDelay, times(2)).beforeDelay("bad"); verify(returnValueDelay, times(2)).afterDelay("bad"); verify(exceptionDelay).beforeDelay(exception1); verify(exceptionDelay).afterDelay(exception1); } @Test public void returnValueAndExceptionAsyncRetryToSuccess() throws Exception { Delay<Throwable> exceptionDelay = spy(ofSeconds(1)); Delay<String> returnValueDelay = spy(ofSeconds(1)); Retryer.ForReturnValue<String> forReturnValue = retryer .upon(IOException.class, asList(exceptionDelay)) .uponReturn("bad", asList(returnValueDelay, returnValueDelay)); IOException exception = new IOException(); when(action.runAsync()) .thenReturn(completedFuture("bad")) .thenReturn(exceptionally(exception)) .thenReturn(completedFuture("bad")) .thenReturn(completedFuture("fixed")); CompletionStage<String> stage = forReturnValue.retryAsync(action::runAsync, executor); assertPending(stage); elapse(4, Duration.ofSeconds(1)); assertCompleted(stage).isEqualTo("fixed"); verify(action, times(4)).runAsync(); verify(returnValueDelay, times(2)).beforeDelay("bad"); verify(returnValueDelay, times(2)).afterDelay("bad"); verify(exceptionDelay).beforeDelay(exception); verify(exceptionDelay).afterDelay(exception); } @Test public void returnValueAndExceptionAsyncRetriedButStillReturnBad() throws Exception { Delay<Throwable> exceptionDelay = spy(ofSeconds(1)); Delay<String> returnValueDelay = spy(ofSeconds(1)); Retryer.ForReturnValue<String> forReturnValue = retryer .upon(IOException.class, asList(exceptionDelay)) .uponReturn("bad", asList(returnValueDelay, returnValueDelay)); IOException exception = new IOException(); when(action.runAsync()) .thenReturn(completedFuture("bad")) .thenThrow(exception) .thenReturn(completedFuture("bad")) .thenReturn(completedFuture("bad")) .thenReturn(completedFuture("fixed")); CompletionStage<String> stage = forReturnValue.retryAsync(action::runAsync, executor); assertPending(stage); elapse(4, Duration.ofSeconds(1)); assertCompleted(stage).isEqualTo("bad"); verify(action, times(4)).runAsync(); verify(returnValueDelay, times(2)).beforeDelay("bad"); verify(returnValueDelay, times(2)).afterDelay("bad"); verify(exceptionDelay).beforeDelay(exception); verify(exceptionDelay).afterDelay(exception); } @Test public void returnValueAndExceptionAsyncRetriedButStillThrows() throws Exception { Delay<Throwable> exceptionDelay = spy(ofSeconds(1)); Delay<String> returnValueDelay = spy(ofSeconds(1)); Retryer.ForReturnValue<String> forReturnValue = retryer .upon(IOException.class, asList(exceptionDelay)) .uponReturn("bad", asList(returnValueDelay, returnValueDelay)); IOException exception1 = new IOException(); IOException exception = new IOException(); when(action.runAsync()) .thenReturn(completedFuture("bad")) .thenReturn(exceptionally(exception1)) .thenReturn(completedFuture("bad")) .thenThrow(exception) .thenReturn(completedFuture("fixed")); CompletionStage<String> stage = forReturnValue.retryAsync(action::runAsync, executor); assertPending(stage); elapse(4, Duration.ofSeconds(1)); assertCauseOf(ExecutionException.class, stage).isSameAs(exception); assertThat(exception.getSuppressed()).asList().containsExactly(exception1); verify(action, times(4)).runAsync(); verify(returnValueDelay, times(2)).beforeDelay("bad"); verify(returnValueDelay, times(2)).afterDelay("bad"); verify(exceptionDelay).beforeDelay(exception1); verify(exceptionDelay).afterDelay(exception1); } @Test public void testCustomDelay() throws Exception { TestDelay<IOException> delay = new TestDelay<IOException>() { @Override public Duration duration() { return Duration.ofMillis(1); } }; upon(IOException.class, asList(delay).stream()); // to make sure the stream overload works. IOException exception = new IOException(); when(action.run()).thenThrow(exception).thenReturn("fixed"); CompletionStage<String> stage = retry(action::run); elapse(Duration.ofMillis(1)); assertCompleted(stage).isEqualTo("fixed"); verify(action, times(2)).run(); assertThat(delay.before).isSameAs(exception); assertThat(delay.after).isSameAs(exception); } @Test public void testImmutable() throws IOException { retryer.upon(IOException.class, asList(ofSeconds(1))); // Should have no effect IOException exception = new IOException("bad"); when(action.run()).thenThrow(exception); assertCauseOf(ExecutionException.class, retry(action::run)).isSameAs(exception); verify(action).run(); } @Test public void testTimed() { List<Delay<?>> delays = asList(1L, 8L, 1L).stream() .map(Delay::ofMillis) .collect(toList()); List<Delay<?>> timed = Delay.ofMillis(10).timed(delays, clock); assertThat(timed).hasSize(3); assertThat(timed).isNotEmpty(); assertThat(timed).containsExactlyElementsIn(delays); elapse(Duration.ofMillis(1)); assertThat(timed).containsExactlyElementsIn(delays); elapse(Duration.ofMillis(1)); assertThat(timed.get(0)).isEqualTo(delays.get(0)); assertThrows(IndexOutOfBoundsException.class, () -> timed.get(1)); } @Test public void testNulls() throws Exception { Stream<?> statelessStream = (Stream<?>) Proxy.newProxyInstance( RetryerTest.class.getClassLoader(), new Class<?>[] {Stream.class}, (p, method, args) -> method.invoke(Stream.of(), args)); new NullPointerTester() .setDefault(Stream.class, statelessStream) .ignore(Retryer.class.getMethod("uponReturn", Object.class, Stream.class)) .ignore(Retryer.class.getMethod("uponReturn", Object.class, List.class)) .testAllPublicInstanceMethods(new Retryer()); } @Test public void testForReturnValue_nulls() { new NullPointerTester() .testAllPublicInstanceMethods(new Retryer().uponReturn("bad", asList())); } @Test public void testDelay_nulls() { new NullPointerTester().testAllPublicStaticMethods(Delay.class); new NullPointerTester() .setDefault(Clock.class, Clock.systemUTC()) .testAllPublicInstanceMethods(new ExceptionDelay()); } @Test public void testDelay_multiplied() { assertThat(ofDays(1).multipliedBy(0)).isEqualTo(ofDays(0)); assertThat(ofDays(2).multipliedBy(1)).isEqualTo(ofDays(2)); assertThat(ofDays(3).multipliedBy(2)).isEqualTo(ofDays(6)); assertThrows(IllegalArgumentException.class, () -> ofDays(1).multipliedBy(-1)); assertThat(ofDays(1).multipliedBy(Double.MIN_VALUE)).isEqualTo(Delay.ofMillis(1)); } @Test public void testDelay_exponentialBackoff() { assertThat(ofDays(1).exponentialBackoff(2, 3)) .containsExactly(ofDays(1), ofDays(2), ofDays(4)) .inOrder(); assertThat(ofDays(1).exponentialBackoff(1, 2)) .containsExactly(ofDays(1), ofDays(1)) .inOrder(); assertThat(ofDays(1).exponentialBackoff(1, 0)).isEmpty(); assertThrows(IllegalArgumentException.class, () -> ofDays(1).exponentialBackoff(0, 1)); assertThrows(IllegalArgumentException.class, () -> ofDays(1).exponentialBackoff(-1, 1)); assertThrows(IllegalArgumentException.class, () -> ofDays(1).exponentialBackoff(2, -1)); assertThrows(IndexOutOfBoundsException.class, () -> ofDays(1).exponentialBackoff(1, 1).get(-1)); assertThrows(IndexOutOfBoundsException.class, () -> ofDays(1).exponentialBackoff(1, 1).get(1)); } @Test public void testDelay_fibonacci() { assertThat(ofDays(1).fibonacci(1)).containsExactly(ofDays(1)).inOrder(); assertThat(ofDays(1).fibonacci(2)).containsExactly(ofDays(1), ofDays(1)).inOrder(); assertThat(ofDays(1).fibonacci(3)).containsExactly(ofDays(1), ofDays(1), ofDays(2)).inOrder(); assertThat(ofDays(1).fibonacci(5)) .containsExactly(ofDays(1), ofDays(1), ofDays(2), ofDays(3), ofDays(5)) .inOrder(); assertThat(ofDays(1).fibonacci(500).get(499)).isEqualTo(Delay.ofMillis(Long.MAX_VALUE)); assertThat(ofDays(1).fibonacci(0)).isEmpty(); assertThrows(IllegalArgumentException.class, () -> ofDays(1).fibonacci(-1)); assertThrows(IndexOutOfBoundsException.class, () -> ofDays(1).fibonacci(1).get(-1)); assertThrows(IndexOutOfBoundsException.class, () -> ofDays(1).fibonacci(1).get(1)); } @Test public void testDelay_randomized_invalid() { assertThrows(IllegalArgumentException.class, () -> ofDays(1).randomized(new Random(), -0.1)); assertThrows(IllegalArgumentException.class, () -> ofDays(1).randomized(new Random(), 1.1)); } @Test public void testDelay_randomized_zeroRandomness() { Delay<?> delay = ofDays(1).randomized(new Random(), 0); assertThat(delay).isEqualTo(ofDays(1)); } @Test public void testDelay_randomized_halfRandomness() { Random random = Mockito.mock(Random.class); when(random.nextDouble()).thenReturn(0D).thenReturn(0.5D).thenReturn(1D); assertThat(ofDays(1).randomized(random, 0.5).duration()).isEqualTo(Duration.ofHours(12)); assertThat(ofDays(1).randomized(random, 0.5).duration()).isEqualTo(Duration.ofHours(24)); assertThat(ofDays(1).randomized(random, 0.5).duration()).isEqualTo(Duration.ofHours(36)); } @Test public void testDelay_randomized_fullRandomness() { Random random = Mockito.mock(Random.class); when(random.nextDouble()).thenReturn(0D).thenReturn(0.5D).thenReturn(1D); assertThat(ofDays(1).randomized(random, 1).duration()).isEqualTo(Duration.ofHours(0)); assertThat(ofDays(1).randomized(random, 1).duration()).isEqualTo(Duration.ofHours(24)); assertThat(ofDays(1).randomized(random, 1).duration()).isEqualTo(Duration.ofHours(48)); } @Test public void testDelay_equals() { new EqualsTester() .addEqualityGroup( Delay.ofMillis(1000), Delay.of(Duration.ofMillis(1000)), Delay.of(Duration.ofSeconds(1))) .addEqualityGroup(Delay.ofMillis(2)) .testEquals(); } @Test public void testDelay_compareTo() { assertThat(Delay.ofMillis(1)).isLessThan(Delay.ofMillis(2)); assertThat(Delay.ofMillis(1)).isGreaterThan(Delay.ofMillis(0)); assertThat(Delay.ofMillis(1)).isEquivalentAccordingToCompareTo(Delay.ofMillis(1)); } @Test public void testDelay_of() { assertThat(Delay.ofMillis(Long.MAX_VALUE).duration()) .isEqualTo(Duration.ofMillis(Long.MAX_VALUE)); assertThat(Delay.ofMillis(0).duration()).isEqualTo(Duration.ofMillis(0)); assertThat(Delay.ofMillis(1).duration()).isEqualTo(Duration.ofMillis(1)); assertThat(ofDays(0).duration()).isEqualTo(Duration.ofDays(0)); assertThat(ofDays(1).duration()).isEqualTo(Duration.ofDays(1)); } @Test public void testDelay_invalid() { assertThrows(ArithmeticException.class, () -> ofDays(Long.MAX_VALUE)); assertThrows(IllegalArgumentException.class, () -> Delay.ofMillis(-1)); assertThrows(ArithmeticException.class, () -> Delay.ofMillis(Long.MIN_VALUE)); assertThrows(IllegalArgumentException.class, () -> Delay.of(Duration.ofDays(-1))); } @Test public void testDelay_forEvents() { Delay<String> delay = spy(new SpyableDelay<String>(Duration.ofDays(1))); Delay<Integer> mapped = delay.forEvents(Object::toString); assertThat(mapped).isEqualTo(delay); mapped.beforeDelay(123); verify(delay).beforeDelay("123"); mapped.afterDelay(456); verify(delay).afterDelay("456"); } @Test public void testFakeScheduledExecutorService_taskScheduledButNotRunYet() { Runnable runnable = mock(Runnable.class); executor.schedule(runnable, 2, TimeUnit.MILLISECONDS); elapse(Duration.ofMillis(1)); Mockito.verifyZeroInteractions(runnable); } @Test public void testFakeScheduledExecutorService_taskScheduledAndRun() { Runnable runnable = mock(Runnable.class); executor.schedule(runnable, 2, TimeUnit.MILLISECONDS); elapse(Duration.ofMillis(2)); verify(runnable).run(); elapse(Duration.ofMillis(2)); Mockito.verifyNoMoreInteractions(runnable); } @Test public void testFakeScheduledExecutorService_taskScheduleAnotherTask() { Runnable runnable = mock(Runnable.class); Runnable scheduled = () -> executor.schedule(runnable, 3, TimeUnit.MILLISECONDS); executor.schedule(scheduled, 2, TimeUnit.MILLISECONDS); elapse(Duration.ofMillis(2)); elapse(Duration.ofMillis(3)); verify(runnable).run(); Mockito.verifyNoMoreInteractions(runnable); } @Test public void testFibonacci() { assertThat(Math.round(Retryer.fib(0))).isEqualTo(0); assertThat(Math.round(Retryer.fib(1))).isEqualTo(1); List<Long> results = new ArrayList<>(); results.add(0L); results.add(1L); for (int i = 2; i < 93; i++) { long f = Math.round(Retryer.fib(i)); assertThat(f).named("fibonacci(%s)", i).isLessThan(Long.MAX_VALUE); assertThat((double) f).named("fibonacci(%s)", i) .isWithin(f / 1000).of(results.get(i - 2).doubleValue() + results.get(i - 1).doubleValue()); results.add(f); } } private static CompletionStage<String> exceptionally(Throwable e) { CompletableFuture<String> future = new CompletableFuture<>(); future.completeExceptionally(e); return future; } private static <E> Delay<E> ofSeconds(long seconds) { return new SpyableDelay<>(Duration.ofSeconds(seconds)); } private static <E> Delay<E> ofDays(long days) { return new SpyableDelay<>(Duration.ofDays(days)); } private <E extends Throwable> void upon( Class<E> exceptionType, List<? extends Delay<? super E>> delays) { retryer = retryer.upon(exceptionType, delays); } private <E extends Throwable> void upon( Class<E> exceptionType, Stream<? extends Delay<? super E>> delays) { retryer = retryer.upon(exceptionType, delays); } private <T> CompletionStage<T> retry(CheckedSupplier<T, ?> supplier) { return retryer.retry(supplier, executor); } private <T> CompletionStage<T> retryAsync( CheckedSupplier<? extends CompletionStage<T>, ?> supplier) { return retryer.retryAsync(supplier, executor); } private static ThrowableSubject assertException( Class<? extends Throwable> exceptionType, Executable executable) { Throwable thrown = assertThrows(exceptionType, executable); return assertThat(thrown); } private void elapse(int counts, Duration duration) { for (int i = 0; i < counts; i++) { elapse(duration); } } private void elapse(Duration duration) { clock.elapse(duration); executor.tick(); } abstract class TestDelay<E> extends Delay<E> { E before; E after; @Override public void beforeDelay(E exception) { before = exception; } @Override public void afterDelay(E exception) { after = exception; } } abstract static class FakeClock extends Clock { private Instant now = Instant.ofEpochMilli(123456789L); @Override public Instant instant() { return now; } void elapse(Duration duration) { now = now.plus(duration); } } abstract class FakeScheduledExecutorService implements ScheduledExecutorService { private List<Schedule> schedules = new ArrayList<>(); void tick() { Instant now = clock.instant(); List<Schedule> ready = schedules.stream().filter(s -> s.ready(now)).collect(toList()); schedules = schedules.stream() .filter(s -> s.pending(now)) .collect(toCollection(ArrayList::new)); ready.forEach(s -> s.command.run()); } @Override public void execute(Runnable command) { schedule(command, 1, TimeUnit.MILLISECONDS); } @Override public ScheduledFuture<?> schedule( Runnable command, long delay, TimeUnit unit) { assertThat(unit).isEqualTo(TimeUnit.MILLISECONDS); schedules.add(new Schedule(clock.instant().plus(delay, ChronoUnit.MILLIS), command)); return addScheduledFuture(); } private <T> ScheduledFuture<T> addScheduledFuture() { @SuppressWarnings("unchecked") // mock is safe. ScheduledFuture<T> scheduled = Mockito.mock(ScheduledFuture.class); scheduledFutures.add(scheduled); return scheduled; } @Deprecated // Should not accidentally call this one since we don't use it. @Override public <V> ScheduledFuture<V> schedule( Callable<V> callable, long delay, TimeUnit unit) { throw new UnsupportedOperationException(); } } private static final class Schedule { private final Instant time; final Runnable command; Schedule(Instant time, Runnable command) { this.time = requireNonNull(time); this.command = requireNonNull(command); } boolean ready(Instant now) { return !pending(now); } boolean pending(Instant now) { return now.isBefore(time); } } private interface Action { String run() throws IOException; CompletionStage<String> runAsync() throws IOException; } @SuppressWarnings("serial") private static final class MyError extends Error { MyError(String message) { super(message); } } private static final class ExceptionDelay extends Delay<Throwable> { @Override public Duration duration() { return Duration.ofMillis(1); } @Override public void beforeDelay(Throwable exception) { requireNonNull(exception); } @Override public void afterDelay(Throwable exception) { requireNonNull(exception); } } }