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
1 change: 1 addition & 0 deletions docker-compose/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ services:
- PERFORMANCE_POSTGRES_URL=${PERFORMANCE_POSTGRES_URL}
- PERFORMANCE_POSTGRES_USERNAME=${PERFORMANCE_POSTGRES_USERNAME}
- PERFORMANCE_POSTGRES_PASSWORD=${PERFORMANCE_POSTGRES_PASSWORD}
- DISCORD_WEBHOOK_URL=${GF_DISCORD_WEBHOOK_URL}
- REDIS_NODE_1=${REDIS_NODE_1}
- REDIS_NODE_2=${REDIS_NODE_2}
- REDIS_NODE_3=${REDIS_NODE_3}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.ticketPing.performance.application.service.PerformanceService;
import com.ticketPing.performance.domain.model.entity.Performance;
import com.ticketPing.performance.infrastructure.service.DiscordNotificationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
Expand All @@ -17,31 +18,32 @@
public class SeatCacheScheduler {
private final PerformanceService performanceService;
private final RedissonClient redissonClient;
private final DiscordNotificationService discordNotificationService;

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

@Scheduled(cron = "0 0/10 * * * *")
public void runScheduler() {
try {
log.info("[SeatCacheScheduler] 스케줄러 실행 시작");
log.info("Scheduler started");
RLock lock = redissonClient.getLock(LOCK_KEY);
boolean acquired = lock.tryLock(10, LOCK_TIMEOUT, TimeUnit.SECONDS);
boolean acquired = lock.tryLock(0, LOCK_TIMEOUT, TimeUnit.SECONDS);

if (acquired) {
Performance performance = performanceService.getUpcomingPerformance();
if (performance != null) {
performanceService.cacheAllSeatsForPerformance(performance.getId());
log.info("[SeatCacheScheduler] 캐싱 완료");
log.info("Caching completed");
} else {
log.info("[SeatCacheScheduler] 예정된 공연이 없음");
log.info("No upcoming performance");
}
} else {
log.warn("[SeatCacheScheduler] 다른 서버에서 스케줄러 실행 중");
log.warn("Another server is running the scheduler");
}
} catch (Exception e) {
log.error("[SeatCacheScheduler] 실행 중 오류 발생: {}", e.getMessage(), e);
// TODO: discord 알림?
log.error("Error occurred during execution: {}", e.getMessage(), e);
discordNotificationService.sendErrorNotification(e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public Performance getUpcomingPerformance() {
LocalDateTime now = LocalDateTime.now();
LocalDateTime tenMinutesLater = now.plusMinutes(10);

return performanceRepository.findFirstByReservationStartDateBetween(now, tenMinutesLater);
return performanceRepository.findUpcomingPerformance(now, tenMinutesLater);
}

public void cacheAllSeatsForPerformance(UUID performanceId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.ticketPing.performance.common.exception;

import exception.ErrorCase;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;

@Getter
@RequiredArgsConstructor
public enum MessageExceptionCase implements ErrorCase {
MESSAGE_SEND_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "알람을 보낼 수 없습니다.");

private final HttpStatus httpStatus;
private final String message;
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ public interface PerformanceRepository {

Optional<Performance> findByIdWithDetails(UUID id);

Performance findFirstByReservationStartDateBetween(LocalDateTime start, LocalDateTime end);
Performance findUpcomingPerformance(LocalDateTime start, LocalDateTime end);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.ticketPing.performance.infrastructure.config;

import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.BufferingClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;

import java.nio.charset.StandardCharsets;
import java.time.Duration;

@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder
.requestFactory(() -> new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()))
.setConnectTimeout(Duration.ofMillis(5000))
.setReadTimeout(Duration.ofMillis(300000))
.additionalMessageConverters(new StringHttpMessageConverter(StandardCharsets.UTF_8))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

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

import java.time.LocalDateTime;
import java.util.Optional;
import java.util.UUID;

import com.ticketPing.performance.domain.repository.PerformanceRepository;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

@Repository
Expand All @@ -23,4 +24,10 @@ public interface PerformanceJpaRepository extends PerformanceRepository, JpaRepo
"LEFT JOIN FETCH p.seatCosts sc " +
"WHERE p.id = :id ")
Optional<Performance> findByIdWithDetails(UUID id);

@Query("SELECT p " +
"FROM Performance p " +
"WHERE p.reservationStartDate > :now " +
"AND p.reservationStartDate <= :tenMinutesLater")
Performance findUpcomingPerformance(@Param("now") LocalDateTime now, @Param("tenMinutesLater") LocalDateTime tenMinutesLater);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.ticketPing.performance.infrastructure.service;

import com.ticketPing.performance.common.exception.MessageExceptionCase;
import exception.ApplicationException;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.client.RestTemplate;

@Slf4j
@Service
@RequiredArgsConstructor
public class DiscordNotificationService {

@Value("${discord.webhook-url}")
private String discordWebhookUrl;

private final RestTemplate restTemplate;

public void sendErrorNotification(String errorMessage) {
try {
String message = String.format("**Error occurred in SeatCacheScheduler**: %s", errorMessage);
String payload = String.format("{\"content\": \"%s\"}", message);

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(payload, headers);

restTemplate.exchange(discordWebhookUrl, HttpMethod.POST, entity, String.class);
} catch (Exception e) {
log.error("Error occurred during execution: {}", e.getMessage(), e);
throw new ApplicationException(MessageExceptionCase.MESSAGE_SEND_FAIL);
}
}
}
2 changes: 2 additions & 0 deletions services/performance/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ server:
seat:
pre-reserve-ttl: 300

discord:
webhook-url: ${DISCORD_WEBHOOK_URL}
Loading