Skip to content

Commit 0f51ff5

Browse files
committed
Reset global rollback-only status when rolling back to savepoint
Issue: SPR-6568
1 parent 1ee0626 commit 0f51ff5

File tree

5 files changed

+195
-42
lines changed

5 files changed

+195
-42
lines changed

spring-jdbc/src/main/java/org/springframework/jdbc/datasource/JdbcTransactionObjectSupport.java

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -31,19 +31,18 @@
3131
import org.springframework.transaction.support.SmartTransactionObject;
3232

3333
/**
34-
* Convenient base class for JDBC-aware transaction objects.
35-
* Can contain a {@link ConnectionHolder}, and implements the
36-
* {@link org.springframework.transaction.SavepointManager}
37-
* interface based on that ConnectionHolder.
34+
* Convenient base class for JDBC-aware transaction objects. Can contain a
35+
* {@link ConnectionHolder} with a JDBC {@code Connection}, and implements the
36+
* {@link SavepointManager} interface based on that {@code ConnectionHolder}.
3837
*
39-
* <p>Allows for programmatic management of JDBC 3.0
40-
* {@link java.sql.Savepoint Savepoints}. Spring's
41-
* {@link org.springframework.transaction.support.DefaultTransactionStatus}
42-
* will automatically delegate to this, as it autodetects transaction
43-
* objects that implement the SavepointManager interface.
38+
* <p>Allows for programmatic management of JDBC {@link java.sql.Savepoint Savepoints}.
39+
* Spring's {@link org.springframework.transaction.support.DefaultTransactionStatus}
40+
* automatically delegates to this, as it autodetects transaction objects which
41+
* implement the {@link SavepointManager} interface.
4442
*
4543
* @author Juergen Hoeller
4644
* @since 1.1
45+
* @see DataSourceTransactionManager
4746
*/
4847
public abstract class JdbcTransactionObjectSupport implements SavepointManager, SmartTransactionObject {
4948

@@ -107,6 +106,10 @@ public Object createSavepoint() throws TransactionException {
107106
throw new NestedTransactionNotSupportedException(
108107
"Cannot create a nested transaction because savepoints are not supported by your JDBC driver");
109108
}
109+
if (conHolder.isRollbackOnly()) {
110+
throw new CannotCreateTransactionException(
111+
"Cannot create savepoint for transaction which is already marked as rollback-only");
112+
}
110113
return conHolder.createSavepoint();
111114
}
112115
catch (SQLException ex) {
@@ -123,6 +126,7 @@ public void rollbackToSavepoint(Object savepoint) throws TransactionException {
123126
ConnectionHolder conHolder = getConnectionHolderForSavepoint();
124127
try {
125128
conHolder.getConnection().rollback((Savepoint) savepoint);
129+
conHolder.resetRollbackOnly();
126130
}
127131
catch (Throwable ex) {
128132
throw new TransactionSystemException("Could not roll back to JDBC savepoint", ex);
@@ -151,7 +155,7 @@ protected ConnectionHolder getConnectionHolderForSavepoint() throws TransactionE
151155
}
152156
if (!hasConnectionHolder()) {
153157
throw new TransactionUsageException(
154-
"Cannot create nested transaction if not exposing a JDBC transaction");
158+
"Cannot create nested transaction when not exposing a JDBC transaction");
155159
}
156160
return getConnectionHolder();
157161
}

spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1369,6 +1369,122 @@ protected void doInTransactionWithoutResult(TransactionStatus status) throws Run
13691369
verify(con).close();
13701370
}
13711371

1372+
@Test
1373+
public void testExistingTransactionWithPropagationNestedAndRequiredRollback() throws Exception {
1374+
DatabaseMetaData md = mock(DatabaseMetaData.class);
1375+
Savepoint sp = mock(Savepoint.class);
1376+
1377+
given(md.supportsSavepoints()).willReturn(true);
1378+
given(con.getMetaData()).willReturn(md);
1379+
given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp);
1380+
1381+
final TransactionTemplate tt = new TransactionTemplate(tm);
1382+
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED);
1383+
assertTrue("Hasn't thread connection", !TransactionSynchronizationManager.hasResource(ds));
1384+
assertTrue("Synchronization not active", !TransactionSynchronizationManager.isSynchronizationActive());
1385+
1386+
tt.execute(new TransactionCallbackWithoutResult() {
1387+
@Override
1388+
protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException {
1389+
assertTrue("Is new transaction", status.isNewTransaction());
1390+
assertTrue("Isn't nested transaction", !status.hasSavepoint());
1391+
try {
1392+
tt.execute(new TransactionCallbackWithoutResult() {
1393+
@Override
1394+
protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException {
1395+
assertTrue("Has thread connection", TransactionSynchronizationManager.hasResource(ds));
1396+
assertTrue("Synchronization active", TransactionSynchronizationManager.isSynchronizationActive());
1397+
assertTrue("Isn't new transaction", !status.isNewTransaction());
1398+
assertTrue("Is nested transaction", status.hasSavepoint());
1399+
TransactionTemplate ntt = new TransactionTemplate(tm);
1400+
ntt.execute(new TransactionCallbackWithoutResult() {
1401+
@Override
1402+
protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException {
1403+
assertTrue("Has thread connection", TransactionSynchronizationManager.hasResource(ds));
1404+
assertTrue("Synchronization active", TransactionSynchronizationManager.isSynchronizationActive());
1405+
assertTrue("Isn't new transaction", !status.isNewTransaction());
1406+
assertTrue("Is regular transaction", !status.hasSavepoint());
1407+
throw new IllegalStateException();
1408+
}
1409+
});
1410+
}
1411+
});
1412+
fail("Should have thrown IllegalStateException");
1413+
}
1414+
catch (IllegalStateException ex) {
1415+
// expected
1416+
}
1417+
assertTrue("Is new transaction", status.isNewTransaction());
1418+
assertTrue("Isn't nested transaction", !status.hasSavepoint());
1419+
}
1420+
});
1421+
1422+
assertTrue("Hasn't thread connection", !TransactionSynchronizationManager.hasResource(ds));
1423+
verify(con).rollback(sp);
1424+
verify(con).releaseSavepoint(sp);
1425+
verify(con).commit();
1426+
verify(con).isReadOnly();
1427+
verify(con).close();
1428+
}
1429+
1430+
@Test
1431+
public void testExistingTransactionWithPropagationNestedAndRequiredRollbackOnly() throws Exception {
1432+
DatabaseMetaData md = mock(DatabaseMetaData.class);
1433+
Savepoint sp = mock(Savepoint.class);
1434+
1435+
given(md.supportsSavepoints()).willReturn(true);
1436+
given(con.getMetaData()).willReturn(md);
1437+
given(con.setSavepoint("SAVEPOINT_1")).willReturn(sp);
1438+
1439+
final TransactionTemplate tt = new TransactionTemplate(tm);
1440+
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED);
1441+
assertTrue("Hasn't thread connection", !TransactionSynchronizationManager.hasResource(ds));
1442+
assertTrue("Synchronization not active", !TransactionSynchronizationManager.isSynchronizationActive());
1443+
1444+
tt.execute(new TransactionCallbackWithoutResult() {
1445+
@Override
1446+
protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException {
1447+
assertTrue("Is new transaction", status.isNewTransaction());
1448+
assertTrue("Isn't nested transaction", !status.hasSavepoint());
1449+
try {
1450+
tt.execute(new TransactionCallbackWithoutResult() {
1451+
@Override
1452+
protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException {
1453+
assertTrue("Has thread connection", TransactionSynchronizationManager.hasResource(ds));
1454+
assertTrue("Synchronization active", TransactionSynchronizationManager.isSynchronizationActive());
1455+
assertTrue("Isn't new transaction", !status.isNewTransaction());
1456+
assertTrue("Is nested transaction", status.hasSavepoint());
1457+
TransactionTemplate ntt = new TransactionTemplate(tm);
1458+
ntt.execute(new TransactionCallbackWithoutResult() {
1459+
@Override
1460+
protected void doInTransactionWithoutResult(TransactionStatus status) throws RuntimeException {
1461+
assertTrue("Has thread connection", TransactionSynchronizationManager.hasResource(ds));
1462+
assertTrue("Synchronization active", TransactionSynchronizationManager.isSynchronizationActive());
1463+
assertTrue("Isn't new transaction", !status.isNewTransaction());
1464+
assertTrue("Is regular transaction", !status.hasSavepoint());
1465+
status.setRollbackOnly();
1466+
}
1467+
});
1468+
}
1469+
});
1470+
fail("Should have thrown UnexpectedRollbackException");
1471+
}
1472+
catch (UnexpectedRollbackException ex) {
1473+
// expected
1474+
}
1475+
assertTrue("Is new transaction", status.isNewTransaction());
1476+
assertTrue("Isn't nested transaction", !status.hasSavepoint());
1477+
}
1478+
});
1479+
1480+
assertTrue("Hasn't thread connection", !TransactionSynchronizationManager.hasResource(ds));
1481+
verify(con).rollback(sp);
1482+
verify(con).releaseSavepoint(sp);
1483+
verify(con).commit();
1484+
verify(con).isReadOnly();
1485+
verify(con).close();
1486+
}
1487+
13721488
@Test
13731489
public void testExistingTransactionWithManualSavepoint() throws Exception {
13741490
DatabaseMetaData md = mock(DatabaseMetaData.class);

spring-orm/src/main/java/org/springframework/orm/jpa/JpaTransactionManager.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -677,12 +677,17 @@ public void flush() {
677677

678678
@Override
679679
public Object createSavepoint() throws TransactionException {
680+
if (this.entityManagerHolder.isRollbackOnly()) {
681+
throw new CannotCreateTransactionException(
682+
"Cannot create savepoint for transaction which is already marked as rollback-only");
683+
}
680684
return getSavepointManager().createSavepoint();
681685
}
682686

683687
@Override
684688
public void rollbackToSavepoint(Object savepoint) throws TransactionException {
685689
getSavepointManager().rollbackToSavepoint(savepoint);
690+
this.entityManagerHolder.resetRollbackOnly();
686691
}
687692

688693
@Override

spring-tx/src/main/java/org/springframework/transaction/support/AbstractPlatformTransactionManager.java

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -693,20 +693,15 @@ public final void commit(TransactionStatus status) throws TransactionException {
693693
if (defStatus.isDebug()) {
694694
logger.debug("Transactional code has requested rollback");
695695
}
696-
processRollback(defStatus);
696+
processRollback(defStatus, false);
697697
return;
698698
}
699+
699700
if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
700701
if (defStatus.isDebug()) {
701702
logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
702703
}
703-
processRollback(defStatus);
704-
// Throw UnexpectedRollbackException only at outermost transaction boundary
705-
// or if explicitly asked to.
706-
if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) {
707-
throw new UnexpectedRollbackException(
708-
"Transaction rolled back because it has been marked as rollback-only");
709-
}
704+
processRollback(defStatus, true);
710705
return;
711706
}
712707

@@ -722,30 +717,35 @@ public final void commit(TransactionStatus status) throws TransactionException {
722717
private void processCommit(DefaultTransactionStatus status) throws TransactionException {
723718
try {
724719
boolean beforeCompletionInvoked = false;
720+
725721
try {
722+
boolean unexpectedRollback = false;
726723
prepareForCommit(status);
727724
triggerBeforeCommit(status);
728725
triggerBeforeCompletion(status);
729726
beforeCompletionInvoked = true;
730-
boolean globalRollbackOnly = false;
731-
if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) {
732-
globalRollbackOnly = status.isGlobalRollbackOnly();
733-
}
727+
734728
if (status.hasSavepoint()) {
735729
if (status.isDebug()) {
736730
logger.debug("Releasing transaction savepoint");
737731
}
732+
unexpectedRollback = status.isGlobalRollbackOnly();
738733
status.releaseHeldSavepoint();
739734
}
740735
else if (status.isNewTransaction()) {
741736
if (status.isDebug()) {
742737
logger.debug("Initiating transaction commit");
743738
}
739+
unexpectedRollback = status.isGlobalRollbackOnly();
744740
doCommit(status);
745741
}
742+
else if (isFailEarlyOnGlobalRollbackOnly()) {
743+
unexpectedRollback = status.isGlobalRollbackOnly();
744+
}
745+
746746
// Throw UnexpectedRollbackException if we have a global rollback-only
747747
// marker but still didn't get a corresponding exception from commit.
748-
if (globalRollbackOnly) {
748+
if (unexpectedRollback) {
749749
throw new UnexpectedRollbackException(
750750
"Transaction silently rolled back because it has been marked as rollback-only");
751751
}
@@ -803,7 +803,7 @@ public final void rollback(TransactionStatus status) throws TransactionException
803803
}
804804

805805
DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
806-
processRollback(defStatus);
806+
processRollback(defStatus, false);
807807
}
808808

809809
/**
@@ -812,10 +812,13 @@ public final void rollback(TransactionStatus status) throws TransactionException
812812
* @param status object representing the transaction
813813
* @throws TransactionException in case of rollback failure
814814
*/
815-
private void processRollback(DefaultTransactionStatus status) {
815+
private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
816816
try {
817+
boolean unexpectedRollback = unexpected;
818+
817819
try {
818820
triggerBeforeCompletion(status);
821+
819822
if (status.hasSavepoint()) {
820823
if (status.isDebug()) {
821824
logger.debug("Rolling back transaction to savepoint");
@@ -828,28 +831,42 @@ else if (status.isNewTransaction()) {
828831
}
829832
doRollback(status);
830833
}
831-
else if (status.hasTransaction()) {
832-
if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
833-
if (status.isDebug()) {
834-
logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
834+
else {
835+
// Participating in larger transaction
836+
if (status.hasTransaction()) {
837+
if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
838+
if (status.isDebug()) {
839+
logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
840+
}
841+
doSetRollbackOnly(status);
842+
}
843+
else {
844+
if (status.isDebug()) {
845+
logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
846+
}
835847
}
836-
doSetRollbackOnly(status);
837848
}
838849
else {
839-
if (status.isDebug()) {
840-
logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
841-
}
850+
logger.debug("Should roll back transaction but cannot - no transaction available");
851+
}
852+
// Unexpected rollback only matters here if we're asked to fail early
853+
if (!isFailEarlyOnGlobalRollbackOnly()) {
854+
unexpectedRollback = false;
842855
}
843-
}
844-
else {
845-
logger.debug("Should roll back transaction but cannot - no transaction available");
846856
}
847857
}
848858
catch (RuntimeException | Error ex) {
849859
triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
850860
throw ex;
851861
}
862+
852863
triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
864+
865+
// Raise UnexpectedRollbackException if we had a global rollback-only marker
866+
if (unexpectedRollback) {
867+
throw new UnexpectedRollbackException(
868+
"Transaction rolled back because it has been marked as rollback-only");
869+
}
853870
}
854871
finally {
855872
cleanupAfterCompletion(status);

spring-tx/src/main/java/org/springframework/transaction/support/ResourceHolderSupport.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,9 +23,9 @@
2323
/**
2424
* Convenient base class for resource holders.
2525
*
26-
* <p>Features rollback-only support for nested transactions.
27-
* Can expire after a certain number of seconds or milliseconds,
28-
* to determine transactional timeouts.
26+
* <p>Features rollback-only support for participating transactions.
27+
* Can expire after a certain number of seconds or milliseconds
28+
* in order to determine a transactional timeout.
2929
*
3030
* @author Juergen Hoeller
3131
* @since 02.02.2004
@@ -66,6 +66,17 @@ public void setRollbackOnly() {
6666
this.rollbackOnly = true;
6767
}
6868

69+
/**
70+
* Reset the rollback-only status for this resource transaction.
71+
* <p>Only really intended to be called after custom rollback steps which
72+
* keep the original resource in action, e.g. in case of a savepoint.
73+
* @since 5.0
74+
* @see org.springframework.transaction.SavepointManager#rollbackToSavepoint
75+
*/
76+
public void resetRollbackOnly() {
77+
this.rollbackOnly = false;
78+
}
79+
6980
/**
7081
* Return whether the resource transaction is marked as rollback-only.
7182
*/

0 commit comments

Comments
 (0)