Browse Source

DATAJDBC-488 - Avoid deadlocks by first accessing the root entity.

The previous process of deleting referencing entities, updating the root and then inserting referencing entities hat the potential of causing deadlocks.
When one process didn't obtain a lock on delete because there wasn't anything to delete root and referencing entities got locks in opposite order, which is a classical cause for deadlocks.

Original pull request: #191.
pull/195/head
mhyeon-lee 6 years ago committed by Jens Schauder
parent
commit
06529abf5a
No known key found for this signature in database
GPG Key ID: 996B1389BA0721C3
  1. 140
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryConcurrencyIntegrationTests.java
  2. 2
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryConcurrencyIntegrationTests-mysql.sql
  3. 8
      spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java
  4. 5
      spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityUpdateWriterUnitTests.java
  5. 19
      spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java

140
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryConcurrencyIntegrationTests.java

@ -0,0 +1,140 @@ @@ -0,0 +1,140 @@
package org.springframework.data.jdbc.repository;
import junit.framework.AssertionFailedError;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.With;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
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.data.annotation.Id;
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
import org.springframework.data.jdbc.testing.TestConfiguration;
import org.springframework.data.repository.CrudRepository;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
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;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Myeonghyeon Lee
*/
@ContextConfiguration
@ActiveProfiles("mysql")
public class JdbcRepositoryConcurrencyIntegrationTests {
@Configuration
@Import(TestConfiguration.class)
static class Config {
@Autowired JdbcRepositoryFactory factory;
@Bean
Class<?> testClass() {
return JdbcRepositoryConcurrencyIntegrationTests.class;
}
@Bean
DummyEntityRepository dummyEntityRepository() {
return factory.getRepository(DummyEntityRepository.class);
}
}
@ClassRule
public static final SpringClassRule classRule = new SpringClassRule();
@Rule
public SpringMethodRule methodRule = new SpringMethodRule();
@Autowired
NamedParameterJdbcTemplate template;
@Autowired
DummyEntityRepository repository;
@Autowired
PlatformTransactionManager transactionManager;
@Test // DATAJDBC-488
public void updateConcurrencyWithEmptyReferences() throws Exception {
DummyEntity entity = createDummyEntity();
entity = repository.save(entity);
assertThat(entity.getId()).isNotNull();
List<DummyEntity> concurrencyEntities = new ArrayList<>();
Element element1 = new Element(null, 1L);
Element element2 = new Element(null, 2L);
for (int i = 0; i < 100; i++) {
List<Element> newContent = Arrays.asList(
element1.withContent(element1.content + i + 2),
element2.withContent(element2.content + i + 2)
);
concurrencyEntities.add(entity
.withName(entity.getName() + i)
.withContent(newContent));
}
TransactionTemplate transactionTemplate = new TransactionTemplate(this.transactionManager);
List<Exception> exceptions = new CopyOnWriteArrayList<>();
CountDownLatch countDownLatch = new CountDownLatch(concurrencyEntities.size());
concurrencyEntities.stream()
.map(e -> new Thread(() -> {
countDownLatch.countDown();
try {
transactionTemplate.execute(status -> repository.save(e));
} catch (Exception ex) {
exceptions.add(ex);
}
}))
.forEach(Thread::start);
countDownLatch.await();
Thread.sleep(1000);
DummyEntity reloaded = repository.findById(entity.id).orElseThrow(AssertionFailedError::new);
assertThat(reloaded.content).hasSize(2);
assertThat(exceptions).isEmpty();
}
private static DummyEntity createDummyEntity() {
return new DummyEntity(null, "Entity Name", new ArrayList<>());
}
interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {
}
@Getter
@AllArgsConstructor
static class DummyEntity {
@Id
private Long id;
@With
String name;
@With
final List<Element> content;
}
@AllArgsConstructor
static class Element {
@Id private Long id;
@With final Long content;
}
}

2
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryConcurrencyIntegrationTests-mysql.sql

@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
CREATE TABLE dummy_entity ( id BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(100));
CREATE TABLE element (id BIGINT AUTO_INCREMENT PRIMARY KEY, content BIGINT, Dummy_Entity_key BIGINT,dummy_entity BIGINT);

8
spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java

@ -37,6 +37,7 @@ import org.springframework.util.Assert; @@ -37,6 +37,7 @@ import org.springframework.util.Assert;
* @author Jens Schauder
* @author Bastian Wilhelm
* @author Mark Paluch
* @author Myeonghyeon Lee
*/
class WritingContext {
@ -73,14 +74,17 @@ class WritingContext { @@ -73,14 +74,17 @@ class WritingContext {
/**
* Leaves out the isNew check as defined in #DATAJDBC-282
* Possible Deadlocks in Execution Order in #DATAJDBC-488
*
* @return List of {@link DbAction}s
* @see <a href="https://jira.spring.io/browse/DATAJDBC-282">DAJDBC-282</a>
* @see <a href="https://jira.spring.io/browse/DATAJDBC-488">DAJDBC-488</a>
*/
List<DbAction<?>> update() {
List<DbAction<?>> actions = new ArrayList<>(deleteReferenced());
List<DbAction<?>> actions = new ArrayList<>();
actions.add(setRootAction(new DbAction.UpdateRoot<>(entity)));
actions.addAll(deleteReferenced());
actions.addAll(insertReferenced());
return actions;
}
@ -94,8 +98,8 @@ class WritingContext { @@ -94,8 +98,8 @@ class WritingContext {
actions.addAll(insertReferenced());
} else {
actions.addAll(deleteReferenced());
actions.add(setRootAction(new DbAction.UpdateRoot<>(entity)));
actions.addAll(deleteReferenced());
actions.addAll(insertReferenced());
}

5
spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityUpdateWriterUnitTests.java

@ -30,6 +30,7 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext @@ -30,6 +30,7 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext
* Unit tests for the {@link RelationalEntityUpdateWriter}
*
* @author Thomas Lang
* @author Myeonghyeon Lee
*/
@RunWith(MockitoJUnitRunner.class)
public class RelationalEntityUpdateWriterUnitTests {
@ -51,8 +52,8 @@ public class RelationalEntityUpdateWriterUnitTests { @@ -51,8 +52,8 @@ public class RelationalEntityUpdateWriterUnitTests {
.extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::extractPath, DbActionTestSupport::actualEntityType,
DbActionTestSupport::isWithDependsOn) //
.containsExactly( //
tuple(DbAction.Delete.class, Element.class, "other", null, false), //
tuple(DbAction.UpdateRoot.class, SingleReferenceEntity.class, "", SingleReferenceEntity.class, false) //
tuple(DbAction.UpdateRoot.class, SingleReferenceEntity.class, "", SingleReferenceEntity.class, false), //
tuple(DbAction.Delete.class, Element.class, "other", null, false) //
);
}

19
spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java

@ -49,6 +49,7 @@ import org.springframework.lang.Nullable; @@ -49,6 +49,7 @@ import org.springframework.lang.Nullable;
* @author Jens Schauder
* @author Bastian Wilhelm
* @author Mark Paluch
* @author Myeonghyeon Lee
*/
@RunWith(MockitoJUnitRunner.class)
public class RelationalEntityWriterUnitTests {
@ -156,9 +157,9 @@ public class RelationalEntityWriterUnitTests { @@ -156,9 +157,9 @@ public class RelationalEntityWriterUnitTests {
DbActionTestSupport::extractPath, //
DbActionTestSupport::actualEntityType, //
DbActionTestSupport::isWithDependsOn) //
.containsExactly( //
tuple(Delete.class, Element.class, "other", null, false), //
tuple(UpdateRoot.class, SingleReferenceEntity.class, "", SingleReferenceEntity.class, false) //
.containsExactly(
tuple(UpdateRoot.class, SingleReferenceEntity.class, "", SingleReferenceEntity.class, false), //
tuple(Delete.class, Element.class, "other", null, false) //
);
}
@ -180,8 +181,8 @@ public class RelationalEntityWriterUnitTests { @@ -180,8 +181,8 @@ public class RelationalEntityWriterUnitTests {
DbActionTestSupport::actualEntityType, //
DbActionTestSupport::isWithDependsOn) //
.containsExactly( //
tuple(Delete.class, Element.class, "other", null, false), //
tuple(UpdateRoot.class, SingleReferenceEntity.class, "", SingleReferenceEntity.class, false), //
tuple(Delete.class, Element.class, "other", null, false), //
tuple(Insert.class, Element.class, "other", Element.class, true) //
);
}
@ -291,9 +292,9 @@ public class RelationalEntityWriterUnitTests { @@ -291,9 +292,9 @@ public class RelationalEntityWriterUnitTests {
DbActionTestSupport::actualEntityType, //
DbActionTestSupport::isWithDependsOn) //
.containsExactly( //
tuple(UpdateRoot.class, CascadingReferenceEntity.class, "", CascadingReferenceEntity.class, false), //
tuple(Delete.class, Element.class, "other.element", null, false),
tuple(Delete.class, CascadingReferenceMiddleElement.class, "other", null, false),
tuple(UpdateRoot.class, CascadingReferenceEntity.class, "", CascadingReferenceEntity.class, false), //
tuple(Insert.class, CascadingReferenceMiddleElement.class, "other", CascadingReferenceMiddleElement.class,
true), //
tuple(Insert.class, CascadingReferenceMiddleElement.class, "other", CascadingReferenceMiddleElement.class,
@ -447,8 +448,8 @@ public class RelationalEntityWriterUnitTests { @@ -447,8 +448,8 @@ public class RelationalEntityWriterUnitTests {
this::getMapKey, //
DbActionTestSupport::extractPath) //
.containsExactly( //
tuple(Delete.class, Element.class, null, "elements"), //
tuple(UpdateRoot.class, MapContainer.class, null, ""), //
tuple(Delete.class, Element.class, null, "elements"), //
tuple(Insert.class, Element.class, "one", "elements") //
);
}
@ -469,8 +470,8 @@ public class RelationalEntityWriterUnitTests { @@ -469,8 +470,8 @@ public class RelationalEntityWriterUnitTests {
this::getListKey, //
DbActionTestSupport::extractPath) //
.containsExactly( //
tuple(Delete.class, Element.class, null, "elements"), //
tuple(UpdateRoot.class, ListContainer.class, null, ""), //
tuple(Delete.class, Element.class, null, "elements"), //
tuple(Insert.class, Element.class, 0, "elements") //
);
}
@ -494,9 +495,9 @@ public class RelationalEntityWriterUnitTests { @@ -494,9 +495,9 @@ public class RelationalEntityWriterUnitTests {
a -> getQualifier(a, listMapContainerElements), //
DbActionTestSupport::extractPath) //
.containsExactly( //
tuple(UpdateRoot.class, ListMapContainer.class, null, null, ""), //
tuple(Delete.class, Element.class, null, null, "maps.elements"), //
tuple(Delete.class, MapContainer.class, null, null, "maps"), //
tuple(UpdateRoot.class, ListMapContainer.class, null, null, ""), //
tuple(Insert.class, MapContainer.class, 0, null, "maps"), //
tuple(Insert.class, Element.class, null, "one", "maps.elements") //
);
@ -521,9 +522,9 @@ public class RelationalEntityWriterUnitTests { @@ -521,9 +522,9 @@ public class RelationalEntityWriterUnitTests {
a -> getQualifier(a, noIdListMapContainerElements), //
DbActionTestSupport::extractPath) //
.containsExactly( //
tuple(UpdateRoot.class, NoIdListMapContainer.class, null, null, ""), //
tuple(Delete.class, NoIdElement.class, null, null, "maps.elements"), //
tuple(Delete.class, NoIdMapContainer.class, null, null, "maps"), //
tuple(UpdateRoot.class, NoIdListMapContainer.class, null, null, ""), //
tuple(Insert.class, NoIdMapContainer.class, 0, null, "maps"), //
tuple(Insert.class, NoIdElement.class, 0, "one", "maps.elements") //
);

Loading…
Cancel
Save