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