diff --git a/src/main/java/com/interplug/qcast/batch/JobLauncherController.java b/src/main/java/com/interplug/qcast/batch/JobLauncherController.java index f9a904d0..a7958ba6 100644 --- a/src/main/java/com/interplug/qcast/batch/JobLauncherController.java +++ b/src/main/java/com/interplug/qcast/batch/JobLauncherController.java @@ -1,7 +1,6 @@ package com.interplug.qcast.batch; import java.time.Duration; -import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -10,9 +9,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.batch.core.*; import org.springframework.batch.core.explore.JobExplorer; import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.launch.JobOperator; import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException; -import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException; -import org.springframework.batch.core.repository.JobRestartException; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.web.bind.annotation.GetMapping; @@ -26,6 +24,7 @@ public class JobLauncherController { private final Map jobs; private final JobLauncher jobLauncher; private final JobExplorer jobExplorer; + private final JobOperator jobOperator; @Value("${qsp.master-admin-user-batch-url}") private String qspInterfaceUrl; @@ -170,7 +169,7 @@ public class JobLauncherController { * 공통 스케줄러 실행 메소드 */ private String executeScheduledJob(String jobName) { - // 1. 가장 먼저 스케줄러 설정 확인 + // 1. 스케줄러 설정 확인 (materialJob, commonCodeJob 예외) if (!"Y".equals(scheduler) && !"materialJob".equals(jobName) && !"commonCodeJob".equals(jobName)) { log.info("Scheduler disabled, skipping job {}", jobName); return "Scheduler disabled"; @@ -183,18 +182,19 @@ public class JobLauncherController { return "Job " + jobName + " not found"; } - // 3. 다른 Job 실행 중인지 확인 - if (isAnyJobRunning()) { - log.warn("Another job is running, skipping job {}", jobName); - return "Another job is running"; - } - - // 4. 같은 Job이 실행 중인지 확인 - if (runningJobs.contains(jobName) || isJobRunning(jobName)) { + // 3. 같은 Job이 실행 중인지 확인 + if (isJobActuallyRunning(jobName)) { log.warn("Job {} is already running", jobName); return "Job already running"; } + // 4. 다른 Job이 실행 중인지 확인 + String runningJobName = findCurrentlyRunningJob(); + if (runningJobName != null) { + log.warn("Another job '{}' is running, skipping job {}", runningJobName, jobName); + return "Another job is running: " + runningJobName; + } + // 5. Job 실행 try { runningJobs.add(jobName); @@ -205,7 +205,7 @@ public class JobLauncherController { .toJobParameters(); JobExecution jobExecution = jobLauncher.run(job, jobParameters); - log.info("Job {} started successfully", jobName); + log.info("Job {} started successfully with execution ID: {}", jobName, jobExecution.getId()); return "OK"; } catch (JobExecutionAlreadyRunningException e) { @@ -219,6 +219,22 @@ public class JobLauncherController { } } + /** + * 현재 실행 중인 Job 찾기 + */ + private String findCurrentlyRunningJob() { + String[] jobNames = {"storeAdditionalJob", "materialJob", "bomJob", "businessChargerJob", + "adminUserJob", "priceJob", "commonCodeJob", "specialNoteDispItemAdditionalJob", + "planConfirmJob", "estimateSyncJob"}; + + for (String jobName : jobNames) { + if (isJobActuallyRunning(jobName)) { + return jobName; + } + } + return null; + } + /** * Job 실행 상태 확인 */ @@ -241,18 +257,103 @@ public class JobLauncherController { } } - /** - * 실행 중인 Job이 있는지 확인 - */ - private boolean isAnyJobRunning() { - String[] jobNames = {"storeAdditionalJob", "materialJob", "bomJob", "businessChargerJob", - "adminUserJob", "priceJob", "commonCodeJob", "specialNoteDispItemAdditionalJob", - "planConfirmJob", "estimateSyncJob"}; - return Arrays.stream(jobNames) - .anyMatch(this::isJobRunning); + /** + * Job이 실제로 실행 중인지 확인 + */ + private boolean isJobActuallyRunning(String jobName) { + try { + // 1. 메모리 Set에서 확인 + if (runningJobs.contains(jobName)) { + log.debug("Job {} found in runningJobs set", jobName); + + // 2. JobOperator로 실제 실행 상태 확인 + try { + Set runningExecutions = jobOperator.getRunningExecutions(jobName); + if (!runningExecutions.isEmpty()) { + log.debug("Job {} has {} running executions", jobName, runningExecutions.size()); + return true; + } else { + log.warn("Job {} in runningJobs set but no actual running executions found", jobName); + runningJobs.remove(jobName); // 메모리 Set 정리 + return false; + } + } catch (Exception e) { + log.warn("Error checking JobOperator for {}: {}", jobName, e.getMessage()); + return false; + } + } + + // 3. JobOperator로 직접 확인 + try { + Set runningExecutions = jobOperator.getRunningExecutions(jobName); + if (!runningExecutions.isEmpty()) { + log.info("Job {} has running executions but not in runningJobs set", jobName); + runningJobs.add(jobName); // 메모리 Set 동기화 + return true; + } + } catch (Exception e) { + log.warn("Error checking JobOperator for {}: {}", jobName, e.getMessage()); + } + + // 4. JobExplorer로 DB 상태 확인 (보조적) + return isJobRunningInDatabase(jobName); + + } catch (Exception e) { + log.error("Error checking job running status for {}: {}", jobName, e.getMessage()); + return false; + } } + /** + * DB에서 Job 실행 상태 확인 (시간 기반 필터링) + */ + private boolean isJobRunningInDatabase(String jobName) { + try { + List jobInstances = jobExplorer.findJobInstancesByJobName(jobName, 0, 1); + + if (jobInstances.isEmpty()) { + return false; + } + + JobInstance latestJobInstance = jobInstances.get(0); + List jobExecutions = jobExplorer.getJobExecutions(latestJobInstance); + + if (jobExecutions.isEmpty()) { + return false; + } + + JobExecution latestExecution = jobExecutions.get(0); + BatchStatus status = latestExecution.getStatus(); + + if (status == BatchStatus.STARTED || status == BatchStatus.STARTING) { + if (latestExecution.getStartTime() != null) { + long minutesAgo = java.time.Duration.between( + latestExecution.getStartTime(), + java.time.LocalDateTime.now() + ).toMinutes(); + + // 30분 이상 된 실행은 중단된 것으로 간주 + if (minutesAgo > 30) { + log.warn("Job {} has stale execution in DB (started {} minutes ago)", jobName, minutesAgo); + return false; + } + + log.debug("Job {} appears to be running in DB (started {} minutes ago)", jobName, minutesAgo); + return true; + } + } + + return false; + + } catch (Exception e) { + log.error("Error checking job running status in DB for {}: {}", jobName, e.getMessage()); + return false; + } + } + + + /** * 현재 실행 중인 Job 목록 조회 */ @@ -311,4 +412,60 @@ public class JobLauncherController { return jobStatus; } + + /** + * 실행 중인 Job 상태 상세 확인 + */ + @GetMapping("/batch/debug/running-jobs-detail") + public Map getRunningJobsDetail() { + Map result = new HashMap<>(); + String[] jobNames = {"storeAdditionalJob", "materialJob", "bomJob", "businessChargerJob", + "adminUserJob", "priceJob", "commonCodeJob", "specialNoteDispItemAdditionalJob", + "planConfirmJob", "estimateSyncJob"}; + + Map jobDetails = new HashMap<>(); + List actuallyRunningJobs = new ArrayList<>(); + + for (String jobName : jobNames) { + Map jobDetail = new HashMap<>(); + + // 메모리 Set 확인 + boolean inMemorySet = runningJobs.contains(jobName); + jobDetail.put("inMemorySet", inMemorySet); + + // JobOperator 확인 + try { + Set runningExecutions = jobOperator.getRunningExecutions(jobName); + boolean hasRunningExecutions = !runningExecutions.isEmpty(); + + jobDetail.put("runningExecutions", runningExecutions); + jobDetail.put("hasRunningExecutions", hasRunningExecutions); + } catch (Exception e) { + jobDetail.put("operatorError", e.getMessage()); + jobDetail.put("hasRunningExecutions", false); + } + + // DB 상태 확인 + boolean runningInDB = isJobRunningInDatabase(jobName); + jobDetail.put("runningInDB", runningInDB); + + // 실제 실행 여부 + boolean actuallyRunning = isJobActuallyRunning(jobName); + jobDetail.put("actuallyRunning", actuallyRunning); + + if (actuallyRunning) { + actuallyRunningJobs.add(jobName); + } + + jobDetails.put(jobName, jobDetail); + } + + result.put("jobDetails", jobDetails); + result.put("actuallyRunningJobs", actuallyRunningJobs); + result.put("isAnyJobRunning", !actuallyRunningJobs.isEmpty()); + result.put("schedulerEnabled", scheduler); + + return result; + } + } \ No newline at end of file