Skip to content

Commit

Permalink
Adds more retry examples and docs
Browse files Browse the repository at this point in the history
  • Loading branch information
franciscoengenheiro committed Mar 29, 2024
1 parent af240ff commit d1e8583
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 9 deletions.
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
95 changes: 94 additions & 1 deletion resilience4j/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,97 @@
## Retry

TODO()
### Configuration

<table>
<tr>
<th>Config property</th>
<th>Default value</th>
<th>Description</th>
</tr>
<tr>
<td>maxAttempts</td>
<td><code>3</code></td>
<td>The maximum number of attempts (including the initial call as the first attempt).</td>
</tr>
<tr>
<td>waitDuration</td>
<td><code>500 [ms]</code></td>
<td>A fixed wait duration between retry attempts.</td>
<tr>
<td>intervalFunction</td>
<td><code>numOfAttempts -> waitDuration</code></td>
<td>A function to modify the waiting interval after a failure. By default the wait duration remains constant.</td>
</tr>
<tr>
<td>intervalBiFunction</td>
<td><code>(numOfAttempts, Either&ltthrowable, result&lt) -> waitDuration</code></td>
<td>A function to modify the waiting interval after a failure based on attempt number and a result or exception.</td>
</tr>
<tr>
<td>retryOnResultPredicate</td>
<td><code>result -> false</code></td>
<td>Configures a <code>Predicate</code> which evaluates if a result should be retried. The <code>Predicate</code> must return <code>true</code>, if the result should be retried, otherwise it must return <code>false</code>.</td>
</tr>
<tr>
<td>retryExceptions</td>
<td><code>empty</code></td>
<td>Configures a list of <code>Throwable</code> classes that are recorded as a failure and thus are retried. This parameter supports subtyping.</td>
</tr>
<tr>
<td>ignoreExceptions</td>
<td><code>empty</code></td>
<td>Configures a list of <code>Throwable</code> classes that are ignored and thus are not retried. This parameter supports subtyping.</td>
</tr>
<tr>
<td>failAfterMaxAttempts</td>
<td><code>false</code></td>
<td>A boolean to enable or disable throwing of <code>MaxRetriesExceededException</code> when the <code>Retry</code> has reached the configured <code>maxAttempts</code>, and the result is still not passing the <code>retryOnResultPredicate</code></td>
</tr>
</table>

From: [Retry](https://resilience4j.readme.io/docs/retry#create-and-configure-retry)

> [!IMPORTANT]
> In the <code>Retry</code> configuration the <code>intervalFunction</code> and <code>intervalBiFunction</code> are
> mutually exclusive.
> If both are set it will throw an <code>IllegalStateException</code>.
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
2 changes: 1 addition & 1 deletion resilience4j/src/main/java/RemoteService.java
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@FunctionalInterface
public interface RemoteService {
int process(int i);
String message();
}
7 changes: 7 additions & 0 deletions resilience4j/src/main/java/exceptions/NetworkException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package exceptions;

public final class NetworkException extends RemoteServiceException {
public NetworkException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}

7 changes: 7 additions & 0 deletions resilience4j/src/main/java/exceptions/TimeoutException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package exceptions;

public final class TimeoutException extends RemoteServiceException {
public TimeoutException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package exceptions;

public final class WebServiceException extends RemoteServiceException {
public WebServiceException(String message) {
super(message);
}
}
72 changes: 66 additions & 6 deletions resilience4j/src/test/java/RetryServiceTest.java
Original file line number Diff line number Diff line change
@@ -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<Integer, Void> 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<String> retryableSupplier = Retry
.decorateCheckedSupplier(retry, remoteService::message);

// and: a callback to recover from the exception
Callable<String> callable = () -> {
try {
return retryableSupplier.get();
} catch (Throwable e) {
throw new RuntimeException(e);
}
};
String recoveryMessage = "Hello world from recovery function";
Callable<String> recoveredCallable = recover(callable, (Throwable t) -> recoveryMessage);
Try<String> 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);
}
}

0 comments on commit d1e8583

Please sign in to comment.