Skip to content

Commit 3defc1b

Browse files
Merge remote-tracking branch 'upstream/master'
2 parents 9677cc4 + 0824529 commit 3defc1b

13 files changed

+852
-0
lines changed

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@
151151
<module>eip-wire-tap</module>
152152
<module>eip-splitter</module>
153153
<module>eip-aggregator</module>
154+
<module>retry</module>
154155
</modules>
155156

156157
<dependencyManagement>

retry/README.md

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
---
2+
layout: pattern
3+
title: Retry
4+
folder: retry
5+
permalink: /patterns/retry/
6+
categories: other
7+
tags:
8+
- java
9+
- difficulty-expert
10+
- performance
11+
---
12+
13+
## Retry / resiliency
14+
Enables an application to handle transient failures from external resources.
15+
16+
## Intent
17+
Transparently retry certain operations that involve communication with external
18+
resources, particularly over the network, isolating calling code from the
19+
retry implementation details.
20+
21+
![alt text](./etc/retry.png "Retry")
22+
23+
## Explanation
24+
The `Retry` pattern consists retrying operations on remote resources over the
25+
network a set number of times. It closely depends on both business and technical
26+
requirements: how much time will the business allow the end user to wait while
27+
the operation finishes? What are the performance characteristics of the
28+
remote resource during peak loads as well as our application as more threads
29+
are waiting for the remote resource's availability? Among the errors returned
30+
by the remote service, which can be safely ignored in order to retry? Is the
31+
operation [idempotent](https://en.wikipedia.org/wiki/Idempotence)?
32+
33+
Another concern is the impact on the calling code by implementing the retry
34+
mechanism. The retry mechanics should ideally be completely transparent to the
35+
calling code (service interface remains unaltered). There are two general
36+
approaches to this problem: from an enterprise architecture standpoint
37+
(**strategic**), and a shared library standpoint (**tactical**).
38+
39+
*(As an aside, one interesting property is that, since implementations tend to
40+
be configurable at runtime, daily monitoring and operation of this capability
41+
is shifted over to operations support instead of the developers themselves.)*
42+
43+
From a strategic point of view, this would be solved by having requests
44+
be redirected to a separate intermediary system, traditionally an
45+
[ESB](https://en.wikipedia.org/wiki/Enterprise_service_bus), but more recently
46+
a [Service Mesh](https://medium.com/microservices-in-practice/service-mesh-for-microservices-2953109a3c9a).
47+
48+
From a tactical point of view, this would be solved by reusing shared libraries
49+
like [Hystrix](https://github.com/Netflix/Hystrix)[1]. This is the type of
50+
solution showcased in the simple example that accompanies this *README*.
51+
52+
In our hypothetical application, we have a generic interface for all
53+
operations on remote interfaces:
54+
55+
```java
56+
public interface BusinessOperation<T> {
57+
T perform() throws BusinessException;
58+
}
59+
```
60+
61+
And we have an implementation of this interface that finds our customers
62+
by looking up a database:
63+
64+
```java
65+
public final class FindCustomer implements BusinessOperation<String> {
66+
@Override
67+
public String perform() throws BusinessException {
68+
...
69+
}
70+
}
71+
```
72+
73+
Our `FindCustomer` implementation can be configured to throw
74+
`BusinessException`s before returning the customer's ID, thereby simulating a
75+
'flaky' service that intermittently fails. Some exceptions, like the
76+
`CustomerNotFoundException`, are deemed to be recoverable after some
77+
hypothetical analysis because the root cause of the error stems from "some
78+
database locking issue". However, the `DatabaseNotAvailableException` is
79+
considered to be a definite showstopper - the application should not attempt
80+
to recover from this error.
81+
82+
We can model a 'recoverable' scenario by instantiating `FindCustomer` like this:
83+
84+
```java
85+
final BusinessOperation<String> op = new FindCustomer(
86+
"12345",
87+
new CustomerNotFoundException("not found"),
88+
new CustomerNotFoundException("still not found"),
89+
new CustomerNotFoundException("don't give up yet!")
90+
);
91+
```
92+
93+
In this configuration, `FindCustomer` will throw `CustomerNotFoundException`
94+
three times, after which it will consistently return the customer's ID
95+
(`12345`).
96+
97+
In our hypothetical scenario, our analysts indicate that this operation
98+
typically fails 2-4 times for a given input during peak hours, and that each
99+
worker thread in the database subsystem typically needs 50ms to
100+
"recover from an error". Applying these policies would yield something like
101+
this:
102+
103+
```java
104+
final BusinessOperation<String> op = new Retry<>(
105+
new FindCustomer(
106+
"1235",
107+
new CustomerNotFoundException("not found"),
108+
new CustomerNotFoundException("still not found"),
109+
new CustomerNotFoundException("don't give up yet!")
110+
),
111+
5,
112+
100,
113+
e -> CustomerNotFoundException.class.isAssignableFrom(e.getClass())
114+
);
115+
```
116+
117+
Executing `op` *once* would automatically trigger at most 5 retry attempts,
118+
with a 100 millisecond delay between attempts, ignoring any
119+
`CustomerNotFoundException` thrown while trying. In this particular scenario,
120+
due to the configuration for `FindCustomer`, there will be 1 initial attempt
121+
and 3 additional retries before finally returning the desired result `12345`.
122+
123+
If our `FindCustomer` operation were instead to throw a fatal
124+
`DatabaseNotFoundException`, which we were instructed not to ignore, but
125+
more importantly we did *not* instruct our `Retry` to ignore, then the operation
126+
would have failed immediately upon receiving the error, not matter how many
127+
attempts were left.
128+
129+
<br/><br/>
130+
131+
[1] Please note that *Hystrix* is a complete implementation of the *Circuit
132+
Breaker* pattern, of which the *Retry* pattern can be considered a subset of.
133+
134+
## Applicability
135+
Whenever an application needs to communicate with an external resource,
136+
particularly in a cloud environment, and if the business requirements allow it.
137+
138+
## Presentations
139+
You can view Microsoft's article [here](https://docs.microsoft.com/en-us/azure/architecture/patterns/retry).
140+
141+
## Consequences
142+
**Pros:**
143+
144+
* Resiliency
145+
* Provides hard data on external failures
146+
147+
**Cons:**
148+
149+
* Complexity
150+
* Operations maintenance
151+
152+
## Related Patterns
153+
* [Circuit Breaker](https://martinfowler.com/bliki/CircuitBreaker.html)

retry/etc/retry.png

50.8 KB
Loading

retry/pom.xml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
The MIT License
4+
Copyright (c) 2014 Ilkka Seppälä
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
THE SOFTWARE.
23+
-->
24+
<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">
25+
<modelVersion>4.0.0</modelVersion>
26+
<parent>
27+
<groupId>com.iluwatar</groupId>
28+
<artifactId>java-design-patterns</artifactId>
29+
<version>1.18.0-SNAPSHOT</version>
30+
</parent>
31+
<artifactId>retry</artifactId>
32+
<packaging>jar</packaging>
33+
<dependencies>
34+
<dependency>
35+
<groupId>junit</groupId>
36+
<artifactId>junit</artifactId>
37+
<scope>test</scope>
38+
</dependency>
39+
<dependency>
40+
<groupId>org.hamcrest</groupId>
41+
<artifactId>hamcrest-core</artifactId>
42+
<scope>test</scope>
43+
</dependency>
44+
</dependencies>
45+
</project>
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* The MIT License (MIT)
3+
*
4+
* Copyright (c) 2014-2016 Ilkka Seppälä
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
package com.iluwatar.retry;
26+
27+
import org.slf4j.Logger;
28+
import org.slf4j.LoggerFactory;
29+
30+
/**
31+
* The <em>Retry</em> pattern enables applications to handle potentially recoverable failures from
32+
* the environment if the business requirements and nature of the failures allow it. By retrying
33+
* failed operations on external dependencies, the application may maintain stability and minimize
34+
* negative impact on the user experience.
35+
* <p>
36+
* In our example, we have the {@link BusinessOperation} interface as an abstraction over
37+
* all operations that our application performs involving remote systems. The calling code should
38+
* remain decoupled from implementations.
39+
* <p>
40+
* {@link FindCustomer} is a business operation that looks up a customer's record and returns
41+
* its ID. Imagine its job is performed by looking up the customer in our local database and
42+
* returning its ID. We can pass {@link CustomerNotFoundException} as one of its
43+
* {@link FindCustomer#FindCustomer(java.lang.String, com.iluwatar.retry.BusinessException...)
44+
* constructor parameters} in order to simulate not finding the customer.
45+
* <p>
46+
* Imagine that, lately, this operation has experienced intermittent failures due to some weird
47+
* corruption and/or locking in the data. After retrying a few times the customer is found. The
48+
* database is still, however, expected to always be available. While a definitive solution is
49+
* found to the problem, our engineers advise us to retry the operation a set number
50+
* of times with a set delay between retries, although not too many retries otherwise the end user
51+
* will be left waiting for a long time, while delays that are too short will not allow the database
52+
* to recover from the load.
53+
* <p>
54+
* To keep the calling code as decoupled as possible from this workaround, we have implemented the
55+
* retry mechanism as a {@link BusinessOperation} named {@link Retry}.
56+
*
57+
* @author George Aristy (george.aristy@gmail.com)
58+
* @see <a href="https://docs.microsoft.com/en-us/azure/architecture/patterns/retry">Retry pattern (Microsoft Azure Docs)</a>
59+
*/
60+
public final class App {
61+
private static final Logger LOG = LoggerFactory.getLogger(App.class);
62+
private static BusinessOperation<String> op;
63+
64+
/**
65+
* Entry point.
66+
*
67+
* @param args not used
68+
* @throws Exception not expected
69+
*/
70+
public static void main(String[] args) throws Exception {
71+
noErrors();
72+
errorNoRetry();
73+
errorWithRetry();
74+
}
75+
76+
private static void noErrors() throws Exception {
77+
op = new FindCustomer("123");
78+
op.perform();
79+
LOG.info("Sometimes the operation executes with no errors.");
80+
}
81+
82+
private static void errorNoRetry() throws Exception {
83+
op = new FindCustomer("123", new CustomerNotFoundException("not found"));
84+
try {
85+
op.perform();
86+
} catch (CustomerNotFoundException e) {
87+
LOG.info("Yet the operation will throw an error every once in a while.");
88+
}
89+
}
90+
91+
private static void errorWithRetry() throws Exception {
92+
final Retry<String> retry = new Retry<>(
93+
new FindCustomer("123", new CustomerNotFoundException("not found")),
94+
3, //3 attempts
95+
100, //100 ms delay between attempts
96+
e -> CustomerNotFoundException.class.isAssignableFrom(e.getClass())
97+
);
98+
op = retry;
99+
final String customerId = op.perform();
100+
LOG.info(String.format(
101+
"However, retrying the operation while ignoring a recoverable error will eventually yield "
102+
+ "the result %s after a number of attempts %s", customerId, retry.attempts()
103+
));
104+
}
105+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* The MIT License (MIT)
3+
*
4+
* Copyright (c) 2014-2016 Ilkka Seppälä
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
package com.iluwatar.retry;
26+
27+
/**
28+
* The top-most type in our exception hierarchy that signifies that an unexpected error
29+
* condition occurred. Its use is reserved as a "catch-all" for cases where no other subtype
30+
* captures the specificity of the error condition in question. Calling code is not expected to
31+
* be able to handle this error and should be reported to the maintainers immediately.
32+
*
33+
* @author George Aristy (george.aristy@gmail.com)
34+
*/
35+
public class BusinessException extends Exception {
36+
private static final long serialVersionUID = 6235833142062144336L;
37+
38+
/**
39+
* Ctor
40+
*
41+
* @param message the error message
42+
*/
43+
public BusinessException(String message) {
44+
super(message);
45+
}
46+
}

0 commit comments

Comments
 (0)