(NEW) Module "retry"
(NEW) Illustrative classes: - App: simulates a production application - BusinessOperation<T>: abstraction over any operation that can potentially fail - FindCustomer <: BusinessOperation<String>: illustrative operation that can throw an error - Retry <: BusinessOperation<T>: transparently implements the retry mechanism - Several "business" exceptions: - BusinessException: top-level - CustomerNotFoundException: can be ignored - DatabaseNotAvailableException: fatal error (NEW) .puml and .png for UML
This commit is contained in:
parent
4e290416df
commit
f7c396b0fd
153
retry/README.md
Normal file
153
retry/README.md
Normal file
@ -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.
|
||||
|
||||

|
||||
|
||||
## 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> {
|
||||
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<String> {
|
||||
@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<String> 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<String> 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.
|
||||
|
||||
<br/><br/>
|
||||
|
||||
[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)
|
BIN
retry/etc/retry.png
Normal file
BIN
retry/etc/retry.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 51 KiB |
38
retry/etc/retry.urm.puml
Normal file
38
retry/etc/retry.urm.puml
Normal file
@ -0,0 +1,38 @@
|
||||
@startuml
|
||||
package com.iluwatar.retry {
|
||||
class App {
|
||||
- LOG : Logger {static}
|
||||
- op : BusinessOperation<String> {static}
|
||||
+ App()
|
||||
- errorNoRetry() {static}
|
||||
- errorWithRetry() {static}
|
||||
+ main(args : String[]) {static}
|
||||
- noErrors() {static}
|
||||
}
|
||||
interface BusinessOperation<T> {
|
||||
+ perform() : T {abstract}
|
||||
}
|
||||
class FindCustomer {
|
||||
- customerId : String
|
||||
- errors : Deque<BusinessException>
|
||||
+ FindCustomer(customerId : String, errors : BusinessException[])
|
||||
+ perform() : String
|
||||
}
|
||||
class Retry<T> {
|
||||
- attempts : AtomicInteger
|
||||
- delay : long
|
||||
- errors : List<Exception>
|
||||
- maxAttempts : int
|
||||
- op : BusinessOperation<T>
|
||||
- test : Predicate<Exception>
|
||||
+ Retry<T>(op : BusinessOperation<T>, maxAttempts : int, delay : long, ignoreTests : Predicate<Exception>[])
|
||||
+ attempts() : int
|
||||
+ errors() : List<Exception>
|
||||
+ perform() : T
|
||||
}
|
||||
}
|
||||
Retry --> "-op" BusinessOperation
|
||||
App --> "-op" BusinessOperation
|
||||
FindCustomer ..|> BusinessOperation
|
||||
Retry ..|> BusinessOperation
|
||||
@enduml
|
45
retry/pom.xml
Normal file
45
retry/pom.xml
Normal file
@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
The MIT License
|
||||
Copyright (c) 2014 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.
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.iluwatar</groupId>
|
||||
<artifactId>java-design-patterns</artifactId>
|
||||
<version>1.17.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>retry</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hamcrest</groupId>
|
||||
<artifactId>hamcrest-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
107
retry/src/main/java/com/iluwatar/retry/App.java
Normal file
107
retry/src/main/java/com/iluwatar/retry/App.java
Normal file
@ -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 <em>Retry</em> 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* {@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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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 <a href="https://docs.microsoft.com/en-us/azure/architecture/patterns/retry">Retry pattern (Microsoft Azure Docs)</a>
|
||||
* @since 1.17.0
|
||||
*/
|
||||
public final class App {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(App.class);
|
||||
private static BusinessOperation<String> 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<String> 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()
|
||||
));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 <T> the return type
|
||||
* @since 1.17.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface BusinessOperation<T> {
|
||||
/**
|
||||
* 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;
|
||||
}
|
@ -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.
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
65
retry/src/main/java/com/iluwatar/retry/FindCustomer.java
Normal file
65
retry/src/main/java/com/iluwatar/retry/FindCustomer.java
Normal file
@ -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.
|
||||
* <p>
|
||||
* 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<String> {
|
||||
private final String customerId;
|
||||
private final Deque<BusinessException> 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;
|
||||
}
|
||||
}
|
114
retry/src/main/java/com/iluwatar/retry/Retry.java
Normal file
114
retry/src/main/java/com/iluwatar/retry/Retry.java
Normal file
@ -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 <T> the remote op's return type
|
||||
* @since 1.17.0
|
||||
*/
|
||||
public final class Retry<T> implements BusinessOperation<T> {
|
||||
private final BusinessOperation<T> op;
|
||||
private final int maxAttempts;
|
||||
private final long delay;
|
||||
private final AtomicInteger attempts;
|
||||
private final Predicate<Exception> test;
|
||||
private final List<Exception> 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<T> op,
|
||||
int maxAttempts,
|
||||
long delay,
|
||||
Predicate<Exception>... 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<Exception> 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);
|
||||
}
|
||||
}
|
91
retry/src/test/java/com/iluwatar/retry/FindCustomerTest.java
Normal file
91
retry/src/test/java/com/iluwatar/retry/FindCustomerTest.java
Normal file
@ -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<String> 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")
|
||||
);
|
||||
}
|
||||
}
|
117
retry/src/test/java/com/iluwatar/retry/RetryTest.java
Normal file
117
retry/src/test/java/com/iluwatar/retry/RetryTest.java
Normal file
@ -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<String> 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<String> 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<String> 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)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user