From d1e8583922bf4f9579d8a8c931c950da741181f2 Mon Sep 17 00:00:00 2001 From: Francisco Engenheiro Date: Fri, 29 Mar 2024 17:44:49 +0000 Subject: [PATCH] Adds more retry examples and docs --- gradle/libs.versions.toml | 2 +- resilience4j/README.md | 95 ++++++++++++++++++- resilience4j/src/main/java/RemoteService.java | 2 +- .../java/exceptions/NetworkException.java | 7 ++ .../exceptions/RemoteServiceException.java | 9 ++ .../java/exceptions/TimeoutException.java | 7 ++ .../java/exceptions/WebServiceException.java | 7 ++ .../src/test/java/RetryServiceTest.java | 72 ++++++++++++-- 8 files changed, 192 insertions(+), 9 deletions(-) create mode 100644 resilience4j/src/main/java/exceptions/NetworkException.java create mode 100644 resilience4j/src/main/java/exceptions/RemoteServiceException.java create mode 100644 resilience4j/src/main/java/exceptions/TimeoutException.java create mode 100644 resilience4j/src/main/java/exceptions/WebServiceException.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8570c9a..e4a0a41 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ compose-compiler = "1.5.4" compose-material3 = "1.2.1" androidx-activityCompose = "1.8.2" kotlinx-serialization = "1.6.3" -junit = "5.9.1" +junit = "5.9.3" resilience4j = "2.2.0" [libraries] diff --git a/resilience4j/README.md b/resilience4j/README.md index 6f0112c..5431f7d 100644 --- a/resilience4j/README.md +++ b/resilience4j/README.md @@ -5,4 +5,97 @@ ## Retry -TODO() \ No newline at end of file +### Configuration + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Config propertyDefault valueDescription
maxAttempts3The maximum number of attempts (including the initial call as the first attempt).
waitDuration500 [ms]A fixed wait duration between retry attempts.
intervalFunctionnumOfAttempts -> waitDurationA function to modify the waiting interval after a failure. By default the wait duration remains constant.
intervalBiFunction(numOfAttempts, Either<throwable, result<) -> waitDurationA function to modify the waiting interval after a failure based on attempt number and a result or exception.
retryOnResultPredicateresult -> falseConfigures a Predicate which evaluates if a result should be retried. The Predicate must return true, if the result should be retried, otherwise it must return false.
retryExceptionsemptyConfigures a list of Throwable classes that are recorded as a failure and thus are retried. This parameter supports subtyping.
ignoreExceptionsemptyConfigures a list of Throwable classes that are ignored and thus are not retried. This parameter supports subtyping.
failAfterMaxAttemptsfalseA boolean to enable or disable throwing of MaxRetriesExceededException when the Retry has reached the configured maxAttempts, and the result is still not passing the retryOnResultPredicate
+ +From: [Retry](https://resilience4j.readme.io/docs/retry#create-and-configure-retry) + +> [!IMPORTANT] +> In the Retry configuration the intervalFunction and intervalBiFunction are +> mutually exclusive. +> If both are set it will throw an IllegalStateException. + +The configuration uses the [builder](https://en.wikipedia.org/wiki/Builder_pattern) pattern to create a `RetryConfig` instance. + +```java +RetryConfig config = RetryConfig.custom() + .maxAttempts(2) + .waitDuration(Duration.ofMillis(1000)) + .retryOnResult(response -> response.getStatus() == 500) + .retryOnException(e -> e instanceof WebServiceException) + .retryExceptions(IOException.class, TimeoutException.class) + .ignoreExceptions(BusinessException.class, OtherBusinessException.class) + .failAfterMaxAttempts(true) + .build(); +``` + +Or using default configuration: + +```java +RetryConfig config = RetryConfig.ofDefaults(); +``` + +### Register + +Register a `Retry` instance in the `RetryRegistry` with a configuration. + +```java +RetryRegistry registry = RetryRegistry.of(config); +Retry retry = registry.retry("name"); +``` + +Or: + +```java +Retry retry = Retry.of("name", config); +``` + +### Events + +### Interval Functions \ No newline at end of file diff --git a/resilience4j/src/main/java/RemoteService.java b/resilience4j/src/main/java/RemoteService.java index b5e524b..d44ca5d 100644 --- a/resilience4j/src/main/java/RemoteService.java +++ b/resilience4j/src/main/java/RemoteService.java @@ -1,4 +1,4 @@ -@FunctionalInterface public interface RemoteService { int process(int i); + String message(); } diff --git a/resilience4j/src/main/java/exceptions/NetworkException.java b/resilience4j/src/main/java/exceptions/NetworkException.java new file mode 100644 index 0000000..d86edc9 --- /dev/null +++ b/resilience4j/src/main/java/exceptions/NetworkException.java @@ -0,0 +1,7 @@ +package exceptions; + +public final class NetworkException extends RemoteServiceException { + public NetworkException(String message) { + super(message); + } +} diff --git a/resilience4j/src/main/java/exceptions/RemoteServiceException.java b/resilience4j/src/main/java/exceptions/RemoteServiceException.java new file mode 100644 index 0000000..7cf8fe1 --- /dev/null +++ b/resilience4j/src/main/java/exceptions/RemoteServiceException.java @@ -0,0 +1,9 @@ +package exceptions; + +// TODO: Why cant it extend only Exception? +public sealed class RemoteServiceException extends RuntimeException permits WebServiceException, TimeoutException, NetworkException { + public RemoteServiceException(String message) { + super(message); + } +} + diff --git a/resilience4j/src/main/java/exceptions/TimeoutException.java b/resilience4j/src/main/java/exceptions/TimeoutException.java new file mode 100644 index 0000000..bf4157e --- /dev/null +++ b/resilience4j/src/main/java/exceptions/TimeoutException.java @@ -0,0 +1,7 @@ +package exceptions; + +public final class TimeoutException extends RemoteServiceException { + public TimeoutException(String message) { + super(message); + } +} diff --git a/resilience4j/src/main/java/exceptions/WebServiceException.java b/resilience4j/src/main/java/exceptions/WebServiceException.java new file mode 100644 index 0000000..fbee569 --- /dev/null +++ b/resilience4j/src/main/java/exceptions/WebServiceException.java @@ -0,0 +1,7 @@ +package exceptions; + +public final class WebServiceException extends RemoteServiceException { + public WebServiceException(String message) { + super(message); + } +} diff --git a/resilience4j/src/test/java/RetryServiceTest.java b/resilience4j/src/test/java/RetryServiceTest.java index efe6cc9..8439a88 100644 --- a/resilience4j/src/test/java/RetryServiceTest.java +++ b/resilience4j/src/test/java/RetryServiceTest.java @@ -1,39 +1,99 @@ +import exceptions.NetworkException; +import exceptions.RemoteServiceException; +import exceptions.WebServiceException; +import io.github.resilience4j.core.functions.CheckedSupplier; import io.github.resilience4j.retry.Retry; import io.github.resilience4j.retry.RetryConfig; import io.github.resilience4j.retry.RetryRegistry; import org.junit.jupiter.api.Test; +import org.junit.platform.commons.function.Try; + +import java.time.Duration; +import java.util.concurrent.Callable; import java.util.function.Function; + +import static io.github.resilience4j.core.CallableUtils.recover; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.*; public class RetryServiceTest { @Test - public void testRetryService() { + public void decoratesAFunctionalInterfaceWithCustomConfig() { + // given: a remote service RemoteService service = mock(RemoteService.class); - int maxAttempts = 2; + + // and: a retry configuration + int maxAttempts = 10; RetryConfig config = RetryConfig .custom() .maxAttempts(maxAttempts) - // .retryExceptions(RuntimeException.class) - .waitDuration(java.time.Duration.ofMillis(100)) + // .retryExceptions(RemoteServiceException.class) + .retryExceptions(RemoteServiceException.class) + .waitDuration(Duration.ofMillis(100)) .build(); + + // when: the retry is registered RetryRegistry registry = RetryRegistry.of(config); Retry retry = registry.retry("myRetry"); - retry.getEventPublisher().onRetry(event -> System.out.println("Retried an event: , " + event)); + + // and: the service is decorated with the retry mechanism Function decorated = Retry.decorateFunction(retry, (Integer s) -> { service.process(s); return null; }); - when(service.process(anyInt())).thenThrow(new RuntimeException()); + // given: a remote service configuration to always throw an exception to simulate a failure + when(service.process(anyInt())) + .thenThrow(new WebServiceException("BAM!")); try { + + // when: the service is called decorated.apply(1); fail("Expected an exception to be thrown if all retries failed"); } catch (Exception e) { + // then: it should be retried the maximum number of times specified in the retry configuration verify(service, times(maxAttempts)).process(anyInt()); } } + @Test + public void decoratesASupplierWithDefaultConfig() throws Exception { + // given: a remote service + RemoteService remoteService = mock(RemoteService.class); + + // and: a retry with default configuration + Retry retry = Retry.ofDefaults("id"); + + // when: the service is decorated with the retry mechanism using a supplier + CheckedSupplier retryableSupplier = Retry + .decorateCheckedSupplier(retry, remoteService::message); + + // and: a callback to recover from the exception + Callable callable = () -> { + try { + return retryableSupplier.get(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + }; + String recoveryMessage = "Hello world from recovery function"; + Callable recoveredCallable = recover(callable, (Throwable t) -> recoveryMessage); + Try result = Try.call(recoveredCallable); + + // when: the service is called and throws an exception + when(remoteService.message()) + .thenThrow(new NetworkException("Thanks Vodafone!")); + + // then: the service should be retried the default number of times + then(remoteService) + .should(times(3)) + .message(); + + // and the exception should be handled by the recovery function + assertEquals(result.get(), recoveryMessage); + } }