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 | |
17 | package org.springframework.batch.retry.policy; |
18 | |
19 | import org.apache.commons.logging.Log; |
20 | import org.apache.commons.logging.LogFactory; |
21 | import org.springframework.batch.repeat.support.RepeatSynchronizationManager; |
22 | import org.springframework.batch.retry.ExhaustedRetryException; |
23 | import org.springframework.batch.retry.RecoveryCallback; |
24 | import org.springframework.batch.retry.RetryCallback; |
25 | import org.springframework.batch.retry.RetryContext; |
26 | import org.springframework.batch.retry.RetryException; |
27 | import org.springframework.batch.retry.RetryPolicy; |
28 | import org.springframework.batch.retry.TerminatedRetryException; |
29 | import org.springframework.batch.retry.callback.RecoveryRetryCallback; |
30 | import org.springframework.batch.retry.context.RetryContextSupport; |
31 | import org.springframework.util.Assert; |
32 | |
33 | /** |
34 | * A {@link RetryPolicy} that detects an {@link RecoveryRetryCallback} when it |
35 | * opens a new context, and uses it to make sure the item is in place for later |
36 | * decisions about how to retry or backoff. The callback should be an instance |
37 | * of {@link RecoveryRetryCallback} otherwise an exception will be thrown when |
38 | * the context is created. |
39 | * |
40 | * @author Dave Syer |
41 | * |
42 | */ |
43 | public class RecoveryCallbackRetryPolicy extends AbstractStatefulRetryPolicy { |
44 | |
45 | protected Log logger = LogFactory.getLog(getClass()); |
46 | |
47 | public static final String EXHAUSTED = RecoveryCallbackRetryPolicy.class.getName() + ".EXHAUSTED"; |
48 | |
49 | private RetryPolicy delegate; |
50 | |
51 | /** |
52 | * Convenience constructor to set delegate on init. |
53 | * |
54 | * @param delegate |
55 | */ |
56 | public RecoveryCallbackRetryPolicy(RetryPolicy delegate) { |
57 | super(); |
58 | this.delegate = delegate; |
59 | } |
60 | |
61 | /** |
62 | * Default constructor. Creates a new {@link SimpleRetryPolicy} for the |
63 | * delegate. |
64 | */ |
65 | public RecoveryCallbackRetryPolicy() { |
66 | this(new SimpleRetryPolicy()); |
67 | } |
68 | |
69 | /** |
70 | * Setter for delegate. |
71 | * |
72 | * @param delegate |
73 | */ |
74 | public void setDelegate(RetryPolicy delegate) { |
75 | this.delegate = delegate; |
76 | } |
77 | |
78 | /** |
79 | * Check the history of this item, and if it has reached the retry limit, |
80 | * then return false. |
81 | * |
82 | * @see org.springframework.batch.retry.RetryPolicy#canRetry(org.springframework.batch.retry.RetryContext) |
83 | */ |
84 | public boolean canRetry(RetryContext context) { |
85 | return ((RetryPolicy) context).canRetry(context); |
86 | } |
87 | |
88 | /** |
89 | * Delegates to the delegate context. |
90 | * |
91 | * @see org.springframework.batch.retry.RetryPolicy#close(org.springframework.batch.retry.RetryContext) |
92 | */ |
93 | public void close(RetryContext context) { |
94 | ((RetryPolicy) context).close(context); |
95 | } |
96 | |
97 | /** |
98 | * Create a new context for the execution of the callback, which must be an |
99 | * instance of {@link RecoveryRetryCallback}. |
100 | * |
101 | * @see org.springframework.batch.retry.RetryPolicy#open(org.springframework.batch.retry.RetryCallback, |
102 | * RetryContext) |
103 | * |
104 | * @throws IllegalStateException if the callback is not of the required |
105 | * type. |
106 | */ |
107 | public RetryContext open(RetryCallback callback, RetryContext parent) { |
108 | Assert.state(callback instanceof RecoveryRetryCallback, "Callback must be RecoveryRetryCallback"); |
109 | RecoveryCallbackRetryContext context = new RecoveryCallbackRetryContext((RecoveryRetryCallback) callback, parent); |
110 | context.open(callback, null); |
111 | return context; |
112 | } |
113 | |
114 | /** |
115 | * If {@link #canRetry(RetryContext)} is false then take remedial action (if |
116 | * implemented by subclasses), and remove the current item from the history. |
117 | * |
118 | * @see org.springframework.batch.retry.RetryPolicy#registerThrowable(org.springframework.batch.retry.RetryContext, |
119 | * java.lang.Throwable) |
120 | */ |
121 | public void registerThrowable(RetryContext context, Throwable throwable) throws TerminatedRetryException { |
122 | ((RetryPolicy) context).registerThrowable(context, throwable); |
123 | // The throwable is stored in the delegate context. |
124 | } |
125 | |
126 | /** |
127 | * Call recovery path (if any) and clean up context history. |
128 | * |
129 | * @see org.springframework.batch.retry.policy.AbstractStatefulRetryPolicy#handleRetryExhausted(org.springframework.batch.retry.RetryContext) |
130 | */ |
131 | public Object handleRetryExhausted(RetryContext context) throws ExhaustedRetryException { |
132 | return ((RetryPolicy) context).handleRetryExhausted(context); |
133 | } |
134 | |
135 | private class RecoveryCallbackRetryContext extends RetryContextSupport implements RetryPolicy { |
136 | |
137 | final private Object key; |
138 | |
139 | final private int initialHashCode; |
140 | |
141 | // The delegate context... |
142 | private RetryContext delegateContext; |
143 | |
144 | final private RecoveryCallback recoverer; |
145 | |
146 | final private boolean forceRefresh; |
147 | |
148 | public RecoveryCallbackRetryContext(RecoveryRetryCallback callback, RetryContext parent) { |
149 | super(parent); |
150 | this.recoverer = callback.getRecoveryCallback(); |
151 | this.key = callback.getKey(); |
152 | this.forceRefresh = callback.isForceRefresh(); |
153 | this.initialHashCode = key.hashCode(); |
154 | } |
155 | |
156 | public boolean canRetry(RetryContext context) { |
157 | return delegate.canRetry(this.delegateContext); |
158 | } |
159 | |
160 | public void close(RetryContext context) { |
161 | delegate.close(this.delegateContext); |
162 | } |
163 | |
164 | public RetryContext open(RetryCallback callback, RetryContext parent) { |
165 | if (forceRefresh) { |
166 | // Avoid a cache hit if the caller tells us this is a fresh item |
167 | this.delegateContext = delegate.open(callback, null); |
168 | } |
169 | else if (retryContextCache.containsKey(key)) { |
170 | this.delegateContext = retryContextCache.get(key); |
171 | if (this.delegateContext == null) { |
172 | throw new RetryException("Inconsistent state for failed item: no history found. " |
173 | + "Consider whether equals() or hashCode() for the item might be inconsistent, " |
174 | + "or if you need to supply a better ItemKeyGenerator"); |
175 | } |
176 | } |
177 | else { |
178 | // Only create a new context if we don't know the history of |
179 | // this item: |
180 | this.delegateContext = delegate.open(callback, null); |
181 | } |
182 | // The return value shouldn't be used... |
183 | return null; |
184 | } |
185 | |
186 | public void registerThrowable(RetryContext context, Throwable throwable) throws TerminatedRetryException { |
187 | // TODO: this comparison assumes that hashCode is the limiting |
188 | // factor. Actually the cache should be able to decide for us. |
189 | if (this.initialHashCode != key.hashCode()) { |
190 | throw new RetryException("Inconsistent state for failed item key: hashCode has changed. " |
191 | + "Consider whether equals() or hashCode() for the item might be inconsistent, " |
192 | + "or if you need to supply a better ItemKeyGenerator"); |
193 | } |
194 | retryContextCache.put(key, this.delegateContext); |
195 | delegate.registerThrowable(this.delegateContext, throwable); |
196 | } |
197 | |
198 | public boolean shouldRethrow(RetryContext context) { |
199 | // Not called... |
200 | throw new UnsupportedOperationException("Not supported - this code should be unreachable."); |
201 | } |
202 | |
203 | public Object handleRetryExhausted(RetryContext context) throws ExhaustedRetryException { |
204 | // If there is no going back, then we can remove the history |
205 | retryContextCache.remove(key); |
206 | RepeatSynchronizationManager.setCompleteOnly(); |
207 | if (recoverer != null) { |
208 | return recoverer.recover(context); |
209 | } |
210 | logger.info("No recovery callback provided. Returning null from recovery step."); |
211 | // Don't want to call the delegate here - it would throw an exception |
212 | return null; |
213 | } |
214 | |
215 | public Throwable getLastThrowable() { |
216 | return delegateContext.getLastThrowable(); |
217 | } |
218 | |
219 | public int getRetryCount() { |
220 | return delegateContext.getRetryCount(); |
221 | } |
222 | |
223 | } |
224 | |
225 | } |