스케줄러 구조 리팩토링

This commit is contained in:
2025-03-13 14:42:17 +09:00
parent 3acb92a579
commit 64c6791cc3
20 changed files with 345 additions and 1443 deletions

View File

@@ -0,0 +1,23 @@
package com.caliverse.admin.scheduler;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public abstract class CommonScheduler implements Scheduler {
@Override
public void execute() {
try {
log.info("Start executing scheduler: {}", getSchedulerType());
executeInternal();
log.info("Finished executing scheduler: {}", getSchedulerType());
} catch (Exception e) {
log.error("Error executing scheduler {}: {}", getSchedulerType(), e.getMessage(), e);
}
}
/**
* 스케줄러 구현체에서 실제 작업을 수행하는 메소드
*/
protected abstract void executeInternal();
}

View File

@@ -1,185 +0,0 @@
package com.caliverse.admin.scheduler;
import com.caliverse.admin.domain.RabbitMq.MessageHandlerService;
import com.caliverse.admin.domain.entity.BlackList;
import com.caliverse.admin.domain.entity.Event;
import com.caliverse.admin.domain.entity.InGame;
import com.caliverse.admin.domain.entity.Mail;
import com.caliverse.admin.domain.service.*;
import com.caliverse.admin.global.common.utils.ExcelUtils;
import com.caliverse.admin.logs.logservice.indicators.*;
import com.caliverse.admin.redis.service.RedisUserInfoService;
import com.caliverse.admin.scheduler.service.BlackListScheduler;
import com.caliverse.admin.scheduler.service.EventScheduler;
import com.caliverse.admin.scheduler.service.MailScheduler;
import com.caliverse.admin.scheduler.service.NoticeScheduler;
import org.springframework.beans.factory.annotation.Autowired;
// import org.springframework.batch.core.JobParameters;
// import org.springframework.batch.core.JobParametersBuilder;
// import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import com.caliverse.admin.global.common.constants.AdminConstants;
import com.caliverse.admin.logs.Indicatordomain.StartEndTime;
import com.caliverse.admin.logs.logservice.LogServiceHelper;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
@Component
@Slf4j
@EnableScheduling
public class ScheduleRunner {
@Autowired private IndicatorsAuService auService;
@Autowired private IndicatorsDauService dauService;
@Autowired private IndicatorsWauService wauService;
@Autowired private IndicatorsMauService mauService;
@Autowired private IndicatorsMcuService mcuService;
@Autowired private IndicatorsNruService statNruService;
@Autowired private RedisUserInfoService userInfoService;
@Autowired private SchedulerManager schedulerManager;
@Autowired private DynamicScheduler dynamicScheduler;
@Autowired private MailService mailService;
@Autowired private NoticeService noticeService;
@Autowired private BlackListService blackListService;
@Autowired private EventService eventService;
@Autowired
private RedisUserInfoService redisUserInfoService;
@Autowired
private MessageHandlerService messageHandlerService;
@Autowired
private ExcelUtils excelUtils;
/*
매일 UTC 기준 00시 50분 00초에 실행, (한국 시간 9시 50분) 30분에 돌릴경우 데이터가 다 넘어 오지 않는 경우 있어서 수정처리
이게 가장 먼저 실행 되어야 한다.
로그가 많을 경우 성능 이슈 있을 수 있음
*/
@Scheduled(cron = "0 50 0 * * *")
public void auScheduler() {
//이걸 나중에 어떻게 활용할지 생각해보자.
log.info("run auScheduler");
StartEndTime startEndTime = LogServiceHelper.getCurrentLogSearchEndTime(AdminConstants.STAT_DAY_NUM);
auService.collectActiveUser(startEndTime.getStartTime(), startEndTime.getEndTime());
log.info("end auScheduler");
}
// @Scheduled(cron = "00 55 0 * * *") // 매일 UTC 기준 00시 56분 00초에 실행
@Scheduled(cron = "1 * * * * *") // 매일 UTC 기준 00시 56분 00초에 실행
public void dauScheduler() {
log.info("run dauScheduler");
StartEndTime startEndTime = LogServiceHelper.getCurrentLogSearchEndTime(AdminConstants.STAT_DAY_NUM);
dauService.collectDailyActiveUser(startEndTime.getStartTime(), startEndTime.getEndTime());
log.info("end dauScheduler");
}
@Scheduled(cron = "00 56 0 * * *") // 매일 UTC 기준 00시 56분 00초에 실행
public void wauScheduler() {
log.info("run wauScheduler");
StartEndTime startEndTime = LogServiceHelper.getCurrentLogSearchEndTime(AdminConstants.STAT_WEEK_NUM);
wauService.collectWeeklyActiveUser(startEndTime.getStartTime(), startEndTime.getEndTime());
log.info("end wauScheduler");
}
@Scheduled(cron = "00 57 0 * * *") // 매일 UTC 기준 00시 57분 00초에 실행
public void mauScheduler() {
log.info("run mauScheduler");
StartEndTime startEndTime = LogServiceHelper.getCurrentLogSearchEndTime(AdminConstants.STAT_MONTH_NUM);
mauService.collectMonthlyActiveUser(startEndTime.getStartTime(), startEndTime.getEndTime());
log.info("end mauScheduler");
}
@Scheduled(cron = "00 58 0 * * *") // 매일 UTC 기준 00시 58분 00초에 실행
public void mcuScheduler() {
log.info("run mcuScheduler");
StartEndTime startEndTime = LogServiceHelper.getCurrentLogSearchEndTime(AdminConstants.STAT_DAY_NUM);
mcuService.collectMaxCountUser(startEndTime.getStartTime(), startEndTime.getEndTime());
log.info("end mcuScheduler");
}
@Scheduled(cron = "00 59 0 * * *") // 매일 UTC 기준 00시 59분 00초에 실행
public void nruScheduler() {
log.info("run mcuScheduler");
//StartEndTime startEndTime = LogServiceHelper.getCurrentLogSearchEndTime(AdminConstants.STAT_DAY_NUM);
//statNruService.collectStatLogs(startEndTime.getStartTime(), startEndTime.getEndTime());
log.info("end mcuScheduler");
}
@Scheduled(cron = "1 * * * * *")
public void noticeJob(){
log.info("run noticeJob");
List<InGame> noticeList = noticeService.getScheduleNoticeList();
noticeList.forEach(notice -> {
if (notice.getIsRepeat() &&
notice.getRepeatType() == InGame.REPEATTYPE.COUNT &&
notice.getSendCnt() >= notice.getRepeatCnt()) {
log.info("Skipping notice - already reached max count. NoticeId: {}", notice.getId());
noticeService.updateNoticeStatus(notice.getId(), InGame.SENDSTATUS.FINISH);
return;
}
NoticeScheduler task = NoticeScheduler.builder()
.notice(notice)
.noticeService(noticeService)
.redisUserInfoService(redisUserInfoService)
.messageHandlerService(messageHandlerService)
.build();
schedulerManager.scheduleTask(task);
});
log.info("end noticeJob");
}
@Scheduled(cron = "2 * * * * *")
public void mailJob(){
log.info("run mailJob");
List<Mail> mailList = mailService.getScheduleMailList();
mailList.stream()
.filter(mail -> mail.getSendStatus().equals(Mail.SENDSTATUS.WAIT))
.forEach(mail -> {
MailScheduler task = MailScheduler.builder()
.mail(mail)
.mailService(mailService)
.redisUserInfoService(redisUserInfoService)
.messageHandlerService(messageHandlerService)
.excelUtils(excelUtils)
.build();
schedulerManager.scheduleTask(task);
});
log.info("end mailJob");
}
@Scheduled(cron = "3 * * * * *")
public void blackListJob(){
log.info("run blackListJob");
List<BlackList> blackList = blackListService.getScheduleBlackList();
blackList.forEach(blockUser -> {
BlackListScheduler task = BlackListScheduler.builder()
.blackList(blockUser)
.blackListService(blackListService)
.build();
schedulerManager.scheduleTask(task);
});
log.info("end blackListJob");
}
@Scheduled(cron = "4 * * * * *")
public void eventJob(){
log.info("run eventJob");
List<Event> eventList = eventService.getScheduleMailList();
eventList.forEach(event -> {
EventScheduler task = EventScheduler.builder()
.event(event)
.eventService(eventService)
.redisUserInfoService(redisUserInfoService)
.messageHandlerService(messageHandlerService)
.build();
schedulerManager.scheduleTask(task);
});
log.info("end eventJob");
}
}

View File

@@ -3,7 +3,7 @@ package com.caliverse.admin.scheduler;
import com.caliverse.admin.domain.service.CaliumService; import com.caliverse.admin.domain.service.CaliumService;
import com.caliverse.admin.global.common.utils.CommonUtils; import com.caliverse.admin.global.common.utils.CommonUtils;
import com.caliverse.admin.logs.logservice.indicators.*; import com.caliverse.admin.logs.logservice.indicators.*;
import com.caliverse.admin.scheduler.service.LogCompressService; import com.caliverse.admin.scheduler.batch.service.LogCompressService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;

View File

@@ -1,10 +1,8 @@
package com.caliverse.admin.scheduler; package com.caliverse.admin.scheduler;
import com.caliverse.admin.scheduler.config.ScheduleExecutionConfig; import com.caliverse.admin.scheduler.entity.SchedulerType;
public interface Scheduler { public interface Scheduler {
void execute(); void execute();
boolean isRepeatable(); SchedulerType getSchedulerType();
ScheduleType getScheduleType();
ScheduleExecutionConfig getSchedulerConfig();
} }

View File

@@ -1,433 +1,56 @@
package com.caliverse.admin.scheduler; package com.caliverse.admin.scheduler;
import com.caliverse.admin.scheduler.config.MissedExecutionConfig; import com.caliverse.admin.scheduler.entity.SchedulerType;
import com.caliverse.admin.scheduler.config.ScheduleExecutionConfig;
import com.caliverse.admin.scheduler.service.NoticeScheduler;
import jakarta.annotation.PreDestroy;
import lombok.*;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.Duration; import java.util.List;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Map; import java.util.Map;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.stream.Collectors;
@Service @Service
@Slf4j @Slf4j
@RequiredArgsConstructor
public class SchedulerManager { public class SchedulerManager {
private final ScheduledExecutorService scheduler;
// 실행 중인 작업 추적을 위한 Map
private final Map<String, ScheduledTaskInfo> scheduledTasks = new ConcurrentHashMap<>();
// 재시도 설정 // 재시도 설정
private static final int MAX_RETRY_COUNT = 3; private static final int MAX_RETRY_COUNT = 3;
private static final long RETRY_DELAY_MS = 1000; private static final long RETRY_DELAY_MS = 1000;
@Data // 실행 중인 작업 추적을 위한 Map
@Builder private final Map<SchedulerType, Scheduler> schedulers = new ConcurrentHashMap<>();
//스케줄 프로세스 관리
private static class ScheduledTaskInfo {
private final Scheduler task;
private final ScheduledFuture<?> future;
private final LocalDateTime startTime;
private int retryCount;
private ScheduleStatus status;
public static ScheduledTaskInfo of(Scheduler task, ScheduledFuture<?> future) { public SchedulerManager(List<Scheduler> schedulerList) {
return ScheduledTaskInfo.builder() schedulerList.forEach(scheduler ->
.task(task) schedulers.put(scheduler.getSchedulerType(), scheduler)
.future(future)
.startTime(LocalDateTime.now())
.retryCount(0)
.status(ScheduleStatus.SCHEDULED)
.build();
}
}
@Getter
@AllArgsConstructor
public enum ScheduleStatus {
SCHEDULED("예약됨"),
RUNNING("실행 중"),
COMPLETED("완료됨"),
FAILED("실패"),
CANCELLED("취소됨");
private final String description;
}
//스케줄 실행
public void scheduleTask(Scheduler task) {
String taskId = task.getSchedulerConfig().getTaskId();
// 이미 존재하는 작업 체크
if (scheduledTasks.containsKey(taskId)) {
ScheduledTaskInfo existingTask = scheduledTasks.get(taskId);
if (shouldReplaceExisting(existingTask)) {
log.info("Replacing existing task: {}", taskId);
cancelTask(taskId);
} else {
log.warn("Task {} is already scheduled and running", taskId);
return;
}
}
try {
//타입별 스케줄러 등록
ScheduledFuture<?> future = scheduleTaskByType(task);
if (future != null) {
ScheduledTaskInfo taskInfo = ScheduledTaskInfo.of(task, future);
scheduledTasks.put(taskId, taskInfo);
log.info("Successfully scheduled task: {}", taskId);
}
} catch (Exception e) {
log.error("Failed to schedule task: {}", taskId, e);
throw new SchedulerException("Failed to schedule task: " + taskId, e);
}
}
//스케줄 상태 체크
private boolean shouldReplaceExisting(ScheduledTaskInfo taskInfo) {
return taskInfo.getStatus() == ScheduleStatus.FAILED ||
taskInfo.getStatus() == ScheduleStatus.COMPLETED ||
taskInfo.getFuture().isDone();
}
//스케줄 타입 별 처리
private ScheduledFuture<?> scheduleTaskByType(Scheduler task) {
return switch (task.getScheduleType()) {
case IMMEDIATE -> scheduleImmediate(task);
case DELAYED -> scheduleDelayed(task);
case RECURRING -> scheduleRecurring(task);
case PERIODIC -> schedulePeriodic(task);
};
}
//즉시 실행
private ScheduledFuture<?> scheduleImmediate(Scheduler task) {
return (ScheduledFuture<?>) scheduler.submit(() -> executeTaskWithRetry(task));
}
//지연 실행
private ScheduledFuture<?> scheduleDelayed(Scheduler task) {
ScheduleExecutionConfig config = task.getSchedulerConfig();
long delay = calculateDelay(config.getStartTime());
if (delay < 0) {
log.error("Task {} scheduled for past time", config.getTaskId());
return null;
}
return scheduler.schedule(
() -> executeTaskWithRetry(task),
delay,
TimeUnit.MILLISECONDS
);
}
//반복 실행
private ScheduledFuture<?> scheduleRecurring(Scheduler task) {
ScheduleExecutionConfig config = task.getSchedulerConfig();
long initialDelay = calculateDelay(config.getStartTime());
if (initialDelay < 0) {
Duration timeSinceStart = Duration.between(config.getStartTime(), LocalDateTime.now());
// 종료 시간 체크
if (config.getEndTime() != null && LocalDateTime.now().isAfter(config.getEndTime())) {
log.warn("Task {} scheduled end time has passed", config.getTaskId());
return null;
}
// 다음 실행 시간 계산
long interval = config.getInterval().toMillis();
long missedExecutions = timeSinceStart.toMillis() / interval;
initialDelay = interval - (timeSinceStart.toMillis() % interval);
log.info("Task {} was scheduled to start {} ago. Missed {} executions. " +
"Next execution in {} ms",
config.getTaskId(),
timeSinceStart,
missedExecutions,
initialDelay);
if (shouldProcessMissedExecutions(task)) {
processMissedExecutions(task, (int) missedExecutions);
}
}
return scheduler.scheduleAtFixedRate(
() -> {
String taskId = config.getTaskId();
ScheduledTaskInfo taskInfo = scheduledTasks.get(taskId);
try {
// NoticeSchedule 경우 추가 검증
if (task instanceof NoticeScheduler noticeTask) {
if (noticeTask.hasReachedMaxExecutions()) {
cancelTask(taskId);
log.info("Cancelling notice task - reached max executions. TaskId: {}", taskId);
return;
}
if (!noticeTask.isValidExecutionCount()) {
log.error("Invalid execution count detected. TaskId: {}", taskId);
cancelTask(taskId);
return;
}
}
if (shouldStopRecurring(task)) {
cancelTask(taskId);
return;
}
taskInfo.setStatus(ScheduleStatus.RUNNING);
task.execute();
taskInfo.setStatus(ScheduleStatus.SCHEDULED);
} catch (Exception e) {
handleTaskExecutionFailure(task, taskInfo, e);
}
},
initialDelay,
config.getInterval().toMillis(),
TimeUnit.MILLISECONDS
);
}
//주기적 실행
private ScheduledFuture<?> schedulePeriodic(Scheduler task){
ScheduleExecutionConfig config = task.getSchedulerConfig();
LocalTime executionTime = config.getDailyExecutionTime();
if (executionTime == null) {
log.error("Daily execution time is not set for task: {}", config.getTaskId());
return null;
}
// 현재 시간
LocalDateTime now = LocalDateTime.now();
// 시작일이 미래인 경우, 시작일의 지정 시간을 첫 실행 시간으로 설정
// 시작일이 과거인 경우, 오늘 또는 다음 날의 지정 시간을 첫 실행 시간으로 설정
LocalDateTime firstExecution = calculateFirstExecution(now, config.getStartTime(), executionTime);
// 종료 시간 확인
if (config.getEndTime() != null && firstExecution.isAfter(config.getEndTime())) {
log.warn("Task {} end time has already passed", config.getTaskId());
return null;
}
// 첫 실행까지의 지연 시간 계산
long initialDelay = Duration.between(now, firstExecution).toMillis();
// 24시간을 밀리초로 변환
long dailyInterval = Duration.ofDays(1).toMillis();
return scheduler.scheduleAtFixedRate(
() -> executeDaily(task),
initialDelay,
dailyInterval,
TimeUnit.MILLISECONDS
);
}
private LocalDateTime calculateFirstExecution(
LocalDateTime now,
LocalDateTime startTime,
LocalTime executionTime
) {
LocalDateTime candidateTime = now.with(executionTime);
// 시작 시간이 미래인 경우
if (startTime.isAfter(now)) {
candidateTime = startTime.with(executionTime);
}
// 만약 오늘의 실행 시간이 이미 지났다면 다음 날로 설정
if (candidateTime.isBefore(now)) {
candidateTime = candidateTime.plusDays(1);
}
return candidateTime;
}
private void executeDaily(Scheduler task) {
String taskId = task.getSchedulerConfig().getTaskId();
ScheduledTaskInfo taskInfo = scheduledTasks.get(taskId);
try {
// 현재 시간이 종료 시간을 지났는지 확인
if (shouldStopDaily(task)) {
cancelTask(taskId);
return;
}
// 지정된 시간에만 실행
LocalTime currentTime = LocalTime.now();
LocalTime executionTime = task.getSchedulerConfig().getDailyExecutionTime();
if (currentTime.getHour() == executionTime.getHour() &&
currentTime.getMinute() == executionTime.getMinute()) {
taskInfo.setStatus(ScheduleStatus.RUNNING);
task.execute();
taskInfo.setStatus(ScheduleStatus.SCHEDULED);
log.info("Daily task {} executed at scheduled time: {}",
taskId, executionTime);
}
} catch (Exception e) {
handleTaskExecutionFailure(task, taskInfo, e);
}
}
//종료일자 체크
private boolean shouldStopDaily(Scheduler task) {
ScheduleExecutionConfig config = task.getSchedulerConfig();
if (config.getEndTime() == null) {
return false;
}
return LocalDateTime.now().isAfter(config.getEndTime());
}
private boolean shouldProcessMissedExecutions(Scheduler task) {
// 작업 유형에 따라 누락된 실행 처리 여부 결정
return task.getSchedulerConfig().isProcessMissedExecutions();
}
private void processMissedExecutions(Scheduler task, int missedCount) {
// 누락된 실행 처리를 위한 설정 가져오기
MissedExecutionConfig missedConfig = task.getSchedulerConfig().getMissedExecutionConfig();
if (missedConfig == null) {
return;
}
int executionsToProcess = Math.min(
missedCount,
missedConfig.getMaxMissedExecutionsToProcess()
); );
if (executionsToProcess <= 0) { log.info("Initialized {} schedulers: {}",
return; schedulerList.size(),
} schedulerList.stream()
.map(Scheduler::getSchedulerType)
CompletableFuture.runAsync(() -> { .collect(Collectors.toList()));
try {
log.info("Processing {} missed executions for task {}", executionsToProcess, task.getSchedulerConfig().getTaskId());
for (int i = 0; i < executionsToProcess; i++) {
if (missedConfig.isSequentialProcessing()) {
task.execute();
} else {
// 마지막 실행만 처리
if (i == executionsToProcess - 1) {
task.execute();
}
}
}
} catch (Exception e) {
log.error("Error processing missed executions for task {}",
task.getSchedulerConfig().getTaskId(), e);
}
}, scheduler);
} }
private void executeTaskWithRetry(Scheduler task) { public void executeScheduler(SchedulerType type) {
String taskId = task.getSchedulerConfig().getTaskId(); Scheduler scheduler = schedulers.get(type);
ScheduledTaskInfo taskInfo = scheduledTasks.get(taskId); if (scheduler != null) {
scheduler.execute();
try {
taskInfo.setStatus(ScheduleStatus.RUNNING);
task.execute();
taskInfo.setStatus(ScheduleStatus.COMPLETED);
log.info("Task {} completed successfully", taskId);
} catch (Exception e) {
handleTaskExecutionFailure(task, taskInfo, e);
}
}
private void handleTaskExecutionFailure(Scheduler task, ScheduledTaskInfo taskInfo, Exception e) {
String taskId = task.getSchedulerConfig().getTaskId();
taskInfo.setRetryCount(taskInfo.getRetryCount() + 1);
if (taskInfo.getRetryCount() < MAX_RETRY_COUNT) {
log.warn("Task {} failed, attempting retry {}/{}",
taskId, taskInfo.getRetryCount(), MAX_RETRY_COUNT, e);
scheduler.schedule(
() -> executeTaskWithRetry(task),
RETRY_DELAY_MS,
TimeUnit.MILLISECONDS
);
} else { } else {
log.error("Task {} failed after {} retries", taskId, MAX_RETRY_COUNT, e); log.warn("No scheduler found for type: {}", type);
taskInfo.setStatus(ScheduleStatus.FAILED);
cancelTask(taskId);
} }
} }
private boolean shouldStopRecurring(Scheduler task) { public void executeAllSchedulers() {
ScheduleExecutionConfig config = task.getSchedulerConfig(); schedulers.values().forEach(Scheduler::execute);
// 종료 시간 체크
if (config.getEndTime() != null && LocalDateTime.now().isAfter(config.getEndTime())) {
return true;
}
// 반복 횟수 체크
if (config.getRepeatCount() != null) {
String taskId = config.getTaskId();
ScheduledTaskInfo taskInfo = scheduledTasks.get(taskId);
return taskInfo.getRetryCount() >= config.getRepeatCount();
}
return false;
} }
public void cancelTask(String taskId) { public void addScheduler(Scheduler scheduler) {
ScheduledTaskInfo taskInfo = scheduledTasks.get(taskId); schedulers.put(scheduler.getSchedulerType(), scheduler);
if (taskInfo != null) { log.info("Added scheduler: {}", scheduler.getSchedulerType());
taskInfo.getFuture().cancel(false);
taskInfo.setStatus(ScheduleStatus.CANCELLED);
scheduledTasks.remove(taskId);
log.info("Task {} cancelled and removed from scheduler", taskId);
}
} }
public void cancelAllTasks() { public void removeScheduler(SchedulerType type) {
scheduledTasks.keySet().forEach(this::cancelTask); schedulers.remove(type);
} log.info("Removed scheduler: {}", type);
private long calculateDelay(LocalDateTime startTime) {
return Duration.between(LocalDateTime.now(), startTime).toMillis();
}
@PreDestroy
public void shutdown() {
cancelAllTasks();
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(60, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
// 스케줄러 관련 예외 클래스
public static class SchedulerException extends RuntimeException {
public SchedulerException(String message, Throwable cause) {
super(message, cause);
}
} }
} }

View File

@@ -1,53 +0,0 @@
package com.caliverse.admin.scheduler;
import com.caliverse.admin.domain.entity.InGame;
import com.caliverse.admin.domain.entity.Mail;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
@Service
public class SchedulerService {
private final Map<String, ScheduledFuture<?>> scheduledTasks = new ConcurrentHashMap<>();
private final Map<Long, Mail> mailTask = new ConcurrentHashMap<>();
private final Map<Long, InGame> noticeTask = new ConcurrentHashMap<>();
public boolean isTaskExist(String key){
return scheduledTasks.containsKey(key);
}
public void addTask(String key, ScheduledFuture<?> schdule){
scheduledTasks.put(key, schdule);
}
public void closeTask(String key) {
ScheduledFuture<?> scheduledFuture = scheduledTasks.get(key);
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
scheduledTasks.remove(key);
}
}
// public boolean isTaskExist(String type, Long id) {
// switch (type) {
// case "mail" -> {
// return mailTask.containsKey(id);
// }
// case "notice" -> {
// return noticeTask.containsKey(id);
// }
// default -> {
// return false;
// }
// }
// }
//
// public void createTask(Mail mail) {
// mailTask.put(mail.getId(), mail);
// }
}

View File

@@ -0,0 +1,31 @@
package com.caliverse.admin.scheduler.batch;
import com.caliverse.admin.domain.RabbitMq.MessageHandlerService;
import com.caliverse.admin.domain.service.BlackListService;
import com.caliverse.admin.domain.service.EventService;
import com.caliverse.admin.domain.service.MailService;
import com.caliverse.admin.domain.service.NoticeService;
import com.caliverse.admin.global.common.utils.ExcelUtils;
import com.caliverse.admin.logs.logservice.indicators.*;
import com.caliverse.admin.redis.service.RedisUserInfoService;
import com.caliverse.admin.scheduler.DynamicScheduler;
import com.caliverse.admin.scheduler.SchedulerManager;
import com.caliverse.admin.scheduler.entity.SchedulerType;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
@Slf4j
@EnableScheduling
@RequiredArgsConstructor
public class ScheduleRunnerBatch {
private final SchedulerManager schedulerManager;
}

View File

@@ -1,4 +1,4 @@
package com.caliverse.admin.scheduler.service; package com.caliverse.admin.scheduler.batch.service;
public interface LogCompressService { public interface LogCompressService {
void compressLastMonthLogs(); void compressLastMonthLogs();

View File

@@ -1,6 +1,6 @@
package com.caliverse.admin.scheduler.service.impl; package com.caliverse.admin.scheduler.batch.service.impl;
import com.caliverse.admin.scheduler.service.LogCompressService; import com.caliverse.admin.scheduler.batch.service.LogCompressService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;

View File

@@ -1,18 +0,0 @@
package com.caliverse.admin.scheduler.config;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MissedExecutionConfig {
@Builder.Default
private int maxMissedExecutionsToProcess = 1; // 기본값으로 최근 1회만 처리
@Builder.Default
private boolean sequentialProcessing = false; // 기본값으로 순차 처리하지 않음
}

View File

@@ -1,29 +0,0 @@
package com.caliverse.admin.scheduler.config;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.LocalTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ScheduleExecutionConfig {
private String taskId;
private LocalDateTime startTime;
private LocalDateTime endTime;
private Duration interval;
private Integer repeatCount;
private String cronExpression;
private LocalTime dailyExecutionTime;
@Builder.Default
private boolean processMissedExecutions = false;
private MissedExecutionConfig missedExecutionConfig;
}

View File

@@ -1,24 +0,0 @@
package com.caliverse.admin.scheduler.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@Configuration
@Slf4j
public class ScheduleSystemConfig {
@Bean
public ScheduledExecutorService scheduledExecutorService() {
return Executors.newScheduledThreadPool(
Runtime.getRuntime().availableProcessors(),
r -> {
Thread thread = new Thread(r, "scheduler-");
thread.setDaemon(true);
return thread;
}
);
}
}

View File

@@ -1,4 +1,4 @@
package com.caliverse.admin.scheduler; package com.caliverse.admin.scheduler.entity;
public enum ScheduleType { public enum ScheduleType {
IMMEDIATE, //즉시 실행 IMMEDIATE, //즉시 실행

View File

@@ -0,0 +1,12 @@
package com.caliverse.admin.scheduler.entity;
public enum SchedulerType {
MAIL,
NOTICE,
BLACKLIST,
EVENT,
LAND_AUCTION,
BATTLE_EVENT,
WEB3,
LAND_OWNER_CHANGES
}

View File

@@ -0,0 +1,182 @@
package com.caliverse.admin.scheduler.polling;
import com.caliverse.admin.scheduler.SchedulerManager;
import com.caliverse.admin.scheduler.entity.SchedulerType;
import com.caliverse.admin.domain.RabbitMq.MessageHandlerService;
import com.caliverse.admin.domain.service.*;
import com.caliverse.admin.global.common.utils.ExcelUtils;
import com.caliverse.admin.logs.logservice.indicators.*;
import com.caliverse.admin.redis.service.RedisUserInfoService;
import com.caliverse.admin.scheduler.DynamicScheduler;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
// import org.springframework.batch.core.JobParameters;
// import org.springframework.batch.core.JobParametersBuilder;
// import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
@Component
@Slf4j
@EnableScheduling
@RequiredArgsConstructor
public class ScheduleRunnerPolling {
@Autowired private IndicatorsAuService auService;
@Autowired private IndicatorsDauService dauService;
@Autowired private IndicatorsWauService wauService;
@Autowired private IndicatorsMauService mauService;
@Autowired private IndicatorsMcuService mcuService;
@Autowired private IndicatorsNruService statNruService;
@Autowired private RedisUserInfoService userInfoService;
@Autowired private DynamicScheduler dynamicScheduler;
@Autowired private MailService mailService;
@Autowired private NoticeService noticeService;
@Autowired private BlackListService blackListService;
@Autowired private EventService eventService;
@Autowired
private RedisUserInfoService redisUserInfoService;
@Autowired
private MessageHandlerService messageHandlerService;
@Autowired
private ExcelUtils excelUtils;
private final SchedulerManager schedulerManager;
@Scheduled(cron = "7 * * * * *")
public void landOwnerChangesJob(){
schedulerManager.executeScheduler(SchedulerType.LAND_OWNER_CHANGES);
}
/*
매일 UTC 기준 00시 50분 00초에 실행, (한국 시간 9시 50분) 30분에 돌릴경우 데이터가 다 넘어 오지 않는 경우 있어서 수정처리
이게 가장 먼저 실행 되어야 한다.
로그가 많을 경우 성능 이슈 있을 수 있음
*/
// @Scheduled(cron = "* * * * * *")
// public void auScheduler() {
// //이걸 나중에 어떻게 활용할지 생각해보자.
// log.info("run auScheduler");
// StartEndTime startEndTime = LogServiceHelper.getCurrentLogSearchEndTime(AdminConstants.STAT_DAY_NUM);
// auService.collectActiveUser(startEndTime.getStartTime(), startEndTime.getEndTime());
// log.info("end auScheduler");
// }
//// @Scheduled(cron = "00 55 0 * * *") // 매일 UTC 기준 00시 56분 00초에 실행
// @Scheduled(cron = "1 * * * * *") // 매일 UTC 기준 00시 56분 00초에 실행
// public void dauScheduler() {
// log.info("run dauScheduler");
// StartEndTime startEndTime = LogServiceHelper.getCurrentLogSearchEndTime(AdminConstants.STAT_DAY_NUM);
// dauService.collectDailyActiveUser(startEndTime.getStartTime(), startEndTime.getEndTime());
// log.info("end dauScheduler");
// }
//
// @Scheduled(cron = "00 56 0 * * *") // 매일 UTC 기준 00시 56분 00초에 실행
// public void wauScheduler() {
// log.info("run wauScheduler");
// StartEndTime startEndTime = LogServiceHelper.getCurrentLogSearchEndTime(AdminConstants.STAT_WEEK_NUM);
// wauService.collectWeeklyActiveUser(startEndTime.getStartTime(), startEndTime.getEndTime());
// log.info("end wauScheduler");
// }
//
// @Scheduled(cron = "00 57 0 * * *") // 매일 UTC 기준 00시 57분 00초에 실행
// public void mauScheduler() {
// log.info("run mauScheduler");
// StartEndTime startEndTime = LogServiceHelper.getCurrentLogSearchEndTime(AdminConstants.STAT_MONTH_NUM);
// mauService.collectMonthlyActiveUser(startEndTime.getStartTime(), startEndTime.getEndTime());
// log.info("end mauScheduler");
// }
//
// @Scheduled(cron = "00 58 0 * * *") // 매일 UTC 기준 00시 58분 00초에 실행
// public void mcuScheduler() {
// log.info("run mcuScheduler");
// StartEndTime startEndTime = LogServiceHelper.getCurrentLogSearchEndTime(AdminConstants.STAT_DAY_NUM);
// mcuService.collectMaxCountUser(startEndTime.getStartTime(), startEndTime.getEndTime());
// log.info("end mcuScheduler");
// }
//
// @Scheduled(cron = "00 59 0 * * *") // 매일 UTC 기준 00시 59분 00초에 실행
// public void nruScheduler() {
// log.info("run mcuScheduler");
// //StartEndTime startEndTime = LogServiceHelper.getCurrentLogSearchEndTime(AdminConstants.STAT_DAY_NUM);
// //statNruService.collectStatLogs(startEndTime.getStartTime(), startEndTime.getEndTime());
// log.info("end mcuScheduler");
// }
// @Scheduled(cron = "1 * * * * *")
// public void noticeJob(){
// log.info("run noticeJob");
// List<InGame> noticeList = noticeService.getScheduleNoticeList();
// noticeList.forEach(notice -> {
// if (notice.getIsRepeat() &&
// notice.getRepeatType() == InGame.REPEATTYPE.COUNT &&
// notice.getSendCnt() >= notice.getRepeatCnt()) {
// log.info("Skipping notice - already reached max count. NoticeId: {}", notice.getId());
// noticeService.updateNoticeStatus(notice.getId(), InGame.SENDSTATUS.FINISH);
// return;
// }
//
// NoticeScheduler task = NoticeScheduler.builder()
// .notice(notice)
// .noticeService(noticeService)
// .redisUserInfoService(redisUserInfoService)
// .messageHandlerService(messageHandlerService)
// .build();
// schedulerManager.scheduleTask(task);
// });
// log.info("end noticeJob");
// }
//
// @Scheduled(cron = "2 * * * * *")
// public void mailJob(){
// log.info("run mailJob");
// List<Mail> mailList = mailService.getScheduleMailList();
// mailList.stream()
// .filter(mail -> mail.getSendStatus().equals(Mail.SENDSTATUS.WAIT))
// .forEach(mail -> {
// MailScheduler task = MailScheduler.builder()
// .mail(mail)
// .mailService(mailService)
// .redisUserInfoService(redisUserInfoService)
// .messageHandlerService(messageHandlerService)
// .excelUtils(excelUtils)
// .build();
// schedulerManager.scheduleTask(task);
// });
// log.info("end mailJob");
// }
//
// @Scheduled(cron = "3 * * * * *")
// public void blackListJob(){
// log.info("run blackListJob");
// List<BlackList> blackList = blackListService.getScheduleBlackList();
// blackList.forEach(blockUser -> {
// BlackListScheduler task = BlackListScheduler.builder()
// .blackList(blockUser)
// .blackListService(blackListService)
// .build();
// schedulerManager.scheduleTask(task);
// });
// log.info("end blackListJob");
// }
//
// @Scheduled(cron = "4 * * * * *")
// public void eventJob(){
// log.info("run eventJob");
// List<Event> eventList = eventService.getScheduleMailList();
// eventList.forEach(event -> {
// EventScheduler task = EventScheduler.builder()
// .event(event)
// .eventService(eventService)
// .redisUserInfoService(redisUserInfoService)
// .messageHandlerService(messageHandlerService)
// .build();
// schedulerManager.scheduleTask(task);
// });
// log.info("end eventJob");
// }
}

View File

@@ -0,0 +1,64 @@
package com.caliverse.admin.scheduler.polling.service;
import com.caliverse.admin.scheduler.CommonScheduler;
import com.caliverse.admin.scheduler.entity.SchedulerType;
import com.caliverse.admin.domain.entity.LandOwnerChange;
import com.caliverse.admin.domain.request.LandRequest;
import com.caliverse.admin.domain.service.LandService;
import com.caliverse.admin.dynamodb.service.DynamodbLandService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Component
public class LandOwnerChangesScheduler extends CommonScheduler {
private final LandService landService;
private final DynamodbLandService dynamodbLandService;
public LandOwnerChangesScheduler(LandService landService, DynamodbLandService dynamodbLandService) {
this.landService = landService;
this.dynamodbLandService = dynamodbLandService;
}
@Override
protected void executeInternal() {
List<LandOwnerChange> scheduleList = landService.getScheduleLandOwnerChangesList();
scheduleList.forEach(landOwnerChange -> {
LandOwnerChange.CHANGE_STATUS status = landOwnerChange.getStatus();
LocalDateTime reserve_dt = landOwnerChange.getReservationDt();
int landId = landOwnerChange.getLandId();
String guid = landOwnerChange.getUserGuid();
LocalDateTime now = LocalDateTime.now();
if(status.equals(LandOwnerChange.CHANGE_STATUS.WAIT) && reserve_dt.isBefore(now)) {
LandRequest landRequest = LandRequest.builder()
.landId(landId)
.landName(landOwnerChange.getLandName())
.buildingId(landOwnerChange.getBuildingId())
.buildingName(landOwnerChange.getBuildingName())
.userGuid(guid)
.userName(landOwnerChange.getUserName())
.build();
dynamodbLandService.ChangesLandOwner(landRequest);
Map map = new HashMap<>();
map.put("id", landOwnerChange.getId());
map.put("status", LandOwnerChange.CHANGE_STATUS.FINISH);
landService.updateLandOwnedChangeStatus(map);
}
});
}
@Override
public SchedulerType getSchedulerType() {
return SchedulerType.LAND_OWNER_CHANGES;
}
}

View File

@@ -1,84 +0,0 @@
package com.caliverse.admin.scheduler.service;
import com.caliverse.admin.domain.entity.BlackList;
import com.caliverse.admin.domain.service.BlackListService;
import com.caliverse.admin.domain.service.UserGameSessionService;
import com.caliverse.admin.scheduler.ScheduleType;
import com.caliverse.admin.scheduler.Scheduler;
import com.caliverse.admin.scheduler.config.ScheduleExecutionConfig;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
@Slf4j
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BlackListScheduler implements Scheduler {
private BlackList blackList;
private BlackListService blackListService;
private UserGameSessionService userGameSessionService;
@Override
public void execute() {
try {
processBlackList();
} catch (Exception e) {
log.error("BlackList execution failed for ID: {}", blackList.getId(), e);
blackListService.updateBlackListStatus(blackList.getId(), BlackList.STATUSTYPE.FAIL);
}
}
@Override
public boolean isRepeatable() {
return false;
}
@Override
public ScheduleType getScheduleType() {
return ScheduleType.DELAYED;
}
@Override
public ScheduleExecutionConfig getSchedulerConfig() {
return ScheduleExecutionConfig.builder()
.taskId("blacklist-" + blackList.getId())
.startTime(blackList.getStartDt())
.endTime(blackList.getEndDt())
.build();
}
private void processBlackList() {
LocalDateTime nowDate = LocalDateTime.now();
LocalDateTime startTime = blackList.getStartDt();
LocalDateTime endTime = blackList.getEndDt();
// 이미 지난시간이면 fail 처리
if(nowDate.isAfter(endTime) && blackList.getStatus().equals(BlackList.STATUSTYPE.WAIT)) {
blackListService.updateBlackListStatus(blackList.getId(), BlackList.STATUSTYPE.FAIL);
log.error("blackListJob blackListSchedule timeOut : {}", blackList);
return;
}
// 시작시간 지났으면 정지 처리
if(!nowDate.isBefore(startTime) && blackList.getStatus().equals(BlackList.STATUSTYPE.WAIT)) {
// user kick 처리
userGameSessionService.kickUserSession(blackList.getGuid());
blackListService.updateScheduleBlockUser(blackList, BlackList.STATUSTYPE.INPROGRESS);
blackListService.updateBlackListStatus(blackList.getId(), BlackList.STATUSTYPE.INPROGRESS);
log.info("blackListJob blackListSchedule block start : {}", blackList);
}
// 종료시간 지났으면 만료 처리
if(!nowDate.isBefore(endTime) && blackList.getStatus().equals(BlackList.STATUSTYPE.INPROGRESS)) {
blackListService.updateScheduleBlockUser(blackList, BlackList.STATUSTYPE.EXPIRATION);
blackListService.updateBlackListStatus(blackList.getId(), BlackList.STATUSTYPE.EXPIRATION);
log.info("blackListJob blackListSchedule block end : {}", blackList);
}
}
}

View File

@@ -1,216 +0,0 @@
package com.caliverse.admin.scheduler.service;
import com.caliverse.admin.domain.RabbitMq.MessageHandlerService;
import com.caliverse.admin.domain.RabbitMq.message.LanguageType;
import com.caliverse.admin.domain.RabbitMq.message.MailItem;
import com.caliverse.admin.domain.entity.*;
import com.caliverse.admin.domain.service.EventService;
import com.caliverse.admin.domain.service.HistoryService;
import com.caliverse.admin.dynamodb.entity.SystemMessage;
import com.caliverse.admin.redis.service.RedisUserInfoService;
import com.caliverse.admin.global.common.utils.CommonUtils;
import com.caliverse.admin.global.common.utils.JsonUtils;
import com.caliverse.admin.scheduler.config.ScheduleExecutionConfig;
import com.caliverse.admin.scheduler.ScheduleType;
import com.caliverse.admin.scheduler.Scheduler;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class EventScheduler implements Scheduler {
private Event event;
private EventService eventService;
private RedisUserInfoService redisUserInfoService;
private MessageHandlerService messageHandlerService;
private HistoryService historyService;
@Override
public void execute() {
try {
processEvent();
} catch (Exception e) {
log.error("Event execution failed for event ID: {}", event.getId(), e);
eventService.updateEventStatus(event.getId(), Event.EVENTSTATUS.FAIL);
historyService.setScheduleLog(HISTORYTYPE.SCHEDULE_EVENT_FAIL, e.getMessage());
}
}
@Override
public boolean isRepeatable() {
return false;
}
@Override
public ScheduleType getScheduleType() {
return ScheduleType.DELAYED;
}
@Override
public ScheduleExecutionConfig getSchedulerConfig() {
return ScheduleExecutionConfig.builder()
.taskId("event-" + event.getId())
.startTime(event.getStartDt())
.endTime(event.getEndDt())
.build();
}
private void processEvent() {
LocalDateTime nowDate = LocalDateTime.now();
LocalDateTime startTime = event.getStartDt();
LocalDateTime endTime = event.getEndDt();
// 이벤트가 이미 종료되었는지 체크
if (nowDate.isAfter(endTime) && event.getStatus().equals(Event.EVENTSTATUS.WAIT)) {
eventService.updateEventStatus(event.getId(), Event.EVENTSTATUS.FAIL);
log.error("eventJob eventSchedule timeOut : {}", event);
return;
}
// 시작 30분 전에 게임 DB에 insert
if (event.getStatus().equals(Event.EVENTSTATUS.WAIT)
&& Duration.between(nowDate, startTime).abs().toMinutes() <= 30
&& !event.isAddFlag()) {
systemMailInsert();
eventService.updateEventStatus(event.getId(), Event.EVENTSTATUS.RUNNING);
log.info("eventJob eventSchedule dynamoDB Insert & Start: {}", event);
}
// 종료 시간이 되면 이벤트 종료
if (!nowDate.isBefore(endTime) && event.getStatus().equals(Event.EVENTSTATUS.RUNNING)) {
eventService.updateEventStatus(event.getId(), Event.EVENTSTATUS.FINISH);
log.info("eventJob eventSchedule block end : {}", event);
}
}
private void systemMailInsert() {
try {
log.info("systemMailInsert Info: {}", event);
// 메시지 처리
List<SystemMessage> mailTitleMessages = new ArrayList<>();
List<SystemMessage> mailTextMessages = new ArrayList<>();
List<SystemMessage> mailSenderMessages = new ArrayList<>();
List<MailItem> mailItems = new ArrayList<>();
processMessages(mailTitleMessages, mailTextMessages, mailSenderMessages);
processItems(mailItems);
// JSON 변환
ObjectMapper objectMapper = new ObjectMapper();
ArrayNode mailTitleArray = convertToJsonArray(objectMapper, mailTitleMessages);
ArrayNode mailTextArray = convertToJsonArray(objectMapper, mailTextMessages);
ArrayNode mailSenderArray = convertToJsonArray(objectMapper, mailSenderMessages);
ArrayNode mailItemArray = convertToJsonArray(objectMapper, mailItems);
// 시스템 메일 등록
eventService.insertSystemMail(
event,
mailTitleArray,
mailTextArray,
mailSenderArray,
mailItemArray
);
} catch (Exception e) {
log.error("systemMailInsert Exception: {}", e.getMessage());
eventService.updateEventStatus(event.getId(), Event.EVENTSTATUS.FAIL);
historyService.setScheduleLog(HISTORYTYPE.SCHEDULE_EVENT_FAIL, e.getMessage());
throw new RuntimeException("Failed to insert system mail", e);
}
}
private void processMessages(
List<SystemMessage> titleMessages,
List<SystemMessage> textMessages,
List<SystemMessage> senderMessages
) {
List<Message> msgList = eventService.getMessageList(event.getId());
for (Message msg : msgList) {
LanguageConfig langConfig = getLanguageConfig(msg.getLanguage());
// Title message
titleMessages.add(SystemMessage.builder()
.languageType(langConfig.getLangType())
.text(msg.getTitle())
.build());
// Content message
textMessages.add(SystemMessage.builder()
.languageType(langConfig.getLangType())
.text(msg.getContent())
.build());
// Sender message
senderMessages.add(SystemMessage.builder()
.languageType(langConfig.getLangType())
.text(langConfig.getSender())
.build());
}
}
private void processItems(List<MailItem> mailItems) {
List<Item> itemList = eventService.getItemList(event.getId());
for (Item item : itemList) {
mailItems.add(MailItem.newBuilder()
.setItemId(CommonUtils.stringToInt(item.getItem()))
.setCount(item.getItemCnt())
.build());
}
}
@Getter
@AllArgsConstructor
private static class LanguageConfig {
int langType;
String sender;
static LanguageConfig from(String language) {
return switch (language) {
case "EN" -> new LanguageConfig(
LanguageType.LanguageType_en.getNumber(),
"CALIVERSE"
);
case "JA" -> new LanguageConfig(
LanguageType.LanguageType_ja.getNumber(),
"カリバース"
);
default -> new LanguageConfig(
LanguageType.LanguageType_ko.getNumber(),
"칼리버스"
);
};
}
}
private LanguageConfig getLanguageConfig(String language) {
return LanguageConfig.from(language);
}
private <T> ArrayNode convertToJsonArray(ObjectMapper objectMapper, List<T> items) {
ArrayNode arrayNode = objectMapper.createArrayNode();
items.forEach(item -> {
if (item instanceof SystemMessage) {
arrayNode.add(JsonUtils.createSystemMessage((SystemMessage) item));
} else if (item instanceof MailItem) {
arrayNode.add(JsonUtils.createMAilItem((MailItem) item));
}
});
return arrayNode;
}
}

View File

@@ -1,194 +0,0 @@
package com.caliverse.admin.scheduler.service;
import com.caliverse.admin.domain.RabbitMq.MessageHandlerService;
import com.caliverse.admin.domain.RabbitMq.message.LanguageType;
import com.caliverse.admin.domain.RabbitMq.message.MailItem;
import com.caliverse.admin.domain.RabbitMq.message.OperationSystemMessage;
import com.caliverse.admin.domain.entity.*;
import com.caliverse.admin.domain.service.MailService;
import com.caliverse.admin.redis.service.RedisUserInfoService;
import com.caliverse.admin.domain.entity.redis.RedisLoginInfo;
import com.caliverse.admin.global.common.utils.CommonUtils;
import com.caliverse.admin.global.common.utils.ExcelUtils;
import com.caliverse.admin.scheduler.config.ScheduleExecutionConfig;
import com.caliverse.admin.scheduler.ScheduleType;
import com.caliverse.admin.scheduler.Scheduler;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Slf4j
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MailScheduler implements Scheduler {
private Mail mail;
private MailService mailService;
private RedisUserInfoService redisUserInfoService;
private MessageHandlerService messageHandlerService;
private ExcelUtils excelUtils;
@Override
public void execute() {
try {
processMail();
} catch (Exception e) {
log.error("Mail execution failed for mail ID: {}", mail.getId(), e);
mailService.updateMailStatus(mail.getId(), Mail.SENDSTATUS.FAIL);
}
}
@Override
public boolean isRepeatable() {
return false;
}
@Override
public ScheduleType getScheduleType() {
return mail.getSendType() == Mail.SENDTYPE.DIRECT_SEND
? ScheduleType.IMMEDIATE
: ScheduleType.DELAYED;
}
@Override
public ScheduleExecutionConfig getSchedulerConfig() {
return ScheduleExecutionConfig.builder()
.taskId("mail-" + mail.getId())
.startTime(mail.getSendDt())
.build();
}
private void processMail() {
if (mail.getReceiveType().equals(Mail.RECEIVETYPE.SINGLE)) {
boolean isSend = mailSend(mail);
if (isSend) {
mailService.updateMailStatus(mail.getId(), Mail.SENDSTATUS.FINISH);
}
} else {
List<Excel> excelList = excelUtils.getExcelListData(mail.getTarget());
log.info("mailJob mailSchedule schedule run ExcelList : {}", excelList);
boolean isSend;
mailService.updateMailStatus(mail.getId(), Mail.SENDSTATUS.RUNNING);
for (Excel excel : excelList) {
String guid = mailService.getGuid(excel.getUser(), excel.getType());
Mail tempMail = copyMailWithNewTarget(mail, guid);
isSend = mailSend(tempMail);
if (!isSend) {
log.error("mailJob mailSchedule Excel fail user : {}", excel.getUser());
mailService.setScheduleLog(HISTORYTYPE.SCHEDULE_MAIL_FAIL,
"mail schedule id: " + mail.getId() + " Excel Send Fail User: " + excel.getUser());
} else {
log.info("mailJob mailSchedule Excel send user : {}", excel.getUser());
}
}
mailService.updateMailStatus(mail.getId(), Mail.SENDSTATUS.FINISH);
}
}
private Mail copyMailWithNewTarget(Mail originalMail, String newTarget) {
Mail newMail = new Mail();
BeanUtils.copyProperties(originalMail, newMail);
newMail.setTarget(newTarget);
return newMail;
}
// 메일 전송
private boolean mailSend(Mail mail) {
try {
RedisLoginInfo info = redisUserInfoService.getUserLoginSessionInfo(mail.getTarget());
String serverName = Optional.ofNullable(info)
.map(RedisLoginInfo::getCurrentServer)
.orElseGet(() -> {
String firstChannel = redisUserInfoService.getFirstChannel();
if (firstChannel == null) {
log.error("mailJob mailSend serverName is empty");
mailService.updateMailStatus(mail.getId(), Mail.SENDSTATUS.FAIL);
mailService.setScheduleLog(HISTORYTYPE.MAIL_SEND_FAIL, "is null server name");
return null;
}
return firstChannel;
});
if (serverName == null) {
return false;
}
List<Message> msgList = mailService.getMailMessageList(mail.getId());
List<OperationSystemMessage> titleList = new ArrayList<>();
List<OperationSystemMessage> contentList = new ArrayList<>();
List<OperationSystemMessage> senderList = new ArrayList<>();
for (Message msg : msgList) {
LanguageType lang;
String sender;
switch (msg.getLanguage()) {
case "EN" -> {
lang = LanguageType.LanguageType_en;
sender = "Administrator";
}
case "JA" -> {
lang = LanguageType.LanguageType_ja;
sender = "アドミニストレーター";
}
default -> {
lang = LanguageType.LanguageType_ko;
sender = "시스템 관리자";
}
}
titleList.add(createOperationSystemMessage(lang, msg.getTitle()));
contentList.add(createOperationSystemMessage(lang, msg.getContent()));
senderList.add(createOperationSystemMessage(lang, sender));
}
List<Item> itemList = mailService.getMailItemList(mail.getId());
List<MailItem> mailItemList = itemList.stream()
.map(item -> MailItem.newBuilder()
.setItemId(CommonUtils.stringToInt(item.getItem()))
.setCount(item.getItemCnt())
.build())
.collect(Collectors.toList());
log.info("Send Mail Message : {}, target : {}, type :{}",
contentList, mail.getTarget(), mail.getMailType());
messageHandlerService.sendMailMessage(
serverName,
mail.getTarget(),
mail.getMailType().toString(),
titleList,
contentList,
mailItemList,
senderList
);
log.info("mailJob mailSend completed");
return true;
} catch (Exception e) {
log.error("mailSend Exception: {}", e.getMessage());
mailService.updateMailStatus(mail.getId(), Mail.SENDSTATUS.FAIL);
mailService.setScheduleLog(HISTORYTYPE.MAIL_SEND_FAIL, e.getMessage());
return false;
}
}
private OperationSystemMessage createOperationSystemMessage(LanguageType lang, String text) {
return OperationSystemMessage.newBuilder()
.setLanguageType(lang)
.setText(CommonUtils.stringToByte(text))
.build();
}
}

View File

@@ -1,228 +0,0 @@
package com.caliverse.admin.scheduler.service;
import com.caliverse.admin.domain.RabbitMq.MessageHandlerService;
import com.caliverse.admin.domain.RabbitMq.message.LanguageType;
import com.caliverse.admin.domain.RabbitMq.message.OperationSystemMessage;
import com.caliverse.admin.domain.entity.HISTORYTYPE;
import com.caliverse.admin.domain.entity.InGame;
import com.caliverse.admin.domain.entity.Message;
import com.caliverse.admin.domain.service.NoticeService;
import com.caliverse.admin.redis.service.RedisUserInfoService;
import com.caliverse.admin.global.common.utils.CommonUtils;
import com.caliverse.admin.scheduler.config.ScheduleExecutionConfig;
import com.caliverse.admin.scheduler.ScheduleType;
import com.caliverse.admin.scheduler.Scheduler;
import jakarta.annotation.PostConstruct;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.time.Duration;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class NoticeScheduler implements Scheduler {
private InGame notice;
private NoticeService noticeService;
private RedisUserInfoService redisUserInfoService;
private MessageHandlerService messageHandlerService;
private final AtomicInteger currentExecutionCount = new AtomicInteger(0); // 현재 실행 횟수를 추적
@Override
public void execute() {
try {
// 실행 전 횟수 체크
if (shouldStopExecution()) {
log.info("Notice execution stopped - Reached maximum count. NoticeId: {}, Current count: {}, Max count: {}",
notice.getId(), getCurrentCount(), getMaxCount());
noticeService.updateNoticeStatus(notice.getId(), InGame.SENDSTATUS.FINISH);
return;
}
// 실행 횟수 증가 및 검증
int newCount = currentExecutionCount.incrementAndGet();
if (newCount > getMaxCount()) {
log.warn("Notice execution exceeded max count. NoticeId: {}, Current count: {}, Max count: {}",
notice.getId(), newCount, getMaxCount());
noticeService.updateNoticeStatus(notice.getId(), InGame.SENDSTATUS.FINISH);
return;
}
boolean isSend = noticeSend();
if (!isSend) {
// 전송 실패 시 카운트 롤백
currentExecutionCount.decrementAndGet();
return;
}
if(notice.getSendStatus().equals(InGame.SENDSTATUS.WAIT)) {
noticeService.updateNoticeStatus(notice.getId(), InGame.SENDSTATUS.RUNNING);
}
// DB 업데이트 전 한번 더 체크
if (newCount <= getMaxCount()) {
noticeService.updateNoticeCount(notice.getId());
log.info("Notice execution successful. NoticeId: {}, Current count: {}, Max count: {}",
notice.getId(), newCount, getMaxCount());
// 최대 횟수 도달 시 상태 업데이트
if (newCount >= getMaxCount()) {
noticeService.updateNoticeStatus(notice.getId(), InGame.SENDSTATUS.FINISH);
log.info("Notice completed all executions. NoticeId: {}", notice.getId());
}
}
} catch (Exception e) {
currentExecutionCount.decrementAndGet();
log.error("Notice execution failed for notice ID: {}", notice.getId(), e);
noticeService.updateNoticeStatus(notice.getId(), InGame.SENDSTATUS.FAIL);
noticeService.setScheduleLog(HISTORYTYPE.SCHEDULE_NOTICE_FAIL, e.getMessage());
}
}
private boolean shouldStopExecution() {
if (!notice.getIsRepeat() || notice.getRepeatType() != InGame.REPEATTYPE.COUNT) {
return false;
}
return getCurrentCount() >= getMaxCount();
}
private int getCurrentCount() {
return currentExecutionCount.get();
}
private int getMaxCount() {
return notice.getRepeatCnt().intValue();
}
@Override
public boolean isRepeatable() {
return notice.getIsRepeat();
}
@Override
public ScheduleType getScheduleType() {
if (!notice.getIsRepeat()) {
return ScheduleType.DELAYED;
}
return switch (notice.getRepeatType()) {
case COUNT, DATE -> ScheduleType.RECURRING;
case TIME -> ScheduleType.PERIODIC;
default -> ScheduleType.DELAYED;
};
}
@Override
public ScheduleExecutionConfig getSchedulerConfig() {
return ScheduleExecutionConfig.builder()
.taskId("notice-" + notice.getId())
.startTime(notice.getSendDt())
.endTime(notice.getEndDt())
.interval(getRepeatInterval())
.repeatCount(notice.getRepeatCnt().intValue())
.build();
}
private Duration getRepeatInterval() {
if (!notice.getIsRepeat()) {
return null;
}
return switch (notice.getRepeatType()) {
case COUNT -> Duration.ofSeconds(LocalTime.parse(notice.getRepeatDt()).toSecondOfDay());
case DATE -> Duration.ofMillis(CommonUtils.intervalToMillis(notice.getRepeatDt()));
case TIME -> null;
};
}
@PostConstruct
private void initialize() {
// 기존 실행 횟수로 초기화
currentExecutionCount.set(notice.getSendCnt().intValue());
}
// SchedulerManager에서 사용할 수 있도록 현재 실행 상태 제공
public boolean hasReachedMaxExecutions() {
return getCurrentCount() >= getMaxCount();
}
// 실행 횟수 검증을 위한 메서드
public boolean isValidExecutionCount() {
return getCurrentCount() <= getMaxCount();
}
private boolean noticeSend() {
try {
List<String> serverList = redisUserInfoService.getAllServerList();
if (serverList.isEmpty()) {
log.error("noticeJob noticeSend serverList is empty");
noticeService.updateNoticeStatus(notice.getId(), InGame.SENDSTATUS.FAIL);
noticeService.setScheduleLog(HISTORYTYPE.NOTICE_SEND_FAIL, "is null server name");
return false;
}
List<Message> msgList = noticeService.getNoticeMessageList(notice.getId());
List<OperationSystemMessage> contentList = new ArrayList<>();
List<OperationSystemMessage> senderList = new ArrayList<>();
for (Message msg : Collections.unmodifiableList(msgList)) {
LanguageType lang;
String sender;
switch (msg.getLanguage()) {
case "EN" -> {
lang = LanguageType.LanguageType_en;
sender = "Administrator";
}
case "JA" -> {
lang = LanguageType.LanguageType_ja;
sender = "アドミニストレーター";
}
default -> {
lang = LanguageType.LanguageType_ko;
sender = "시스템 관리자";
}
}
contentList.add(createOperationSystemMessage(lang, msg.getContent()));
senderList.add(createOperationSystemMessage(lang, sender));
}
log.info("Send Notice Message: {}, type: {}", contentList, notice.getMessageType());
messageHandlerService.sendNoticeMessage(
serverList,
notice.getMessageType().toString(),
contentList,
senderList
);
log.info("noticeJob noticeSend completed");
return true;
} catch (Exception e) {
log.error("noticeSend Exception: {}", e.getMessage());
noticeService.updateNoticeStatus(notice.getId(), InGame.SENDSTATUS.FAIL);
noticeService.setScheduleLog(HISTORYTYPE.NOTICE_SEND_FAIL, e.getMessage());
return false;
}
}
private OperationSystemMessage createOperationSystemMessage(LanguageType lang, String text) {
return OperationSystemMessage.newBuilder()
.setLanguageType(lang)
.setText(CommonUtils.stringToByte(text))
.build();
}
}