#329 - Translate driver exceptions in R2dbcTransactionManager.

This commit is contained in:
Mark Paluch
2020-05-11 16:58:23 +02:00
parent 53b84e6a08
commit af7dea07ad
2 changed files with 115 additions and 9 deletions

View File

@@ -18,13 +18,17 @@ package org.springframework.data.r2dbc.connectionfactory;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.IsolationLevel;
import io.r2dbc.spi.R2dbcException;
import io.r2dbc.spi.Result;
import reactor.core.publisher.Mono;
import java.time.Duration;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.dao.DataAccessException;
import org.springframework.data.r2dbc.core.DatabaseClient;
import org.springframework.data.r2dbc.support.R2dbcExceptionSubclassTranslator;
import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator;
import org.springframework.lang.Nullable;
import org.springframework.transaction.CannotCreateTransactionException;
import org.springframework.transaction.TransactionDefinition;
@@ -76,6 +80,8 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
private boolean enforceReadOnly = false;
private R2dbcExceptionTranslator exceptionTranslator = new R2dbcExceptionSubclassTranslator();
/**
* Create a new @link ConnectionFactoryTransactionManager} instance. A ConnectionFactory has to be set to be able to
* use it.
@@ -124,6 +130,19 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
return this.connectionFactory;
}
/**
* Set the exception translator for this instance.
* <p>
* If no custom translator is provided, a default {@link R2dbcExceptionSubclassTranslator} is used which translates
* {@link R2dbcException}'s subclasses into Springs {@link DataAccessException} hierarchy.
*
* @see R2dbcExceptionSubclassTranslator
* @since 1.1
*/
public void setExceptionTranslator(R2dbcExceptionTranslator exceptionTranslator) {
this.exceptionTranslator = exceptionTranslator;
}
/**
* Obtain the {@link ConnectionFactory} for actual use.
*
@@ -242,17 +261,22 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
}
}).thenReturn(con).onErrorResume(e -> {
CannotCreateTransactionException ex = new CannotCreateTransactionException(
"Could not open R2DBC Connection for transaction", e);
if (txObject.isNewConnectionHolder()) {
return ConnectionFactoryUtils.releaseConnection(con, obtainConnectionFactory()).doOnTerminate(() -> {
txObject.setConnectionHolder(null, false);
}).then(Mono.error(ex));
}).then(Mono.error(e));
}
return Mono.error(ex);
return Mono.error(e);
});
}).onErrorResume(e -> {
CannotCreateTransactionException ex = new CannotCreateTransactionException(
"Could not open R2DBC Connection for transaction",
e instanceof R2dbcException ? potentiallyTranslateException("Open R2DBC Connection", (R2dbcException) e)
: e);
return Mono.error(ex);
});
}).then();
}
@@ -321,7 +345,7 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
}
return Mono.from(connection.commitTransaction())
.onErrorMap(ex -> new TransactionSystemException("Could not commit R2DBC transaction", ex));
.onErrorMap(R2dbcException.class, ex -> translateException("R2DBC commit", ex));
}
/*
@@ -339,7 +363,7 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
}
return Mono.from(connection.rollbackTransaction())
.onErrorMap(ex -> new TransactionSystemException("Could not roll back R2DBC transaction", ex));
.onErrorMap(R2dbcException.class, ex -> translateException("R2DBC rollback", ex));
}
/*
@@ -496,6 +520,31 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
return null;
}
/**
* Translate the given R2DBC commit/rollback exception to a common Spring exception to propagate from the
* {@link #commit}/{@link #rollback} call.
* <p>
* The default implementation throws a {@link TransactionSystemException}. Subclasses may specifically identify
* concurrency failures etc.
*
* @param task the task description (commit or rollback).
* @param ex the SQLException thrown from commit/rollback.
* @return the translated exception to throw, either a {@link org.springframework.dao.DataAccessException} or a
* {@link org.springframework.transaction.TransactionException}
* @since 1.1
*/
protected RuntimeException translateException(String task, R2dbcException ex) {
Exception translated = potentiallyTranslateException(task, ex);
return new TransactionSystemException(task + " failed", translated);
}
private Exception potentiallyTranslateException(String task, R2dbcException ex) {
DataAccessException translated = exceptionTranslator.translate(task, null, ex);
return translated != null ? translated : ex;
}
/**
* ConnectionFactory transaction object, representing a ConnectionHolder. Used as transaction object by
* ConnectionFactoryTransactionManager.

View File

@@ -23,6 +23,7 @@ import static org.mockito.Mockito.*;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.IsolationLevel;
import io.r2dbc.spi.R2dbcBadGrammarException;
import io.r2dbc.spi.Statement;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -32,6 +33,8 @@ import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.r2dbc.BadSqlGrammarException;
import org.springframework.transaction.CannotCreateTransactionException;
import org.springframework.transaction.IllegalTransactionStateException;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.reactive.TransactionSynchronization;
@@ -94,6 +97,29 @@ public class R2dbcTransactionManagerUnitTests {
assertThat(sync.afterCompletionCalled).isTrue();
}
@Test // gh-329
public void testBeginFails() {
reset(connectionFactoryMock);
when(connectionFactoryMock.create()).thenReturn(Mono.error(new R2dbcBadGrammarException("fail")));
when(connectionMock.rollbackTransaction()).thenReturn(Mono.empty());
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
TransactionalOperator operator = TransactionalOperator.create(tm, definition);
ConnectionFactoryUtils.getConnection(connectionFactoryMock).as(operator::transactional) //
.as(StepVerifier::create) //
.expectErrorSatisfies(actual -> {
assertThat(actual).isInstanceOf(CannotCreateTransactionException.class)
.hasCauseInstanceOf(BadSqlGrammarException.class);
}).verify();
}
@Test // gh-107
public void appliesIsolationLevel() {
@@ -214,9 +240,11 @@ public class R2dbcTransactionManagerUnitTests {
public void testCommitFails() {
when(connectionMock.commitTransaction()).thenReturn(Mono.defer(() -> {
return Mono.error(new IllegalStateException("Commit should fail"));
return Mono.error(new R2dbcBadGrammarException("Commit should fail"));
}));
when(connectionMock.rollbackTransaction()).thenReturn(Mono.empty());
TransactionalOperator operator = TransactionalOperator.create(tm);
ConnectionFactoryUtils.getConnection(connectionFactoryMock) //
@@ -225,7 +253,7 @@ public class R2dbcTransactionManagerUnitTests {
}).then() //
.as(operator::transactional) //
.as(StepVerifier::create) //
.verifyError();
.verifyError(IllegalTransactionStateException.class);
verify(connectionMock).isAutoCommit();
verify(connectionMock).beginTransaction();
@@ -263,6 +291,35 @@ public class R2dbcTransactionManagerUnitTests {
verifyNoMoreInteractions(connectionMock);
}
@Test // gh-329
public void testRollbackFails() {
when(connectionMock.rollbackTransaction()).thenReturn(Mono.defer(() -> {
return Mono.error(new R2dbcBadGrammarException("Commit should fail"));
}), Mono.empty());
TransactionalOperator operator = TransactionalOperator.create(tm);
operator.execute(reactiveTransaction -> {
reactiveTransaction.setRollbackOnly();
return ConnectionFactoryUtils.getConnection(connectionFactoryMock) //
.doOnNext(it -> {
it.createStatement("foo");
}).then();
}).as(StepVerifier::create) //
.verifyError(IllegalTransactionStateException.class);
verify(connectionMock).isAutoCommit();
verify(connectionMock).beginTransaction();
verify(connectionMock).createStatement("foo");
verify(connectionMock, never()).commitTransaction();
verify(connectionMock).rollbackTransaction();
verify(connectionMock).close();
verifyNoMoreInteractions(connectionMock);
}
@Test // gh-107
public void testTransactionSetRollbackOnly() {