forked from iluwatar/java-design-patterns
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request iluwatar#684 from llorllale/master
iluwatar#451 Retry pattern
- Loading branch information
Showing
13 changed files
with
852 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> { | ||
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) |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.18.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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
/* | ||
* 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 ([email protected]) | ||
* @see <a href="https://docs.microsoft.com/en-us/azure/architecture/patterns/retry">Retry pattern (Microsoft Azure Docs)</a> | ||
*/ | ||
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 | ||
*/ | ||
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() | ||
)); | ||
} | ||
} |
46 changes: 46 additions & 0 deletions
46
retry/src/main/java/com/iluwatar/retry/BusinessException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
|
||
/** | ||
* 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 ([email protected]) | ||
*/ | ||
public class BusinessException extends Exception { | ||
private static final long serialVersionUID = 6235833142062144336L; | ||
|
||
/** | ||
* Ctor | ||
* | ||
* @param message the error message | ||
*/ | ||
public BusinessException(String message) { | ||
super(message); | ||
} | ||
} |
Oops, something went wrong.