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