/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.camel.cdi.transaction;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import javax.transaction.TransactionRolledbackException;
import org.apache.camel.AsyncCallback;
import org.apache.camel.AsyncProcessor;
import org.apache.camel.CamelContext;
import org.apache.camel.Exchange;
import org.apache.camel.LoggingLevel;
import org.apache.camel.Navigate;
import org.apache.camel.Processor;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.processor.ErrorHandlerSupport;
import org.apache.camel.processor.exceptionpolicy.ExceptionPolicyStrategy;
import org.apache.camel.spi.ShutdownPrepared;
import org.apache.camel.util.ExchangeHelper;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.ServiceHelper;
/**
* Does transactional execution according given policy. This class is based on
* {@link org.apache.camel.spring.spi.TransactionErrorHandler} excluding
* redelivery functionality. In the Spring implementation redelivering is done
* within the transaction which is not appropriate in JTA since every error
* breaks the current transaction.
*/
public class TransactionErrorHandler extends ErrorHandlerSupport
implements AsyncProcessor, ShutdownPrepared, Navigate<Processor> {
protected final Processor output;
protected volatile boolean preparingShutdown;
private ExceptionPolicyStrategy exceptionPolicy;
private JtaTransactionPolicy transactionPolicy;
private final String transactionKey;
private final LoggingLevel rollbackLoggingLevel;
/**
* Creates the transaction error handler.
*
* @param camelContext
* the camel context
* @param output
* outer processor that should use this default error handler
* @param exceptionPolicyStrategy
* strategy for onException handling
* @param transactionPolicy
* the transaction policy
* @param executorService
* the {@link java.util.concurrent.ScheduledExecutorService} to
* be used for redelivery thread pool. Can be <tt>null</tt>.
* @param rollbackLoggingLevel
* logging level to use for logging transaction rollback occurred
*/
public TransactionErrorHandler(CamelContext camelContext, Processor output,
ExceptionPolicyStrategy exceptionPolicyStrategy, JtaTransactionPolicy transactionPolicy,
ScheduledExecutorService executorService, LoggingLevel rollbackLoggingLevel) {
this.output = output;
this.transactionPolicy = transactionPolicy;
this.rollbackLoggingLevel = rollbackLoggingLevel;
this.transactionKey = ObjectHelper.getIdentityHashCode(transactionPolicy);
setExceptionPolicy(exceptionPolicyStrategy);
}
public void process(Exchange exchange) throws Exception {
// we have to run this synchronously as a JTA Transaction does *not*
// support using multiple threads to span a transaction
if (exchange.getUnitOfWork().isTransactedBy(transactionKey)) {
// already transacted by this transaction template
// so lets just let the error handler process it
processByErrorHandler(exchange);
} else {
// not yet wrapped in transaction so lets do that
// and then have it invoke the error handler from within that
// transaction
processInTransaction(exchange);
}
}
public boolean process(Exchange exchange, AsyncCallback callback) {
// invoke this synchronous method as JTA Transaction does *not*
// support using multiple threads to span a transaction
try {
process(exchange);
} catch (Throwable e) {
exchange.setException(e);
}
// notify callback we are done synchronously
callback.done(true);
return true;
}
protected void processInTransaction(final Exchange exchange) throws Exception {
// is the exchange redelivered, for example JMS brokers support such
// details
Boolean externalRedelivered = exchange.isExternalRedelivered();
final String redelivered = externalRedelivered != null ? externalRedelivered.toString() : "unknown";
final String ids = ExchangeHelper.logIds(exchange);
try {
// mark the beginning of this transaction boundary
exchange.getUnitOfWork().beginTransactedBy(transactionKey);
// do in transaction
logTransactionBegin(redelivered, ids);
doInTransactionTemplate(exchange);
logTransactionCommit(redelivered, ids);
} catch (TransactionRolledbackException e) {
// do not set as exception, as its just a dummy exception to force
// spring TX to rollback
logTransactionRollback(redelivered, ids, null, true);
} catch (Throwable e) {
exchange.setException(e);
logTransactionRollback(redelivered, ids, e, false);
} finally {
// mark the end of this transaction boundary
exchange.getUnitOfWork().endTransactedBy(transactionKey);
}
// if it was a local rollback only then remove its marker so outer
// transaction wont see the marker
Boolean onlyLast = (Boolean) exchange.removeProperty(Exchange.ROLLBACK_ONLY_LAST);
if (onlyLast != null && onlyLast) {
// we only want this logged at debug level
if (log.isDebugEnabled()) {
// log exception if there was a cause exception so we have the
// stack trace
Exception cause = exchange.getException();
if (cause != null) {
log.debug("Transaction rollback ({}) redelivered({}) for {} "
+ "due exchange was marked for rollbackOnlyLast and caught: ",
transactionKey, redelivered, ids, cause);
} else {
log.debug("Transaction rollback ({}) redelivered({}) for {} "
+ "due exchange was marked for rollbackOnlyLast",
transactionKey, redelivered, ids);
}
}
// remove caused exception due we was marked as rollback only last
// so by removing the exception, any outer transaction will not be
// affected
exchange.setException(null);
}
}
public void setTransactionPolicy(JtaTransactionPolicy transactionPolicy) {
this.transactionPolicy = transactionPolicy;
}
protected void doInTransactionTemplate(final Exchange exchange) throws Throwable {
// spring transaction template is working best with rollback if you
// throw it a runtime exception
// otherwise it may not rollback messages send to JMS queues etc.
transactionPolicy.run(new JtaTransactionPolicy.Runnable() {
@Override
public void run() throws Throwable {
// wrapper exception to throw if the exchange failed
// IMPORTANT: Must be a runtime exception to let Spring regard
// it as to do "rollback"
Throwable rce;
// and now let process the exchange by the error handler
processByErrorHandler(exchange);
// after handling and still an exception or marked as rollback
// only then rollback
if (exchange.getException() != null || exchange.isRollbackOnly()) {
// wrap exception in transacted exception
if (exchange.getException() != null) {
rce = exchange.getException();
} else {
// create dummy exception to force spring transaction
// manager to rollback
rce = new TransactionRolledbackException();
}
// throw runtime exception to force rollback (which works
// best to rollback with Spring transaction manager)
if (log.isTraceEnabled()) {
log.trace("Throwing runtime exception to force transaction to rollback on {}",
transactionPolicy);
}
throw rce;
}
}
});
}
/**
* Processes the {@link Exchange} using the error handler.
* <p/>
* This implementation will invoke ensure this occurs synchronously, that
* means if the async routing engine did kick in, then this implementation
* will wait for the task to complete before it continues.
*
* @param exchange
* the exchange
*/
protected void processByErrorHandler(final Exchange exchange) {
try {
output.process(exchange);
} catch (Throwable e) {
throw new RuntimeCamelException(e);
}
}
/**
* Logs the transaction begin
*/
private void logTransactionBegin(String redelivered, String ids) {
if (log.isDebugEnabled()) {
log.debug("Transaction begin ({}) redelivered({}) for {})",
transactionKey, redelivered, ids);
}
}
/**
* Logs the transaction commit
*/
private void logTransactionCommit(String redelivered, String ids) {
if ("true".equals(redelivered)) {
// okay its a redelivered message so log at INFO level if
// rollbackLoggingLevel is INFO or higher
// this allows people to know that the redelivered message was
// committed this time
if (rollbackLoggingLevel == LoggingLevel.INFO || rollbackLoggingLevel == LoggingLevel.WARN
|| rollbackLoggingLevel == LoggingLevel.ERROR) {
log.info("Transaction commit ({}) redelivered({}) for {})",
transactionKey, redelivered, ids);
// return after we have logged
return;
}
}
// log non redelivered by default at DEBUG level
log.debug("Transaction commit ({}) redelivered({}) for {})", transactionKey, redelivered, ids);
}
/**
* Logs the transaction rollback.
*/
private void logTransactionRollback(String redelivered, String ids, Throwable e, boolean rollbackOnly) {
if (rollbackLoggingLevel == LoggingLevel.OFF) {
return;
} else if (rollbackLoggingLevel == LoggingLevel.ERROR && log.isErrorEnabled()) {
if (rollbackOnly) {
log.error("Transaction rollback ({}) redelivered({}) for {} due exchange was marked for rollbackOnly",
transactionKey, redelivered, ids);
} else {
log.error("Transaction rollback ({}) redelivered({}) for {} caught: {}",
transactionKey, redelivered, ids, e.getMessage());
}
} else if (rollbackLoggingLevel == LoggingLevel.WARN && log.isWarnEnabled()) {
if (rollbackOnly) {
log.warn("Transaction rollback ({}) redelivered({}) for {} due exchange was marked for rollbackOnly",
transactionKey, redelivered, ids);
} else {
log.warn("Transaction rollback ({}) redelivered({}) for {} caught: {}",
transactionKey, redelivered, ids, e.getMessage());
}
} else if (rollbackLoggingLevel == LoggingLevel.INFO && log.isInfoEnabled()) {
if (rollbackOnly) {
log.info("Transaction rollback ({}) redelivered({}) for {} due exchange was marked for rollbackOnly",
transactionKey, redelivered, ids);
} else {
log.info("Transaction rollback ({}) redelivered({}) for {} caught: {}",
transactionKey, redelivered, ids, e.getMessage());
}
} else if (rollbackLoggingLevel == LoggingLevel.DEBUG && log.isDebugEnabled()) {
if (rollbackOnly) {
log.debug("Transaction rollback ({}) redelivered({}) for {} due exchange was marked for rollbackOnly",
transactionKey, redelivered, ids);
} else {
log.debug("Transaction rollback ({}) redelivered({}) for {} caught: {}",
transactionKey, redelivered, ids, e.getMessage());
}
} else if (rollbackLoggingLevel == LoggingLevel.TRACE && log.isTraceEnabled()) {
if (rollbackOnly) {
log.trace("Transaction rollback ({}) redelivered({}) for {} due exchange was marked for rollbackOnly",
transactionKey, redelivered, ids);
} else {
log.trace("Transaction rollback ({}) redelivered({}) for {} caught: {}",
transactionKey, redelivered, ids, e.getMessage());
}
}
}
public void setExceptionPolicy(ExceptionPolicyStrategy exceptionPolicy) {
this.exceptionPolicy = exceptionPolicy;
}
public ExceptionPolicyStrategy getExceptionPolicy() {
return exceptionPolicy;
}
@Override
public Processor getOutput() {
return output;
}
@Override
protected void doStart() throws Exception {
ServiceHelper.startServices(output);
preparingShutdown = false;
}
@Override
protected void doStop() throws Exception {
// noop, do not stop any services which we only do when shutting down
// as the error handler can be context scoped, and should not stop in
// case a route stops
}
@Override
protected void doShutdown() throws Exception {
ServiceHelper.stopAndShutdownServices(output);
}
@Override
public boolean supportTransacted() {
return true;
}
public boolean hasNext() {
return output != null;
}
@Override
public List<Processor> next() {
if (!hasNext()) {
return null;
}
List<Processor> answer = new ArrayList<>(1);
answer.add(output);
return answer;
}
@Override
public void prepareShutdown(boolean suspendOnly, boolean forced) {
// prepare for shutdown, eg do not allow redelivery if configured
log.trace("Prepare shutdown on error handler {}", this);
preparingShutdown = true;
}
}