package io.github.resilience4j.retry.internal;
import io.github.resilience4j.retry.AsyncRetry;
import io.github.resilience4j.retry.RetryConfig;
import io.github.resilience4j.test.AsyncHelloWorldService;
import io.vavr.control.Try;
import org.assertj.core.api.Assertions;
import org.junit.Before;
import org.junit.Test;
import org.mockito.BDDMockito;
import org.mockito.Mockito;
import javax.xml.ws.WebServiceException;
import java.util.concurrent.*;
import java.util.function.Supplier;
import static java.util.concurrent.CompletableFuture.completedFuture;
import static java.util.concurrent.CompletableFuture.supplyAsync;
public class AsyncRetryTest {
private static final long DEFAULT_TIMEOUT_SECONDS = 5;
private AsyncHelloWorldService helloWorldService;
private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
@Before
public void setUp(){
helloWorldService = Mockito.mock(AsyncHelloWorldService.class);
}
@Test
public void shouldNotRetry() throws InterruptedException, ExecutionException, TimeoutException {
// Given the HelloWorldService returns Hello world
BDDMockito.given(helloWorldService.returnHelloWorld()).willReturn(completedFuture("Hello world"));
// Create a Retry with default configuration
AsyncRetry retryContext = AsyncRetry.ofDefaults("id");
// Decorate the invocation of the HelloWorldService
Supplier<CompletionStage<String>> supplier = AsyncRetry.decorateCompletionStage(
retryContext,
scheduler,
() -> helloWorldService.returnHelloWorld());
// When
String result = awaitResult(supplier);
// Then the helloWorldService should be invoked 1 time
BDDMockito.then(helloWorldService).should(Mockito.times(1)).returnHelloWorld();
Assertions.assertThat(result).isEqualTo("Hello world");
}
@Test
public void shouldRetryInCaseOfExceptionAtSyncStage() {
// Given the HelloWorldService throws an exception
BDDMockito.given(helloWorldService.returnHelloWorld())
.willThrow(new WebServiceException("BAM!"))
.willReturn(completedFuture("Hello world"));
// Create a Retry with default configuration
AsyncRetry retryContext = AsyncRetry.ofDefaults("id");
// Decorate the invocation of the HelloWorldService
Supplier<CompletionStage<String>> supplier = AsyncRetry.decorateCompletionStage(
retryContext,
scheduler,
() -> helloWorldService.returnHelloWorld());
// When
String result = awaitResult(supplier.get());
// Then the helloWorldService should be invoked 2 times
BDDMockito.then(helloWorldService).should(Mockito.times(2)).returnHelloWorld();
Assertions.assertThat(result).isEqualTo("Hello world");
}
@Test
public void shouldRetryInCaseOfAnExceptionAtAsyncStage() {
// Given the HelloWorldService throws an exception
BDDMockito.given(helloWorldService.returnHelloWorld())
.willReturn(supplyAsync(() -> { throw new WebServiceException("BAM!"); }))
.willReturn(completedFuture("Hello world"));
// Create a Retry with default configuration
AsyncRetry retryContext = AsyncRetry.ofDefaults("id");
// Decorate the invocation of the HelloWorldService
Supplier<CompletionStage<String>> supplier = AsyncRetry.decorateCompletionStage(
retryContext,
scheduler,
() -> helloWorldService.returnHelloWorld());
// When
String result = awaitResult(supplier.get());
// Then the helloWorldService should be invoked 2 times
BDDMockito.then(helloWorldService).should(Mockito.times(2)).returnHelloWorld();
Assertions.assertThat(result).isEqualTo("Hello world");
}
@Test
public void shouldCompleteFutureAfterOneAttemptInCaseOfExceptionAtSyncStage() {
shouldCompleteFutureAfterAttemptsInCaseOfExceptionAtSyncStage(1);
}
@Test
public void shouldCompleteFutureAfterTwoAttemptsInCaseOfExceptionAtSyncStage() {
shouldCompleteFutureAfterAttemptsInCaseOfExceptionAtSyncStage(2);
}
@Test
public void shouldCompleteFutureAfterThreeAttemptsInCaseOfExceptionAtSyncStage() {
shouldCompleteFutureAfterAttemptsInCaseOfExceptionAtSyncStage(3);
}
private void shouldCompleteFutureAfterAttemptsInCaseOfExceptionAtSyncStage(int noOfAttempts) {
// Given the HelloWorldService throws an exception
BDDMockito.given(helloWorldService.returnHelloWorld())
.willThrow(new WebServiceException("BAM!"));
// Create a Retry with default configuration
AsyncRetry retryContext = AsyncRetry.of(
"id",
RetryConfig
.custom()
.maxAttempts(noOfAttempts)
.build());
// Decorate the invocation of the HelloWorldService
Supplier<CompletionStage<String>> supplier = AsyncRetry.decorateCompletionStage(
retryContext,
scheduler,
() -> helloWorldService.returnHelloWorld());
// When
Try<String> resultTry = Try.of(() -> awaitResult(supplier.get()));
// Then the helloWorldService should be invoked n + 1 times
BDDMockito.then(helloWorldService).should(Mockito.times(noOfAttempts + 1)).returnHelloWorld();
Assertions.assertThat(resultTry.isFailure()).isTrue();
Assertions.assertThat(resultTry.getCause().getCause()).isInstanceOf(WebServiceException.class);
}
@Test
public void shouldCompleteFutureAfterOneAttemptInCaseOfExceptionAtAsyncStage() {
shouldCompleteFutureAfterAttemptsInCaseOfExceptionAtAsyncStage(1);
}
@Test
public void shouldCompleteFutureAfterTwoAttemptsInCaseOfExceptionAtAsyncStage() {
shouldCompleteFutureAfterAttemptsInCaseOfExceptionAtAsyncStage(2);
}
@Test
public void shouldCompleteFutureAfterThreeAttemptsInCaseOfExceptionAtAsyncStage() {
shouldCompleteFutureAfterAttemptsInCaseOfExceptionAtAsyncStage(3);
}
private void shouldCompleteFutureAfterAttemptsInCaseOfExceptionAtAsyncStage(int noOfAttempts) {
// Given the HelloWorldService throws an exception
BDDMockito.given(helloWorldService.returnHelloWorld())
.willReturn(supplyAsync(() -> { throw new WebServiceException("BAM!"); }));
// Create a Retry with default configuration
AsyncRetry retryContext = AsyncRetry.of(
"id",
RetryConfig
.custom()
.maxAttempts(noOfAttempts)
.build());
// Decorate the invocation of the HelloWorldService
Supplier<CompletionStage<String>> supplier = AsyncRetry.decorateCompletionStage(
retryContext,
scheduler,
() -> helloWorldService.returnHelloWorld());
// When
Try<String> resultTry = Try.of(() -> awaitResult(supplier.get()));
// Then the helloWorldService should be invoked n + 1 times
BDDMockito.then(helloWorldService).should(Mockito.times(noOfAttempts + 1)).returnHelloWorld();
Assertions.assertThat(resultTry.isFailure()).isTrue();
Assertions.assertThat(resultTry.getCause().getCause()).isInstanceOf(WebServiceException.class);
}
private static class RuntimeExecutionException extends RuntimeException {
RuntimeExecutionException(Throwable cause) {
super(cause);
}
}
private static <T> T awaitResult(CompletionStage<T> completionStage, long timeoutSeconds) {
try {
return completionStage.toCompletableFuture().get(timeoutSeconds, TimeUnit.SECONDS);
} catch (InterruptedException | TimeoutException e) {
throw new AssertionError(e);
} catch (ExecutionException e) {
throw new RuntimeExecutionException(e.getCause());
}
}
private static <T> T awaitResult(CompletionStage<T> completionStage) {
return awaitResult(completionStage, DEFAULT_TIMEOUT_SECONDS);
}
private static <T> T awaitResult(Supplier<CompletionStage<T>> completionStageSupplier, long timeoutSeconds) {
return awaitResult(completionStageSupplier.get(), timeoutSeconds);
}
private static <T> T awaitResult(Supplier<CompletionStage<T>> completionStageSupplier) {
return awaitResult(completionStageSupplier, DEFAULT_TIMEOUT_SECONDS);
}
}