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