Skip to content
Merged
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,9 @@
package com.ticketPing.order.presentation.request;

import java.util.UUID;

public record CreateOrderRequest (
UUID scheduleId,
UUID seatId
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ public record SeatResponse (
Integer row,
Integer col,
String seatStatus,
String seatGrade,
Integer cost
String seatGrade
) {
public static SeatResponse of(Seat seat) {
return SeatResponse.builder()
Expand All @@ -24,7 +23,6 @@ public static SeatResponse of(Seat seat) {
.col(seat.getCol())
.seatStatus(seat.getSeatStatus().getValue())
.seatGrade(seat.getSeatCost().getSeatGrade())
.cost(seat.getSeatCost().getCost())
.build();
}

Expand All @@ -35,7 +33,6 @@ public static SeatResponse of(SeatCache seatCache) {
.col(seatCache.getCol())
.seatStatus(seatCache.getSeatStatus())
.seatGrade(seatCache.getSeatGrade())
.cost(seatCache.getCost())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,49 +1,49 @@
package com.ticketPing.performance.application.scheduler;

import com.ticketPing.performance.application.service.NotificationService;
import com.ticketPing.performance.application.service.PerformanceService;
import com.ticketPing.performance.domain.model.entity.Performance;
import com.ticketPing.performance.infrastructure.service.DiscordNotificationService;
import com.ticketPing.performance.infrastructure.service.DistributedLockService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Slf4j
@Component
@RequiredArgsConstructor
public class SeatCacheScheduler {
private final PerformanceService performanceService;
private final RedissonClient redissonClient;
private final DiscordNotificationService discordNotificationService;
private final DistributedLockService lockService;
private final NotificationService notificationService;

private static final String LOCK_KEY = "SchedulerLock";
private static final int LOCK_TIMEOUT = 300;

@Scheduled(cron = "0 0/10 * * * *")
public void runScheduler() {
log.info("Scheduler triggered");
try {
log.info("Scheduler started");
RLock lock = redissonClient.getLock(LOCK_KEY);
boolean acquired = lock.tryLock(0, LOCK_TIMEOUT, TimeUnit.SECONDS);

if (acquired) {
Performance performance = performanceService.getUpcomingPerformance();
if (performance != null) {
performanceService.cacheAllSeatsForPerformance(performance.getId());
log.info("Caching completed");
} else {
log.info("No upcoming performance");
}
} else {
boolean executed = lockService.executeWithLock(LOCK_KEY, 0, LOCK_TIMEOUT, this::cacheSeatsForUpcomingPerformance);
if (!executed) {
log.warn("Another server is running the scheduler");
}
} catch (Exception e) {
log.error("Error occurred during execution: {}", e.getMessage(), e);
discordNotificationService.sendErrorNotification(e.getMessage());
log.error("Unexpected error in scheduler: {}", e.getMessage(), e);
notificationService.sendErrorNotification(
String.format("Error in SeatCacheScheduler: %s", e.getMessage())
);
}
}

private void cacheSeatsForUpcomingPerformance() {
Performance performance = performanceService.getUpcomingPerformance();
if (performance != null) {
performanceService.cacheAllSeatsForPerformance(performance.getId());
log.info("Caching completed for performance ID: {}", performance.getId());
} else {
log.info("No upcoming performance found");
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.ticketPing.performance.application.service;

public interface NotificationService {
void sendErrorNotification(String errorMessage);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
import com.ticketPing.performance.domain.model.entity.Performance;
import com.ticketPing.performance.domain.model.entity.Schedule;
import com.ticketPing.performance.domain.repository.PerformanceRepository;
import com.ticketPing.performance.infrastructure.repository.CacheRepositoryImpl;
import exception.ApplicationException;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
Expand All @@ -23,14 +24,15 @@
public class PerformanceService {

private final PerformanceRepository performanceRepository;
private final CacheRepositoryImpl cacheRepositoryImpl;
private final SeatService seatService;

public PerformanceResponse getPerformance(UUID performanceId) {
Performance performance = findPerformanceWithDetails(performanceId);
return PerformanceResponse.of(performance);
}

public Page<PerformanceListResponse> getAllPerformances(Pageable pageable) {
public Slice<PerformanceListResponse> getAllPerformances(Pageable pageable) {
return performanceRepository.findAll(pageable)
.map(PerformanceListResponse::of);
}
Expand Down Expand Up @@ -58,8 +60,7 @@ public void cacheAllSeatsForPerformance(UUID performanceId) {
long availableSeats = seatService.cacheSeatsForSchedule(schedule);
totalAvailableSeats += availableSeats;
}

seatService.cacheAvailableSeatsForPerformance(performanceId, totalAvailableSeats);
cacheRepositoryImpl.cacheAvailableSeats(performanceId, totalAvailableSeats);
}

private Performance findPerformanceWithSchedules(UUID id) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
package com.ticketPing.performance.application.service;

import com.ticketPing.performance.application.dtos.ScheduleResponse;
import com.ticketPing.performance.application.dtos.SeatResponse;
import com.ticketPing.performance.common.exception.ScheduleExceptionCase;
import com.ticketPing.performance.domain.model.entity.Schedule;
import com.ticketPing.performance.domain.model.entity.SeatCache;
import com.ticketPing.performance.domain.repository.ScheduleRepository;
import com.ticketPing.performance.infrastructure.service.CacheService;
import exception.ApplicationException;
import com.ticketPing.performance.infrastructure.repository.CacheRepositoryImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

Expand All @@ -19,21 +14,10 @@
@RequiredArgsConstructor
public class ScheduleService {

private final ScheduleRepository scheduleRepository;
private final CacheService cacheService;

public ScheduleResponse getSchedule(UUID id) {
Schedule schedule = findScheduleById(id);
return ScheduleResponse.of(schedule);
}
private final CacheRepositoryImpl cacheRepositoryImpl;

public List<SeatResponse> getAllScheduleSeats(UUID scheduleId) {
Map<String, SeatCache> seatMap = cacheService.getSeatsFromCache(scheduleId);
Map<String, SeatCache> seatMap = cacheRepositoryImpl.getSeatCaches(scheduleId);
return seatMap.values().stream().map(SeatResponse::of).toList();
}

private Schedule findScheduleById(UUID id) {
return scheduleRepository.findById(id)
.orElseThrow(() -> new ApplicationException(ScheduleExceptionCase.SCHEDULE_NOT_FOUND));
}
}
Original file line number Diff line number Diff line change
@@ -1,102 +1,105 @@
package com.ticketPing.performance.application.service;

import com.ticketPing.performance.application.dtos.OrderSeatResponse;
import com.ticketPing.performance.application.dtos.SeatResponse;
import com.ticketPing.performance.common.exception.SeatExceptionCase;
import com.ticketPing.performance.domain.model.entity.Schedule;
import com.ticketPing.performance.domain.model.entity.Seat;
import com.ticketPing.performance.domain.model.entity.SeatCache;
import com.ticketPing.performance.domain.model.enums.SeatStatus;
import com.ticketPing.performance.domain.repository.CacheRepository;
import com.ticketPing.performance.domain.repository.SeatRepository;
import com.ticketPing.performance.infrastructure.service.CacheService;
import com.ticketPing.performance.infrastructure.service.LuaScriptService;
import exception.ApplicationException;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;

import static com.ticketPing.performance.common.constants.SeatConstants.PRE_RESERVE_TTL;

@Service
@RequiredArgsConstructor
public class SeatService {

private final SeatRepository seatRepository;
private final CacheService cacheService;
private final LuaScriptService luaScriptService;

@Value("${seat.pre-reserve-ttl}")
private int PRE_RESERVE_TTL;

public SeatResponse getSeat(UUID id) {
Seat seat = seatRepository.findByIdWithSeatCost(id)
.orElseThrow(() -> new ApplicationException(SeatExceptionCase.SEAT_NOT_FOUND));
return SeatResponse.of(seat);
}
private final CacheRepository cacheRepository;

public void preReserveSeat(UUID scheduleId, UUID seatId, UUID userId) {
luaScriptService.preReserveSeat(scheduleId, seatId, userId);
cacheRepository.preReserveSeatCache(scheduleId, seatId, userId);
}

public void cancelPreReserveSeat(UUID scheduleId, UUID seatId, UUID userId) {
validatePreserve(scheduleId, seatId, userId);
cacheService.canclePreReserveSeat(scheduleId, seatId);
cancelPreReserveSeatInCache(scheduleId, seatId);
}

@Transactional
public void reserveSeat(String scheduleId, String seatId) {
reserveSeatInDB(seatId);
reserveSeatInCache(UUID.fromString(scheduleId), UUID.fromString(seatId));
}

public OrderSeatResponse getOrderSeatInfo(UUID scheduleId, UUID seatId, UUID userId) {
validatePreserve(scheduleId, seatId, userId);

Seat seat = seatRepository.findByIdWithAll(seatId)
.orElseThrow(() -> new ApplicationException(SeatExceptionCase.SEAT_NOT_FOUND));

Seat seat = getSeatWithDetails(seatId);
extendPreReserveTTL(scheduleId, seatId);
return OrderSeatResponse.of(seat);
}

public void extendPreReserveTTL(UUID scheduleId, UUID seatId) {
cacheService.extendPreReserveTTL(scheduleId, seatId, Duration.ofSeconds(PRE_RESERVE_TTL));
}

public void reserveSeat(String scheduleId, String seatId) {
Seat seat = seatRepository.findById(UUID.fromString(seatId))
.orElseThrow(() -> new ApplicationException(SeatExceptionCase.SEAT_NOT_FOUND));
cacheService.reserveSeat(scheduleId, seatId);
cacheRepository.extendPreReserveTTL(scheduleId, seatId, Duration.ofSeconds(PRE_RESERVE_TTL));
}

public long cacheSeatsForSchedule(Schedule schedule) {
List<Seat> seats = seatRepository.findByScheduleWithSeatCost(schedule);

Map<String, SeatCache> seatMap = seats.stream()
.collect(Collectors.toMap(seat -> seat.getId().toString(), SeatCache::from));
Map<String, SeatCache> seatMap = seats.stream().collect(Collectors.toMap(seat -> seat.getId().toString(), SeatCache::from));

LocalDateTime expiration = schedule.getStartDate().atTime(23, 59, 59);
Duration ttl = Duration.between(LocalDateTime.now(), expiration);

cacheService.cacheSeats(schedule.getId(), seatMap, ttl);
cacheRepository.cacheSeats(schedule.getId(), seatMap, ttl);

return seats.stream()
.filter(seat -> seat.getSeatStatus() == SeatStatus.AVAILABLE)
.count();
return seats.stream().filter(seat -> seat.getSeatStatus() == SeatStatus.AVAILABLE).count();
}

public void cacheAvailableSeatsForPerformance(UUID performanceId, long availableSeats) {
cacheService.cacheAvailableSeats(performanceId, availableSeats);
private Seat getSeatWithDetails(UUID seatId) {
return seatRepository.findByIdWithAll(seatId)
.orElseThrow(() -> new ApplicationException(SeatExceptionCase.SEAT_NOT_FOUND));
}

private void validatePreserve(UUID scheduleId, UUID seatId, UUID userId) {
SeatCache seatCache = cacheService.getSeatFromCache(scheduleId, seatId);
@Transactional
private void reserveSeatInDB(String seatId) {
Seat seat = seatRepository.findById(UUID.fromString(seatId))
.orElseThrow(() -> new ApplicationException(SeatExceptionCase.SEAT_NOT_FOUND));
seat.reserveSeat();
}

if(!seatCache.getSeatStatus().equals(SeatStatus.HELD.getValue())) {
throw new ApplicationException(SeatExceptionCase.SEAT_NOT_PRE_RESERVED);
} else if(!seatCache.getUserId().equals(userId)) {
System.out.println(seatCache.getUserId() + " " + userId);
private void validatePreserve(UUID scheduleId, UUID seatId, UUID userId) {
String preReserveUserId = cacheRepository.getPreReservTTL(scheduleId, seatId);
if(!preReserveUserId.equals(userId.toString()))
throw new ApplicationException(SeatExceptionCase.USER_NOT_MATCH);
}
}

private void cancelPreReserveSeatInCache(UUID scheduleId, UUID seatId) {
cacheRepository.deletePreReserveTTL(scheduleId, seatId);
SeatCache seatCache = cacheRepository.getSeatCache(scheduleId, seatId);
seatCache.cancelPreReserveSeat();
cacheRepository.putSeatCache(seatCache, scheduleId, seatId);
}

private void reserveSeatInCache(UUID scheduleId, UUID seatId) {
cacheRepository.deletePreReserveTTL(scheduleId, seatId);
SeatCache seatCache = cacheRepository.getSeatCache(scheduleId, seatId);
seatCache.reserveSeat();
cacheRepository.putSeatCache(seatCache, scheduleId, seatId);
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ public class SeatCache {
private Integer col;
private String seatStatus;
private String seatGrade;
private Integer cost;
private UUID userId;

public static SeatCache from(Seat seat) {
return SeatCache.builder()
Expand All @@ -27,13 +25,11 @@ public static SeatCache from(Seat seat) {
.col(seat.getCol())
.seatStatus(seat.getSeatStatus().getValue())
.seatGrade(seat.getSeatCost().getSeatGrade())
.cost(seat.getSeatCost().getCost())
.build();
}

public void cancelPreReserveSeat() {
seatStatus = SeatStatus.AVAILABLE.getValue();
userId = null;
}

public void reserveSeat() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.ticketPing.performance.domain.repository;

import com.ticketPing.performance.domain.model.entity.SeatCache;

import java.time.Duration;
import java.util.Map;
import java.util.UUID;

public interface CacheRepository {
void cacheSeats(UUID scheduleId, Map<String, SeatCache> seatMap, Duration ttl);

Map<String, SeatCache> getSeatCaches(UUID scheduleId);

SeatCache getSeatCache(UUID scheduleId, UUID seatId);

void putSeatCache(SeatCache seatCache, UUID scheduleId, UUID seatId);

void preReserveSeatCache(UUID scheduleId, UUID seatId, UUID userId);

String getPreReservTTL(UUID scheduleId, UUID seatId);

void extendPreReserveTTL(UUID scheduleId, UUID seatId, Duration ttl);

void deletePreReserveTTL(UUID scheduleId, UUID seatId);

void cacheAvailableSeats(UUID performanceId, long availableSeats);
}
Loading
Loading