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 */ 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, false, retryWhile, executorService, null); 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 (transactionTemplate.getPropagationBehavior() != TransactionDefinition.PROPAGATION_REQUIRES_NEW && 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 " + TransactionErrorHandler.this.toString(); 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}