diff --git a/retry/README.md b/retry/README.md new file mode 100644 index 000000000..e6bd1a60d --- /dev/null +++ b/retry/README.md @@ -0,0 +1,153 @@ +--- +layout: pattern +title: Retry +folder: retry +permalink: /patterns/retry/ +categories: other +tags: + - java + - difficulty-expert + - performance +--- + +## Retry / resiliency +Enables an application to handle transient failures from external resources. + +## Intent +Transparently retry certain operations that involve communication with external +resources, particularly over the network, isolating calling code from the +retry implementation details. + +![alt text](./etc/retry.png "Retry") + +## Explanation +The `Retry` pattern consists retrying operations on remote resources over the +network a set number of times. It closely depends on both business and technical +requirements: how much time will the business allow the end user to wait while +the operation finishes? What are the performance characteristics of the +remote resource during peak loads as well as our application as more threads +are waiting for the remote resource's availability? Among the errors returned +by the remote service, which can be safely ignored in order to retry? Is the +operation [idempotent](https://en.wikipedia.org/wiki/Idempotence)? + +Another concern is the impact on the calling code by implementing the retry +mechanism. The retry mechanics should ideally be completely transparent to the +calling code (service interface remains unaltered). There are two general +approaches to this problem: from an enterprise architecture standpoint +(**strategic**), and a shared library standpoint (**tactical**). + +*(As an aside, one interesting property is that, since implementations tend to +be configurable at runtime, daily monitoring and operation of this capability +is shifted over to operations support instead of the developers themselves.)* + +From a strategic point of view, this would be solved by having requests +be redirected to a separate intermediary system, traditionally an +[ESB](https://en.wikipedia.org/wiki/Enterprise_service_bus), but more recently +a [Service Mesh](https://medium.com/microservices-in-practice/service-mesh-for-microservices-2953109a3c9a). + +From a tactical point of view, this would be solved by reusing shared libraries +like [Hystrix](https://github.com/Netflix/Hystrix)[1]. This is the type of +solution showcased in the simple example that accompanies this *README*. + +In our hypothetical application, we have a generic interface for all +operations on remote interfaces: + +```java +public interface BusinessOperation { + T perform() throws BusinessException; +} +``` + +And we have an implementation of this interface that finds our customers +by looking up a database: + +```java +public final class FindCustomer implements BusinessOperation { + @Override + public String perform() throws BusinessException { + ... + } +} +``` + +Our `FindCustomer` implementation can be configured to throw +`BusinessException`s before returning the customer's ID, thereby simulating a +'flaky' service that intermittently fails. Some exceptions, like the +`CustomerNotFoundException`, are deemed to be recoverable after some +hypothetical analysis because the root cause of the error stems from "some +database locking issue". However, the `DatabaseNotAvailableException` is +considered to be a definite showstopper - the application should not attempt +to recover from this error. + +We can model a 'recoverable' scenario by instantiating `FindCustomer` like this: + +```java +final BusinessOperation op = new FindCustomer( + "12345", + new CustomerNotFoundException("not found"), + new CustomerNotFoundException("still not found"), + new CustomerNotFoundException("don't give up yet!") +); +``` + +In this configuration, `FindCustomer` will throw `CustomerNotFoundException` +three times, after which it will consistently return the customer's ID +(`12345`). + +In our hypothetical scenario, our analysts indicate that this operation +typically fails 2-4 times for a given input during peak hours, and that each +worker thread in the database subsystem typically needs 50ms to +"recover from an error". Applying these policies would yield something like +this: + +```java +final BusinessOperation op = new Retry<>( + new FindCustomer( + "1235", + new CustomerNotFoundException("not found"), + new CustomerNotFoundException("still not found"), + new CustomerNotFoundException("don't give up yet!") + ), + 5, + 100, + e -> CustomerNotFoundException.class.isAssignableFrom(e.getClass()) +); +``` + +Executing `op` *once* would automatically trigger at most 5 retry attempts, +with a 100 millisecond delay between attempts, ignoring any +`CustomerNotFoundException` thrown while trying. In this particular scenario, +due to the configuration for `FindCustomer`, there will be 1 initial attempt +and 3 additional retries before finally returning the desired result `12345`. + +If our `FindCustomer` operation were instead to throw a fatal +`DatabaseNotFoundException`, which we were instructed not to ignore, but +more importantly we did *not* instruct our `Retry` to ignore, then the operation +would have failed immediately upon receiving the error, not matter how many +attempts were left. + +

+ +[1] Please note that *Hystrix* is a complete implementation of the *Circuit +Breaker* pattern, of which the *Retry* pattern can be considered a subset of. + +## Applicability +Whenever an application needs to communicate with an external resource, +particularly in a cloud environment, and if the business requirements allow it. + +## Presentations +You can view Microsoft's article [here](https://docs.microsoft.com/en-us/azure/architecture/patterns/retry). + +## Consequences +**Pros:** + +* Resiliency +* Provides hard data on external failures + +**Cons:** + +* Complexity +* Operations maintenance + +## Related Patterns +* [Circuit Breaker](https://martinfowler.com/bliki/CircuitBreaker.html) diff --git a/retry/etc/retry.png b/retry/etc/retry.png new file mode 100644 index 000000000..3ef6d3800 Binary files /dev/null and b/retry/etc/retry.png differ diff --git a/retry/etc/retry.urm.puml b/retry/etc/retry.urm.puml new file mode 100644 index 000000000..67f645514 --- /dev/null +++ b/retry/etc/retry.urm.puml @@ -0,0 +1,38 @@ +@startuml +package com.iluwatar.retry { + class App { + - LOG : Logger {static} + - op : BusinessOperation {static} + + App() + - errorNoRetry() {static} + - errorWithRetry() {static} + + main(args : String[]) {static} + - noErrors() {static} + } + interface BusinessOperation { + + perform() : T {abstract} + } + class FindCustomer { + - customerId : String + - errors : Deque + + FindCustomer(customerId : String, errors : BusinessException[]) + + perform() : String + } + class Retry { + - attempts : AtomicInteger + - delay : long + - errors : List + - maxAttempts : int + - op : BusinessOperation + - test : Predicate + + Retry(op : BusinessOperation, maxAttempts : int, delay : long, ignoreTests : Predicate[]) + + attempts() : int + + errors() : List + + perform() : T + } +} +Retry --> "-op" BusinessOperation +App --> "-op" BusinessOperation +FindCustomer ..|> BusinessOperation +Retry ..|> BusinessOperation +@enduml \ No newline at end of file diff --git a/retry/pom.xml b/retry/pom.xml new file mode 100644 index 000000000..8ee97a084 --- /dev/null +++ b/retry/pom.xml @@ -0,0 +1,45 @@ + + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.17.0-SNAPSHOT + + retry + jar + + + junit + junit + test + + + org.hamcrest + hamcrest-core + test + + + \ No newline at end of file diff --git a/retry/src/main/java/com/iluwatar/retry/App.java b/retry/src/main/java/com/iluwatar/retry/App.java new file mode 100644 index 000000000..d1712ae9a --- /dev/null +++ b/retry/src/main/java/com/iluwatar/retry/App.java @@ -0,0 +1,107 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2014-2016 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.iluwatar.retry; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The Retry pattern enables applications to handle potentially recoverable failures from + * the environment if the business requirements and nature of the failures allow it. By retrying + * failed operations on external dependencies, the application may maintain stability and minimize + * negative impact on the user experience. + *

+ * In our example, we have the {@link BusinessOperation} interface as an abstraction over + * all operations that our application performs involving remote systems. The calling code should + * remain decoupled from implementations. + *

+ * {@link FindCustomer} is a business operation that looks up a customer's record and returns + * its ID. Imagine its job is performed by looking up the customer in our local database and + * returning its ID. We can pass {@link CustomerNotFoundException} as one of its + * {@link FindCustomer#FindCustomer(java.lang.String, com.iluwatar.retry.BusinessException...) + * constructor parameters} in order to simulate not finding the customer. + *

+ * Imagine that, lately, this operation has experienced intermittent failures due to some weird + * corruption and/or locking in the data. After retrying a few times the customer is found. The + * database is still, however, expected to always be available. While a definitive solution is + * found to the problem, our engineers advise us to retry the operation a set number + * of times with a set delay between retries, although not too many retries otherwise the end user + * will be left waiting for a long time, while delays that are too short will not allow the database + * to recover from the load. + *

+ * To keep the calling code as decoupled as possible from this workaround, we have implemented the + * retry mechanism as a {@link BusinessOperation} named {@link Retry}. + * + * @author George Aristy (george.aristy@gmail.com) + * @see Retry pattern (Microsoft Azure Docs) + * @since 1.17.0 + */ +public final class App { + private static final Logger LOG = LoggerFactory.getLogger(App.class); + private static BusinessOperation op; + + /** + * Entry point. + * + * @param args not used + * @throws Exception not expected + * @since 1.17.0 + */ + public static void main(String[] args) throws Exception { + noErrors(); + errorNoRetry(); + errorWithRetry(); + } + + 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 Retry 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 String 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() + )); + } +} diff --git a/retry/src/main/java/com/iluwatar/retry/BusinessException.java b/retry/src/main/java/com/iluwatar/retry/BusinessException.java new file mode 100644 index 000000000..b25b46204 --- /dev/null +++ b/retry/src/main/java/com/iluwatar/retry/BusinessException.java @@ -0,0 +1,48 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2014-2016 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.iluwatar.retry; + +/** + * The top-most type in our exception hierarchy that signifies that an unexpected error + * condition occurred. Its use is reserved as a "catch-all" for cases where no other subtype + * captures the specificity of the error condition in question. Calling code is not expected to + * be able to handle this error and should be reported to the maintainers immediately. + * + * @author George Aristy (george.aristy@gmail.com) + * @since 1.17.0 + */ +public class BusinessException extends Exception { + private static final long serialVersionUID = 6235833142062144336L; + + /** + * Ctor + * + * @param message the error message + * @since 1.17.0 + */ + public BusinessException(String message) { + super(message); + } +} diff --git a/retry/src/main/java/com/iluwatar/retry/BusinessOperation.java b/retry/src/main/java/com/iluwatar/retry/BusinessOperation.java new file mode 100644 index 000000000..657bf1a97 --- /dev/null +++ b/retry/src/main/java/com/iluwatar/retry/BusinessOperation.java @@ -0,0 +1,46 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2014-2016 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.iluwatar.retry; + +/** + * Performs some business operation. + * + * @author George Aristy (george.aristy@gmail.com) + * @param the return type + * @since 1.17.0 + */ +@FunctionalInterface +public interface BusinessOperation { + /** + * Performs some business operation, returning a value {@code T} if successful, otherwise throwing + * an exception if an error occurs. + * + * @return the return value + * @throws BusinessException if the operation fails. Implementations are allowed to throw more + * specific subtypes depending on the error conditions + * @since 1.17.0 + */ + T perform() throws BusinessException; +} diff --git a/retry/src/main/java/com/iluwatar/retry/CustomerNotFoundException.java b/retry/src/main/java/com/iluwatar/retry/CustomerNotFoundException.java new file mode 100644 index 000000000..7b704883f --- /dev/null +++ b/retry/src/main/java/com/iluwatar/retry/CustomerNotFoundException.java @@ -0,0 +1,48 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2014-2016 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.iluwatar.retry; + +/** + * Indicates that the customer was not found. + *

+ * The severity of this error is bounded by its context: was the search for the customer triggered + * by an input from some end user, or were the search parameters pulled from your database? + * + * @author George Aristy (george.aristy@gmail.com) + * @since 1.17.0 + */ +public final class CustomerNotFoundException extends BusinessException { + private static final long serialVersionUID = -6972888602621778664L; + + /** + * Ctor. + * + * @param message the error message + * @since 1.17.0 + */ + public CustomerNotFoundException(String message) { + super(message); + } +} diff --git a/retry/src/main/java/com/iluwatar/retry/DatabaseNotAvailableException.java b/retry/src/main/java/com/iluwatar/retry/DatabaseNotAvailableException.java new file mode 100644 index 000000000..ad57d99e0 --- /dev/null +++ b/retry/src/main/java/com/iluwatar/retry/DatabaseNotAvailableException.java @@ -0,0 +1,45 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2014-2016 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.iluwatar.retry; + +/** + * Catastrophic error indicating that we have lost connection to our database. + * + * @author George Aristy (george.aristy@gmail.com) + * @since 1.17.0 + */ +public final class DatabaseNotAvailableException extends BusinessException { + private static final long serialVersionUID = -3750769625095997799L; + + /** + * Ctor. + * + * @param message the error message + * @since 1.17.0 + */ + public DatabaseNotAvailableException(String message) { + super(message); + } +} diff --git a/retry/src/main/java/com/iluwatar/retry/FindCustomer.java b/retry/src/main/java/com/iluwatar/retry/FindCustomer.java new file mode 100644 index 000000000..0ca484670 --- /dev/null +++ b/retry/src/main/java/com/iluwatar/retry/FindCustomer.java @@ -0,0 +1,65 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2014-2016 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.iluwatar.retry; + +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Deque; + +/** + * Finds a customer, returning its ID from our records. + *

+ * This is an imaginary operation that, for some imagined input, returns the ID for a customer. + * However, this is a "flaky" operation that is supposed to fail intermittently, but for the + * purposes of this example it fails in a programmed way depending on the constructor parameters. + * + * @author George Aristy (george.aristy@gmail.com) + * @since 1.17.0 + */ +public final class FindCustomer implements BusinessOperation { + private final String customerId; + private final Deque errors; + + /** + * Ctor. + * + * @param customerId the final result of the remote operation + * @param errors the errors to throw before returning {@code customerId} + * @since 1.17.0 + */ + public FindCustomer(String customerId, BusinessException... errors) { + this.customerId = customerId; + this.errors = new ArrayDeque<>(Arrays.asList(errors)); + } + + @Override + public String perform() throws BusinessException { + if (!this.errors.isEmpty()) { + throw this.errors.pop(); + } + + return this.customerId; + } +} diff --git a/retry/src/main/java/com/iluwatar/retry/Retry.java b/retry/src/main/java/com/iluwatar/retry/Retry.java new file mode 100644 index 000000000..647d5f22f --- /dev/null +++ b/retry/src/main/java/com/iluwatar/retry/Retry.java @@ -0,0 +1,114 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2014-2016 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.iluwatar.retry; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; + +/** + * Decorates {@link BusinessOperation business operation} with "retry" capabilities. + * + * @author George Aristy (george.aristy@gmail.com) + * @param the remote op's return type + * @since 1.17.0 + */ +public final class Retry implements BusinessOperation { + private final BusinessOperation op; + private final int maxAttempts; + private final long delay; + private final AtomicInteger attempts; + private final Predicate test; + private final List errors; + + /** + * Ctor. + * + * @param op the {@link BusinessOperation} to retry + * @param maxAttempts number of times to retry + * @param delay delay (in milliseconds) between attempts + * @param ignoreTests tests to check whether the remote exception can be ignored. No exceptions + * will be ignored if no tests are given + * @since 1.17.0 + */ + @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<>(); + } + + /** + * The errors encountered while retrying, in the encounter order. + * + * @return the errors encountered while retrying + * @since 1.17.0 + */ + public List errors() { + return Collections.unmodifiableList(this.errors); + } + + /** + * The number of retries performed. + * + * @return the number of retries performed + * @since 1.17.0 + */ + 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); + } +} diff --git a/retry/src/test/java/com/iluwatar/retry/FindCustomerTest.java b/retry/src/test/java/com/iluwatar/retry/FindCustomerTest.java new file mode 100644 index 000000000..d93b0a943 --- /dev/null +++ b/retry/src/test/java/com/iluwatar/retry/FindCustomerTest.java @@ -0,0 +1,91 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2014-2016 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.iluwatar.retry; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.*; +import org.junit.Test; + +/** + * Unit tests for {@link FindCustomer}. + * + * @author George Aristy (george.aristy@gmail.com) + * @since 1.17.0 + */ +public class FindCustomerTest { + /** + * Returns the given result with no exceptions. + * + * @since 1.17.0 + */ + @Test + public void noExceptions() throws Exception { + assertThat( + new FindCustomer("123").perform(), + is("123") + ); + } + + /** + * Throws the given exception. + * + * @throws Exception the expected exception + * @since 1.17.0 + */ + @Test(expected = BusinessException.class) + public void oneException() throws Exception { + new FindCustomer("123", new BusinessException("test")).perform(); + } + + /** + * Should first throw the given exceptions, then return the given result. + * + * @throws Exception not an expected exception + * @since 1.17.0 + */ + @Test + public void resultAfterExceptions() throws Exception { + final BusinessOperation op = new FindCustomer( + "123", + new CustomerNotFoundException("not found"), + new DatabaseNotAvailableException("not available") + ); + try { + op.perform(); + } catch (CustomerNotFoundException e) { + //ignore + } + try { + op.perform(); + } catch (DatabaseNotAvailableException e) { + //ignore + } + + assertThat( + op.perform(), + is("123") + ); + } +} diff --git a/retry/src/test/java/com/iluwatar/retry/RetryTest.java b/retry/src/test/java/com/iluwatar/retry/RetryTest.java new file mode 100644 index 000000000..f91a66d10 --- /dev/null +++ b/retry/src/test/java/com/iluwatar/retry/RetryTest.java @@ -0,0 +1,117 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2014-2016 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.iluwatar.retry; + +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.is; +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * Unit tests for {@link Retry}. + * + * @author George Aristy (george.aristy@gmail.com) + * @since 1.17.0 + */ +public class RetryTest { + /** + * Should contain all errors thrown. + * + * @since 1.17.0 + */ + @Test + public void errors() throws Exception { + final BusinessException e = new BusinessException("unhandled"); + final Retry retry = new Retry<>( + () -> { throw e; }, + 2, + 0 + ); + try { + retry.perform(); + } catch (BusinessException ex) { + //ignore + } + + assertThat( + retry.errors(), + hasItem(e) + ); + } + + /** + * No exceptions will be ignored, hence final number of attempts should be 1 even if we're asking + * it to attempt twice. + * + * @since 1.17.0 + */ + @Test + public void attempts() { + final BusinessException e = new BusinessException("unhandled"); + final Retry retry = new Retry<>( + () -> { throw e; }, + 2, + 0 + ); + try { + retry.perform(); + } catch (BusinessException ex) { + //ignore + } + + assertThat( + retry.attempts(), + is(1) + ); + } + + /** + * Final number of attempts should be equal to the number of attempts asked because we are + * asking it to ignore the exception that will be thrown. + * + * @since 1.17.0 + */ + @Test + public void ignore() throws Exception { + final BusinessException e = new CustomerNotFoundException("customer not found"); + final Retry retry = new Retry<>( + () -> { throw e; }, + 2, + 0, + ex -> CustomerNotFoundException.class.isAssignableFrom(ex.getClass()) + ); + try { + retry.perform(); + } catch (BusinessException ex) { + //ignore + } + + assertThat( + retry.attempts(), + is(2) + ); + } + +} \ No newline at end of file