dev #288

Merged
ysCha merged 5 commits from dev into prd-deploy 2026-01-02 15:46:02 +09:00
2 changed files with 1018 additions and 964 deletions

View File

@ -1,5 +1,6 @@
package com.interplug.qcast.batch; package com.interplug.qcast.batch;
import java.time.Duration;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -10,12 +11,16 @@ import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher; import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException; import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException; import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.repository.JobRestartException; import org.springframework.batch.core.repository.JobRestartException;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.context.event.EventListener;
import java.time.LocalDateTime;
@RestController @RestController
@RequiredArgsConstructor @RequiredArgsConstructor
@ -25,6 +30,7 @@ public class JobLauncherController {
private final JobLauncher jobLauncher; private final JobLauncher jobLauncher;
private final JobExplorer jobExplorer; private final JobExplorer jobExplorer;
private final JobRepository jobRepository; // 생성자 주입을 위해 필드 추가
@Value("${qsp.master-admin-user-batch-url}") @Value("${qsp.master-admin-user-batch-url}")
private String qspInterfaceUrl; private String qspInterfaceUrl;
@ -32,6 +38,26 @@ public class JobLauncherController {
@Value("${spring.profiles.scheduler}") @Value("${spring.profiles.scheduler}")
private String scheduler; private String scheduler;
/**
* 서버 시작 실행 (STARTED) 상태로 남은 Job들을 FAILED 처리
*/
@EventListener(ApplicationReadyEvent.class)
public void resetFailedJobs() {
log.info("Checking for 'STARTED' jobs to reset after reboot...");
for (String jobName : jobs.keySet()) {
Set<JobExecution> runningExecutions = jobExplorer.findRunningJobExecutions(jobName);
for (JobExecution execution : runningExecutions) {
execution.setStatus(BatchStatus.FAILED);
execution.setExitStatus(ExitStatus.FAILED.addExitDescription("Reset on application startup"));
execution.setEndTime(LocalDateTime.now()); // new Date() 대신 LocalDateTime.now() 사용
jobRepository.update(execution);
log.info("Reset job execution {} for job {}", execution.getId(), jobName);
}
}
}
/** /**
* 특정 Job을 매핑으로 실행하는 메소드 * 특정 Job을 매핑으로 실행하는 메소드
* *
@ -236,7 +262,10 @@ public class JobLauncherController {
"specialNoteDispItemAdditionalJob", "specialNoteDispItemAdditionalJob",
"planConfirmJob", "planConfirmJob",
"estimateSyncJob", "estimateSyncJob",
"bomJob" "bomJob",
"storeAdditionalJob",
"businessChargerJob",
"adminUserJob"
)); ));
// 스케줄러가 비활성화되어 있고, 허용된 작업이 아닌 경우 // 스케줄러가 비활성화되어 있고, 허용된 작업이 아닌 경우
@ -264,19 +293,42 @@ public class JobLauncherController {
*/ */
private boolean isJobRunning(String jobName) { private boolean isJobRunning(String jobName) {
try { try {
List<JobInstance> jobInstances = jobExplorer.findJobInstancesByJobName(jobName, 0, 1); Set<JobExecution> runningExecutions = jobExplorer.findRunningJobExecutions(jobName);
if (jobInstances.isEmpty()) { if (runningExecutions.isEmpty()) {
return false; return false;
} }
JobInstance latestJobInstance = jobInstances.get(0); boolean isActuallyBlocked = false;
List<JobExecution> jobExecutions = jobExplorer.getJobExecutions(latestJobInstance); for (JobExecution execution : runningExecutions) {
LocalDateTime startTime = execution.getStartTime();
if (startTime == null) continue;
return jobExecutions.stream() // 현재 시간과 시작 시간의 차이 계산
.anyMatch(execution -> execution.getStatus() == BatchStatus.STARTED Duration duration = Duration.between(startTime, LocalDateTime.now());
|| execution.getStatus() == BatchStatus.STARTING); long hours = duration.toHours();
if (hours >= 1) {
// 1시간 이상 경과한 경우 로그 출력 DB 강제 업데이트
log.warn("Job {} (Execution ID: {}) 가 실행된 지 {}시간이 지났습니다. 상태를 FAILED로 초기화하고 재실행을 시도합니다.",
jobName, execution.getId(), hours);
execution.setStatus(BatchStatus.FAILED);
execution.setExitStatus(ExitStatus.FAILED.addExitDescription("Forcefully reset: Running for more than 1 hour"));
execution.setEndTime(LocalDateTime.now());
jobRepository.update(execution);
log.info("Job {} 의 이전 실행 상태가 성공적으로 초기화되었습니다.", jobName);
} else {
// 1시간 미만으로 실행 중인 잡이 있다면 진짜 실행 중인 것으로 간주
log.info("Job {} 가 현재 실행 중입니다. (시작 시간: {}, 경과 시간: {}분)",
jobName, startTime, duration.toMinutes());
isActuallyBlocked = true;
}
}
return isActuallyBlocked;
} catch (Exception e) { } catch (Exception e) {
log.error("Error checking job running status: {}", e.getMessage()); log.error("Job 상태 확인 중 오류 발생: {}", e.getMessage());
return false; return false;
} }
} }

File diff suppressed because it is too large Load Diff