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