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