test: add reservation concurrency tests
This commit is contained in:
1 parent
4431dc3e93
commit
1deee32195
1 file changed
+102
@@ -0,0 +1,102 @@
|
|||||||
|
package com.slotline.reservation;
|
||||||
|
|
||||||
|
import com.slotline.organization.Organization;
|
||||||
|
import com.slotline.organization.OrganizationRepository;
|
||||||
|
import com.slotline.reservation.dto.ReservationRequest;
|
||||||
|
import com.slotline.resource.Resource;
|
||||||
|
import com.slotline.resource.ResourceRepository;
|
||||||
|
import com.slotline.security.AuthenticatedUser;
|
||||||
|
import com.slotline.user.Role;
|
||||||
|
import com.slotline.user.User;
|
||||||
|
import com.slotline.user.UserRepository;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.dao.ConcurrencyFailureException;
|
||||||
|
import org.springframework.dao.DataIntegrityViolationException;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
|
||||||
|
import com.slotline.common.SlotConflictException;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
class ReservationConcurrencyTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ReservationService reservationService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ResourceRepository resourceRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private OrganizationRepository organizationRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserRepository userRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ReservationRepository reservationRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void onlyOneBookingWinsForTheSameSlotUnderConcurrency() throws Exception {
|
||||||
|
Organization organization = organizationRepository.save(
|
||||||
|
new Organization("Concurrency Org", "concurrency-org", Instant.now()));
|
||||||
|
Resource court = resourceRepository.save(
|
||||||
|
new Resource(organization.getId(), "Court 1", "Outdoor tennis court", Instant.now()));
|
||||||
|
User booker = userRepository.save(
|
||||||
|
new User(organization.getId(), "racer", passwordEncoder.encode("password123"), Role.USER, Instant.now()));
|
||||||
|
AuthenticatedUser actor =
|
||||||
|
new AuthenticatedUser(booker.getId(), booker.getUsername(), booker.getRole(), organization.getId());
|
||||||
|
|
||||||
|
Instant start = Instant.parse("2026-09-01T10:00:00Z");
|
||||||
|
Instant end = Instant.parse("2026-09-01T11:00:00Z");
|
||||||
|
ReservationRequest sameSlot = new ReservationRequest(court.getId(), start, end);
|
||||||
|
|
||||||
|
int attempts = 12;
|
||||||
|
ExecutorService pool = Executors.newFixedThreadPool(attempts);
|
||||||
|
CountDownLatch startGate = new CountDownLatch(1);
|
||||||
|
AtomicInteger successes = new AtomicInteger();
|
||||||
|
AtomicInteger conflicts = new AtomicInteger();
|
||||||
|
|
||||||
|
List<Future<?>> futures = new ArrayList<>();
|
||||||
|
for (int i = 0; i < attempts; i++) {
|
||||||
|
futures.add(pool.submit(() -> {
|
||||||
|
startGate.await();
|
||||||
|
try {
|
||||||
|
reservationService.create(actor, sameSlot);
|
||||||
|
successes.incrementAndGet();
|
||||||
|
} catch (SlotConflictException | ConcurrencyFailureException | DataIntegrityViolationException conflict) {
|
||||||
|
conflicts.incrementAndGet();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
startGate.countDown();
|
||||||
|
for (Future<?> future : futures) {
|
||||||
|
future.get(20, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
pool.shutdownNow();
|
||||||
|
|
||||||
|
assertThat(successes.get()).isEqualTo(1);
|
||||||
|
assertThat(conflicts.get()).isEqualTo(attempts - 1);
|
||||||
|
assertThat(reservationRepository.findByResourceIdAndStatusOrderByStartTimeAsc(
|
||||||
|
court.getId(), ReservationStatus.ACTIVE)).hasSize(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user