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     */
017    package org.apache.camel.spring.spi;
018    
019    import java.util.concurrent.CountDownLatch;
020    import java.util.concurrent.ScheduledExecutorService;
021    
022    import org.apache.camel.AsyncCallback;
023    import org.apache.camel.CamelContext;
024    import org.apache.camel.Exchange;
025    import org.apache.camel.LoggingLevel;
026    import org.apache.camel.Predicate;
027    import org.apache.camel.Processor;
028    import org.apache.camel.processor.RedeliveryErrorHandler;
029    import org.apache.camel.processor.RedeliveryPolicy;
030    import org.apache.camel.processor.exceptionpolicy.ExceptionPolicyStrategy;
031    import org.apache.camel.util.CamelLogger;
032    import org.apache.camel.util.ExchangeHelper;
033    import org.apache.camel.util.ObjectHelper;
034    import org.springframework.transaction.TransactionDefinition;
035    import org.springframework.transaction.TransactionStatus;
036    import org.springframework.transaction.support.TransactionCallbackWithoutResult;
037    import 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     */
045    public 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, retryWhile, executorService);
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 (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 " + this;
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    }