spring.datasource.tomcat.test-on-borrow=true
spring.datasource.tomcat.validationQuery=SELECT 1 from dummy
@Repository
@Transactional
public interface ItemRepository extends JpaRepository<Item, Integer> {
@Query(value = "SELECT ID, CONTENT FROM ITEM WHERE ID=?1 WITH HINT( RESULT_LAG ('hana_sr', 60) )", nativeQuery = true)
Item findOneOnSecondary(Integer id);
}
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
@RestController
@RequestMapping(path = "/item")
public class ItemController {
@Autowired
private ItemRepository itemRepository;
@Retryable(backoff = @Backoff(delay = 10000), maxAttempts = 60)
@RequestMapping(path = "{id}")
public Item getItem(@PathVariable("id") Integer id) {
return this.itemRepository.findOneOnSecondary( id );
}
@Retryable(backoff = @Backoff(delay = 10000), maxAttempts = 60)
@RequestMapping(path = "", method = RequestMethod.POST)
public int addItem(@RequestParam(name = "content") String content) {
Item item = new Item();
item.setContent( content );
this.itemRepository.save( item );
return item.getId();
}
@Retryable(backoff = @Backoff(delay = 10000), maxAttempts = 60)
@RequestMapping(path = "{id}", method = RequestMethod.DELETE)
public void deleteItem(@PathVariable("id") Integer id) {
this.itemRepository.delete( id );
}
}
import java.sql.SQLNonTransientConnectionException;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.PersistenceUnit;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.resource.jdbc.spi.LogicalConnectionImplementor;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.orm.jpa.EntityManagerHolder;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryListener;
import org.springframework.transaction.CannotCreateTransactionException;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import com.sap.db.jdbc.exceptions.JDBCDriverException;
public class TakeoverHandlingRetryListener implements RetryListener {
@PersistenceUnit
private EntityManagerFactory emf;
@Override
public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
return true;
}
@Override
public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
}
@Override
public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
// check if the root cause is a connection failure
if ( isSQLNonTransientConnectionExceptionSapDB( throwable ) ) {
EntityManagerHolder emHolder = (EntityManagerHolder) TransactionSynchronizationManager.getResource( this.emf );
if ( emHolder != null ) {
EntityManager entityManager = emHolder.getEntityManager();
if ( entityManager != null ) {
SessionImplementor s = entityManager.unwrap( SessionImplementor.class );
try {
// extract the logical database connection
LogicalConnectionImplementor logicalConnection = s.getJdbcCoordinator().getLogicalConnection();
if ( logicalConnection.isOpen() ) {
// close the physical connection
JdbcUtils.closeConnection( logicalConnection.getPhysicalConnection() );
// manually disconnect
s.getJdbcCoordinator().getLogicalConnection().manualDisconnect();
}
}
catch (Exception e) {
// ignore
}
}
}
}
// check if a transaction couldn't be started because of an invalid state
else if ( throwable instanceof CannotCreateTransactionException && throwable.getCause() instanceof IllegalStateException ) {
EntityManagerHolder emHolder = (EntityManagerHolder) TransactionSynchronizationManager.getResource( this.emf );
if ( emHolder != null ) {
EntityManager entityManager = emHolder.getEntityManager();
if ( entityManager != null ) {
try {
// mark the transaction as read-only to prevent any changes to be written to the database
entityManager.getTransaction().setRollbackOnly();
}
catch (Exception e) {
// ignore
}
try {
// execute a commit which will reset the transaction state so it can be restarted
SessionImplementor s = entityManager.unwrap( SessionImplementor.class );
s.getTransactionCoordinator().getTransactionDriverControl().commit();
}
catch (Exception e) {
// ignore
}
}
}
}
}
private boolean isSQLNonTransientConnectionExceptionSapDB(Throwable throwable) {
// check if the exception indicates a connection loss
if ( throwable instanceof SQLNonTransientConnectionException || throwable instanceof JDBCDriverException ) {
return true;
}
// check the cause of the exception if there is one
if ( throwable.getCause() != null ) {
return isSQLNonTransientConnectionExceptionSapDB( throwable.getCause() );
}
return false;
}
}
@Bean
public RetryListener retryListener() {
return new TakeoverHandlingRetryListener();
}
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.sql.SQLNonTransientConnectionException;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.resource.jdbc.spi.LogicalConnectionImplementor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.orm.jpa.EntityManagerHolder;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.transaction.CannotCreateTransactionException;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.UnexpectedRollbackException;
import org.springframework.transaction.support.DefaultTransactionStatus;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.ReflectionUtils;
import com.sap.db.jdbc.exceptions.JDBCDriverException;
public class TakeoverHandlingJpaTransactionManager extends JpaTransactionManager {
private static final long serialVersionUID = 1L;
/**
* The maximum number of times a transaction start should be retried
*/
@Value("${transaction.retries:60}")
private int retries;
/**
* The number of milliseconds to wait between retries
*/
@Value("${transaction.timeout:10000}")
private long timeout;
public TakeoverHandlingJpaTransactionManager(EntityManagerFactory emf) {
super( emf );
}
/**
* {@inheritDoc}
*/
@Override
protected void doCommit(DefaultTransactionStatus status) {
EntityManager entityManager = getEntityManager( status.getTransaction() );
if ( entityManager == null ) {
// no entity manager found => execute normally
super.doCommit( status );
return;
}
SessionImplementor s = entityManager.unwrap( SessionImplementor.class );
LogicalConnectionImplementor logicalConnection = s.getJdbcCoordinator().getLogicalConnection();
if ( !isConnectionValid( logicalConnection ) ) {
try {
// roll back the transaction to reset the status
rollback( status );
}
catch (TransactionException e) {
// ignore
}
// throw an UnexpectedRollbackException to trigger the correct behavior in the parent commit() call
throw new UnexpectedRollbackException( "The connection is invalid" );
}
try {
// commit the transaction
super.doCommit( status );
}
catch (Exception e) {
// Check if the connection needs to be reset and then re-throw the exception
resetTransactionAndThrow( e, logicalConnection );
}
}
/**
* Get the entity manager either from the current transaction object or from the
* {@link TransactionSynchronizationManager}.
*
* @param transaction The current transaction object
* @return The entity manager if found, or {@code null} otherwise
*/
private EntityManager getEntityManager(Object transaction) {
// extract the entity manager from the transaction object
Method method = ReflectionUtils.findMethod( transaction.getClass(), "getEntityManagerHolder" );
method.setAccessible( true );
EntityManagerHolder emHolder = (EntityManagerHolder) ReflectionUtils.invokeMethod( method, transaction );
if ( emHolder == null ) {
// no entity manager holder found on the transaction object => try the TransactionSynchronizationManager
emHolder = (EntityManagerHolder) TransactionSynchronizationManager.getResource( getEntityManagerFactory() );
}
if ( emHolder == null ) {
return null;
}
return emHolder.getEntityManager();
}
/**
* Check if the {@link Throwable} is caused by a connection loss
*
* @param throwable The throwable to check
* @return {@code true} if the {@link Throwable} is caused by a connection loss, {@code false} otherwise
*/
private boolean isSQLNonTransientConnectionExceptionSapDB(Throwable throwable) {
if ( throwable instanceof SQLNonTransientConnectionException || throwable instanceof JDBCDriverException ) {
return true;
}
if ( throwable.getCause() != null ) {
return isSQLNonTransientConnectionExceptionSapDB( throwable.getCause() );
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
protected void doRollback(DefaultTransactionStatus status) {
try {
super.doRollback( status );
}
catch (Exception e) {
EntityManager entityManager = getEntityManager( status.getTransaction() );
if ( entityManager == null ) {
throw e;
}
SessionImplementor s = entityManager.unwrap( SessionImplementor.class );
LogicalConnectionImplementor logicalConnection = s.getJdbcCoordinator().getLogicalConnection();
// Check if the connection needs to be reset and then re-throw the exception
resetTransactionAndThrow( e, logicalConnection );
}
}
/**
* Check if the given logical connection is valid
*
* @param logicalConnection The connection to check
* @return {@code true} if the connection is valid, {@code false} otherwise
*/
private boolean isConnectionValid(LogicalConnectionImplementor logicalConnection) {
if ( logicalConnection.isOpen() ) {
try {
// check if the connection is valid
if ( logicalConnection.getPhysicalConnection().isValid( 1000 ) ) {
return true;
}
// the connection is invalid => disconnect manually
logicalConnection.manualDisconnect();
}
catch (SQLException e) {
try {
// Check if the connection needs to be reset and then re-throw the exception
resetTransactionAndThrow( e, logicalConnection );
}
catch (Exception ex) {
// ignore
}
}
}
return false;
}
/**
* Check if the given connection is potentially caused by a connection loss. If so, disconnect the connection
* explicitly before re-throwing the exception.
*
* @param e The exception to check
* @param logicalConnection The logical connection that will be disconnection if necessary
*/
private void resetTransactionAndThrow(Exception e, LogicalConnectionImplementor logicalConnection) {
if ( isSQLNonTransientConnectionExceptionSapDB( e ) ) {
try {
if ( logicalConnection.isOpen() ) {
logicalConnection.manualDisconnect();
}
}
catch (Exception ex) {
// ignore
}
sneakyThrow( e );
}
else {
sneakyThrow( e );
}
}
/**
* Sneakily throw an exception
*
* @param e The exception to throw
* @throws E The type of the exception to throw
* @see <a href="https://www.baeldung.com/java-sneaky-throws">https://www.baeldung.com/java-sneaky-throws</a>
*/
@SuppressWarnings("unchecked")
private static <E extends Exception> void sneakyThrow(Exception e) throws E {
throw (E) e;
}
/**
* {@inheritDoc}
*/
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
EntityManager entityManager = getEntityManager( transaction );
if ( entityManager == null ) {
// no entity manager found => execute normally
super.doBegin( transaction, definition );
return;
}
SessionImplementor s = entityManager.unwrap( SessionImplementor.class );
int count = 0;
do {
LogicalConnectionImplementor logicalConnection = s.getJdbcCoordinator().getLogicalConnection();
// check if the connection is valid
if ( !isConnectionValid( logicalConnection ) ) {
try {
Thread.sleep( this.timeout );
}
catch (InterruptedException ex) {
sneakyThrow( ex );
}
continue;
}
try {
super.doBegin( transaction, definition );
}
catch (CannotCreateTransactionException e) {
if ( e.getCause() instanceof IllegalStateException ) {
try {
// mark the transaction as read-only to prevent any changes to be written to the database
entityManager.getTransaction().setRollbackOnly();
}
catch (Exception ex) {
// ignore
}
try {
// execute a commit which will reset the transaction state so it can be restarted
s.getTransactionCoordinator().getTransactionDriverControl().commit();
}
catch (Exception ex) {
// ignore
}
try {
// wait for the timeout period to start the next try
Thread.sleep( this.timeout );
}
catch (InterruptedException ex) {
sneakyThrow( ex );
}
count++;
continue;
}
throw e;
}
return;
} while ( count < this.retries );
if ( count >= this.retries ) {
throw new CannotCreateTransactionException( "Timeout reached while trying to create a transaction" );
}
}
}
@PersistenceUnit
private EntityManagerFactory emf;
@Bean
public PlatformTransactionManager transactionManager() {
return new TakeoverHandlingJpaTransactionManager( this.emf );
}
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
13 | |
11 | |
10 | |
9 | |
9 | |
7 | |
6 | |
5 | |
5 | |
5 |