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