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