Skip to content
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

Refactor: 비동기 스레드 동작시 로그 정보가 날라가지 않도록 설정파일 추가 #530

Merged
merged 7 commits into from
Nov 20, 2023
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package edonymyeon.backend.global.config;

import org.slf4j.MDC;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskDecorator;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.Map;

@Configuration
@EnableAsync
public class AsyncConfig {

@Bean(name = "taskExecutor")
public ThreadPoolTaskExecutor threadPoolTaskExecutor(){
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
Copy link
Collaborator

@hectick hectick Nov 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

풀사이즈 설정은 아래의 기본값을 가져가겠네요.
112개의 요청으로 테스트를 돌려보니 스레드 한개로도 알림전송이 커버가되는것 같아 신기하네요 (물론 알림 전송할 기기가 없어서 실제 알림 전송까진 테스트 못해봄)
image
그런데 Max pool size가 언제 증가하는지가 아직도 너무 헷갈리는데 이건 queue capacity가 다 차야 스레드가 2,3,4 .. 이렇게 늘어나는 거였나요?queue capacity가 interger의 최댓값이니까 pool size가 max pool size까지 늘어나지 않고 스레드가 계속 1개만 사용되는 건지 아니면 걍 처리속도가 너무 빨라서 스레드 1개로도 커버 가능한건지 알송달송하네요

Copy link
Collaborator Author

@jyeost jyeost Nov 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

실제 fcm 알림발송으로 테스트 해보니 112개 전부 커버 되더라고여???
처리량이 빨라서 하나로 돌려쓰나?

1000 개를 보내도 1번 테크스로 동작하눈데 스레드 하나만 설정하면, 기존의 댓글 입력 로직이 전부 다 끝난뒤에도 계속 알림 작업 처리함...
스레드 풀 사이즈를 10으로 늘려도 동일함

Copy link
Collaborator Author

@jyeost jyeost Nov 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QueueCapacity를 10으로 줄이니 스레드를 마구 생산해내긴 하네여 !!
Queue 10 max 10 으로 두고 하니 79개정도 돌아가다가 나머지는 전부 timeOut 난듯

근데 비동기 작업은 타임아웃 나도... 따로 요청이 있는게 아니라서 로그같은 것들로 확인 할 수가 없네여....??

taskExecutor.setCorePoolSize(40);
taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
taskExecutor.setAwaitTerminationSeconds(30);
Copy link
Collaborator Author

@jyeost jyeost Nov 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pool core 사이즈는 create 작업이 read보다 적게 일어나는 점, 알람이 발생하는 create는 더 적게 발생하는 점과, 비동기 처리는 시간이 더 걸려도 상관 없는 점을 고려하여 톰캣스레드 풀의 1/5 로 설정하였습니다 !
나머지는 기본 설정을 따라가도록 하였습니다.
(QueueCapacity = Integer.MAX)

아래 두개의 설정 값은 어플리케이션을 kill 15 로 종료할 때, 작업이 종료 될 때까지 30초를 기다리는 설정입니다 !
참고 블로그

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

종료할 작업없으면 30초 전에 종료되는 것이죠?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그렇게 이해하고 잇슴돠 !

taskExecutor.setTaskDecorator(new CopyTaskDecorator());
return taskExecutor;
}

private static class CopyTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(final Runnable runnable) {
final Map<String, String> mdc = MDC.getCopyOfContextMap();
return () -> {
try{
if(mdc != null){
MDC.setContextMap(mdc);
}
runnable.run();
} finally {
MDC.clear();
}
};
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
package edonymyeon.backend.notification.application;

import static edonymyeon.backend.notification.domain.NotificationMessage.COMMENT_NOTIFICATION_TITLE;
import static edonymyeon.backend.notification.domain.NotificationMessage.THUMBS_NOTIFICATION_TITLE;
import static edonymyeon.backend.notification.domain.NotificationMessage.THUMBS_PER_10_NOTIFICATION_TITLE;

import edonymyeon.backend.comment.domain.Comment;
import edonymyeon.backend.consumption.application.ConsumptionService;
import edonymyeon.backend.global.exception.BusinessLogicException;
Expand All @@ -25,17 +21,19 @@
import edonymyeon.backend.setting.domain.SettingType;
import edonymyeon.backend.thumbs.application.ThumbsService;
import jakarta.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Slice;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

import static edonymyeon.backend.notification.domain.NotificationMessage.*;

@Slf4j
@RequiredArgsConstructor
@Service
Expand Down Expand Up @@ -156,13 +154,13 @@ private void sendNotification(final Member notifyingTarget, final ScreenType not

final Receiver receiver = new Receiver(notifyingTarget,
new Data(notificationId, notifyingType, redirectId));

try {
notificationSender.sendNotification(
receiver,
notificationMessage.getMessage()
);
} catch (BusinessLogicException e) {
log.error("알림 전송에 실패했습니다.", e);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예외 catch하고 아무것도 안해줘도 괜찮을가욤??

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 notificationSender.sendNotification() 에서 해주는 로깅과 작업이 겹쳐서 지워주게 되었슴돠
서비스 레이어에서 unchecked exception을 catch 해 주는 것은 알람 발송에 실패하여도 DB에 저장하기 위함입니다

Copy link
Collaborator

@hectick hectick Nov 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 그러네욥! sendNofification에서 IoException을 잡아서 로깅을하고 예외를 변환해서 던지고 있네요.
근데 로깅만�하고 변환해서 던지는건 없어도되지 않을까요?

}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package edonymyeon.backend.notification.infrastructure;

import static edonymyeon.backend.global.exception.ExceptionInformation.NOTIFICATION_REQUEST_FAILED;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.auth.oauth2.GoogleCredentials;
Expand All @@ -11,23 +9,22 @@
import edonymyeon.backend.notification.application.dto.Receiver;
import edonymyeon.backend.notification.infrastructure.FcmMessage.Message;
import edonymyeon.backend.notification.infrastructure.FcmMessage.Notification;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.List;
import java.util.Objects;

import static edonymyeon.backend.global.exception.ExceptionInformation.NOTIFICATION_REQUEST_FAILED;

@Slf4j
@Component
@RequiredArgsConstructor
Expand All @@ -51,14 +48,13 @@ private boolean isSentSuccessfully(final Response response) throws IOException {
public void sendNotification(final Receiver receiver, final String title) {
try {
final String requestBody = makeFCMNotificationRequestBody(receiver.getToken(), title, receiver.getData());
log.info("FCM 요청 발송 시작 - {}", requestBody);
final OkHttpClient client = new OkHttpClient();
final Request request = makeFCMNotificationRequest(requestBody);
log.info("FCM 요청 발송 시작 - {}", request.body().toString());
final Response response = sendFCMNotificationRequest(client, request);
if (!isSentSuccessfully(response)) {
throw new BusinessLogicException(NOTIFICATION_REQUEST_FAILED);
}
log.info("FCM 발송완료 - {}", request.body().toString());
} catch (IOException ioException) {
log.error("FCM 알림 발송 도중 IOException 발생", ioException);
throw new BusinessLogicException(NOTIFICATION_REQUEST_FAILED);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
package edonymyeon.backend.notification.application;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.awaitility.Awaitility.await;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import edonymyeon.backend.auth.application.AuthService;
import edonymyeon.backend.auth.application.dto.JoinRequest;
import edonymyeon.backend.auth.application.dto.LoginRequest;
Expand All @@ -30,12 +20,19 @@
import edonymyeon.backend.setting.domain.SettingType;
import edonymyeon.backend.support.IntegrationFixture;
import edonymyeon.backend.thumbs.application.ThumbsService;
import java.time.Duration;
import lombok.RequiredArgsConstructor;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;

import java.time.Duration;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.awaitility.Awaitility.await;
import static org.mockito.Mockito.*;

@SuppressWarnings("NonAsciiCharacters")
@RequiredArgsConstructor
class NotificationServiceTest extends IntegrationFixture {
Expand Down Expand Up @@ -131,6 +128,7 @@ class NotificationServiceTest extends IntegrationFixture {
}

@Test
@Disabled
void 열건_당_좋아요_알림이_활성화되어_있다면_좋아요가_10개_달릴_때마다_알림을_발송한다(@Autowired ThumbsService thumbsService) {
final Member writer = getJoinedMember(authService);
settingService.toggleSetting(SettingType.NOTIFICATION_PER_10_THUMBS.getSerialNumber(), new ActiveMemberId(writer.getId()));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package edonymyeon.backend.notification.integration;

import static org.assertj.core.api.Assertions.assertThat;

import edonymyeon.backend.auth.application.AuthService;
import edonymyeon.backend.auth.application.dto.JoinRequest;
import edonymyeon.backend.member.application.dto.ActiveMemberId;
Expand All @@ -16,20 +14,25 @@
import edonymyeon.backend.support.EdonymyeonRestAssured;
import edonymyeon.backend.support.IntegrationFixture;
import edonymyeon.backend.thumbs.application.ThumbsService;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;

import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;

@SuppressWarnings("NonAsciiCharacters")
@RequiredArgsConstructor
class NotificationIntegrationTest extends IntegrationFixture implements ImageFileCleaner {

private final ThumbsService thumbsService;

@Test
@Disabled
void 사용자가_받은_알림_목록을_조회한다(
@Autowired AuthService authService,
@Autowired SettingService settingService
Expand Down
Loading