diff --git a/src/main/java/com/interplug/qcast/batch/BatchMonitorController.java b/src/main/java/com/interplug/qcast/batch/BatchMonitorController.java new file mode 100644 index 00000000..0f859ca4 --- /dev/null +++ b/src/main/java/com/interplug/qcast/batch/BatchMonitorController.java @@ -0,0 +1,108 @@ +package com.interplug.qcast.batch; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.*; +import org.springframework.batch.core.explore.JobExplorer; +import org.springframework.batch.core.launch.JobOperator; +import org.springframework.web.bind.annotation.*; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.*; + +@RestController +@RequiredArgsConstructor +@Slf4j +public class BatchMonitorController { + private final JobExplorer jobExplorer; + private final JobOperator jobOperator; + + @GetMapping("/batch/status") + public Map getBatchStatus() { + Map status = new HashMap<>(); + String[] jobNames = {"storeAdditionalJob", "priceJob", "materialJob", "bomJob", + "businessChargerJob", "adminUserJob", "commonCodeJob", + "specialNoteDispItemAdditionalJob", "planConfirmJob", "estimateSyncJob"}; + + for (String jobName : jobNames) { + status.put(jobName, getJobStatus(jobName)); + } + + return status; + } + + private Map getJobStatus(String jobName) { + Map jobStatus = new HashMap<>(); + + try { + List jobInstances = jobExplorer.findJobInstancesByJobName(jobName, 0, 1); + if (jobInstances.isEmpty()) { + jobStatus.put("status", "NEVER_EXECUTED"); + return jobStatus; + } + + JobInstance latestJobInstance = jobInstances.get(0); + List jobExecutions = jobExplorer.getJobExecutions(latestJobInstance); + + if (!jobExecutions.isEmpty()) { + JobExecution latestExecution = jobExecutions.get(0); + jobStatus.put("status", latestExecution.getStatus().toString()); + jobStatus.put("startTime", latestExecution.getStartTime()); + jobStatus.put("endTime", latestExecution.getEndTime()); + + // Duration 계산 (LocalDateTime용) + if (latestExecution.getEndTime() != null && latestExecution.getStartTime() != null) { + Duration duration = Duration.between(latestExecution.getStartTime(), latestExecution.getEndTime()); + jobStatus.put("durationSeconds", duration.getSeconds()); + jobStatus.put("durationMinutes", duration.toMinutes()); + } else if (latestExecution.getStartTime() != null) { + // 실행 중인 경우 현재까지의 경과 시간 + Duration duration = Duration.between(latestExecution.getStartTime(), LocalDateTime.now()); + jobStatus.put("runningDurationSeconds", duration.getSeconds()); + jobStatus.put("runningDurationMinutes", duration.toMinutes()); + } + } + + } catch (Exception e) { + jobStatus.put("status", "ERROR"); + jobStatus.put("error", e.getMessage()); + } + + return jobStatus; + } + + /** + * 강제 Job 중지 + */ + @PostMapping("/batch/stop/{jobName}") + public Map stopJob(@PathVariable String jobName) { + Map result = new HashMap<>(); + + try { + Set runningExecutions = jobOperator.getRunningExecutions(jobName); + + if (runningExecutions.isEmpty()) { + result.put("code", "NO_RUNNING_JOBS"); + result.put("message", "No running executions found for job: " + jobName); + return result; + } + + List stopResults = new ArrayList<>(); + for (Long executionId : runningExecutions) { + boolean stopped = jobOperator.stop(executionId); + stopResults.add("Execution " + executionId + ": " + (stopped ? "STOPPED" : "FAILED_TO_STOP")); + } + + result.put("code", "SUCCESS"); + result.put("message", "Stop commands sent"); + result.put("results", stopResults); + + } catch (Exception e) { + result.put("code", "ERROR"); + result.put("message", "Error stopping job: " + e.getMessage()); + } + + return result; + } +} \ No newline at end of file diff --git a/src/main/java/com/interplug/qcast/batch/JobLauncherController.java b/src/main/java/com/interplug/qcast/batch/JobLauncherController.java index 0558d78d..eb4fd3d5 100644 --- a/src/main/java/com/interplug/qcast/batch/JobLauncherController.java +++ b/src/main/java/com/interplug/qcast/batch/JobLauncherController.java @@ -1,17 +1,14 @@ package com.interplug.qcast.batch; -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + import lombok.RequiredArgsConstructor; -import org.springframework.batch.core.BatchStatus; -import org.springframework.batch.core.ExitStatus; -import org.springframework.batch.core.Job; -import org.springframework.batch.core.JobExecution; -import org.springframework.batch.core.JobParameters; -import org.springframework.batch.core.JobParametersBuilder; -import org.springframework.batch.core.JobParametersInvalidException; +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.repository.JobExecutionAlreadyRunningException; import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException; @@ -24,370 +21,290 @@ import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor +@Slf4j public class JobLauncherController { - private final Map jobs; // 여러 Job을 주입받도록 변경 - + private final Map jobs; private final JobLauncher jobLauncher; + private final JobExplorer jobExplorer; @Value("${qsp.master-admin-user-batch-url}") private String qspInterfaceUrl; @Value("${spring.profiles.scheduler}") private String scheduler; + + // 현재 실행 중인 Job 추적을 위한 Set + private final Set runningJobs = ConcurrentHashMap.newKeySet(); + /** * 특정 Job을 매핑으로 실행하는 메소드 - * - * @param jobName - * @return - * @throws JobInstanceAlreadyCompleteException - * @throws JobExecutionAlreadyRunningException - * @throws JobParametersInvalidException - * @throws JobRestartException */ - @GetMapping("/batch/job/{jobName}") // Path Variable로 jobName을 받음 - public Map launchJob(@PathVariable String jobName) - throws JobInstanceAlreadyCompleteException, JobExecutionAlreadyRunningException, - JobParametersInvalidException, JobRestartException { - + @GetMapping("/batch/job/{jobName}") + public Map launchJob(@PathVariable String jobName) { Job job = jobs.get(jobName); - Map resultMap = new HashMap(); + Map resultMap = new HashMap<>(); + if (job == null) { - // return "Job " + jobName + " not found"; resultMap.put("code", "FAILED"); - resultMap.put("message", "Job" + jobName + " not found"); + resultMap.put("message", "Job " + jobName + " not found"); return resultMap; } -// String[] arryJob = { -// "storeAdditionalJob" -// ,"BOMJob" -// ,"materialJob" -// ,"businessChargerJob" -// ,"adminUserJob" -// ,"priceJob" -//// ,"commonCodeJob" -// ,"specialNoteDispItemAdditionalJob" -// ,"planConfirmJob" -// ,"estimateSyncJob" -// }; -// -// -// if(!batchJobEnabled ){ -// for(String jobName2 :arryJob){ -// if(jobName2.equals(jobName)){ -// resultMap.put("code", "FAILED"); -// resultMap.put("message", "application.yml :: batch.job.enabled true"); -// return resultMap; -// } -// } -// } - JobParameters jobParameters = new JobParametersBuilder().addString("jobName", jobName) - .addDate("time", new Date()).toJobParameters(); + // 실행 중인 Job 확인 + if (runningJobs.contains(jobName) || isJobRunning(jobName)) { + resultMap.put("code", "ALREADY_RUNNING"); + resultMap.put("message", "Job " + jobName + " is already running"); + return resultMap; + } - JobExecution jobExecution = jobLauncher.run(job, jobParameters); + try { + runningJobs.add(jobName); + JobParameters jobParameters = new JobParametersBuilder() + .addString("jobName", jobName) + .addDate("time", new Date()) + .toJobParameters(); + JobExecution jobExecution = jobLauncher.run(job, jobParameters); - BatchStatus status = jobExecution.getStatus(); - ExitStatus exitStatus = jobExecution.getExitStatus(); + BatchStatus status = jobExecution.getStatus(); + ExitStatus exitStatus = jobExecution.getExitStatus(); - resultMap.put("code", status.toString()); - resultMap.put("message", exitStatus.getExitDescription()); + resultMap.put("code", status.toString()); + resultMap.put("message", exitStatus.getExitDescription()); + } catch (JobExecutionAlreadyRunningException e) { + resultMap.put("code", "ALREADY_RUNNING"); + resultMap.put("message", "Job " + jobName + " is already running"); + } catch (Exception e) { + resultMap.put("code", "FAILED"); + resultMap.put("message", "Error: " + e.getMessage()); + } finally { + runningJobs.remove(jobName); + } - // return "Job " + jobName + " started"; return resultMap; } /** * Q.CAST 판매점 / 사용자 / 즐겨찾기 / 노출 아이템 동기화 배치 - * - * @return - * @throws JobInstanceAlreadyCompleteException - * @throws JobExecutionAlreadyRunningException - * @throws JobParametersInvalidException - * @throws JobRestartException */ - // @Scheduled(cron = "*/5 * * * * *") - @Scheduled(cron = "0 55 23 * * *") - public String storeAdditionalJob() throws JobInstanceAlreadyCompleteException, - JobExecutionAlreadyRunningException, JobParametersInvalidException, JobRestartException { - - String jobName = "storeAdditionalJob"; - Job job = jobs.get(jobName); - if (job == null) { - return "Job " + jobName + " not found"; - } - if ("Y".equals(scheduler)) { - JobParameters jobParameters = - new JobParametersBuilder().addDate("time", new Date()).toJobParameters(); - - jobLauncher.run(job, jobParameters); - - } - return "OK"; + @Scheduled(cron = "0 30 23 * * *") + public String storeAdditionalJob() { + return executeScheduledJob("storeAdditionalJob"); } /** * 아이템 동기화 배치 - * - * @return - * @throws JobInstanceAlreadyCompleteException - * @throws JobExecutionAlreadyRunningException - * @throws JobParametersInvalidException - * @throws JobRestartException */ @Scheduled(cron = "0 30 02 * * *") - public String materialJob() throws JobInstanceAlreadyCompleteException, - JobExecutionAlreadyRunningException, JobParametersInvalidException, JobRestartException { - - String jobName = "materialJob"; - Job job = jobs.get(jobName); - if (job == null) { - return "Job " + jobName + " not found"; - } - - // if ("Y".equals(scheduler)) { - JobParameters jobParameters = - new JobParametersBuilder().addDate("time", new Date()).toJobParameters(); - - jobLauncher.run(job, jobParameters); - - // } - return "OK"; + public String materialJob() { + return executeScheduledJob("materialJob"); } /** * BOM 아이템 동기화 배치 - * - * @return - * @throws JobInstanceAlreadyCompleteException - * @throws JobExecutionAlreadyRunningException - * @throws JobParametersInvalidException - * @throws JobRestartException */ @Scheduled(cron = "0 40 02 * * *") - public String bomJob() throws JobInstanceAlreadyCompleteException, - JobExecutionAlreadyRunningException, JobParametersInvalidException, JobRestartException { - - String jobName = "bomJob"; - Job job = jobs.get(jobName); - if (job == null) { - return "Job " + jobName + " not found"; - } - - if ("Y".equals(scheduler)) { - JobParameters jobParameters = - new JobParametersBuilder().addDate("time", new Date()).toJobParameters(); - - jobLauncher.run(job, jobParameters); - - } - - return "OK"; + public String bomJob() { + return executeScheduledJob("bomJob"); } /** * 영업사원 동기화 배치 - * - * @return - * @throws JobInstanceAlreadyCompleteException - * @throws JobExecutionAlreadyRunningException - * @throws JobParametersInvalidException - * @throws JobRestartException */ @Scheduled(cron = "0 40 03 * * *") - public String businessChargerJob() throws JobInstanceAlreadyCompleteException, - JobExecutionAlreadyRunningException, JobParametersInvalidException, JobRestartException { - - String jobName = "businessChargerJob"; - Job job = jobs.get(jobName); - if (job == null) { - return "Job " + jobName + " not found"; - } - - if ("Y".equals(scheduler)) { - JobParameters jobParameters = - new JobParametersBuilder().addDate("time", new Date()).toJobParameters(); - jobLauncher.run(job, jobParameters); - - } - - return "OK"; + public String businessChargerJob() { + return executeScheduledJob("businessChargerJob"); } /** * 관리자 유저 동기화 배치 - * - * @return - * @throws JobInstanceAlreadyCompleteException - * @throws JobExecutionAlreadyRunningException - * @throws JobParametersInvalidException - * @throws JobRestartException */ @Scheduled(cron = "0 0 01 * * *") - public String adminUserJob() throws JobInstanceAlreadyCompleteException, - JobExecutionAlreadyRunningException, JobParametersInvalidException, JobRestartException { - - String jobName = "adminUserJob"; - Job job = jobs.get(jobName); - if (job == null) { - return "Job " + jobName + " not found"; - } - - if ("Y".equals(scheduler)) { - JobParameters jobParameters = - new JobParametersBuilder().addDate("time", new Date()).toJobParameters(); - jobLauncher.run(job, jobParameters); - - } - - return "OK"; + public String adminUserJob() { + return executeScheduledJob("adminUserJob"); } /** * 가격 동기화 배치 - * - * @return - * @throws JobInstanceAlreadyCompleteException - * @throws JobExecutionAlreadyRunningException - * @throws JobParametersInvalidException - * @throws JobRestartException */ - @Scheduled(cron = "0 0 0 * * *") - public String priceJob() throws JobInstanceAlreadyCompleteException, - JobExecutionAlreadyRunningException, JobParametersInvalidException, JobRestartException { - - String jobName = "priceJob"; - Job job = jobs.get(jobName); - if (job == null) { - return "Job " + jobName + " not found"; - } - - if ("Y".equals(scheduler)) { - JobParameters jobParameters = - new JobParametersBuilder().addDate("time", new Date()).toJobParameters(); - jobLauncher.run(job, jobParameters); - - } - - return "OK"; + @Scheduled(cron = "0 30 0 * * *") // 시간 조정: 00:30 + public String priceJob() { + return executeScheduledJob("priceJob"); } /** * 공통코드 M_COMM_H, M_COMM_L 동기화 배치 - * - * @return - * @throws JobInstanceAlreadyCompleteException - * @throws JobExecutionAlreadyRunningException - * @throws JobParametersInvalidException - * @throws JobRestartException */ @Scheduled(cron = "0 10 03 * * *") - public String commonCodeJob() throws JobInstanceAlreadyCompleteException, - JobExecutionAlreadyRunningException, JobParametersInvalidException, JobRestartException { - - String jobName = "commonCodeJob"; - Job job = jobs.get(jobName); - if (job == null) { - return "Job " + jobName + " not found"; - } - - if ("Y".equals(scheduler) || "N".equals(scheduler)) { - JobParameters jobParameters = - new JobParametersBuilder().addDate("time", new Date()).toJobParameters(); - - jobLauncher.run(job, jobParameters); - } - return "OK"; + public String commonCodeJob() { + return executeScheduledJob("commonCodeJob"); } /** * Q.CAST 견적특이사항 / 아이템 표시, 미표시 동기화 배치 - * - * @return - * @throws JobInstanceAlreadyCompleteException - * @throws JobExecutionAlreadyRunningException - * @throws JobParametersInvalidException - * @throws JobRestartException */ - @Scheduled(cron = "0 30 23 * * *") - public String specialNoteDispItemAdditionalInfoJob() throws JobInstanceAlreadyCompleteException, - JobExecutionAlreadyRunningException, JobParametersInvalidException, JobRestartException { - String jobName = "specialNoteDispItemAdditionalJob"; - Job job = jobs.get(jobName); - if (job == null) { - return "Job " + jobName + " not found"; - } - - if ("Y".equals(scheduler)) { - JobParameters jobParameters = - new JobParametersBuilder().addDate("time", new Date()).toJobParameters(); - jobLauncher.run(job, jobParameters); - - } - - return "OK"; + @Scheduled(cron = "0 45 23 * * *") // 시간 조정: 23:45 + public String specialNoteDispItemAdditionalInfoJob() { + return executeScheduledJob("specialNoteDispItemAdditionalJob"); } /** * Plan Confrim 동기화 배치 - * - * @return - * @throws JobInstanceAlreadyCompleteException - * @throws JobExecutionAlreadyRunningException - * @throws JobParametersInvalidException - * @throws JobRestartException */ @Scheduled(cron = "0 05 04 * * *") - public String planConfirmJob() throws JobInstanceAlreadyCompleteException, - JobExecutionAlreadyRunningException, JobParametersInvalidException, JobRestartException { - - String jobName = "planConfirmJob"; - Job job = jobs.get(jobName); - if (job == null) { - return "Job " + jobName + " not found"; - } - - if ("Y".equals(scheduler)) { - JobParameters jobParameters = - new JobParametersBuilder().addDate("time", new Date()).toJobParameters(); - - - jobLauncher.run(job, jobParameters); - - } - - return "OK"; + public String planConfirmJob() { + return executeScheduledJob("planConfirmJob"); } /** * 견적서 전송 동기화 배치 - * - * @return - * @throws JobInstanceAlreadyCompleteException - * @throws JobExecutionAlreadyRunningException - * @throws JobParametersInvalidException - * @throws JobRestartException */ @Scheduled(cron = "0 20 04 * * *") - public String estimateSyncJob() throws JobInstanceAlreadyCompleteException, - JobExecutionAlreadyRunningException, JobParametersInvalidException, JobRestartException { + public String estimateSyncJob() { + return executeScheduledJob("estimateSyncJob"); + } - String jobName = "estimateSyncJob"; + /** + * 공통 스케줄러 실행 메소드 + */ + private String executeScheduledJob(String jobName) { Job job = jobs.get(jobName); if (job == null) { + log.error("Job {} not found", jobName); return "Job " + jobName + " not found"; } - if ("Y".equals(scheduler)) { - JobParameters jobParameters = - new JobParametersBuilder().addDate("time", new Date()).toJobParameters(); - - jobLauncher.run(job, jobParameters); - + if (!"Y".equals(scheduler) && !"materialJob".equals(jobName) && !"commonCodeJob".equals(jobName)) { + log.info("Scheduler disabled, skipping job {}", jobName); + return "Scheduler disabled"; } - return "OK"; + // 실행 중인 Job 확인 + if (runningJobs.contains(jobName) || isJobRunning(jobName)) { + log.warn("Job {} is already running, skipping execution", jobName); + return "Job already running"; + } + + // 다른 Job이 실행 중인지 확인 + if (isAnyJobRunning()) { + log.warn("Another job is running, skipping job {}", jobName); + return "Another job is running"; + } + + try { + runningJobs.add(jobName); + + JobParameters jobParameters = new JobParametersBuilder() + .addDate("time", new Date()) + .toJobParameters(); + + JobExecution jobExecution = jobLauncher.run(job, jobParameters); + log.info("Job {} started successfully", jobName); + return "OK"; + + } catch (JobExecutionAlreadyRunningException e) { + log.warn("Job {} is already running: {}", jobName, e.getMessage()); + return "Job already running"; + } catch (Exception e) { + log.error("Error executing job {}: {}", jobName, e.getMessage()); + return "Error: " + e.getMessage(); + } finally { + runningJobs.remove(jobName); + } } -} + + /** + * Job 실행 상태 확인 + */ + private boolean isJobRunning(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); + + return jobExecutions.stream() + .anyMatch(execution -> execution.getStatus() == BatchStatus.STARTED + || execution.getStatus() == BatchStatus.STARTING); + } catch (Exception e) { + log.error("Error checking job running status: {}", e.getMessage()); + return false; + } + } + + /** + * 실행 중인 Job이 있는지 확인 + */ + private boolean isAnyJobRunning() { + String[] jobNames = {"storeAdditionalJob", "materialJob", "bomJob", "businessChargerJob", + "adminUserJob", "priceJob", "commonCodeJob", "specialNoteDispItemAdditionalJob", + "planConfirmJob", "estimateSyncJob"}; + + return Arrays.stream(jobNames) + .anyMatch(this::isJobRunning); + } + + /** + * 현재 실행 중인 Job 목록 조회 + */ + @GetMapping("/batch/running-jobs") + public Map getRunningJobs() { + Map result = new HashMap<>(); + result.put("runningJobs", new ArrayList<>(runningJobs)); + + Map jobStatuses = new HashMap<>(); + String[] jobNames = {"storeAdditionalJob", "materialJob", "bomJob", "businessChargerJob", + "adminUserJob", "priceJob", "commonCodeJob", "specialNoteDispItemAdditionalJob", + "planConfirmJob", "estimateSyncJob"}; + + for (String jobName : jobNames) { + jobStatuses.put(jobName, getJobStatus(jobName)); + } + + result.put("jobStatuses", jobStatuses); + return result; + } + + /** + * 특정 Job의 상태 정보 조회 + */ + private Map getJobStatus(String jobName) { + Map jobStatus = new HashMap<>(); + + try { + List jobInstances = jobExplorer.findJobInstancesByJobName(jobName, 0, 1); + if (jobInstances.isEmpty()) { + jobStatus.put("status", "NEVER_EXECUTED"); + return jobStatus; + } + + JobInstance latestJobInstance = jobInstances.get(0); + List jobExecutions = jobExplorer.getJobExecutions(latestJobInstance); + + if (!jobExecutions.isEmpty()) { + JobExecution latestExecution = jobExecutions.get(0); + jobStatus.put("status", latestExecution.getStatus().toString()); + jobStatus.put("startTime", latestExecution.getStartTime()); + jobStatus.put("endTime", latestExecution.getEndTime()); + + // Duration 계산 (LocalDateTime용) + if (latestExecution.getEndTime() != null && latestExecution.getStartTime() != null) { + Duration duration = Duration.between(latestExecution.getStartTime(), latestExecution.getEndTime()); + jobStatus.put("durationSeconds", duration.getSeconds()); + jobStatus.put("durationMillis", duration.toMillis()); + } + } + + } catch (Exception e) { + jobStatus.put("status", "ERROR"); + jobStatus.put("error", e.getMessage()); + } + + return jobStatus; + } +} \ No newline at end of file