/***************************************************************************** * ------------------------------------------------------------------------- * * 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 java.util.Arrays.asList; import static org.mockito.Matchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.only; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.IOException; import java.time.Duration; import org.junit.Before; import org.junit.Test; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.function.Executable; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import com.google.common.truth.ThrowableSubject; import com.google.common.truth.Truth; import com.google.mu.util.Retryer; import com.google.mu.util.Retryer.Delay; @RunWith(PowerMockRunner.class) @PrepareForTest({Thread.class, Retryer.Delay.class}) public class RetryerBlockingTest { @Mock private Action action; @Mock private Interruptible interruptible; private final Delay<Object> delay = Mockito.spy(new SpyableDelay<>(Duration.ofMillis(100))); @Before public void setUpMocks() { MockitoAnnotations.initMocks(this); PowerMockito.mockStatic(Thread.class); } @Before public void noMoreInteractions() { PowerMockito.verifyNoMoreInteractions(Thread.class); } @Test public void noRetryIfReturnValueIsGoodFirstTime() throws IOException { when(action.run()).thenReturn("good"); Retryer retryer = new Retryer(); assertThat(retryer.uponReturn("bad", asList(delay)).retryBlockingly(action::run)) .isEqualTo("good"); verify(action).run(); } @Test public void exceptionFromBeforeDelayPropagatedDuringReturnValueRetry() throws IOException { Retryer.ForReturnValue<String> retryer = new Retryer().uponReturn("bad", asList(delay)); RuntimeException unexpected = new RuntimeException(); when(action.run()).thenReturn("bad"); Mockito.doThrow(unexpected).when(delay).beforeDelay("bad"); assertException(RuntimeException.class, () -> retryer.retryBlockingly(action::run)) .isSameAs(unexpected); assertThat(unexpected.getSuppressed()).isEmpty(); verify(action).run(); verify(delay).beforeDelay("bad"); verify(delay, never()).afterDelay(any()); } @Test public void exceptionFromAfterDelayPropgatedDuringReturnValueRetry() throws IOException, InterruptedException { Retryer.ForReturnValue<String> retryer = new Retryer().uponReturn("bad", asList(delay)); RuntimeException unexpected = new RuntimeException(); when(action.run()).thenReturn("bad"); Mockito.doThrow(unexpected).when(delay).afterDelay("bad"); assertException(RuntimeException.class, () -> retryer.retryBlockingly(action::run)) .isSameAs(unexpected); assertThat(unexpected.getSuppressed()).isEmpty(); verify(action).run(); PowerMockito.verifyStatic(only()); Thread.sleep(delay.duration().toMillis()); verify(delay).beforeDelay("bad"); verify(delay).afterDelay("bad"); } @Test public void returnValueChangesToExpectedAfterRetry() throws IOException, InterruptedException { Retryer.ForReturnValue<String> retryer = new Retryer().uponReturn("bad", asList(delay)); when(action.run()).thenReturn("bad").thenReturn("fixed"); assertThat(retryer.retryBlockingly(action::run)).isEqualTo("fixed"); verify(action, times(2)).run(); PowerMockito.verifyStatic(only()); Thread.sleep(delay.duration().toMillis()); verify(delay).beforeDelay("bad"); verify(delay).afterDelay("bad"); } @Test public void returnValueStillBadEvenAfterRetry() throws IOException, InterruptedException { Retryer.ForReturnValue<String> retryer = new Retryer().uponReturn("bad", Delay.ofMillis(100).exponentialBackoff(10, 2)); when(action.run()).thenReturn("bad"); assertThat(retryer.retryBlockingly(action::run)).isEqualTo("bad"); verify(action, times(3)).run(); PowerMockito.verifyStatic(); Thread.sleep(100); PowerMockito.verifyStatic(); Thread.sleep(1000); } @Test public void interruptedDuringReturnValueRetry() throws IOException, InterruptedException { Retryer.ForReturnValue<String> retryer = new Retryer() .uponReturn("bad", Delay.ofMillis(100).exponentialBackoff(10, 1)); when(action.run()).thenReturn("bad"); PowerMockito.doThrow(new InterruptedException()).when(Thread.class); Thread.sleep(100); Thread thread = PowerMockito.mock(Thread.class); PowerMockito.doReturn(thread).when(Thread.class); Thread.currentThread(); assertThat(retryer.retryBlockingly(action::run)).isEqualTo("bad"); verify(action).run(); verify(thread).interrupt(); PowerMockito.verifyStatic(); Thread.sleep(100); } @Test public void noRetryIfActionSucceedsFirstTime() throws IOException { when(action.run()).thenReturn("good"); assertThat(new Retryer().retryBlockingly(action::run)).isEqualTo("good"); verify(action).run(); } @Test public void notIntrrupted() throws InterruptedException { when(interruptible.compute()).thenReturn("good"); new Retryer().retryBlockingly(interruptible::compute); verify(interruptible).compute(); } @Test public void interruptedExceptionNotRetried() throws InterruptedException { Retryer retryer = new Retryer().upon(Exception.class, asList(delay)); InterruptedException exception = new InterruptedException(); when(interruptible.compute()).thenThrow(exception); assertException( InterruptedException.class, () -> retryer.retryBlockingly(interruptible::compute)) .isSameAs(exception); assertThat(exception.getSuppressed()).isEmpty(); verify(delay, never()).beforeDelay(any()); verify(delay, never()).afterDelay(any()); } @Test public void interruptedTheSecondTimeNotRetriedButCarriesSuppressed() throws InterruptedException { Retryer retryer = new Retryer().upon(Exception.class, asList(delay, delay)); RuntimeException exception1 = new RuntimeException(); InterruptedException exception = new InterruptedException(); when(interruptible.compute()).thenThrow(exception1).thenThrow(exception); assertException( InterruptedException.class, () -> retryer.retryBlockingly(interruptible::compute)) .isSameAs(exception); assertThat(exception.getSuppressed()).asList().containsExactly(exception1); verify(delay).beforeDelay(exception1); verify(delay).afterDelay(exception1); verify(delay, never()).beforeDelay(exception); verify(delay, never()).afterDelay(exception); } @Test public void actionFailsButRetryNotConfigured() throws IOException { IOException exception = new IOException(); when(action.run()).thenThrow(exception); assertException(IOException.class, () -> new Retryer().retryBlockingly(action::run)) .isSameAs(exception); assertThat(exception.getSuppressed()).isEmpty(); verify(action).run(); } @Test public void actionFailsButRetryConfiguredForDifferentException() throws IOException { Retryer retryer = new Retryer() .upon(RuntimeException.class, asList(delay)); IOException exception = new IOException(); when(action.run()).thenThrow(exception); assertException(IOException.class, () -> retryer.retryBlockingly(action::run)) .isSameAs(exception); assertThat(exception.getSuppressed()).isEmpty(); verify(action).run(); verify(delay, never()).beforeDelay(any()); verify(delay, never()).afterDelay(any()); } @Test public void actionFailsWithUncheckedButRetryConfiguredForDifferentException() throws IOException { Retryer retryer = new Retryer() .upon(IOException.class, asList(delay)); RuntimeException exception = new RuntimeException(); when(action.run()).thenThrow(exception); assertException(RuntimeException.class, () -> retryer.retryBlockingly(action::run)) .isSameAs(exception); assertThat(exception.getSuppressed()).isEmpty(); verify(action).run(); verify(delay, never()).beforeDelay(any()); verify(delay, never()).afterDelay(any()); } @Test public void exceptionFromBeforeDelayPropagated() throws IOException { Retryer retryer = new Retryer() .upon(IOException.class, asList(delay)); RuntimeException unexpected = new RuntimeException(); IOException exception = new IOException(); when(action.run()).thenThrow(exception); Mockito.doThrow(unexpected).when(delay).beforeDelay(exception); assertException(RuntimeException.class, () -> retryer.retryBlockingly(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 exceptionFromAfterDelayPropgated() throws IOException, InterruptedException { Retryer retryer = new Retryer() .upon(IOException.class, asList(delay)); RuntimeException unexpected = new RuntimeException(); IOException exception = new IOException(); when(action.run()).thenThrow(exception); Mockito.doThrow(unexpected).when(delay).afterDelay(exception); assertException(RuntimeException.class, () -> retryer.retryBlockingly(action::run)) .isSameAs(unexpected); assertThat(unexpected.getSuppressed()).asList().containsExactly(exception); verify(action).run(); PowerMockito.verifyStatic(only()); Thread.sleep(delay.duration().toMillis()); verify(delay).beforeDelay(exception); verify(delay).afterDelay(exception); } @Test public void actionFailsThenSucceedsAfterRetry() throws IOException, InterruptedException { Retryer retryer = new Retryer() .upon(IOException.class, asList(delay)); IOException exception = new IOException(); when(action.run()).thenThrow(exception).thenReturn("fixed"); assertThat(retryer.retryBlockingly(action::run)).isEqualTo("fixed"); verify(action, times(2)).run(); PowerMockito.verifyStatic(only()); Thread.sleep(delay.duration().toMillis()); verify(delay).beforeDelay(exception); verify(delay).afterDelay(exception); } @Test public void actionFailsWithUncheckedThenSucceedsAfterRetry() throws IOException, InterruptedException { Retryer retryer = new Retryer() .upon(RuntimeException.class, asList(delay)); RuntimeException exception = new RuntimeException(); when(action.run()).thenThrow(exception).thenReturn("fixed"); assertThat(retryer.retryBlockingly(action::run)).isEqualTo("fixed"); verify(action, times(2)).run(); PowerMockito.verifyStatic(only()); Thread.sleep(delay.duration().toMillis()); verify(delay).beforeDelay(exception); verify(delay).afterDelay(exception); } @Test public void actionFailsWithErrorThenSucceedsAfterRetry() throws IOException, InterruptedException { Retryer retryer = new Retryer() .upon(Error.class, asList(delay)); Error exception = new Error(); when(action.run()).thenThrow(exception).thenReturn("fixed"); assertThat(retryer.retryBlockingly(action::run)).isEqualTo("fixed"); verify(action, times(2)).run(); PowerMockito.verifyStatic(only()); Thread.sleep(delay.duration().toMillis()); verify(delay).beforeDelay(exception); verify(delay).afterDelay(exception); } @Test public void actionFailsEvenAfterRetry() throws IOException, InterruptedException { Retryer retryer = new Retryer() .upon(IOException.class, Delay.ofMillis(100).exponentialBackoff(10, 2)); IOException exception1 = new IOException(); IOException exception2 = new IOException(); IOException exception = new IOException(); when(action.run()) .thenThrow(exception1).thenThrow(exception2).thenThrow(exception); assertException(IOException.class, () -> retryer.retryBlockingly(action::run)) .isSameAs(exception); assertThat(exception.getSuppressed()).asList().containsExactly(exception1, exception2).inOrder(); verify(action, times(3)).run(); PowerMockito.verifyStatic(); Thread.sleep(100); PowerMockito.verifyStatic(); Thread.sleep(1000); } @Test public void sameExceptionNotAddedAsCause() throws IOException, InterruptedException { Retryer retryer = new Retryer() .upon(IOException.class, Delay.ofMillis(100).exponentialBackoff(10, 2)); IOException exception1 = new IOException(); IOException exception2 = new IOException(); when(action.run()) .thenThrow(exception1).thenThrow(exception2).thenThrow(exception2); assertException(IOException.class, () -> retryer.retryBlockingly(action::run)) .isSameAs(exception2); assertThat(exception2.getSuppressed()).asList().containsExactly(exception1).inOrder(); verify(action, times(3)).run(); PowerMockito.verifyStatic(); Thread.sleep(100); PowerMockito.verifyStatic(); Thread.sleep(1000); } @Test public void interruptedDuringRetry() throws IOException, InterruptedException { Retryer retryer = new Retryer() .upon(IOException.class, Delay.ofMillis(100).exponentialBackoff(10, 1)); IOException exception = new IOException(); when(action.run()).thenThrow(exception); PowerMockito.doThrow(new InterruptedException()).when(Thread.class); Thread.sleep(100); Thread thread = PowerMockito.mock(Thread.class); PowerMockito.doReturn(thread).when(Thread.class); Thread.currentThread(); assertException(IOException.class, () -> retryer.retryBlockingly(action::run)) .isSameAs(exception); assertThat(exception.getSuppressed()).isEmpty(); verify(action).run(); verify(thread).interrupt(); PowerMockito.verifyStatic(); Thread.sleep(100); } @Test public void twoDifferentExceptionRulesRetriedToSuccess() throws IOException, InterruptedException { Retryer retryer = new Retryer() .upon(IOException.class, asList(delay)) .upon(MyUncheckedException.class, asList(delay, delay)); MyUncheckedException unchecked = new MyUncheckedException(); IOException exception = new IOException(); when(action.run()) .thenThrow(unchecked).thenThrow(exception).thenThrow(unchecked).thenReturn("fixed"); assertThat(retryer.retryBlockingly(action::run)).isEqualTo("fixed"); verify(action, times(4)).run(); PowerMockito.verifyStatic(times(3)); Thread.sleep(delay.duration().toMillis()); verify(delay, times(2)).beforeDelay(unchecked); verify(delay, times(2)).afterDelay(unchecked); verify(delay).beforeDelay(exception); verify(delay).afterDelay(exception); } @Test public void twoDifferentExceptionRulesRetrialFailed() throws IOException, InterruptedException { Retryer retryer = new Retryer() .upon(IOException.class, asList(delay)) .upon(MyUncheckedException.class, asList(delay, delay)); MyUncheckedException unchecked1 = new MyUncheckedException(); IOException exception2 = new IOException(); MyUncheckedException unchecked3 = new MyUncheckedException(); IOException exception4 = new IOException(); when(action.run()) .thenThrow(unchecked1).thenThrow(exception2).thenThrow(unchecked3).thenThrow(exception4); assertException(IOException.class, () -> retryer.retryBlockingly(action::run)) .isSameAs(exception4); assertThat(exception4.getSuppressed()).asList() .containsExactly(unchecked1, exception2, unchecked3).inOrder(); verify(action, times(4)).run(); PowerMockito.verifyStatic(times(3)); Thread.sleep(delay.duration().toMillis()); verify(delay).beforeDelay(unchecked1); verify(delay).afterDelay(unchecked1); verify(delay).beforeDelay(exception2); verify(delay).afterDelay(exception2); verify(delay).beforeDelay(unchecked3); verify(delay).afterDelay(unchecked3); } @Test public void returnValueAndExceptionRetryToSuccess() throws IOException, InterruptedException { Retryer.ForReturnValue<String> retryer = new Retryer() .upon(IOException.class, asList(delay)) .uponReturn("bad", asList(delay, delay)); IOException exception = new IOException(); when(action.run()) .thenReturn("bad").thenThrow(exception).thenReturn("bad").thenReturn("fixed"); assertThat(retryer.retryBlockingly(action::run)).isEqualTo("fixed"); verify(action, times(4)).run(); PowerMockito.verifyStatic(times(3)); Thread.sleep(delay.duration().toMillis()); verify(delay, times(2)).beforeDelay("bad"); verify(delay, times(2)).afterDelay("bad"); verify(delay).beforeDelay(exception); verify(delay).afterDelay(exception); } @Test public void returnValueAndExceptionRetryStillReturnsBad() throws IOException, InterruptedException { Retryer.ForReturnValue<String> retryer = new Retryer() .upon(IOException.class, asList(delay)) .uponReturn("bad", asList(delay, delay)); IOException exception = new IOException(); when(action.run()) .thenReturn("bad").thenThrow(exception).thenReturn("bad").thenReturn("bad") .thenReturn("fixed"); assertThat(retryer.retryBlockingly(action::run)).isEqualTo("bad"); verify(action, times(4)).run(); PowerMockito.verifyStatic(times(3)); Thread.sleep(delay.duration().toMillis()); verify(delay, times(2)).beforeDelay("bad"); verify(delay, times(2)).afterDelay("bad"); verify(delay).beforeDelay(exception); verify(delay).afterDelay(exception); } @Test public void returnValueAndExceptionRetryStillThrows() throws IOException, InterruptedException { Retryer.ForReturnValue<String> retryer = new Retryer() .upon(IOException.class, asList(delay)) .uponReturn("bad", asList(delay, delay)); IOException exception1 = new IOException(); IOException exception = new IOException(); when(action.run()) .thenReturn("bad").thenThrow(exception1).thenReturn("bad").thenThrow(exception) .thenReturn("fixed"); assertException(IOException.class, () -> retryer.retryBlockingly(action::run)) .isSameAs(exception); assertThat(exception.getSuppressed()).asList().containsExactly(exception1); assertThat(exception.getCause()).isNull(); assertThat(exception1.getSuppressed()).isEmpty(); assertThat(exception1.getCause()).isNull(); verify(action, times(4)).run(); PowerMockito.verifyStatic(times(3)); Thread.sleep(delay.duration().toMillis()); verify(delay, times(2)).beforeDelay("bad"); verify(delay, times(2)).afterDelay("bad"); verify(delay).beforeDelay(exception1); verify(delay).afterDelay(exception1); } private static ThrowableSubject assertException( Class<? extends Throwable> exceptionType, Executable executable) { Throwable exception = Assertions.assertThrows(exceptionType, executable); return Truth.assertThat(exception); } private interface Action { String run() throws IOException; } private interface Interruptible { String compute() throws InterruptedException; } @SuppressWarnings("serial") private static class MyUncheckedException extends RuntimeException {} }