From f7c396b0fdcb222cb055a36c38d77b6f07691f68 Mon Sep 17 00:00:00 2001 From: George Aristy Date: Wed, 27 Dec 2017 15:15:04 -0400 Subject: [PATCH] (NEW) Module "retry" (NEW) Illustrative classes: - App: simulates a production application - BusinessOperation: abstraction over any operation that can potentially fail - FindCustomer <: BusinessOperation: illustrative operation that can throw an error - Retry <: BusinessOperation: transparently implements the retry mechanism - Several "business" exceptions: - BusinessException: top-level - CustomerNotFoundException: can be ignored - DatabaseNotAvailableException: fatal error (NEW) .puml and .png for UML --- retry/README.md | 153 ++++++++++++++++++ retry/etc/retry.png | Bin 0 -> 52004 bytes retry/etc/retry.urm.puml | 38 +++++ retry/pom.xml | 45 ++++++ .../src/main/java/com/iluwatar/retry/App.java | 107 ++++++++++++ .../com/iluwatar/retry/BusinessException.java | 48 ++++++ .../com/iluwatar/retry/BusinessOperation.java | 46 ++++++ .../retry/CustomerNotFoundException.java | 48 ++++++ .../retry/DatabaseNotAvailableException.java | 45 ++++++ .../java/com/iluwatar/retry/FindCustomer.java | 65 ++++++++ .../main/java/com/iluwatar/retry/Retry.java | 114 +++++++++++++ .../com/iluwatar/retry/FindCustomerTest.java | 91 +++++++++++ .../java/com/iluwatar/retry/RetryTest.java | 117 ++++++++++++++ 13 files changed, 917 insertions(+) create mode 100644 retry/README.md create mode 100644 retry/etc/retry.png create mode 100644 retry/etc/retry.urm.puml create mode 100644 retry/pom.xml create mode 100644 retry/src/main/java/com/iluwatar/retry/App.java create mode 100644 retry/src/main/java/com/iluwatar/retry/BusinessException.java create mode 100644 retry/src/main/java/com/iluwatar/retry/BusinessOperation.java create mode 100644 retry/src/main/java/com/iluwatar/retry/CustomerNotFoundException.java create mode 100644 retry/src/main/java/com/iluwatar/retry/DatabaseNotAvailableException.java create mode 100644 retry/src/main/java/com/iluwatar/retry/FindCustomer.java create mode 100644 retry/src/main/java/com/iluwatar/retry/Retry.java create mode 100644 retry/src/test/java/com/iluwatar/retry/FindCustomerTest.java create mode 100644 retry/src/test/java/com/iluwatar/retry/RetryTest.java 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 0000000000000000000000000000000000000000..3ef6d3800ee5553d1e4a763b117595b1963cc51e GIT binary patch literal 52004 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 {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