|
6 | 6 |
|
7 | 7 | == Retry
|
8 | 8 |
|
9 |
| -ifndef::onlyonetoggle[] |
10 |
| -include::toggle.adoc[] |
11 |
| -endif::onlyonetoggle[] |
12 |
| - |
13 | 9 | To make processing more robust and less prone to failure, it sometimes helps to
|
14 | 10 | automatically retry a failed operation in case it might succeed on a subsequent attempt.
|
15 | 11 | Errors that are susceptible to intermittent failure are often transient in nature.
|
16 | 12 | Examples include remote calls to a web service that fails because of a network glitch or a
|
17 | 13 | `DeadlockLoserDataAccessException` in a database update.
|
18 | 14 |
|
19 |
| -[[retryTemplate]] |
20 |
| -=== `RetryTemplate` |
21 |
| - |
22 | 15 | [NOTE]
|
23 | 16 | ====
|
24 | 17 | The retry functionality was pulled out of Spring Batch as of 2.2.0.
|
25 | 18 | It is now part of a new library, https://github.com/spring-projects/spring-retry[Spring Retry].
|
| 19 | +Spring Batch still relies on Spring Retry to automate retry operations within the framework. |
| 20 | +Please refer to the reference documentation of Spring Retry for details about |
| 21 | +key APIs and how to use them. |
26 | 22 | ====
|
27 |
| - |
28 |
| -To automate retry operations Spring Batch has the `RetryOperations` strategy. The |
29 |
| -following interface definition for `RetryOperations`: |
30 |
| - |
31 |
| -[source, java] |
32 |
| ----- |
33 |
| -public interface RetryOperations { |
34 |
| -
|
35 |
| - <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback) throws E; |
36 |
| -
|
37 |
| - <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback) |
38 |
| - throws E; |
39 |
| -
|
40 |
| - <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RetryState retryState) |
41 |
| - throws E, ExhaustedRetryException; |
42 |
| -
|
43 |
| - <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback, |
44 |
| - RetryState retryState) throws E; |
45 |
| -
|
46 |
| -} |
47 |
| ----- |
48 |
| - |
49 |
| -The basic callback is a simple interface that lets you insert some business logic to be |
50 |
| -retried, as shown in the following interface definition: |
51 |
| - |
52 |
| -[source, java] |
53 |
| ----- |
54 |
| -public interface RetryCallback<T, E extends Throwable> { |
55 |
| -
|
56 |
| - T doWithRetry(RetryContext context) throws E; |
57 |
| -
|
58 |
| -} |
59 |
| ----- |
60 |
| - |
61 |
| -The callback runs and, if it fails (by throwing an `Exception`), it is retried until |
62 |
| -either it is successful or the implementation aborts. There are a number of overloaded |
63 |
| -`execute` methods in the `RetryOperations` interface. Those methods deal with various use |
64 |
| -cases for recovery when all retry attempts are exhausted and deal with retry state, which |
65 |
| -lets clients and implementations store information between calls (we cover this in more |
66 |
| -detail later in the chapter). |
67 |
| - |
68 |
| -The simplest general purpose implementation of `RetryOperations` is `RetryTemplate`. It |
69 |
| -can be used as follows: |
70 |
| - |
71 |
| -[source, java] |
72 |
| ----- |
73 |
| -RetryTemplate template = new RetryTemplate(); |
74 |
| -
|
75 |
| -TimeoutRetryPolicy policy = new TimeoutRetryPolicy(); |
76 |
| -policy.setTimeout(30000L); |
77 |
| -
|
78 |
| -template.setRetryPolicy(policy); |
79 |
| -
|
80 |
| -Foo result = template.execute(new RetryCallback<Foo>() { |
81 |
| -
|
82 |
| - public Foo doWithRetry(RetryContext context) { |
83 |
| - // Do stuff that might fail, e.g. webservice operation |
84 |
| - return result; |
85 |
| - } |
86 |
| -
|
87 |
| -}); |
88 |
| ----- |
89 |
| - |
90 |
| -In the preceding example, we make a web service call and return the result to the user. If |
91 |
| -that call fails, then it is retried until a timeout is reached. |
92 |
| - |
93 |
| -[[retryContext]] |
94 |
| -==== `RetryContext` |
95 |
| - |
96 |
| -The method parameter for the `RetryCallback` is a `RetryContext`. Many callbacks ignore |
97 |
| -the context, but, if necessary, it can be used as an attribute bag to store data for the |
98 |
| -duration of the iteration. |
99 |
| - |
100 |
| -A `RetryContext` has a parent context if there is a nested retry in progress in the same |
101 |
| -thread. The parent context is occasionally useful for storing data that need to be shared |
102 |
| -between calls to `execute`. |
103 |
| - |
104 |
| -[[recoveryCallback]] |
105 |
| -==== `RecoveryCallback` |
106 |
| - |
107 |
| -When a retry is exhausted, the `RetryOperations` can pass control to a different callback, |
108 |
| -called the `RecoveryCallback`. To use this feature, clients pass in the callbacks together |
109 |
| -to the same method, as shown in the following example: |
110 |
| - |
111 |
| -[source, java] |
112 |
| ----- |
113 |
| -Foo foo = template.execute(new RetryCallback<Foo>() { |
114 |
| - public Foo doWithRetry(RetryContext context) { |
115 |
| - // business logic here |
116 |
| - }, |
117 |
| - new RecoveryCallback<Foo>() { |
118 |
| - Foo recover(RetryContext context) throws Exception { |
119 |
| - // recover logic here |
120 |
| - } |
121 |
| -}); |
122 |
| ----- |
123 |
| - |
124 |
| -If the business logic does not succeed before the template decides to abort, then the |
125 |
| -client is given the chance to do some alternate processing through the recovery callback. |
126 |
| - |
127 |
| -[[statelessRetry]] |
128 |
| -==== Stateless Retry |
129 |
| - |
130 |
| -In the simplest case, a retry is just a while loop. The `RetryTemplate` can just keep |
131 |
| -trying until it either succeeds or fails. The `RetryContext` contains some state to |
132 |
| -determine whether to retry or abort, but this state is on the stack and there is no need |
133 |
| -to store it anywhere globally, so we call this stateless retry. The distinction between |
134 |
| -stateless and stateful retry is contained in the implementation of the `RetryPolicy` (the |
135 |
| -`RetryTemplate` can handle both). In a stateless retry, the retry callback is always |
136 |
| -executed in the same thread it was on when it failed. |
137 |
| - |
138 |
| -[[statefulRetry]] |
139 |
| -==== Stateful Retry |
140 |
| - |
141 |
| -Where the failure has caused a transactional resource to become invalid, there are some |
142 |
| -special considerations. This does not apply to a simple remote call because there is no |
143 |
| -transactional resource (usually), but it does sometimes apply to a database update, |
144 |
| -especially when using Hibernate. In this case it only makes sense to re-throw the |
145 |
| -exception that called the failure immediately, so that the transaction can roll back and |
146 |
| -we can start a new, valid transaction. |
147 |
| - |
148 |
| -In cases involving transactions, a stateless retry is not good enough, because the |
149 |
| -re-throw and roll back necessarily involve leaving the `RetryOperations.execute()` method |
150 |
| -and potentially losing the context that was on the stack. To avoid losing it we have to |
151 |
| -introduce a storage strategy to lift it off the stack and put it (at a minimum) in heap |
152 |
| -storage. For this purpose, Spring Batch provides a storage strategy called |
153 |
| -`RetryContextCache`, which can be injected into the `RetryTemplate`. The default |
154 |
| -implementation of the `RetryContextCache` is in memory, using a simple `Map`. Advanced |
155 |
| -usage with multiple processes in a clustered environment might also consider implementing |
156 |
| -the `RetryContextCache` with a cluster cache of some sort (however, even in a clustered |
157 |
| -environment, this might be overkill). |
158 |
| - |
159 |
| -Part of the responsibility of the `RetryOperations` is to recognize the failed operations |
160 |
| -when they come back in a new execution (and usually wrapped in a new transaction). To |
161 |
| -facilitate this, Spring Batch provides the `RetryState` abstraction. This works in |
162 |
| -conjunction with a special `execute` methods in the `RetryOperations` interface. |
163 |
| - |
164 |
| -The way the failed operations are recognized is by identifying the state across multiple |
165 |
| -invocations of the retry. To identify the state, the user can provide a `RetryState` |
166 |
| -object that is responsible for returning a unique key identifying the item. The identifier |
167 |
| -is used as a key in the `RetryContextCache` interface. |
168 |
| - |
169 |
| -[WARNING] |
170 |
| -==== |
171 |
| -Be very careful with the implementation of `Object.equals()` and `Object.hashCode()` in |
172 |
| -the key returned by `RetryState`. The best advice is to use a business key to identify the |
173 |
| -items. In the case of a JMS message, the message ID can be used. |
174 |
| -==== |
175 |
| - |
176 |
| -When the retry is exhausted, there is also the option to handle the failed item in a |
177 |
| -different way, instead of calling the `RetryCallback` (which is now presumed to be likely |
178 |
| -to fail). Just like in the stateless case, this option is provided by the |
179 |
| -`RecoveryCallback`, which can be provided by passing it in to the `execute` method of |
180 |
| -`RetryOperations`. |
181 |
| - |
182 |
| -The decision to retry or not is actually delegated to a regular `RetryPolicy`, so the |
183 |
| -usual concerns about limits and timeouts can be injected there (described later in this |
184 |
| -chapter). |
185 |
| - |
186 |
| -[[retryPolicies]] |
187 |
| -=== Retry Policies |
188 |
| - |
189 |
| -Inside a `RetryTemplate`, the decision to retry or fail in the `execute` method is |
190 |
| -determined by a `RetryPolicy`, which is also a factory for the `RetryContext`. The |
191 |
| -`RetryTemplate` has the responsibility to use the current policy to create a |
192 |
| -`RetryContext` and pass that in to the `RetryCallback` at every attempt. After a callback |
193 |
| -fails, the `RetryTemplate` has to make a call to the `RetryPolicy` to ask it to update its |
194 |
| -state (which is stored in the `RetryContext`) and then asks the policy if another attempt |
195 |
| -can be made. If another attempt cannot be made (such as when a limit is reached or a |
196 |
| -timeout is detected) then the policy is also responsible for handling the exhausted state. |
197 |
| -Simple implementations throw `RetryExhaustedException`, which causes any enclosing |
198 |
| -transaction to be rolled back. More sophisticated implementations might attempt to take |
199 |
| -some recovery action, in which case the transaction can remain intact. |
200 |
| - |
201 |
| -[TIP] |
202 |
| -==== |
203 |
| -Failures are inherently either retryable or not. If the same exception is always going to |
204 |
| -be thrown from the business logic, it does no good to retry it. So do not retry on all |
205 |
| -exception types. Rather, try to focus on only those exceptions that you expect to be |
206 |
| -retryable. It is not usually harmful to the business logic to retry more aggressively, but |
207 |
| -it is wasteful, because, if a failure is deterministic, you spend time retrying something |
208 |
| -that you know in advance is fatal. |
209 |
| -==== |
210 |
| - |
211 |
| -Spring Batch provides some simple general purpose implementations of stateless |
212 |
| -`RetryPolicy`, such as `SimpleRetryPolicy` and `TimeoutRetryPolicy` (used in the preceding example). |
213 |
| - |
214 |
| -The `SimpleRetryPolicy` allows a retry on any of a named list of exception types, up to a |
215 |
| -fixed number of times. It also has a list of "fatal" exceptions that should never be |
216 |
| -retried, and this list overrides the retryable list so that it can be used to give finer |
217 |
| -control over the retry behavior, as shown in the following example: |
218 |
| - |
219 |
| -[source, java] |
220 |
| ----- |
221 |
| -SimpleRetryPolicy policy = new SimpleRetryPolicy(); |
222 |
| -// Set the max retry attempts |
223 |
| -policy.setMaxAttempts(5); |
224 |
| -// Retry on all exceptions (this is the default) |
225 |
| -policy.setRetryableExceptions(new Class[] {Exception.class}); |
226 |
| -// ... but never retry IllegalStateException |
227 |
| -policy.setFatalExceptions(new Class[] {IllegalStateException.class}); |
228 |
| -
|
229 |
| -// Use the policy... |
230 |
| -RetryTemplate template = new RetryTemplate(); |
231 |
| -template.setRetryPolicy(policy); |
232 |
| -template.execute(new RetryCallback<Foo>() { |
233 |
| - public Foo doWithRetry(RetryContext context) { |
234 |
| - // business logic here |
235 |
| - } |
236 |
| -}); |
237 |
| ----- |
238 |
| - |
239 |
| -There is also a more flexible implementation called `ExceptionClassifierRetryPolicy`, |
240 |
| -which lets the user configure different retry behavior for an arbitrary set of exception |
241 |
| -types though the `ExceptionClassifier` abstraction. The policy works by calling on the |
242 |
| -classifier to convert an exception into a delegate `RetryPolicy`. For example, one |
243 |
| -exception type can be retried more times before failure than another by mapping it to a |
244 |
| -different policy. |
245 |
| - |
246 |
| -Users might need to implement their own retry policies for more customized decisions. For |
247 |
| -instance, a custom retry policy makes sense when there is a well-known, solution-specific |
248 |
| -classification of exceptions into retryable and not retryable. |
249 |
| - |
250 |
| -[[backoffPolicies]] |
251 |
| -=== Backoff Policies |
252 |
| - |
253 |
| -When retrying after a transient failure, it often helps to wait a bit before trying again, |
254 |
| -because usually the failure is caused by some problem that can only be resolved by |
255 |
| -waiting. If a `RetryCallback` fails, the `RetryTemplate` can pause execution according to |
256 |
| -the `BackoffPolicy`. |
257 |
| - |
258 |
| -The following code shows the interface definition for the `BackOffPolicy` interface: |
259 |
| - |
260 |
| -[source, java] |
261 |
| ----- |
262 |
| -public interface BackoffPolicy { |
263 |
| -
|
264 |
| - BackOffContext start(RetryContext context); |
265 |
| -
|
266 |
| - void backOff(BackOffContext backOffContext) |
267 |
| - throws BackOffInterruptedException; |
268 |
| -
|
269 |
| -} |
270 |
| ----- |
271 |
| - |
272 |
| -A `BackoffPolicy` is free to implement the backOff in any way it chooses. The policies |
273 |
| -provided by Spring Batch out of the box all use `Object.wait()`. A common use case is to |
274 |
| -backoff with an exponentially increasing wait period, to avoid two retries getting into |
275 |
| -lock step and both failing (this is a lesson learned from ethernet). For this purpose, |
276 |
| -Spring Batch provides the `ExponentialBackoffPolicy`. |
277 |
| - |
278 |
| -[[retryListeners]] |
279 |
| -=== Listeners |
280 |
| - |
281 |
| -Often, it is useful to be able to receive additional callbacks for cross cutting concerns |
282 |
| -across a number of different retries. For this purpose, Spring Batch provides the |
283 |
| -`RetryListener` interface. The `RetryTemplate` lets users register `RetryListeners`, and |
284 |
| -they are given callbacks with `RetryContext` and `Throwable` where available during the |
285 |
| -iteration. |
286 |
| - |
287 |
| -The following code shows the interface definition for `RetryListener`: |
288 |
| -[source, java] |
289 |
| ----- |
290 |
| -public interface RetryListener { |
291 |
| -
|
292 |
| - <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback); |
293 |
| -
|
294 |
| - <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable); |
295 |
| -
|
296 |
| - <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable); |
297 |
| -} |
298 |
| ----- |
299 |
| - |
300 |
| -The `open` and `close` callbacks come before and after the entire retry in the simplest |
301 |
| -case, and `onError` applies to the individual `RetryCallback` calls. The `close` method |
302 |
| -might also receive a `Throwable`. If there has been an error, it is the last one thrown by |
303 |
| -the `RetryCallback`. |
304 |
| - |
305 |
| -Note that, when there is more than one listener, they are in a list, so there is an order. |
306 |
| -In this case, `open` is called in the same order while `onError` and `close` are called in |
307 |
| -reverse order. |
308 |
| - |
309 |
| -[[declarativeRetry]] |
310 |
| -=== Declarative Retry |
311 |
| - |
312 |
| -Sometimes, there is some business processing that you know you want to retry every time it |
313 |
| -happens. The classic example of this is the remote service call. Spring Batch provides an |
314 |
| -AOP interceptor that wraps a method call in a `RetryOperations` implementation for just |
315 |
| -this purpose. The `RetryOperationsInterceptor` executes the intercepted method and retries |
316 |
| -on failure according to the `RetryPolicy` in the provided `RepeatTemplate`. |
317 |
| - |
318 |
| -[role="xmlContent"] |
319 |
| -The following example shows a declarative retry that uses the Spring AOP namespace to |
320 |
| -retry a service call to a method called `remoteCall` (for more detail on how to configure |
321 |
| -AOP interceptors, see the Spring User Guide): |
322 |
| - |
323 |
| -[source, xml, role="xmlContent"] |
324 |
| ----- |
325 |
| -<aop:config> |
326 |
| - <aop:pointcut id="transactional" |
327 |
| - expression="execution(* com..*Service.remoteCall(..))" /> |
328 |
| - <aop:advisor pointcut-ref="transactional" |
329 |
| - advice-ref="retryAdvice" order="-1"/> |
330 |
| -</aop:config> |
331 |
| -
|
332 |
| -<bean id="retryAdvice" |
333 |
| - class="org.springframework.retry.interceptor.RetryOperationsInterceptor"/> |
334 |
| ----- |
335 |
| - |
336 |
| -[role="javaContent"] |
337 |
| -The following example shows a declarative retry that uses java configuration to retry a |
338 |
| -service call to a method called `remoteCall` (for more detail on how to configure AOP |
339 |
| -interceptors, see the Spring User Guide): |
340 |
| - |
341 |
| -[source, java, role="javaContent"] |
342 |
| ----- |
343 |
| -@Bean |
344 |
| -public MyService myService() { |
345 |
| - ProxyFactory factory = new ProxyFactory(RepeatOperations.class.getClassLoader()); |
346 |
| - factory.setInterfaces(MyService.class); |
347 |
| - factory.setTarget(new MyService()); |
348 |
| -
|
349 |
| - MyService service = (MyService) factory.getProxy(); |
350 |
| - JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut(); |
351 |
| - pointcut.setPatterns(".*remoteCall.*"); |
352 |
| -
|
353 |
| - RetryOperationsInterceptor interceptor = new RetryOperationsInterceptor(); |
354 |
| -
|
355 |
| - ((Advised) service).addAdvisor(new DefaultPointcutAdvisor(pointcut, interceptor)); |
356 |
| -
|
357 |
| - return service; |
358 |
| -} |
359 |
| ----- |
360 |
| - |
361 |
| -The preceding example uses a default `RetryTemplate` inside the interceptor. To change the |
362 |
| -policies or listeners, you can inject an instance of `RetryTemplate` into the interceptor. |
0 commit comments