DATAJDBC-493 - Avoids deadlocks by acquiring lock on aggregate root table.
Introduces infrastructure to obtain locks and uses them to acquire locks on the table of the aggregate root before deleting references. Without this lock deletes access non root entities before the aggregate root, which is the opposite order of updates and thus may cause deadlocks. Original pull request: #196.
This commit is contained in:
committed by
Jens Schauder
parent
fa8b95c5f3
commit
04c29f4004
@@ -19,6 +19,7 @@ import org.springframework.data.relational.core.dialect.AbstractDialect;
|
||||
import org.springframework.data.relational.core.dialect.Dialect;
|
||||
import org.springframework.data.relational.core.dialect.HsqlDbDialect;
|
||||
import org.springframework.data.relational.core.dialect.LimitClause;
|
||||
import org.springframework.data.relational.core.dialect.LockClause;
|
||||
import org.springframework.data.relational.core.sql.IdentifierProcessing;
|
||||
|
||||
/**
|
||||
@@ -27,6 +28,7 @@ import org.springframework.data.relational.core.sql.IdentifierProcessing;
|
||||
* @author Mark Paluch
|
||||
* @author Milan Milanov
|
||||
* @author Jens Schauder
|
||||
* @author Myeonghyeon Lee
|
||||
*/
|
||||
public class NonQuotingDialect extends AbstractDialect implements Dialect {
|
||||
|
||||
@@ -39,6 +41,11 @@ public class NonQuotingDialect extends AbstractDialect implements Dialect {
|
||||
return HsqlDbDialect.INSTANCE.limit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LockClause lock() {
|
||||
return HsqlDbDialect.INSTANCE.lock();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentifierProcessing getIdentifierProcessing() {
|
||||
return IdentifierProcessing.create(new IdentifierProcessing.Quoting(""), IdentifierProcessing.LetterCasing.AS_IS);
|
||||
|
||||
@@ -46,6 +46,7 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
|
||||
import org.springframework.data.relational.core.sql.Aliased;
|
||||
import org.springframework.data.relational.core.sql.LockMode;
|
||||
import org.springframework.data.relational.core.sql.SqlIdentifier;
|
||||
import org.springframework.data.relational.core.sql.Table;
|
||||
|
||||
@@ -96,16 +97,45 @@ public class SqlGeneratorUnitTests {
|
||||
|
||||
SoftAssertions softAssertions = new SoftAssertions();
|
||||
softAssertions.assertThat(sql) //
|
||||
.startsWith("SELECT") //
|
||||
.contains("dummy_entity.id1 AS id1,") //
|
||||
.contains("dummy_entity.x_name AS x_name,") //
|
||||
.contains("dummy_entity.x_other AS x_other,") //
|
||||
.contains("ref.x_l1id AS ref_x_l1id") //
|
||||
.contains("ref.x_content AS ref_x_content").contains(" FROM dummy_entity") //
|
||||
.contains("ON ref.dummy_entity = dummy_entity.id1") //
|
||||
.contains("WHERE dummy_entity.id1 = :id") //
|
||||
// 1-N relationships do not get loaded via join
|
||||
.doesNotContain("Element AS elements");
|
||||
.startsWith("SELECT") //
|
||||
.contains("dummy_entity.id1 AS id1,") //
|
||||
.contains("dummy_entity.x_name AS x_name,") //
|
||||
.contains("dummy_entity.x_other AS x_other,") //
|
||||
.contains("ref.x_l1id AS ref_x_l1id") //
|
||||
.contains("ref.x_content AS ref_x_content").contains(" FROM dummy_entity") //
|
||||
.contains("ON ref.dummy_entity = dummy_entity.id1") //
|
||||
.contains("WHERE dummy_entity.id1 = :id") //
|
||||
// 1-N relationships do not get loaded via join
|
||||
.doesNotContain("Element AS elements");
|
||||
softAssertions.assertAll();
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-493
|
||||
public void getAcquireLockById() {
|
||||
|
||||
String sql = sqlGenerator.getAcquireLockById(LockMode.PESSIMISTIC_WRITE);
|
||||
|
||||
SoftAssertions softAssertions = new SoftAssertions();
|
||||
softAssertions.assertThat(sql) //
|
||||
.startsWith("SELECT") //
|
||||
.contains("dummy_entity.id1") //
|
||||
.contains("WHERE dummy_entity.id1 = :id") //
|
||||
.contains("FOR UPDATE") //
|
||||
.doesNotContain("Element AS elements");
|
||||
softAssertions.assertAll();
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-493
|
||||
public void getAcquireLockAll() {
|
||||
|
||||
String sql = sqlGenerator.getAcquireLockAll(LockMode.PESSIMISTIC_WRITE);
|
||||
|
||||
SoftAssertions softAssertions = new SoftAssertions();
|
||||
softAssertions.assertThat(sql) //
|
||||
.startsWith("SELECT") //
|
||||
.contains("dummy_entity.id1") //
|
||||
.contains("FOR UPDATE") //
|
||||
.doesNotContain("Element AS elements");
|
||||
softAssertions.assertAll();
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
|
||||
import org.springframework.data.jdbc.testing.DatabaseProfileValueSource;
|
||||
@@ -34,7 +35,6 @@ import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
|
||||
import org.springframework.test.annotation.IfProfileValue;
|
||||
import org.springframework.test.annotation.ProfileValueSourceConfiguration;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.rules.SpringClassRule;
|
||||
import org.springframework.test.context.junit4.rules.SpringMethodRule;
|
||||
@@ -123,6 +123,131 @@ public class JdbcRepositoryConcurrencyIntegrationTests {
|
||||
assertThat(exceptions).isEmpty();
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-493
|
||||
public void updateConcurrencyWithDelete() throws Exception {
|
||||
|
||||
DummyEntity entity = createDummyEntity();
|
||||
entity = repository.save(entity);
|
||||
|
||||
Long targetId = entity.getId();
|
||||
assertThat(targetId).isNotNull();
|
||||
|
||||
List<DummyEntity> concurrencyEntities = createEntityStates(entity);
|
||||
|
||||
TransactionTemplate transactionTemplate = new TransactionTemplate(this.transactionManager);
|
||||
|
||||
List<Exception> exceptions = new CopyOnWriteArrayList<>();
|
||||
CountDownLatch startLatch = new CountDownLatch(concurrencyEntities.size() + 1); // latch for all threads to wait on.
|
||||
CountDownLatch doneLatch = new CountDownLatch(concurrencyEntities.size() + 1); // latch for main thread to wait on until all threads are done.
|
||||
|
||||
// update
|
||||
concurrencyEntities.stream() //
|
||||
.map(e -> new Thread(() -> {
|
||||
|
||||
try {
|
||||
|
||||
startLatch.countDown();
|
||||
startLatch.await();
|
||||
|
||||
transactionTemplate.execute(status -> repository.save(e));
|
||||
} catch (Exception ex) {
|
||||
// When the delete execution is complete, the Update execution throws an IncorrectUpdateSemanticsDataAccessException.
|
||||
if (ex.getCause() instanceof IncorrectUpdateSemanticsDataAccessException) {
|
||||
return;
|
||||
}
|
||||
|
||||
exceptions.add(ex);
|
||||
} finally {
|
||||
doneLatch.countDown();
|
||||
}
|
||||
})) //
|
||||
.forEach(Thread::start);
|
||||
|
||||
// delete
|
||||
new Thread(() -> {
|
||||
try {
|
||||
|
||||
startLatch.countDown();
|
||||
startLatch.await();
|
||||
|
||||
transactionTemplate.execute(status -> {
|
||||
repository.deleteById(targetId);
|
||||
return null;
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
exceptions.add(ex);
|
||||
} finally {
|
||||
doneLatch.countDown();
|
||||
}
|
||||
}).start();
|
||||
|
||||
doneLatch.await();
|
||||
|
||||
assertThat(exceptions).isEmpty();
|
||||
assertThat(repository.findById(entity.id)).isEmpty();
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-493
|
||||
public void updateConcurrencyWithDeleteAll() throws Exception {
|
||||
|
||||
DummyEntity entity = createDummyEntity();
|
||||
entity = repository.save(entity);
|
||||
|
||||
List<DummyEntity> concurrencyEntities = createEntityStates(entity);
|
||||
|
||||
TransactionTemplate transactionTemplate = new TransactionTemplate(this.transactionManager);
|
||||
|
||||
List<Exception> exceptions = new CopyOnWriteArrayList<>();
|
||||
CountDownLatch startLatch = new CountDownLatch(concurrencyEntities.size() + 1); // latch for all threads to wait on.
|
||||
CountDownLatch doneLatch = new CountDownLatch(concurrencyEntities.size() + 1); // latch for main thread to wait on until all threads are done.
|
||||
|
||||
// update
|
||||
concurrencyEntities.stream() //
|
||||
.map(e -> new Thread(() -> {
|
||||
|
||||
try {
|
||||
|
||||
startLatch.countDown();
|
||||
startLatch.await();
|
||||
|
||||
transactionTemplate.execute(status -> repository.save(e));
|
||||
} catch (Exception ex) {
|
||||
// When the delete execution is complete, the Update execution throws an IncorrectUpdateSemanticsDataAccessException.
|
||||
if (ex.getCause() instanceof IncorrectUpdateSemanticsDataAccessException) {
|
||||
return;
|
||||
}
|
||||
|
||||
exceptions.add(ex);
|
||||
} finally {
|
||||
doneLatch.countDown();
|
||||
}
|
||||
})) //
|
||||
.forEach(Thread::start);
|
||||
|
||||
// delete
|
||||
new Thread(() -> {
|
||||
try {
|
||||
|
||||
startLatch.countDown();
|
||||
startLatch.await();
|
||||
|
||||
transactionTemplate.execute(status -> {
|
||||
repository.deleteAll();
|
||||
return null;
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
exceptions.add(ex);
|
||||
} finally {
|
||||
doneLatch.countDown();
|
||||
}
|
||||
}).start();
|
||||
|
||||
doneLatch.await();
|
||||
|
||||
assertThat(exceptions).isEmpty();
|
||||
assertThat(repository.count()).isEqualTo(0);
|
||||
}
|
||||
|
||||
private List<DummyEntity> createEntityStates(DummyEntity entity) {
|
||||
|
||||
List<DummyEntity> concurrencyEntities = new ArrayList<>();
|
||||
|
||||
@@ -20,7 +20,9 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.relational.core.dialect.AbstractDialect;
|
||||
import org.springframework.data.relational.core.dialect.ArrayColumns;
|
||||
import org.springframework.data.relational.core.dialect.LimitClause;
|
||||
import org.springframework.data.relational.core.dialect.LockClause;
|
||||
import org.springframework.data.relational.core.sql.IdentifierProcessing;
|
||||
import org.springframework.data.relational.core.sql.LockOptions;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
@@ -28,6 +30,7 @@ import org.springframework.util.ClassUtils;
|
||||
* An SQL dialect for the ANSI SQL standard.
|
||||
*
|
||||
* @author Milan Milanov
|
||||
* @author Myeonghyeon Lee
|
||||
* @since 2.0
|
||||
*/
|
||||
public class AnsiDialect extends AbstractDialect {
|
||||
@@ -78,6 +81,27 @@ public class AnsiDialect extends AbstractDialect {
|
||||
}
|
||||
};
|
||||
|
||||
private static final LockClause LOCK_CLAUSE = new LockClause() {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.relational.core.dialect.LockClause#getLock(LockOptions)
|
||||
*/
|
||||
@Override
|
||||
public String getLock(LockOptions lockOptions) {
|
||||
return "FOR UPDATE";
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.relational.core.dialect.LimitClause#getClausePosition()
|
||||
*/
|
||||
@Override
|
||||
public Position getClausePosition() {
|
||||
return Position.AFTER_ORDER_BY;
|
||||
}
|
||||
};
|
||||
|
||||
private final AnsiArrayColumns ARRAY_COLUMNS = new AnsiArrayColumns();
|
||||
|
||||
/*
|
||||
@@ -89,6 +113,15 @@ public class AnsiDialect extends AbstractDialect {
|
||||
return LIMIT_CLAUSE;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.relational.core.dialect.Dialect#lock()
|
||||
*/
|
||||
@Override
|
||||
public LockClause lock() {
|
||||
return LOCK_CLAUSE;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.relational.core.dialect.Dialect#getArraySupport()
|
||||
|
||||
Reference in New Issue
Block a user