diff --git a/backend/src/main/java/com/goldenchart/service/StrategyEvaluationAiVerifyJobRunner.java b/backend/src/main/java/com/goldenchart/service/StrategyEvaluationAiVerifyJobRunner.java index b92de49..1fe7a7b 100644 --- a/backend/src/main/java/com/goldenchart/service/StrategyEvaluationAiVerifyJobRunner.java +++ b/backend/src/main/java/com/goldenchart/service/StrategyEvaluationAiVerifyJobRunner.java @@ -12,20 +12,20 @@ import org.springframework.stereotype.Component; public class StrategyEvaluationAiVerifyJobRunner { private final StrategyEvaluationLlmService llmService; - private final StrategyEvaluationAiVerifyJobService jobService; + private final StrategyEvaluationAiVerifyJobStore jobStore; @Async("aiVerifyExecutor") public void execute(String jobId, JsonNode context, Long userId, String deviceId) { - jobService.markRunning(jobId); + jobStore.markRunning(jobId); try { var result = llmService.verify(context, userId, deviceId); - jobService.markCompleted(jobId, result); + jobStore.markCompleted(jobId, result); } catch (Exception e) { log.warn("[AiVerifyJob] fail jobId={}: {}", jobId, e.getMessage()); String msg = e.getMessage() != null && !e.getMessage().isBlank() ? e.getMessage() : "AI 검증 중 오류가 발생했습니다."; - jobService.markFailed(jobId, msg); + jobStore.markFailed(jobId, msg); } } } diff --git a/backend/src/main/java/com/goldenchart/service/StrategyEvaluationAiVerifyJobService.java b/backend/src/main/java/com/goldenchart/service/StrategyEvaluationAiVerifyJobService.java index 3c3274a..1f3fa57 100644 --- a/backend/src/main/java/com/goldenchart/service/StrategyEvaluationAiVerifyJobService.java +++ b/backend/src/main/java/com/goldenchart/service/StrategyEvaluationAiVerifyJobService.java @@ -3,110 +3,29 @@ package com.goldenchart.service; import com.fasterxml.jackson.databind.JsonNode; import com.goldenchart.dto.StrategyEvaluationAiVerifyJobStartResponse; import com.goldenchart.dto.StrategyEvaluationAiVerifyJobStatusResponse; -import com.goldenchart.dto.StrategyEvaluationAiVerifyResponse; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import java.util.Iterator; -import java.util.Map; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; @Service @RequiredArgsConstructor public class StrategyEvaluationAiVerifyJobService { - private static final long JOB_TTL_MS = 60L * 60 * 1000; - private final StrategyEvaluationAiVerifyJobRunner jobRunner; - - private final ConcurrentHashMap jobs = new ConcurrentHashMap<>(); + private final StrategyEvaluationAiVerifyJobStore jobStore; public StrategyEvaluationAiVerifyJobStartResponse startJob(JsonNode context, Long userId, String deviceId) { - purgeExpired(); String jobId = UUID.randomUUID().toString(); - JobRecord record = new JobRecord(jobId, userId, System.currentTimeMillis()); - jobs.put(jobId, record); + jobStore.register(jobId, userId); jobRunner.execute(jobId, context, userId, deviceId); return StrategyEvaluationAiVerifyJobStartResponse.builder() .jobId(jobId) - .status(record.status) + .status(jobStore.initialStatus(jobId)) .build(); } public StrategyEvaluationAiVerifyJobStatusResponse getJob(String jobId, Long userId) { - purgeExpired(); - JobRecord record = jobs.get(jobId); - if (record == null || !record.userId.equals(userId)) { - return null; - } - return toResponse(record); - } - - void markRunning(String jobId) { - JobRecord record = jobs.get(jobId); - if (record != null) { - record.status = "RUNNING"; - } - } - - void markCompleted(String jobId, StrategyEvaluationAiVerifyResponse result) { - JobRecord record = jobs.get(jobId); - if (record != null) { - record.status = "COMPLETED"; - record.result = result; - record.error = null; - record.completedAtMs = System.currentTimeMillis(); - } - } - - void markFailed(String jobId, String error) { - JobRecord record = jobs.get(jobId); - if (record != null) { - record.status = "FAILED"; - record.error = error; - record.completedAtMs = System.currentTimeMillis(); - } - } - - private static StrategyEvaluationAiVerifyJobStatusResponse toResponse(JobRecord record) { - return StrategyEvaluationAiVerifyJobStatusResponse.builder() - .jobId(record.jobId) - .status(record.status) - .error(record.error) - .result(record.result) - .createdAtMs(record.createdAtMs) - .completedAtMs(record.completedAtMs) - .build(); - } - - private void purgeExpired() { - long cutoff = System.currentTimeMillis() - JOB_TTL_MS; - Iterator> it = jobs.entrySet().iterator(); - while (it.hasNext()) { - Map.Entry e = it.next(); - long ts = e.getValue().completedAtMs != null - ? e.getValue().completedAtMs - : e.getValue().createdAtMs; - if (ts < cutoff) { - it.remove(); - } - } - } - - private static final class JobRecord { - private final String jobId; - private final Long userId; - private final long createdAtMs; - private String status = "QUEUED"; - private String error; - private StrategyEvaluationAiVerifyResponse result; - private Long completedAtMs; - - private JobRecord(String jobId, Long userId, long createdAtMs) { - this.jobId = jobId; - this.userId = userId; - this.createdAtMs = createdAtMs; - } + return jobStore.getJob(jobId, userId); } } diff --git a/backend/src/main/java/com/goldenchart/service/StrategyEvaluationAiVerifyJobStore.java b/backend/src/main/java/com/goldenchart/service/StrategyEvaluationAiVerifyJobStore.java new file mode 100644 index 0000000..890af80 --- /dev/null +++ b/backend/src/main/java/com/goldenchart/service/StrategyEvaluationAiVerifyJobStore.java @@ -0,0 +1,104 @@ +package com.goldenchart.service; + +import com.goldenchart.dto.StrategyEvaluationAiVerifyJobStatusResponse; +import com.goldenchart.dto.StrategyEvaluationAiVerifyResponse; +import org.springframework.stereotype.Component; + +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** AI 검증 job 상태 저장 — Service·Runner 간 순환 참조 방지 */ +@Component +public class StrategyEvaluationAiVerifyJobStore { + + private static final long JOB_TTL_MS = 60L * 60 * 1000; + + private final ConcurrentHashMap jobs = new ConcurrentHashMap<>(); + + public void register(String jobId, Long userId) { + purgeExpired(); + jobs.put(jobId, new JobRecord(jobId, userId, System.currentTimeMillis())); + } + + public StrategyEvaluationAiVerifyJobStatusResponse getJob(String jobId, Long userId) { + purgeExpired(); + JobRecord record = jobs.get(jobId); + if (record == null || !record.userId.equals(userId)) { + return null; + } + return toResponse(record); + } + + public String initialStatus(String jobId) { + JobRecord record = jobs.get(jobId); + return record != null ? record.status : "QUEUED"; + } + + public void markRunning(String jobId) { + JobRecord record = jobs.get(jobId); + if (record != null) { + record.status = "RUNNING"; + } + } + + public void markCompleted(String jobId, StrategyEvaluationAiVerifyResponse result) { + JobRecord record = jobs.get(jobId); + if (record != null) { + record.status = "COMPLETED"; + record.result = result; + record.error = null; + record.completedAtMs = System.currentTimeMillis(); + } + } + + public void markFailed(String jobId, String error) { + JobRecord record = jobs.get(jobId); + if (record != null) { + record.status = "FAILED"; + record.error = error; + record.completedAtMs = System.currentTimeMillis(); + } + } + + private static StrategyEvaluationAiVerifyJobStatusResponse toResponse(JobRecord record) { + return StrategyEvaluationAiVerifyJobStatusResponse.builder() + .jobId(record.jobId) + .status(record.status) + .error(record.error) + .result(record.result) + .createdAtMs(record.createdAtMs) + .completedAtMs(record.completedAtMs) + .build(); + } + + private void purgeExpired() { + long cutoff = System.currentTimeMillis() - JOB_TTL_MS; + Iterator> it = jobs.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry e = it.next(); + long ts = e.getValue().completedAtMs != null + ? e.getValue().completedAtMs + : e.getValue().createdAtMs; + if (ts < cutoff) { + it.remove(); + } + } + } + + private static final class JobRecord { + private final String jobId; + private final Long userId; + private final long createdAtMs; + private String status = "QUEUED"; + private String error; + private StrategyEvaluationAiVerifyResponse result; + private Long completedAtMs; + + private JobRecord(String jobId, Long userId, long createdAtMs) { + this.jobId = jobId; + this.userId = userId; + this.createdAtMs = createdAtMs; + } + } +}