Skip to content

Commit

Permalink
Merge pull request #122 from farmingsoon/develop
Browse files Browse the repository at this point in the history
배포를 위한 main merge
  • Loading branch information
gkfktkrh153 authored Mar 4, 2024
2 parents 19ffe81 + a5ca141 commit b547b47
Show file tree
Hide file tree
Showing 58 changed files with 840 additions and 162 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'



runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ public class BaseTimeEntity {
@Column(nullable = false)
private LocalDateTime modifiedAt;

private Boolean deleted;
private Boolean deleted = Boolean.FALSE;
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public enum ErrorCode {
INVALID_REFRESH_TOKEN("유효하지 않은 RefreshToken입니다.", HttpStatus.UNAUTHORIZED),
INVALID_CURRENT_PASSWORD("현재 비밀번호가 일치하지 않습니다!", HttpStatus.UNAUTHORIZED),
INVALID_NEW_PASSWORD("새 비밀번호가 일치하지 않습니다!", HttpStatus.UNAUTHORIZED),
SNATCH_TOKEN("Refresh Token 탈취를 감지하여 로그아웃 처리됩니다.", HttpStatus.UNAUTHORIZED),

// 403
NOT_LOGIN("로그인 후 이용할 수 있습니다.", HttpStatus.FORBIDDEN),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import com.api.farmingsoon.common.alert.discord.DiscordService;
import com.api.farmingsoon.common.response.Response;
import io.lettuce.core.RedisCommandTimeoutException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.QueryTimeoutException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
Expand All @@ -23,6 +25,7 @@ public class GlobalExceptionHandler {

/**
* 서비스 로직 도중 발생하는 에러들을 커스텀하여 응답값을 내려줍니다.
* 디스코드로 에러메시지를 전송합니다.
*/
@ExceptionHandler(CustomException.class)
public ResponseEntity<?> handleCustomException(HttpServletRequest request, CustomException e){
Expand All @@ -45,5 +48,11 @@ public ResponseEntity<?> handleValidationExceptions(HttpServletRequest request,M
log.error(ex.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Response.error(HttpStatus.BAD_REQUEST, errors));
}

@ExceptionHandler({RedisCommandTimeoutException.class, QueryTimeoutException.class})
public ResponseEntity<?> handleException(Exception exception){
log.info("Redis response delay");
return ResponseEntity.
status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Response.error(HttpStatus.INTERNAL_SERVER_ERROR, "서버에 문제가 발생했습니다."));
}
}
22 changes: 22 additions & 0 deletions src/main/java/com/api/farmingsoon/common/init/InitData.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.api.farmingsoon.common.init;

import com.api.farmingsoon.common.scheduler.CustomSchedulerRunner;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@RequiredArgsConstructor
public class InitData {
private final CustomSchedulerRunner schedulerRunner;

@Bean
public CommandLineRunner init(){
return args -> {

schedulerRunner.runRedisHealthCheckScheduler();

};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
import com.api.farmingsoon.common.security.jwt.JwtProvider;
import com.api.farmingsoon.common.util.CookieUtils;
import com.api.farmingsoon.common.util.JwtUtils;
import com.api.farmingsoon.domain.chatroom.event.ChatRoomConnectEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
Expand All @@ -22,26 +24,22 @@
@RequiredArgsConstructor
public class StompInterceptor implements ChannelInterceptor {
private final JwtProvider jwtProvider;
private final ApplicationEventPublisher eventPublisher;
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
log.info("command : " + accessor.getCommand());

/*
if (StompCommand.CONNECT.equals(accessor.getCommand()))
validateToken(accessor);
*/

return message;
}

private void validateToken(StompHeaderAccessor accessor) {
String accessToken = JwtUtils.extractBearerToken(accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION));
if (StompCommand.CONNECT.equals(accessor.getCommand()))
eventPublisher.publishEvent(ChatRoomConnectEvent.builder()
.memberId(Long.valueOf(accessor.getFirstNativeHeader("memberId")))
.chatRoomId(Long.valueOf(accessor.getFirstNativeHeader("chatroomId")))
.build()
);

if (accessToken == null)
throw new ForbiddenException(ErrorCode.NOT_LOGIN);

jwtProvider.validateAccessToken(accessToken);

return message;
}
}
10 changes: 9 additions & 1 deletion src/main/java/com/api/farmingsoon/common/redis/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
package com.api.farmingsoon.common.redis;


import io.lettuce.core.ClientOptions;
import io.lettuce.core.TimeoutOptions;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.GenericToStringSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@Configuration
@EnableRedisRepositories
public class RedisConfig {
Expand All @@ -29,7 +34,10 @@ public LettuceConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName(redisHost);
config.setPort(redisPort);
return new LettuceConnectionFactory(config);
LettuceClientConfiguration lettuceClientConfiguration = LettuceClientConfiguration.builder()
.clientOptions(ClientOptions.builder()
.timeoutOptions(TimeoutOptions.enabled(Duration.ofSeconds(30))).build()).build();
return new LettuceConnectionFactory(config, lettuceClientConfiguration);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,12 @@ public Set<String> getKeySet(String domain) {
return redisTemplate.keys(domain);
}

public boolean isExistsKey(String key){
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}

public void addToSet(String key, Long itemId){
if(!redisTemplate.hasKey(key)) {// 키가 없다면(set이 없다면)
if(!isExistsKey(key)) {// 키가 없다면(set이 없다면)
redisTemplate.opsForSet().add(key, String.valueOf(itemId)); // set생성
redisTemplate.expire(key, TimeUtils.getRemainingTimeUntilMidnight(), TimeUnit.SECONDS); // 만료기간 설정
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.api.farmingsoon.common.redis.event;

import lombok.Getter;
import lombok.NoArgsConstructor;

@NoArgsConstructor
@Getter
public class RedisResponseDelayEvent {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.api.farmingsoon.common.redis.event;

import lombok.Getter;
import lombok.NoArgsConstructor;

@NoArgsConstructor
@Getter
public class RedisRestartEvent {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.api.farmingsoon.common.redis.listener;

import com.api.farmingsoon.domain.item.event.BidEndKeyExpiredEvent;
import com.api.farmingsoon.domain.notification.event.ChatNotificationDebounceKeyExpiredEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class KeyExpirationListener extends KeyExpirationEventMessageListener {

private final ApplicationEventPublisher applicationEventPublisher;
public KeyExpirationListener(RedisMessageListenerContainer listenerContainer, ApplicationEventPublisher applicationEventPublisher) {
super(listenerContainer);

this.applicationEventPublisher = applicationEventPublisher;
}

@Override
public void onMessage(Message key, byte[] pattern) {
log.info("key_expired : " + key.toString());
String[] expiredKey = key.toString().split("_");
if (expiredKey[0].equals("bidEnd")) {
applicationEventPublisher.publishEvent(new BidEndKeyExpiredEvent((Long.valueOf(expiredKey[1])))); ; // itemId
} else if (expiredKey[0].equals("chatting")) { // memberId
applicationEventPublisher.publishEvent(new ChatNotificationDebounceKeyExpiredEvent(Long.valueOf(expiredKey[1]))); // receiverId
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.api.farmingsoon.common.redis.listener;

import com.api.farmingsoon.common.redis.RedisService;
import com.api.farmingsoon.common.redis.event.RedisResponseDelayEvent;
import com.api.farmingsoon.common.redis.event.RedisRestartEvent;
import com.api.farmingsoon.common.scheduler.CustomSchedulerRunner;
import com.api.farmingsoon.domain.item.domain.Item;
import com.api.farmingsoon.domain.item.service.ItemService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Component
@RequiredArgsConstructor
public class RedisEventListener {
private final CustomSchedulerRunner schedulerRunner;
private final RedisService redisService;
private final ItemService itemService;

@EventListener
public void runByRedisResponseDelay(RedisResponseDelayEvent event){
schedulerRunner.runRestartMonitoringScheduler();
schedulerRunner.runBidEndScheduler();
schedulerRunner.stopRedisHealthCheckScheduler();
}

@EventListener
@Async("testExecutor")
public void runByRedisRestart(RedisRestartEvent event) {
schedulerRunner.stopRestartMonitoringScheduler();
schedulerRunner.stopBidEndScheduler();
schedulerRunner.runRedisHealthCheckScheduler();


List<Item> biddingItemIdList = itemService.findBiddingItemList();

biddingItemIdList.forEach(item -> redisService.setData(
"bidEnd_" + item.getId(),
"",
Duration.between(LocalDateTime.now(), item.getExpiredAt()).getSeconds(),
TimeUnit.SECONDS));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.api.farmingsoon.common.scheduler;

import com.api.farmingsoon.common.redis.RedisService;
import com.api.farmingsoon.common.redis.event.RedisResponseDelayEvent;
import com.api.farmingsoon.common.redis.event.RedisRestartEvent;
import com.api.farmingsoon.domain.item.event.BidEndSchedulerRunEvent;
import com.api.farmingsoon.domain.item.service.ItemService;
import io.lettuce.core.RedisCommandTimeoutException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.dao.QueryTimeoutException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;

@Component
@Slf4j
@RequiredArgsConstructor
public class CustomSchedulerRunner {
private final Map<String, ScheduledFuture<?>> scheduledTasks = new ConcurrentHashMap<>();
private final RedisConnectionFactory redisConnectionFactory;
private final ApplicationEventPublisher applicationEventPublisher;
@Autowired
private TaskScheduler taskScheduler;

/**
* @Description
* 주기적으로 Redis의 상태 체크를 합니다.
* 만약 응답이 지연된다면 Redis에 장애가 있다고 판단하여 RedisEventListener에서 처리될 RedisResponseDelayEvent를 발행합니다.
* RedisEventListener에서는 마감처리를 진행할 bidEndScheduler와 redis의 재연결을 모니터링하는 restartScheduler를 실행합니다.
*/
public void runRedisHealthCheckScheduler(){
ScheduledFuture<?> task = taskScheduler.scheduleAtFixedRate(
()->{
log.info("runRedisHealthCheckScheduler");
RedisConnection connection = redisConnectionFactory.getConnection();
try {
connection.ping();
}catch (QueryTimeoutException ex)
{
applicationEventPublisher.publishEvent(new RedisResponseDelayEvent());
}
finally {
connection.close();
}
}
, 30000); // ms
scheduledTasks.put("RedisHealthCheckScheduler", task);
}
/**
* @Description
* redis의 응답 지연으로 Redis에 장애가 있다고 판단될 시 실행되는 스케줄러로 주기적으로 ping을 날려 재연결을 모니터링합니다.
* 만약 응답으로 PONG을 받는다면 재연결됐다고 판단하여 redisEventListener에서 실행되는 RedisRestartEvent를 발행합니다.
* 리스너에서는 다시 HealthCheckScheduler를 실행시킵니다.
* 또한, 아직 입찰마감 처리까지 시간이 남았지만 레디스에서 사라진 데이터들을 채워넣습니다.
* 이후, BidEndScheduler와 runRestartMonitoringScheduler를 종료시킵니다.
*/
public void runRestartMonitoringScheduler(){
ScheduledFuture<?> task = taskScheduler.scheduleAtFixedRate(
()->{
log.info("runRestartMonitoringScheduler");
RedisConnection connection = redisConnectionFactory.getConnection();

try {
if(connection.ping().equals("PONG")){
applicationEventPublisher.publishEvent(new RedisRestartEvent());
}
}catch (QueryTimeoutException ex)
{
log.info("재시작 대기중");
}
finally {
connection.close();
}
}
, 30000); // ms
scheduledTasks.put("RestartMonitoringScheduler", task);
}

/**
* @Description
* 현재시간 기준으로 만료일자가 지났지만 입찰중인 상품들을 마감처리합니다.
*/
public void runBidEndScheduler(){
ScheduledFuture<?> task = taskScheduler.scheduleAtFixedRate(
()->{
applicationEventPublisher.publishEvent(new BidEndSchedulerRunEvent());
}
, 5000); // ms
scheduledTasks.put("BidEndScheduler", task);
}
public void stopRestartMonitoringScheduler(){
log.info("stopRestartMonitoringScheduler");
scheduledTasks.get("RestartMonitoringScheduler").cancel(true);
}

public void stopRedisHealthCheckScheduler(){
log.info("stopRedisHealthCheckScheduler");
scheduledTasks.get("RedisHealthCheckScheduler").cancel(true);
}
public void stopBidEndScheduler(){
log.info("stopBidEndScheduler");
scheduledTasks.get("BidEndScheduler").cancel(true);
}
}
Loading

0 comments on commit b547b47

Please sign in to comment.