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 org.apache.camel.AsyncCallback; 020 import org.apache.camel.CamelContext; 021 import org.apache.camel.Exchange; 022 import org.apache.camel.Predicate; 023 import org.apache.camel.Processor; 024 import org.apache.camel.processor.CamelLogger; 025 import org.apache.camel.processor.RedeliveryErrorHandler; 026 import org.apache.camel.processor.RedeliveryPolicy; 027 import org.apache.camel.processor.exceptionpolicy.ExceptionPolicyStrategy; 028 import org.apache.camel.util.ObjectHelper; 029 import org.springframework.transaction.TransactionDefinition; 030 import org.springframework.transaction.TransactionStatus; 031 import org.springframework.transaction.support.TransactionCallbackWithoutResult; 032 import org.springframework.transaction.support.TransactionTemplate; 033 034 /** 035 * The <a href="http://camel.apache.org/transactional-client.html">Transactional Client</a> 036 * EIP pattern. 037 * 038 * @version 039 */ 040 public class TransactionErrorHandler extends RedeliveryErrorHandler { 041 042 private final TransactionTemplate transactionTemplate; 043 private final String transactionKey; 044 045 /** 046 * Creates the transaction error handler. 047 * 048 * @param camelContext the camel context 049 * @param output outer processor that should use this default error handler 050 * @param logger logger to use for logging failures and redelivery attempts 051 * @param redeliveryProcessor an optional processor to run before redelivery attempt 052 * @param redeliveryPolicy policy for redelivery 053 * @param handledPolicy policy for handling failed exception that are moved to the dead letter queue 054 * @param exceptionPolicyStrategy strategy for onException handling 055 * @param transactionTemplate the transaction template 056 * @param retryWhile retry while 057 */ 058 public TransactionErrorHandler(CamelContext camelContext, Processor output, CamelLogger logger, Processor redeliveryProcessor, 059 RedeliveryPolicy redeliveryPolicy, Predicate handledPolicy, ExceptionPolicyStrategy exceptionPolicyStrategy, 060 TransactionTemplate transactionTemplate, Predicate retryWhile) { 061 super(camelContext, output, logger, redeliveryProcessor, redeliveryPolicy, handledPolicy, null, null, false, retryWhile); 062 setExceptionPolicy(exceptionPolicyStrategy); 063 this.transactionTemplate = transactionTemplate; 064 this.transactionKey = ObjectHelper.getIdentityHashCode(transactionTemplate); 065 } 066 067 public boolean supportTransacted() { 068 return true; 069 } 070 071 @Override 072 public String toString() { 073 if (output == null) { 074 // if no output then don't do any description 075 return ""; 076 } 077 return "TransactionErrorHandler:" 078 + propagationBehaviorToString(transactionTemplate.getPropagationBehavior()) 079 + "[" + getOutput() + "]"; 080 } 081 082 @Override 083 public void process(Exchange exchange) throws Exception { 084 // we have to run this synchronously as Spring Transaction does *not* support 085 // using multiple threads to span a transaction 086 if (exchange.getUnitOfWork().isTransactedBy(transactionKey)) { 087 // already transacted by this transaction template 088 // so lets just let the error handler process it 089 processByErrorHandler(exchange); 090 } else { 091 // not yet wrapped in transaction so lets do that 092 // and then have it invoke the error handler from within that transaction 093 processInTransaction(exchange); 094 } 095 } 096 097 @Override 098 public boolean process(Exchange exchange, AsyncCallback callback) { 099 // invoke ths synchronous method as Spring Transaction does *not* support 100 // using multiple threads to span a transaction 101 try { 102 process(exchange); 103 } catch (Throwable e) { 104 exchange.setException(e); 105 } 106 107 // notify callback we are done synchronously 108 callback.done(true); 109 return true; 110 } 111 112 protected void processInTransaction(final Exchange exchange) throws Exception { 113 try { 114 // mark the beginning of this transaction boundary 115 exchange.getUnitOfWork().beginTransactedBy(transactionKey); 116 117 if (log.isDebugEnabled()) { 118 log.debug("Transaction begin (" + transactionKey + ") for ExchangeId: " + exchange.getExchangeId()); 119 } 120 121 doInTransactionTemplate(exchange); 122 123 if (log.isDebugEnabled()) { 124 log.debug("Transaction commit (" + transactionKey + ") for ExchangeId: " + exchange.getExchangeId()); 125 } 126 } catch (TransactionRollbackException e) { 127 // ignore as its just a dummy exception to force spring TX to rollback 128 if (log.isDebugEnabled()) { 129 log.debug("Transaction rollback (" + transactionKey + ") for ExchangeId: " + exchange.getExchangeId() + " due exchange was marked for rollbackOnly"); 130 } 131 } catch (Exception e) { 132 log.warn("Transaction rollback (" + transactionKey + ") for ExchangeId: " + exchange.getExchangeId() + " due exception: " + e.getMessage()); 133 exchange.setException(e); 134 } finally { 135 // mark the end of this transaction boundary 136 exchange.getUnitOfWork().endTransactedBy(transactionKey); 137 } 138 139 // if it was a local rollback only then remove its marker so outer transaction wont see the marker 140 Boolean onlyLast = (Boolean) exchange.removeProperty(Exchange.ROLLBACK_ONLY_LAST); 141 if (onlyLast != null && onlyLast) { 142 if (log.isDebugEnabled()) { 143 // log exception if there was a cause exception so we have the stacktrace 144 Exception cause = exchange.getException(); 145 if (cause != null) { 146 log.debug("Transaction rollback (" + transactionKey + ") for ExchangeId: " + exchange.getExchangeId() 147 + " due exchange was marked for rollbackOnlyLast and due exception: ", cause); 148 } else { 149 log.debug("Transaction rollback (" + transactionKey + ") for ExchangeId: " + exchange.getExchangeId() 150 + " due exchange was marked for rollbackOnlyLast"); 151 } 152 } 153 // remove caused exception due we was marked as rollback only last 154 // so by removing the exception, any outer transaction will not be affected 155 exchange.setException(null); 156 } 157 } 158 159 protected void doInTransactionTemplate(final Exchange exchange) { 160 161 // spring transaction template is working best with rollback if you throw it a runtime exception 162 // otherwise it may not rollback messages send to JMS queues etc. 163 164 transactionTemplate.execute(new TransactionCallbackWithoutResult() { 165 protected void doInTransactionWithoutResult(TransactionStatus status) { 166 // wrapper exception to throw if the exchange failed 167 // IMPORTANT: Must be a runtime exception to let Spring regard it as to do "rollback" 168 RuntimeException rce; 169 170 // and now let process the exchange by the error handler 171 processByErrorHandler(exchange); 172 173 // after handling and still an exception or marked as rollback only then rollback 174 if (exchange.getException() != null || exchange.isRollbackOnly()) { 175 176 // wrap exception in transacted exception 177 if (exchange.getException() != null) { 178 rce = ObjectHelper.wrapRuntimeCamelException(exchange.getException()); 179 } else { 180 // create dummy exception to force spring transaction manager to rollback 181 rce = new TransactionRollbackException(); 182 } 183 184 if (!status.isRollbackOnly()) { 185 status.setRollbackOnly(); 186 } 187 188 // throw runtime exception to force rollback (which works best to rollback with Spring transaction manager) 189 throw rce; 190 } 191 } 192 }); 193 } 194 195 /** 196 * Processes the {@link Exchange} using the error handler. 197 * <p/> 198 * This implementation will invoke ensure this occurs synchronously, that means if the async routing engine 199 * did kick in, then this implementation will wait for the task to complete before it continues. 200 * 201 * @param exchange the exchange 202 */ 203 protected void processByErrorHandler(final Exchange exchange) { 204 // must invoke the async method with empty callback to have it invoke the 205 // super.processErrorHandler 206 // we are transacted so we have to route synchronously so don't worry about returned 207 // value from the process method 208 // and the camel routing engine will detect this is an transacted Exchange and route 209 // it fully synchronously so we don't have to wait here if we hit an async endpoint 210 // all that is taken care of in the camel-core 211 super.process(exchange, new AsyncCallback() { 212 public void done(boolean doneSync) { 213 // noop 214 } 215 }); 216 } 217 218 private static String propagationBehaviorToString(int propagationBehavior) { 219 String rc; 220 switch (propagationBehavior) { 221 case TransactionDefinition.PROPAGATION_MANDATORY: 222 rc = "PROPAGATION_MANDATORY"; 223 break; 224 case TransactionDefinition.PROPAGATION_NESTED: 225 rc = "PROPAGATION_NESTED"; 226 break; 227 case TransactionDefinition.PROPAGATION_NEVER: 228 rc = "PROPAGATION_NEVER"; 229 break; 230 case TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 231 rc = "PROPAGATION_NOT_SUPPORTED"; 232 break; 233 case TransactionDefinition.PROPAGATION_REQUIRED: 234 rc = "PROPAGATION_REQUIRED"; 235 break; 236 case TransactionDefinition.PROPAGATION_REQUIRES_NEW: 237 rc = "PROPAGATION_REQUIRES_NEW"; 238 break; 239 case TransactionDefinition.PROPAGATION_SUPPORTS: 240 rc = "PROPAGATION_SUPPORTS"; 241 break; 242 default: 243 rc = "UNKNOWN"; 244 } 245 return rc; 246 } 247 248 }