-
Notifications
You must be signed in to change notification settings - Fork 4
hotelapp-dev-2-optimistic-lock #215
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,27 @@ | ||||||||||||||||||||||||
package com.yen.HotelApp.exception; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
import com.yen.HotelApp.controller.HotelController; | ||||||||||||||||||||||||
import org.springframework.http.HttpStatus; | ||||||||||||||||||||||||
import org.springframework.http.ResponseEntity; | ||||||||||||||||||||||||
import org.springframework.orm.ObjectOptimisticLockingFailureException; | ||||||||||||||||||||||||
import org.springframework.web.bind.annotation.ControllerAdvice; | ||||||||||||||||||||||||
import org.springframework.web.bind.annotation.ExceptionHandler; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
import jakarta.persistence.OptimisticLockException; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
@ControllerAdvice | ||||||||||||||||||||||||
public class GlobalExceptionHandler { | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
@ExceptionHandler({OptimisticLockException.class, ObjectOptimisticLockingFailureException.class}) | ||||||||||||||||||||||||
public ResponseEntity<HotelController.ErrorResponse> handleOptimisticLockException(Exception e) { | ||||||||||||||||||||||||
String message = "The room was just booked by someone else. Please refresh and try again."; | ||||||||||||||||||||||||
return ResponseEntity.status(HttpStatus.CONFLICT) | ||||||||||||||||||||||||
.body(new HotelController.ErrorResponse(message)); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
Comment on lines
+16
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a good practice to log exceptions on the server side for debugging and monitoring purposes. The
Suggested change
|
||||||||||||||||||||||||
|
||||||||||||||||||||||||
@ExceptionHandler(RuntimeException.class) | ||||||||||||||||||||||||
public ResponseEntity<HotelController.ErrorResponse> handleRuntimeException(RuntimeException e) { | ||||||||||||||||||||||||
return ResponseEntity.badRequest() | ||||||||||||||||||||||||
.body(new HotelController.ErrorResponse(e.getMessage())); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
Comment on lines
+22
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This generic
It's better to return a generic error message for @ExceptionHandler(RuntimeException.class)
public ResponseEntity<HotelController.ErrorResponse> handleRuntimeException(RuntimeException e) {
// Log the exception here for debugging, e.g., using log.error("Unhandled exception", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new HotelController.ErrorResponse("An unexpected internal error occurred."));
} |
||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,111 @@ | ||||||
package com.yen.HotelApp.service; | ||||||
|
||||||
import com.yen.HotelApp.entity.Room; | ||||||
import com.yen.HotelApp.repository.RoomRepository; | ||||||
import org.junit.jupiter.api.Test; | ||||||
import org.springframework.beans.factory.annotation.Autowired; | ||||||
import org.springframework.boot.test.context.SpringBootTest; | ||||||
import org.springframework.orm.ObjectOptimisticLockingFailureException; | ||||||
import org.springframework.test.context.ActiveProfiles; | ||||||
import org.springframework.transaction.annotation.Transactional; | ||||||
|
||||||
import java.time.LocalDate; | ||||||
import java.util.concurrent.CountDownLatch; | ||||||
import java.util.concurrent.ExecutorService; | ||||||
import java.util.concurrent.Executors; | ||||||
import java.util.concurrent.atomic.AtomicInteger; | ||||||
|
||||||
import static org.junit.jupiter.api.Assertions.*; | ||||||
|
||||||
@SpringBootTest | ||||||
@ActiveProfiles("test") | ||||||
public class OptimisticLockingTest { | ||||||
|
||||||
@Autowired | ||||||
private HotelService hotelService; | ||||||
|
||||||
@Autowired | ||||||
private RoomRepository roomRepository; | ||||||
|
||||||
@Test | ||||||
@Transactional | ||||||
public void testOptimisticLockingOnRoomBooking() throws InterruptedException { | ||||||
// Create a test room | ||||||
Room room = new Room("999", "Single", 100.0, "Test room for concurrency"); | ||||||
room = roomRepository.save(room); | ||||||
final Long roomId = room.getId(); | ||||||
|
||||||
// Setup for concurrent booking attempts | ||||||
int numberOfThreads = 3; | ||||||
ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads); | ||||||
CountDownLatch latch = new CountDownLatch(numberOfThreads); | ||||||
|
||||||
AtomicInteger successCount = new AtomicInteger(0); | ||||||
AtomicInteger failureCount = new AtomicInteger(0); | ||||||
|
||||||
// Simulate concurrent booking attempts | ||||||
for (int i = 0; i < numberOfThreads; i++) { | ||||||
final int threadId = i; | ||||||
executor.submit(() -> { | ||||||
try { | ||||||
hotelService.createBooking( | ||||||
roomId, | ||||||
"Guest " + threadId, | ||||||
"guest" + threadId + "@test.com", | ||||||
LocalDate.now().plusDays(1), | ||||||
LocalDate.now().plusDays(3) | ||||||
); | ||||||
successCount.incrementAndGet(); | ||||||
} catch (RuntimeException | ObjectOptimisticLockingFailureException e) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||
failureCount.incrementAndGet(); | ||||||
} finally { | ||||||
latch.countDown(); | ||||||
} | ||||||
}); | ||||||
} | ||||||
|
||||||
latch.await(); // Wait for all threads to complete | ||||||
executor.shutdown(); | ||||||
|
||||||
// Assertions | ||||||
assertEquals(1, successCount.get(), "Only one booking should succeed"); | ||||||
assertEquals(numberOfThreads - 1, failureCount.get(), "Other attempts should fail"); | ||||||
|
||||||
// Verify room is no longer available | ||||||
Room updatedRoom = roomRepository.findById(roomId).orElseThrow(); | ||||||
assertFalse(updatedRoom.getAvailable(), "Room should be unavailable after successful booking"); | ||||||
assertNotNull(updatedRoom.getVersion(), "Room should have a version number"); | ||||||
} | ||||||
|
||||||
@Test | ||||||
public void testVersionFieldsExist() { | ||||||
// Test that version fields are properly added | ||||||
Room room = new Room("998", "Double", 150.0, "Version test room"); | ||||||
room = roomRepository.save(room); | ||||||
|
||||||
assertNotNull(room.getVersion(), "Room should have a version field after save"); | ||||||
assertTrue(room.getVersion() >= 0, "Version should be non-negative"); | ||||||
} | ||||||
|
||||||
@Test | ||||||
public void testOptimisticLockExceptionOnRoomUpdate() { | ||||||
// Create and save a room | ||||||
Room room = new Room("997", "Suite", 250.0, "Lock test room"); | ||||||
room = roomRepository.save(room); | ||||||
|
||||||
// Get two instances of the same room | ||||||
Room room1 = roomRepository.findById(room.getId()).orElseThrow(); | ||||||
Room room2 = roomRepository.findById(room.getId()).orElseThrow(); | ||||||
|
||||||
// Update and save first instance | ||||||
room1.setPrice(300.0); | ||||||
roomRepository.save(room1); | ||||||
|
||||||
// Try to update second instance (should fail due to optimistic locking) | ||||||
room2.setPrice(350.0); | ||||||
|
||||||
assertThrows(ObjectOptimisticLockingFailureException.class, () -> { | ||||||
roomRepository.save(room2); | ||||||
}); | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
spring.application.name=HotelApp-Test | ||
|
||
# MySQL Database Configuration for Testing | ||
spring.datasource.url=jdbc:mysql://localhost:3306/hoteldb_test?createDatabaseIfNotExist=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC | ||
spring.datasource.username=root | ||
spring.datasource.password= | ||
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver | ||
|
||
# JPA Configuration for Testing | ||
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect | ||
spring.jpa.hibernate.ddl-auto=create-drop | ||
spring.jpa.show-sql=true | ||
spring.jpa.properties.hibernate.format_sql=true | ||
|
||
# Connection Pool Settings for Testing | ||
spring.datasource.hikari.maximum-pool-size=5 | ||
spring.datasource.hikari.minimum-idle=2 | ||
spring.datasource.hikari.connection-timeout=30000 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For consistency with other JPA annotations like
@Entity
and@Table
used in this class, it's better to usejakarta.persistence.Version
instead oforg.springframework.data.annotation.Version
. While Spring Data's@Version
is designed for non-JPA data stores, using the standard JPA annotation directly makes the dependency on JPA more explicit and consistent throughout your JPA entities.