diff --git a/jpa/boot-jpa-locks/Dockerfile b/jpa/boot-jpa-locks/Dockerfile index 5f0ad64b6..d0928b237 100644 --- a/jpa/boot-jpa-locks/Dockerfile +++ b/jpa/boot-jpa-locks/Dockerfile @@ -1,4 +1,4 @@ -FROM eclipse-temurin:17.0.10_7-jre-focal as builder +FROM eclipse-temurin:17.0.10_7-jre-focal AS builder WORKDIR application ARG JAR_FILE=target/boot-jpa-locks-0.0.1-SNAPSHOT.jar COPY ${JAR_FILE} application.jar diff --git a/jpa/boot-jpa-locks/src/main/java/com/example/locks/repositories/CustomizedActorRepository.java b/jpa/boot-jpa-locks/src/main/java/com/example/locks/repositories/CustomizedActorRepository.java index 96da39502..528e3865d 100644 --- a/jpa/boot-jpa-locks/src/main/java/com/example/locks/repositories/CustomizedActorRepository.java +++ b/jpa/boot-jpa-locks/src/main/java/com/example/locks/repositories/CustomizedActorRepository.java @@ -1,6 +1,7 @@ package com.example.locks.repositories; import com.example.locks.entities.Actor; +import jakarta.persistence.LockModeType; public interface CustomizedActorRepository { @@ -8,5 +9,5 @@ public interface CustomizedActorRepository { void setLockTimeout(long timeoutDurationInMs); - Actor getActorAndObtainPessimisticWriteLockingOnItById(Long id); + Actor getActorAndObtainPessimisticLockingOnItById(Long id, LockModeType lockModeType); } diff --git a/jpa/boot-jpa-locks/src/main/java/com/example/locks/repositories/CustomizedActorRepositoryImpl.java b/jpa/boot-jpa-locks/src/main/java/com/example/locks/repositories/CustomizedActorRepositoryImpl.java index 417a8e7c2..cea966df9 100644 --- a/jpa/boot-jpa-locks/src/main/java/com/example/locks/repositories/CustomizedActorRepositoryImpl.java +++ b/jpa/boot-jpa-locks/src/main/java/com/example/locks/repositories/CustomizedActorRepositoryImpl.java @@ -69,12 +69,12 @@ public void setLockTimeout(long timeoutDurationInMs) { } @Override - public Actor getActorAndObtainPessimisticWriteLockingOnItById(Long id) { + public Actor getActorAndObtainPessimisticLockingOnItById(Long id, LockModeType lockModeType) { log.info("Trying to obtain pessimistic lock ..."); Query query = em.createQuery("select actor from Actor actor where actor.id = :id"); query.setParameter("id", id); - query.setLockMode(LockModeType.PESSIMISTIC_WRITE); + query.setLockMode(lockModeType); query = setLockTimeoutIfRequired(query); Actor actor = (Actor) query.getSingleResult(); diff --git a/jpa/boot-jpa-locks/src/main/java/com/example/locks/services/ActorService.java b/jpa/boot-jpa-locks/src/main/java/com/example/locks/services/ActorService.java index 29ec590a6..1866b22b1 100644 --- a/jpa/boot-jpa-locks/src/main/java/com/example/locks/services/ActorService.java +++ b/jpa/boot-jpa-locks/src/main/java/com/example/locks/services/ActorService.java @@ -8,6 +8,7 @@ import com.example.locks.model.response.ActorResponse; import com.example.locks.model.response.PagedResult; import com.example.locks.repositories.ActorRepository; +import jakarta.persistence.LockModeType; import java.util.List; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -19,7 +20,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Service @@ -89,14 +89,12 @@ public Actor updateActorWithLock(Long id, String name) { log.info("Received exception for request {}", name); log.error("Found pessimistic lock exception!", e); sleepForAWhile(); - // updateActorWithLock(id, name); } return null; } - @Transactional(propagation = Propagation.REQUIRES_NEW) public Actor obtainPessimisticLockAndUpdate(Long id, String name) { - Actor actor = actorRepository.getActorAndObtainPessimisticWriteLockingOnItById(id); + Actor actor = actorRepository.getActorAndObtainPessimisticLockingOnItById(id, LockModeType.PESSIMISTIC_WRITE); actor.setActorName(name); return actorRepository.save(actor); } @@ -108,4 +106,9 @@ private void sleepForAWhile() { Thread.currentThread().interrupt(); } } + + @Transactional + public Actor getActorWithPessimisticReadLock(Long id) { + return actorRepository.getActorAndObtainPessimisticLockingOnItById(id, LockModeType.PESSIMISTIC_READ); + } } diff --git a/jpa/boot-jpa-locks/src/test/java/com/example/locks/services/ActorServiceIntTest.java b/jpa/boot-jpa-locks/src/test/java/com/example/locks/services/ActorServiceIntTest.java index 125afe258..ce2d5b875 100644 --- a/jpa/boot-jpa-locks/src/test/java/com/example/locks/services/ActorServiceIntTest.java +++ b/jpa/boot-jpa-locks/src/test/java/com/example/locks/services/ActorServiceIntTest.java @@ -12,7 +12,10 @@ import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.stream.IntStream; import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -25,6 +28,11 @@ class ActorServiceIntTest extends AbstractIntegrationTest { @Autowired private ActorRepository actorRepository; + @BeforeEach + void setUp() { + actorRepository.deleteAllInBatch(); + } + @Test void testPessimisticWriteLock() { @@ -66,4 +74,53 @@ void testPessimisticWriteLock() { assertThat(updatedActor.getVersion()).isEqualTo((short) 2); }); } + + @Test + void testPessimisticReadLock() throws ExecutionException, InterruptedException { + ActorResponse actorResponse = actorService.saveActor(new ActorRequest("Actor", null, "Indian")); + + Optional optionalActor = actorRepository.findById(actorResponse.actorId()); + assertThat(optionalActor).isPresent(); + Actor actor = optionalActor.get(); + assertThat(actor.getActorName()).isEqualTo("Actor"); + assertThat(actor.getVersion()).isEqualTo((short) 0); + // Obtaining a pessimistic read lock concurrently by two requests on the same record + List> completableFutureList = IntStream.range(0, 2) + .boxed() + .map(actorName -> CompletableFuture.supplyAsync(() -> { + var readLockActor = new Actor(); + try { + readLockActor = actorService.getActorWithPessimisticReadLock(actorResponse.actorId()); + } catch (Exception e) { + log.error("exception occurred", e); + } + return readLockActor; + })) + .toList(); + + CompletableFuture.allOf(completableFutureList.toArray(CompletableFuture[]::new)) + .join(); + // As pessimistic read lock is a shared lock it will give read access to every request + assertThat(completableFutureList.get(0).get().getActorName()).isEqualTo("Actor"); + assertThat(completableFutureList.get(1).get().getActorName()).isEqualTo("Actor"); + } + + @Test + void testUpdatePessimisticReadLock() { + ActorResponse actorResponse = actorService.saveActor(new ActorRequest("Actor", null, "Indian")); + + Optional optionalActor = actorRepository.findById(actorResponse.actorId()); + assertThat(optionalActor).isPresent(); + Actor actor = optionalActor.get(); + assertThat(actor.getActorName()).isEqualTo("Actor"); + assertThat(actor.getVersion()).isEqualTo((short) 0); + // Obtaining a pessimistic read lock and holding lock for 5 sec + CompletableFuture.runAsync(() -> actorService.getActorWithPessimisticReadLock(actor.getActorId())); + // As pessimistic read lock obtained on the record update can't be performed until the lock is released + await().atMost(Duration.ofSeconds(10)).pollDelay(Duration.ofSeconds(1)).untilAsserted(() -> { + ActorResponse updatedActor = + actorService.updateActor(actor.getActorId(), new ActorRequest("updateActor", null, "Indian")); + assertThat(updatedActor.actorName()).isEqualTo("updateActor"); + }); + } }