Skip to content

[DATAJDBC-488] Resolves an issue with deadlocks in the order of update execution. #191

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
* @author Jens Schauder
* @author Bastian Wilhelm
* @author Mark Paluch
* @author Myeonghyeon Lee
*/
class WritingContext {

Expand Down Expand Up @@ -73,14 +74,17 @@ List<DbAction<?>> insert() {

/**
* 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;
}
Expand All @@ -94,8 +98,8 @@ List<DbAction<?>> save() {
actions.addAll(insertReferenced());
} else {

actions.addAll(deleteReferenced());
actions.add(setRootAction(new DbAction.UpdateRoot<>(entity)));
actions.addAll(deleteReferenced());
actions.addAll(insertReferenced());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
* Unit tests for the {@link RelationalEntityUpdateWriter}
*
* @author Thomas Lang
* @author Myeonghyeon Lee
*/
@RunWith(MockitoJUnitRunner.class)
public class RelationalEntityUpdateWriterUnitTests {
Expand All @@ -51,8 +52,8 @@ public void existingEntityGetsConvertedToDeletePlusUpdate() {
.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) //
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
* @author Jens Schauder
* @author Bastian Wilhelm
* @author Mark Paluch
* @author Myeonghyeon Lee
*/
@RunWith(MockitoJUnitRunner.class)
public class RelationalEntityWriterUnitTests {
Expand Down Expand Up @@ -156,9 +157,9 @@ public void existingEntityGetsConvertedToDeletePlusUpdate() {
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) //
);
}

Expand All @@ -180,8 +181,8 @@ public void newReferenceTriggersDeletePlusInsert() {
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) //
);
}
Expand Down Expand Up @@ -291,9 +292,9 @@ public void cascadingReferencesTriggerCascadingActionsForUpdate() {
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,
Expand Down Expand Up @@ -447,8 +448,8 @@ public void mapTriggersDeletePlusInsert() {
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") //
);
}
Expand All @@ -469,8 +470,8 @@ public void listTriggersDeletePlusInsert() {
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") //
);
}
Expand All @@ -494,9 +495,9 @@ public void multiLevelQualifiedReferencesWithId() {
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") //
);
Expand All @@ -521,9 +522,9 @@ public void multiLevelQualifiedReferencesWithOutId() {
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") //
);
Expand Down