/spring-batch-infrastructure/src/main/java/org/springframework/batch/retry/support/RetryTemplate.java

http://github.com/SpringSource/spring-batch · Java · 490 lines · 226 code · 63 blank · 201 comment · 41 complexity · a502c27877c8acde739e4c4104e1c806 MD5 · raw file

  1. /*
  2. * Copyright 2006-2007 the original author or authors.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package org.springframework.batch.retry.support;
  17. import java.util.ArrayList;
  18. import java.util.Arrays;
  19. import java.util.Collections;
  20. import java.util.List;
  21. import org.apache.commons.logging.Log;
  22. import org.apache.commons.logging.LogFactory;
  23. import org.springframework.batch.repeat.RepeatException;
  24. import org.springframework.batch.retry.ExhaustedRetryException;
  25. import org.springframework.batch.retry.RecoveryCallback;
  26. import org.springframework.batch.retry.RetryCallback;
  27. import org.springframework.batch.retry.RetryContext;
  28. import org.springframework.batch.retry.RetryException;
  29. import org.springframework.batch.retry.RetryListener;
  30. import org.springframework.batch.retry.RetryOperations;
  31. import org.springframework.batch.retry.RetryPolicy;
  32. import org.springframework.batch.retry.RetryState;
  33. import org.springframework.batch.retry.TerminatedRetryException;
  34. import org.springframework.batch.retry.backoff.BackOffContext;
  35. import org.springframework.batch.retry.backoff.BackOffInterruptedException;
  36. import org.springframework.batch.retry.backoff.BackOffPolicy;
  37. import org.springframework.batch.retry.backoff.NoBackOffPolicy;
  38. import org.springframework.batch.retry.policy.MapRetryContextCache;
  39. import org.springframework.batch.retry.policy.RetryContextCache;
  40. import org.springframework.batch.retry.policy.SimpleRetryPolicy;
  41. /**
  42. * Template class that simplifies the execution of operations with retry
  43. * semantics. <br/>
  44. * Retryable operations are encapsulated in implementations of the
  45. * {@link RetryCallback} interface and are executed using one of the supplied
  46. * execute methods. <br/>
  47. *
  48. * By default, an operation is retried if is throws any {@link Exception} or
  49. * subclass of {@link Exception}. This behaviour can be changed by using the
  50. * {@link #setRetryPolicy(RetryPolicy)} method. <br/>
  51. *
  52. * Also by default, each operation is retried for a maximum of three attempts
  53. * with no back off in between. This behaviour can be configured using the
  54. * {@link #setRetryPolicy(RetryPolicy)} and
  55. * {@link #setBackOffPolicy(BackOffPolicy)} properties. The
  56. * {@link org.springframework.batch.retry.backoff.BackOffPolicy} controls how
  57. * long the pause is between each individual retry attempt. <br/>
  58. *
  59. * This class is thread-safe and suitable for concurrent access when executing
  60. * operations and when performing configuration changes. As such, it is possible
  61. * to change the number of retries on the fly, as well as the
  62. * {@link BackOffPolicy} used and no in progress retryable operations will be
  63. * affected.
  64. *
  65. * @author Rob Harrop
  66. * @author Dave Syer
  67. */
  68. public class RetryTemplate implements RetryOperations {
  69. protected final Log logger = LogFactory.getLog(getClass());
  70. private volatile BackOffPolicy backOffPolicy = new NoBackOffPolicy();
  71. private volatile RetryPolicy retryPolicy = new SimpleRetryPolicy(3, Collections
  72. .<Class<? extends Throwable>, Boolean> singletonMap(Exception.class, true));
  73. private volatile RetryListener[] listeners = new RetryListener[0];
  74. private RetryContextCache retryContextCache = new MapRetryContextCache();
  75. /**
  76. * Public setter for the {@link RetryContextCache}.
  77. *
  78. * @param retryContextCache the {@link RetryContextCache} to set.
  79. */
  80. public void setRetryContextCache(RetryContextCache retryContextCache) {
  81. this.retryContextCache = retryContextCache;
  82. }
  83. /**
  84. * Setter for listeners. The listeners are executed before and after a retry
  85. * block (i.e. before and after all the attempts), and on an error (every
  86. * attempt).
  87. *
  88. * @param listeners
  89. * @see RetryListener
  90. */
  91. public void setListeners(RetryListener[] listeners) {
  92. this.listeners = Arrays.asList(listeners).toArray(new RetryListener[listeners.length]);
  93. }
  94. /**
  95. * Register an additional listener.
  96. *
  97. * @param listener
  98. * @see #setListeners(RetryListener[])
  99. */
  100. public void registerListener(RetryListener listener) {
  101. List<RetryListener> list = new ArrayList<RetryListener>(Arrays.asList(listeners));
  102. list.add(listener);
  103. listeners = list.toArray(new RetryListener[list.size()]);
  104. }
  105. /**
  106. * Setter for {@link BackOffPolicy}.
  107. *
  108. * @param backOffPolicy
  109. */
  110. public void setBackOffPolicy(BackOffPolicy backOffPolicy) {
  111. this.backOffPolicy = backOffPolicy;
  112. }
  113. /**
  114. * Setter for {@link RetryPolicy}.
  115. *
  116. * @param retryPolicy
  117. */
  118. public void setRetryPolicy(RetryPolicy retryPolicy) {
  119. this.retryPolicy = retryPolicy;
  120. }
  121. /**
  122. * Keep executing the callback until it either succeeds or the policy
  123. * dictates that we stop, in which case the most recent exception thrown by
  124. * the callback will be rethrown.
  125. *
  126. * @see RetryOperations#execute(RetryCallback)
  127. *
  128. * @throws TerminatedRetryException if the retry has been manually
  129. * terminated by a listener.
  130. */
  131. public final <T> T execute(RetryCallback<T> retryCallback) throws Exception {
  132. return doExecute(retryCallback, null, null);
  133. }
  134. /**
  135. * Keep executing the callback until it either succeeds or the policy
  136. * dictates that we stop, in which case the recovery callback will be
  137. * executed.
  138. *
  139. * @see RetryOperations#execute(RetryCallback, RecoveryCallback)
  140. *
  141. * @throws TerminatedRetryException if the retry has been manually
  142. * terminated by a listener.
  143. */
  144. public final <T> T execute(RetryCallback<T> retryCallback, RecoveryCallback<T> recoveryCallback) throws Exception {
  145. return doExecute(retryCallback, recoveryCallback, null);
  146. }
  147. /**
  148. * Execute the callback once if the policy dictates that we can, re-throwing
  149. * any exception encountered so that clients can re-present the same task
  150. * later.
  151. *
  152. * @see RetryOperations#execute(RetryCallback, RetryState)
  153. *
  154. * @throws ExhaustedRetryException if the retry has been exhausted.
  155. */
  156. public final <T> T execute(RetryCallback<T> retryCallback, RetryState retryState) throws Exception,
  157. ExhaustedRetryException {
  158. return doExecute(retryCallback, null, retryState);
  159. }
  160. /**
  161. * Execute the callback once if the policy dictates that we can, re-throwing
  162. * any exception encountered so that clients can re-present the same task
  163. * later.
  164. *
  165. * @see RetryOperations#execute(RetryCallback, RetryState)
  166. */
  167. public final <T> T execute(RetryCallback<T> retryCallback, RecoveryCallback<T> recoveryCallback,
  168. RetryState retryState) throws Exception, ExhaustedRetryException {
  169. return doExecute(retryCallback, recoveryCallback, retryState);
  170. }
  171. /**
  172. * Execute the callback once if the policy dictates that we can, otherwise
  173. * execute the recovery callback.
  174. *
  175. * @see RetryOperations#execute(RetryCallback, RecoveryCallback, RetryState)
  176. * @throws ExhaustedRetryException if the retry has been exhausted.
  177. */
  178. protected <T> T doExecute(RetryCallback<T> retryCallback, RecoveryCallback<T> recoveryCallback, RetryState state)
  179. throws Exception, ExhaustedRetryException {
  180. RetryPolicy retryPolicy = this.retryPolicy;
  181. BackOffPolicy backOffPolicy = this.backOffPolicy;
  182. // Allow the retry policy to initialise itself...
  183. RetryContext context = open(retryPolicy, state);
  184. if (logger.isTraceEnabled()) {
  185. logger.trace("RetryContext retrieved: " + context);
  186. }
  187. // Make sure the context is available globally for clients who need
  188. // it...
  189. RetrySynchronizationManager.register(context);
  190. Throwable lastException = null;
  191. try {
  192. // Give clients a chance to enhance the context...
  193. boolean running = doOpenInterceptors(retryCallback, context);
  194. if (!running) {
  195. throw new TerminatedRetryException("Retry terminated abnormally by interceptor before first attempt");
  196. }
  197. // Start the backoff context...
  198. BackOffContext backOffContext = backOffPolicy.start(context);
  199. /*
  200. * We allow the whole loop to be skipped if the policy or context
  201. * already forbid the first try. This is used in the case of
  202. * stateful retry to allow a recovery in handleRetryExhausted
  203. * without the callback processing (which would throw an exception).
  204. */
  205. while (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {
  206. try {
  207. logger.debug("Retry: count=" + context.getRetryCount());
  208. // Reset the last exception, so if we are successful
  209. // the close interceptors will not think we failed...
  210. lastException = null;
  211. return retryCallback.doWithRetry(context);
  212. }
  213. catch (Throwable e) {
  214. lastException = e;
  215. doOnErrorInterceptors(retryCallback, context, e);
  216. try {
  217. registerThrowable(retryPolicy, state, context, e);
  218. } catch (Exception ex) {
  219. throw new TerminatedRetryException("Terminated retry after error in policy", ex);
  220. }
  221. if (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {
  222. try {
  223. backOffPolicy.backOff(backOffContext);
  224. }
  225. catch (BackOffInterruptedException ex) {
  226. lastException = e;
  227. // back off was prevented by another thread - fail
  228. // the retry
  229. logger.debug("Abort retry because interrupted: count=" + context.getRetryCount());
  230. throw ex;
  231. }
  232. }
  233. logger.debug("Checking for rethrow: count=" + context.getRetryCount());
  234. if (shouldRethrow(retryPolicy, context, state)) {
  235. logger.debug("Rethrow in retry for policy: count=" + context.getRetryCount());
  236. throw wrapIfNecessary(e);
  237. }
  238. }
  239. /*
  240. * A stateful attempt that can retry should have rethrown the
  241. * exception by now - i.e. we shouldn't get this far for a
  242. * stateful attempt if it can retry.
  243. */
  244. }
  245. logger.debug("Retry failed last attempt: count=" + context.getRetryCount());
  246. if (context.isExhaustedOnly()) {
  247. throw new ExhaustedRetryException("Retry exhausted after last attempt with no recovery path.", context
  248. .getLastThrowable());
  249. }
  250. return handleRetryExhausted(recoveryCallback, context, state);
  251. }
  252. finally {
  253. close(retryPolicy, context, state, lastException == null);
  254. doCloseInterceptors(retryCallback, context, lastException);
  255. RetrySynchronizationManager.clear();
  256. }
  257. }
  258. /**
  259. * Decide whether to proceed with the ongoing retry attempt. This method is
  260. * called before the {@link RetryCallback} is executed, but after the
  261. * backoff and open interceptors.
  262. *
  263. * @param retryPolicy the policy to apply
  264. * @param context the current retry context
  265. * @return true if we can continue with the attempt
  266. */
  267. protected boolean canRetry(RetryPolicy retryPolicy, RetryContext context) {
  268. return retryPolicy.canRetry(context);
  269. }
  270. /**
  271. * Clean up the cache if necessary and close the context provided (if the
  272. * flag indicates that processing was successful).
  273. *
  274. * @param context
  275. * @param state
  276. * @param succeeded
  277. */
  278. protected void close(RetryPolicy retryPolicy, RetryContext context, RetryState state, boolean succeeded) {
  279. if (state != null) {
  280. if (succeeded) {
  281. retryContextCache.remove(state.getKey());
  282. retryPolicy.close(context);
  283. }
  284. }
  285. else {
  286. retryPolicy.close(context);
  287. }
  288. }
  289. /**
  290. * @param retryPolicy
  291. * @param state
  292. * @param context
  293. * @param e
  294. */
  295. protected void registerThrowable(RetryPolicy retryPolicy, RetryState state, RetryContext context, Throwable e) {
  296. if (state != null) {
  297. Object key = state.getKey();
  298. if (context.getRetryCount() > 0 && !retryContextCache.containsKey(key)) {
  299. throw new RetryException("Inconsistent state for failed item key: cache key has changed. "
  300. + "Consider whether equals() or hashCode() for the key might be inconsistent, "
  301. + "or if you need to supply a better key");
  302. }
  303. retryContextCache.put(key, context);
  304. }
  305. retryPolicy.registerThrowable(context, e);
  306. }
  307. /**
  308. * Delegate to the {@link RetryPolicy} having checked in the cache for an
  309. * existing value if the state is not null.
  310. *
  311. * @param retryPolicy a {@link RetryPolicy} to delegate the context creation
  312. * @return a retry context, either a new one or the one used last time the
  313. * same state was encountered
  314. */
  315. protected RetryContext open(RetryPolicy retryPolicy, RetryState state) {
  316. if (state == null) {
  317. return doOpenInternal(retryPolicy);
  318. }
  319. Object key = state.getKey();
  320. if (state.isForceRefresh()) {
  321. return doOpenInternal(retryPolicy);
  322. }
  323. // If there is no cache hit we can avoid the possible expense of the
  324. // cache re-hydration.
  325. if (!retryContextCache.containsKey(key)) {
  326. // The cache is only used if there is a failure.
  327. return doOpenInternal(retryPolicy);
  328. }
  329. RetryContext context = retryContextCache.get(key);
  330. if (context == null) {
  331. if (retryContextCache.containsKey(key)) {
  332. throw new RetryException("Inconsistent state for failed item: no history found. "
  333. + "Consider whether equals() or hashCode() for the item might be inconsistent, "
  334. + "or if you need to supply a better ItemKeyGenerator");
  335. }
  336. // The cache could have been expired in between calls to
  337. // containsKey(), so we have to live with this:
  338. return doOpenInternal(retryPolicy);
  339. }
  340. return context;
  341. }
  342. /**
  343. * @param retryPolicy
  344. * @return
  345. */
  346. private RetryContext doOpenInternal(RetryPolicy retryPolicy) {
  347. return retryPolicy.open(RetrySynchronizationManager.getContext());
  348. }
  349. /**
  350. * Actions to take after final attempt has failed. If there is state clean
  351. * up the cache. If there is a recovery callback, execute that and return
  352. * its result. Otherwise throw an exception.
  353. *
  354. * @param recoveryCallback the callback for recovery (might be null)
  355. * @param context the current retry context
  356. * @throws Exception if the callback does, and if there is no callback and
  357. * the state is null then the last exception from the context
  358. * @throws ExhaustedRetryException if the state is not null and there is no
  359. * recovery callback
  360. */
  361. protected <T> T handleRetryExhausted(RecoveryCallback<T> recoveryCallback, RetryContext context, RetryState state)
  362. throws Exception {
  363. if (state != null) {
  364. retryContextCache.remove(state.getKey());
  365. }
  366. if (recoveryCallback != null) {
  367. return recoveryCallback.recover(context);
  368. }
  369. if (state != null) {
  370. logger.debug("Retry exhausted after last attempt with no recovery path.");
  371. throw new ExhaustedRetryException("Retry exhausted after last attempt with no recovery path", context
  372. .getLastThrowable());
  373. }
  374. throw wrapIfNecessary(context.getLastThrowable());
  375. }
  376. /**
  377. * Extension point for subclasses to decide on behaviour after catching an
  378. * exception in a {@link RetryCallback}. Normal stateless behaviour is not
  379. * to rethrow, and if there is state we rethrow.
  380. *
  381. * @param retryPolicy
  382. * @param context the current context
  383. *
  384. * @return true if the state is not null but subclasses might choose
  385. * otherwise
  386. */
  387. protected boolean shouldRethrow(RetryPolicy retryPolicy, RetryContext context, RetryState state) {
  388. if (state == null) {
  389. return false;
  390. }
  391. else {
  392. return state.rollbackFor(context.getLastThrowable());
  393. }
  394. }
  395. private <T> boolean doOpenInterceptors(RetryCallback<T> callback, RetryContext context) {
  396. boolean result = true;
  397. for (int i = 0; i < listeners.length; i++) {
  398. result = result && listeners[i].open(context, callback);
  399. }
  400. return result;
  401. }
  402. private <T> void doCloseInterceptors(RetryCallback<T> callback, RetryContext context, Throwable lastException) {
  403. for (int i = listeners.length; i-- > 0;) {
  404. listeners[i].close(context, callback, lastException);
  405. }
  406. }
  407. private <T> void doOnErrorInterceptors(RetryCallback<T> callback, RetryContext context, Throwable throwable) {
  408. for (int i = listeners.length; i-- > 0;) {
  409. listeners[i].onError(context, callback, throwable);
  410. }
  411. }
  412. /**
  413. * Re-throws the original throwable if it is unchecked, wraps checked
  414. * exceptions into {@link RepeatException}.
  415. */
  416. private static Exception wrapIfNecessary(Throwable throwable) {
  417. if (throwable instanceof Error) {
  418. throw (Error) throwable;
  419. }
  420. else if (throwable instanceof Exception) {
  421. return (Exception) throwable;
  422. }
  423. else {
  424. return new RetryException("Exception in batch process", throwable);
  425. }
  426. }
  427. }