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