From 2411c9caac5c4f8abf37df6c8a61a614d432c76e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ilkka=20Sepp=C3=A4l=C3=A4?= Date: Mon, 27 May 2024 10:16:09 +0300 Subject: [PATCH] docs: update retry --- retry/README.md | 158 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 123 insertions(+), 35 deletions(-) diff --git a/retry/README.md b/retry/README.md index 1c0dfaf90..92d701bd3 100644 --- a/retry/README.md +++ b/retry/README.md @@ -20,7 +20,7 @@ Transparently retry certain operations that involve communication with external ## Explanation -Real world example +Real-world example > Imagine you're a delivery driver attempting to deliver a package to a customer's house. You ring the doorbell, but no one answers. Instead of leaving immediately, you wait for a few minutes and try again, repeating this process a few times. This is similar to the Retry pattern in software, where a system retries a failed operation (e.g., making a network request) a certain number of times before finally giving up, in hopes that the issue (e.g., transient network glitch) will be resolved and the operation will succeed. @@ -59,48 +59,136 @@ The `Retry` class is where the Retry pattern is implemented. It takes a `Busines ```java public final class Retry implements BusinessOperation { - private final BusinessOperation operation; - private final int maxAttempts; - private final long delay; - private final Predicate isRecoverable; + + private final BusinessOperation op; + private final int maxAttempts; + private final long delay; + private final AtomicInteger attempts; + private final Predicate test; + private final List errors; - public Retry(BusinessOperation operation, int maxAttempts, long delay, Predicate isRecoverable) { - this.operation = operation; - this.maxAttempts = maxAttempts; - this.delay = delay; - this.isRecoverable = isRecoverable; - } - - @Override - public T perform() throws BusinessException { - for (int attempt = 0; attempt < maxAttempts; attempt++) { - try { - return operation.perform(); - } catch (Exception e) { - if (!isRecoverable.test(e) || attempt == maxAttempts - 1) { - throw e; - } - try { - Thread.sleep(delay); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - throw new BusinessException("Retry operation was interrupted", ie); - } - } + @SafeVarargs + public Retry( + BusinessOperation op, + int maxAttempts, + long delay, + Predicate... ignoreTests + ) { + this.op = op; + this.maxAttempts = maxAttempts; + this.delay = delay; + this.attempts = new AtomicInteger(); + this.test = Arrays.stream(ignoreTests).reduce(Predicate::or).orElse(e -> false); + this.errors = new ArrayList<>(); + } + + public List errors() { + return Collections.unmodifiableList(this.errors); + } + + public int attempts() { + return this.attempts.intValue(); + } + + @Override + public T perform() throws BusinessException { + do { + try { + return this.op.perform(); + } catch (BusinessException e) { + this.errors.add(e); + + if (this.attempts.incrementAndGet() >= this.maxAttempts || !this.test.test(e)) { + throw e; + } + + try { + Thread.sleep(this.delay); + } catch (InterruptedException f) { + //ignore + } + } + } while (true); } - throw new BusinessException("Retry attempts exceeded"); - } } ``` In this class, the `perform` method tries to perform the operation. If the operation throws an exception, it checks if the exception is recoverable and if the maximum number of attempts has not been reached. If both conditions are true, it waits for a specified delay and then tries again. If the exception is not recoverable or the maximum number of attempts has been reached, it rethrows the exception. +Finally, here is the `App` class driving the retry pattern example. + +```java +public final class App { + + private static final Logger LOG = LoggerFactory.getLogger(App.class); + public static final String NOT_FOUND = "not found"; + private static BusinessOperation op; + + public static void main(String[] args) throws Exception { + noErrors(); + errorNoRetry(); + errorWithRetry(); + errorWithRetryExponentialBackoff(); + } + + private static void noErrors() throws Exception { + op = new FindCustomer("123"); + op.perform(); + LOG.info("Sometimes the operation executes with no errors."); + } + + private static void errorNoRetry() throws Exception { + op = new FindCustomer("123", new CustomerNotFoundException(NOT_FOUND)); + try { + op.perform(); + } catch (CustomerNotFoundException e) { + LOG.info("Yet the operation will throw an error every once in a while."); + } + } + + private static void errorWithRetry() throws Exception { + final var retry = new Retry<>( + new FindCustomer("123", new CustomerNotFoundException(NOT_FOUND)), + 3, //3 attempts + 100, //100 ms delay between attempts + e -> CustomerNotFoundException.class.isAssignableFrom(e.getClass()) + ); + op = retry; + final var customerId = op.perform(); + LOG.info(String.format( + "However, retrying the operation while ignoring a recoverable error will eventually yield " + + "the result %s after a number of attempts %s", customerId, retry.attempts() + )); + } + + private static void errorWithRetryExponentialBackoff() throws Exception { + final var retry = new RetryExponentialBackoff<>( + new FindCustomer("123", new CustomerNotFoundException(NOT_FOUND)), + 6, //6 attempts + 30000, //30 s max delay between attempts + e -> CustomerNotFoundException.class.isAssignableFrom(e.getClass()) + ); + op = retry; + final var customerId = op.perform(); + LOG.info(String.format( + "However, retrying the operation while ignoring a recoverable error will eventually yield " + + "the result %s after a number of attempts %s", customerId, retry.attempts() + )); + } +} +``` + +Running the code produces the following console output. + +``` +10:12:19.573 [main] INFO com.iluwatar.retry.App -- Sometimes the operation executes with no errors. +10:12:19.575 [main] INFO com.iluwatar.retry.App -- Yet the operation will throw an error every once in a while. +10:12:19.682 [main] INFO com.iluwatar.retry.App -- However, retrying the operation while ignoring a recoverable error will eventually yield the result 123 after a number of attempts 1 +10:12:22.297 [main] INFO com.iluwatar.retry.App -- However, retrying the operation while ignoring a recoverable error will eventually yield the result 123 after a number of attempts 1 +``` + This way, the Retry pattern allows the application to handle temporary failures gracefully, improving its resilience and reliability. -## Class diagram - -![Retry](./etc/retry.png "Retry") - ## Applicability * Use when operations can fail transiently, such as network calls, database connections, or external service integrations. @@ -135,4 +223,4 @@ Trade-offs: * [Design Patterns: Elements of Reusable Object-Oriented Software](https://amzn.to/3w0pvKI) * [Java Concurrency in Practice](https://amzn.to/4aRMruW) * [Release It!: Design and Deploy Production-Ready Software](https://amzn.to/3UPwmPh) -* [Retry pattern - Microsoft](https://docs.microsoft.com/en-us/azure/architecture/patterns/retry) +* [Retry pattern (Microsoft)](https://docs.microsoft.com/en-us/azure/architecture/patterns/retry)