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