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