001 /**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017 package org.apache.camel.spring.spi;
018
019 import java.util.concurrent.CountDownLatch;
020 import java.util.concurrent.ScheduledExecutorService;
021
022 import org.apache.camel.AsyncCallback;
023 import org.apache.camel.CamelContext;
024 import org.apache.camel.Exchange;
025 import org.apache.camel.LoggingLevel;
026 import org.apache.camel.Predicate;
027 import org.apache.camel.Processor;
028 import org.apache.camel.processor.RedeliveryErrorHandler;
029 import org.apache.camel.processor.RedeliveryPolicy;
030 import org.apache.camel.processor.exceptionpolicy.ExceptionPolicyStrategy;
031 import org.apache.camel.util.CamelLogger;
032 import org.apache.camel.util.ExchangeHelper;
033 import org.apache.camel.util.ObjectHelper;
034 import org.springframework.transaction.TransactionDefinition;
035 import org.springframework.transaction.TransactionStatus;
036 import org.springframework.transaction.support.TransactionCallbackWithoutResult;
037 import org.springframework.transaction.support.TransactionTemplate;
038
039 /**
040 * The <a href="http://camel.apache.org/transactional-client.html">Transactional Client</a>
041 * EIP pattern.
042 *
043 * @version
044 */
045 public class TransactionErrorHandler extends RedeliveryErrorHandler {
046
047 private final TransactionTemplate transactionTemplate;
048 private final String transactionKey;
049 private final LoggingLevel rollbackLoggingLevel;
050
051 /**
052 * Creates the transaction error handler.
053 *
054 * @param camelContext the camel context
055 * @param output outer processor that should use this default error handler
056 * @param logger logger to use for logging failures and redelivery attempts
057 * @param redeliveryProcessor an optional processor to run before redelivery attempt
058 * @param redeliveryPolicy policy for redelivery
059 * @param exceptionPolicyStrategy strategy for onException handling
060 * @param transactionTemplate the transaction template
061 * @param retryWhile retry while
062 * @param executorService the {@link java.util.concurrent.ScheduledExecutorService} to be used for redelivery thread pool. Can be <tt>null</tt>.
063 * @param rollbackLoggingLevel logging level to use for logging transaction rollback occurred
064 */
065 public TransactionErrorHandler(CamelContext camelContext, Processor output, CamelLogger logger,
066 Processor redeliveryProcessor, RedeliveryPolicy redeliveryPolicy, ExceptionPolicyStrategy exceptionPolicyStrategy,
067 TransactionTemplate transactionTemplate, Predicate retryWhile, ScheduledExecutorService executorService,
068 LoggingLevel rollbackLoggingLevel) {
069
070 super(camelContext, output, logger, redeliveryProcessor, redeliveryPolicy, null, null, false, retryWhile, executorService);
071 setExceptionPolicy(exceptionPolicyStrategy);
072 this.transactionTemplate = transactionTemplate;
073 this.rollbackLoggingLevel = rollbackLoggingLevel;
074 this.transactionKey = ObjectHelper.getIdentityHashCode(transactionTemplate);
075 }
076
077 public boolean supportTransacted() {
078 return true;
079 }
080
081 @Override
082 public String toString() {
083 if (output == null) {
084 // if no output then don't do any description
085 return "";
086 }
087 return "TransactionErrorHandler:"
088 + propagationBehaviorToString(transactionTemplate.getPropagationBehavior())
089 + "[" + getOutput() + "]";
090 }
091
092 @Override
093 public void process(Exchange exchange) throws Exception {
094 // we have to run this synchronously as Spring Transaction does *not* support
095 // using multiple threads to span a transaction
096 if (exchange.getUnitOfWork().isTransactedBy(transactionKey)) {
097 // already transacted by this transaction template
098 // so lets just let the error handler process it
099 processByErrorHandler(exchange);
100 } else {
101 // not yet wrapped in transaction so lets do that
102 // and then have it invoke the error handler from within that transaction
103 processInTransaction(exchange);
104 }
105 }
106
107 @Override
108 public boolean process(Exchange exchange, AsyncCallback callback) {
109 // invoke ths synchronous method as Spring Transaction does *not* support
110 // using multiple threads to span a transaction
111 try {
112 process(exchange);
113 } catch (Throwable e) {
114 exchange.setException(e);
115 }
116
117 // notify callback we are done synchronously
118 callback.done(true);
119 return true;
120 }
121
122 protected void processInTransaction(final Exchange exchange) throws Exception {
123 // is the exchange redelivered, for example JMS brokers support such details
124 Boolean externalRedelivered = exchange.isExternalRedelivered();
125 final String redelivered = externalRedelivered != null ? externalRedelivered.toString() : "unknown";
126 final String ids = ExchangeHelper.logIds(exchange);
127
128 try {
129 // mark the beginning of this transaction boundary
130 exchange.getUnitOfWork().beginTransactedBy(transactionKey);
131
132 // do in transaction
133 logTransactionBegin(redelivered, ids);
134 doInTransactionTemplate(exchange);
135 logTransactionCommit(redelivered, ids);
136
137 } catch (TransactionRollbackException e) {
138 // do not set as exception, as its just a dummy exception to force spring TX to rollback
139 logTransactionRollback(redelivered, ids, null, true);
140 } catch (Throwable e) {
141 exchange.setException(e);
142 logTransactionRollback(redelivered, ids, e, false);
143 } finally {
144 // mark the end of this transaction boundary
145 exchange.getUnitOfWork().endTransactedBy(transactionKey);
146 }
147
148 // if it was a local rollback only then remove its marker so outer transaction wont see the marker
149 Boolean onlyLast = (Boolean) exchange.removeProperty(Exchange.ROLLBACK_ONLY_LAST);
150 if (onlyLast != null && onlyLast) {
151 // we only want this logged at debug level
152 if (log.isDebugEnabled()) {
153 // log exception if there was a cause exception so we have the stack trace
154 Exception cause = exchange.getException();
155 if (cause != null) {
156 log.debug("Transaction rollback (" + transactionKey + ") redelivered(" + redelivered + ") for "
157 + ids + " due exchange was marked for rollbackOnlyLast and caught: ", cause);
158 } else {
159 log.debug("Transaction rollback ({}) redelivered({}) for {} "
160 + "due exchange was marked for rollbackOnlyLast", new Object[]{transactionKey, redelivered, ids});
161 }
162 }
163 // remove caused exception due we was marked as rollback only last
164 // so by removing the exception, any outer transaction will not be affected
165 exchange.setException(null);
166 }
167 }
168
169 protected void doInTransactionTemplate(final Exchange exchange) {
170
171 // spring transaction template is working best with rollback if you throw it a runtime exception
172 // otherwise it may not rollback messages send to JMS queues etc.
173
174 transactionTemplate.execute(new TransactionCallbackWithoutResult() {
175 protected void doInTransactionWithoutResult(TransactionStatus status) {
176 // wrapper exception to throw if the exchange failed
177 // IMPORTANT: Must be a runtime exception to let Spring regard it as to do "rollback"
178 RuntimeException rce;
179
180 // and now let process the exchange by the error handler
181 processByErrorHandler(exchange);
182
183 // after handling and still an exception or marked as rollback only then rollback
184 if (exchange.getException() != null || exchange.isRollbackOnly()) {
185
186 // wrap exception in transacted exception
187 if (exchange.getException() != null) {
188 rce = ObjectHelper.wrapRuntimeCamelException(exchange.getException());
189 } else {
190 // create dummy exception to force spring transaction manager to rollback
191 rce = new TransactionRollbackException();
192 }
193
194 if (!status.isRollbackOnly()) {
195 status.setRollbackOnly();
196 }
197
198 // throw runtime exception to force rollback (which works best to rollback with Spring transaction manager)
199 if (log.isTraceEnabled()) {
200 log.trace("Throwing runtime exception to force transaction to rollback on {}", transactionTemplate.getName());
201 }
202 throw rce;
203 }
204 }
205 });
206 }
207
208 /**
209 * Processes the {@link Exchange} using the error handler.
210 * <p/>
211 * This implementation will invoke ensure this occurs synchronously, that means if the async routing engine
212 * did kick in, then this implementation will wait for the task to complete before it continues.
213 *
214 * @param exchange the exchange
215 */
216 protected void processByErrorHandler(final Exchange exchange) {
217 final CountDownLatch latch = new CountDownLatch(1);
218 boolean sync = super.process(exchange, new AsyncCallback() {
219 public void done(boolean doneSync) {
220 if (!doneSync) {
221 log.trace("Asynchronous callback received for exchangeId: {}", exchange.getExchangeId());
222 latch.countDown();
223 }
224 }
225
226 @Override
227 public String toString() {
228 return "Done " + this;
229 }
230 });
231 if (!sync) {
232 log.trace("Waiting for asynchronous callback before continuing for exchangeId: {} -> {}",
233 exchange.getExchangeId(), exchange);
234 try {
235 latch.await();
236 } catch (InterruptedException e) {
237 exchange.setException(e);
238 }
239 log.trace("Asynchronous callback received, will continue routing exchangeId: {} -> {}",
240 exchange.getExchangeId(), exchange);
241 }
242 }
243
244 /**
245 * Logs the transaction begin
246 */
247 private void logTransactionBegin(String redelivered, String ids) {
248 if (log.isDebugEnabled()) {
249 log.debug("Transaction begin ({}) redelivered({}) for {})", new Object[]{transactionKey, redelivered, ids});
250 }
251 }
252
253 /**
254 * Logs the transaction commit
255 */
256 private void logTransactionCommit(String redelivered, String ids) {
257 if ("true".equals(redelivered)) {
258 // okay its a redelivered message so log at INFO level if rollbackLoggingLevel is INFO or higher
259 // this allows people to know that the redelivered message was committed this time
260 if (rollbackLoggingLevel == LoggingLevel.INFO || rollbackLoggingLevel == LoggingLevel.WARN || rollbackLoggingLevel == LoggingLevel.ERROR) {
261 log.info("Transaction commit ({}) redelivered({}) for {})", new Object[]{transactionKey, redelivered, ids});
262 // return after we have logged
263 return;
264 }
265 }
266
267 // log non redelivered by default at DEBUG level
268 log.debug("Transaction commit ({}) redelivered({}) for {})", new Object[]{transactionKey, redelivered, ids});
269 }
270
271 /**
272 * Logs the transaction rollback.
273 */
274 private void logTransactionRollback(String redelivered, String ids, Throwable e, boolean rollbackOnly) {
275 if (rollbackLoggingLevel == LoggingLevel.OFF) {
276 return;
277 } else if (rollbackLoggingLevel == LoggingLevel.ERROR && log.isErrorEnabled()) {
278 if (rollbackOnly) {
279 log.error("Transaction rollback ({}) redelivered({}) for {} due exchange was marked for rollbackOnly", new Object[]{transactionKey, redelivered, ids});
280 } else {
281 log.error("Transaction rollback ({}) redelivered({}) for {} caught: {}", new Object[]{transactionKey, redelivered, ids, e.getMessage()});
282 }
283 } else if (rollbackLoggingLevel == LoggingLevel.WARN && log.isWarnEnabled()) {
284 if (rollbackOnly) {
285 log.warn("Transaction rollback ({}) redelivered({}) for {} due exchange was marked for rollbackOnly", new Object[]{transactionKey, redelivered, ids});
286 } else {
287 log.warn("Transaction rollback ({}) redelivered({}) for {} caught: {}", new Object[]{transactionKey, redelivered, ids, e.getMessage()});
288 }
289 } else if (rollbackLoggingLevel == LoggingLevel.INFO && log.isInfoEnabled()) {
290 if (rollbackOnly) {
291 log.info("Transaction rollback ({}) redelivered({}) for {} due exchange was marked for rollbackOnly", new Object[]{transactionKey, redelivered, ids});
292 } else {
293 log.info("Transaction rollback ({}) redelivered({}) for {} caught: {}", new Object[]{transactionKey, redelivered, ids, e.getMessage()});
294 }
295 } else if (rollbackLoggingLevel == LoggingLevel.DEBUG && log.isDebugEnabled()) {
296 if (rollbackOnly) {
297 log.debug("Transaction rollback ({}) redelivered({}) for {} due exchange was marked for rollbackOnly", new Object[]{transactionKey, redelivered, ids});
298 } else {
299 log.debug("Transaction rollback ({}) redelivered({}) for {} caught: {}", new Object[]{transactionKey, redelivered, ids, e.getMessage()});
300 }
301 } else if (rollbackLoggingLevel == LoggingLevel.TRACE && log.isTraceEnabled()) {
302 if (rollbackOnly) {
303 log.trace("Transaction rollback ({}) redelivered({}) for {} due exchange was marked for rollbackOnly", new Object[]{transactionKey, redelivered, ids});
304 } else {
305 log.trace("Transaction rollback ({}) redelivered({}) for {} caught: {}", new Object[]{transactionKey, redelivered, ids, e.getMessage()});
306 }
307 }
308 }
309
310 private static String propagationBehaviorToString(int propagationBehavior) {
311 String rc;
312 switch (propagationBehavior) {
313 case TransactionDefinition.PROPAGATION_MANDATORY:
314 rc = "PROPAGATION_MANDATORY";
315 break;
316 case TransactionDefinition.PROPAGATION_NESTED:
317 rc = "PROPAGATION_NESTED";
318 break;
319 case TransactionDefinition.PROPAGATION_NEVER:
320 rc = "PROPAGATION_NEVER";
321 break;
322 case TransactionDefinition.PROPAGATION_NOT_SUPPORTED:
323 rc = "PROPAGATION_NOT_SUPPORTED";
324 break;
325 case TransactionDefinition.PROPAGATION_REQUIRED:
326 rc = "PROPAGATION_REQUIRED";
327 break;
328 case TransactionDefinition.PROPAGATION_REQUIRES_NEW:
329 rc = "PROPAGATION_REQUIRES_NEW";
330 break;
331 case TransactionDefinition.PROPAGATION_SUPPORTS:
332 rc = "PROPAGATION_SUPPORTS";
333 break;
334 default:
335 rc = "UNKNOWN";
336 }
337 return rc;
338 }
339
340 }