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