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 }