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.RuntimeCamelException;
029import org.apache.camel.processor.RedeliveryErrorHandler;
030import org.apache.camel.processor.RedeliveryPolicy;
031import org.apache.camel.processor.exceptionpolicy.ExceptionPolicyStrategy;
032import org.apache.camel.spi.CamelLogger;
033import org.apache.camel.support.AsyncProcessorSupport;
034import org.apache.camel.support.ExchangeHelper;
035import org.apache.camel.util.ObjectHelper;
036import org.springframework.transaction.TransactionDefinition;
037import org.springframework.transaction.TransactionStatus;
038import org.springframework.transaction.support.TransactionCallbackWithoutResult;
039import org.springframework.transaction.support.TransactionTemplate;
040
041/**
042 * The <a href="http://camel.apache.org/transactional-client.html">Transactional Client</a>
043 * EIP pattern.
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) {
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) {
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 = RuntimeCamelException.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        awaitManager.process(new AsyncProcessorSupport() {
226            @Override
227            public boolean process(Exchange exchange, AsyncCallback callback) {
228                return TransactionErrorHandler.super.process(exchange, callback);
229            }
230        }, exchange);
231    }
232
233    /**
234     * Logs the transaction begin
235     */
236    private void logTransactionBegin(String redelivered, String ids) {
237        if (log.isDebugEnabled()) {
238            log.debug("Transaction begin ({}) redelivered({}) for {})", transactionKey, redelivered, ids);
239        }
240    }
241
242    /**
243     * Logs the transaction commit
244     */
245    private void logTransactionCommit(String redelivered, String ids) {
246        if ("true".equals(redelivered)) {
247            // okay its a redelivered message so log at INFO level if rollbackLoggingLevel is INFO or higher
248            // this allows people to know that the redelivered message was committed this time
249            if (rollbackLoggingLevel == LoggingLevel.INFO || rollbackLoggingLevel == LoggingLevel.WARN || rollbackLoggingLevel == LoggingLevel.ERROR) {
250                log.info("Transaction commit ({}) redelivered({}) for {})", transactionKey, redelivered, ids);
251                // return after we have logged
252                return;
253            }
254        }
255
256        // log non redelivered by default at DEBUG level
257        log.debug("Transaction commit ({}) redelivered({}) for {})", transactionKey, redelivered, ids);
258    }
259
260    /**
261     * Logs the transaction rollback.
262     */
263    private void logTransactionRollback(String redelivered, String ids, Throwable e, boolean rollbackOnly) {
264        if (rollbackLoggingLevel == LoggingLevel.OFF) {
265            return;
266        } else if (rollbackLoggingLevel == LoggingLevel.ERROR && log.isErrorEnabled()) {
267            if (rollbackOnly) {
268                log.error("Transaction rollback ({}) redelivered({}) for {} due exchange was marked for rollbackOnly", transactionKey, redelivered, ids);
269            } else {
270                log.error("Transaction rollback ({}) redelivered({}) for {} caught: {}", transactionKey, redelivered, ids, e.getMessage());
271            }
272        } else if (rollbackLoggingLevel == LoggingLevel.WARN && log.isWarnEnabled()) {
273            if (rollbackOnly) {
274                log.warn("Transaction rollback ({}) redelivered({}) for {} due exchange was marked for rollbackOnly", transactionKey, redelivered, ids);
275            } else {
276                log.warn("Transaction rollback ({}) redelivered({}) for {} caught: {}", transactionKey, redelivered, ids, e.getMessage());
277            }
278        } else if (rollbackLoggingLevel == LoggingLevel.INFO && log.isInfoEnabled()) {
279            if (rollbackOnly) {
280                log.info("Transaction rollback ({}) redelivered({}) for {} due exchange was marked for rollbackOnly", transactionKey, redelivered, ids);
281            } else {
282                log.info("Transaction rollback ({}) redelivered({}) for {} caught: {}", transactionKey, redelivered, ids, e.getMessage());
283            }
284        } else if (rollbackLoggingLevel == LoggingLevel.DEBUG && log.isDebugEnabled()) {
285            if (rollbackOnly) {
286                log.debug("Transaction rollback ({}) redelivered({}) for {} due exchange was marked for rollbackOnly", transactionKey, redelivered, ids);
287            } else {
288                log.debug("Transaction rollback ({}) redelivered({}) for {} caught: {}", transactionKey, redelivered, ids, e.getMessage());
289            }
290        } else if (rollbackLoggingLevel == LoggingLevel.TRACE && log.isTraceEnabled()) {
291            if (rollbackOnly) {
292                log.trace("Transaction rollback ({}) redelivered({}) for {} due exchange was marked for rollbackOnly", transactionKey, redelivered, ids);
293            } else {
294                log.trace("Transaction rollback ({}) redelivered({}) for {} caught: {}", transactionKey, redelivered, ids, e.getMessage());
295            }
296        }
297    }
298
299    private static String propagationBehaviorToString(int propagationBehavior) {
300        String rc;
301        switch (propagationBehavior) {
302        case TransactionDefinition.PROPAGATION_MANDATORY:
303            rc = "PROPAGATION_MANDATORY";
304            break;
305        case TransactionDefinition.PROPAGATION_NESTED:
306            rc = "PROPAGATION_NESTED";
307            break;
308        case TransactionDefinition.PROPAGATION_NEVER:
309            rc = "PROPAGATION_NEVER";
310            break;
311        case TransactionDefinition.PROPAGATION_NOT_SUPPORTED:
312            rc = "PROPAGATION_NOT_SUPPORTED";
313            break;
314        case TransactionDefinition.PROPAGATION_REQUIRED:
315            rc = "PROPAGATION_REQUIRED";
316            break;
317        case TransactionDefinition.PROPAGATION_REQUIRES_NEW:
318            rc = "PROPAGATION_REQUIRES_NEW";
319            break;
320        case TransactionDefinition.PROPAGATION_SUPPORTS:
321            rc = "PROPAGATION_SUPPORTS";
322            break;
323        default:
324            rc = "UNKNOWN";
325        }
326        return rc;
327    }
328
329}