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