Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 6 additions & 15 deletions apps/pre-processing-service/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*

# Chrome 설치
RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - && \
echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list && \
apt-get update && \
apt-get install -y --no-install-recommends google-chrome-stable && \
rm -rf /var/lib/apt/lists/*

# ChromeDriver 설치
RUN LATEST_VERSION=$(curl -s "https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_STABLE") && \
wget -O /tmp/chromedriver-linux64.zip "https://storage.googleapis.com/chrome-for-testing-public/${LATEST_VERSION}/linux64/chromedriver-linux64.zip" && \
unzip /tmp/chromedriver-linux64.zip -d /tmp/ && \
mv /tmp/chromedriver-linux64/chromedriver /usr/local/bin/chromedriver && \
chmod +x /usr/local/bin/chromedriver && \
rm -rf /tmp/* && \
apt-get clean
# Chrome 설치 (블로그 방식 - 직접 .deb 파일 다운로드)
RUN wget -q https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb \
&& apt-get update \
&& apt-get install -y ./google-chrome-stable_current_amd64.deb \
&& rm ./google-chrome-stable_current_amd64.deb \
&& rm -rf /var/lib/apt/lists/*

# MeCab & 사전 설치 (형태소 분석 의존)
RUN apt-get update && apt-get install -y --no-install-recommends \
Expand Down
3 changes: 3 additions & 0 deletions apps/user-service/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ dependencies {
// Scheduler
implementation 'org.springframework.boot:spring-boot-starter-quartz'

// Retry
implementation 'org.springframework.retry:spring-retry'

implementation 'org.springframework.boot:spring-boot-starter-log4j2'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml'
implementation 'org.apache.logging.log4j:log4j-layout-template-json'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.retry.annotation.EnableRetry;

@EnableRetry
@SpringBootApplication
@MapperScan("site.icebang.**.mapper")
public class UserServiceApplication {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,10 @@
@Service
@RequiredArgsConstructor
public class QuartzScheduleService {

private final Scheduler scheduler;

public void addOrUpdateSchedule(Schedule schedule) {
try {
// 기존 스케줄 삭제 (있다면)
deleteSchedule(schedule.getWorkflowId());

JobKey jobKey = JobKey.jobKey("workflow-" + schedule.getWorkflowId());
JobDetail jobDetail = JobBuilder.newJob(WorkflowTriggerJob.class)
.withIdentity(jobKey)
Expand All @@ -34,23 +30,25 @@ public void addOrUpdateSchedule(Schedule schedule) {
.withSchedule(CronScheduleBuilder.cronSchedule(schedule.getCronExpression()))
.build();

if (scheduler.checkExists(jobKey)) {
scheduler.deleteJob(jobKey); // 기존 Job 삭제 후 재생성 (업데이트)
}
scheduler.scheduleJob(jobDetail, trigger);
log.info("Quartz 스케줄 등록/업데이트 완료: Workflow ID {}", schedule.getWorkflowId());
} catch (SchedulerException e) {
log.error("Quartz 스케줄 등록 실패", e);
log.error("Quartz 스케줄 등록 실패: Workflow ID " + schedule.getWorkflowId(), e);
}
}

public void deleteSchedule(Long workflowId) {
try {
JobKey jobkey = JobKey.jobKey("workflow-" + workflowId);
TriggerKey triggerKey = TriggerKey.triggerKey("trigger-for-workflow-" + workflowId);

scheduler.unscheduleJob(triggerKey);
scheduler.deleteJob(jobkey);
log.info("Quartz 스케줄 삭제 완료: Workflow ID {}", workflowId);
JobKey jobKey = JobKey.jobKey("workflow-" + workflowId);
if (scheduler.checkExists(jobKey)) {
scheduler.deleteJob(jobKey);
log.info("Quartz 스케줄 삭제 완료: Workflow ID {}", workflowId);
}
} catch (SchedulerException e) {
log.error("Quartz 스케줄 삭제 실패: Workflow ID {}", workflowId, e);
log.error("Quartz 스케줄 삭제 실패: Workflow ID " + workflowId, e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package site.icebang.domain.workflow.dto;

import java.time.LocalDateTime;

import lombok.Data;

@Data
public class JobDto {
private Long id;
private String name;
private String description;
private Boolean isEnabled;
private LocalDateTime createdAt;
private Long createdBy;
private LocalDateTime updatedAt;
private Long updatedBy;

private Integer executionOrder;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ public class TaskDto {
private Long id;
private String name;
private String type;
private Integer executionOrder;
private JsonNode parameters;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;

private Integer executionOrder;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

import org.apache.ibatis.annotations.Mapper;

import site.icebang.domain.workflow.dto.JobDto;
import site.icebang.domain.workflow.dto.TaskDto;
import site.icebang.domain.workflow.model.Job;

@Mapper
public interface JobMapper {
List<Job> findJobsByWorkflowId(Long workflowId);
List<JobDto> findJobsByWorkflowId(Long workflowId);

List<TaskDto> findTasksByJobId(Long jobId);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package site.icebang.domain.execution.mapper;
package site.icebang.domain.workflow.mapper;

import org.apache.ibatis.annotations.Mapper;

import site.icebang.domain.execution.model.JobRun;
import site.icebang.domain.workflow.model.JobRun;

@Mapper
public interface JobRunMapper {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package site.icebang.domain.execution.mapper;
package site.icebang.domain.workflow.mapper;

import org.apache.ibatis.annotations.Mapper;

import site.icebang.domain.execution.model.TaskRun;
import site.icebang.domain.workflow.model.TaskRun;

@Mapper
public interface TaskRunMapper {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package site.icebang.domain.execution.mapper;
package site.icebang.domain.workflow.mapper;

import org.apache.ibatis.annotations.Mapper;

import site.icebang.domain.execution.model.WorkflowRun;
import site.icebang.domain.workflow.model.WorkflowRun;

@Mapper
public interface WorkflowRunMapper {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import lombok.Getter;
import lombok.NoArgsConstructor;

import site.icebang.domain.workflow.dto.JobDto;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
Expand All @@ -19,4 +21,13 @@ public class Job {
private Long createdBy;
private LocalDateTime updatedAt;
private Long updatedBy;

public Job(JobDto dto) {
this.id = dto.getId();
this.name = dto.getName();
this.description = dto.getDescription();
this.isEnabled = dto.getIsEnabled();
this.createdAt = dto.getCreatedAt();
this.updatedAt = dto.getUpdatedAt();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package site.icebang.domain.execution.model;
package site.icebang.domain.workflow.model;

import java.time.LocalDateTime;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package site.icebang.domain.workflow.model;

import java.time.LocalDateTime;

import com.fasterxml.jackson.databind.JsonNode;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

import site.icebang.domain.workflow.dto.TaskDto;

@Getter
@NoArgsConstructor // MyBatis가 객체를 생성하기 위해 필요
@AllArgsConstructor
public class Task {

private Long id;
Expand All @@ -20,6 +24,10 @@ public class Task {
/** Task 실행에 필요한 파라미터 (JSON) 예: {"url": "http://...", "method": "POST", "body": {...}} */
private JsonNode parameters;

private LocalDateTime createdAt;

private LocalDateTime updatedAt;

public Task(TaskDto taskDto) {
this.id = taskDto.getId();
this.name = taskDto.getName();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package site.icebang.domain.execution.model;
package site.icebang.domain.workflow.model;

import java.time.LocalDateTime;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Workflow {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package site.icebang.domain.execution.model;
package site.icebang.domain.workflow.model;

import java.time.LocalDateTime;
import java.util.UUID;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import com.fasterxml.jackson.databind.node.ObjectNode;

import site.icebang.domain.execution.model.TaskRun;
import site.icebang.domain.workflow.model.Task;
import site.icebang.domain.workflow.model.TaskRun;

/** 워크플로우의 개별 Task를 실행하는 모든 Runner가 구현해야 할 인터페이스 */
public interface TaskRunner {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

import lombok.RequiredArgsConstructor;

import site.icebang.domain.execution.model.TaskRun;
import site.icebang.domain.workflow.model.Task;
import site.icebang.domain.workflow.model.TaskRun;
import site.icebang.domain.workflow.runner.TaskRunner;
import site.icebang.external.fastapi.adapter.FastApiAdapter;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package site.icebang.domain.workflow.service;

import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClientException;

import com.fasterxml.jackson.databind.node.ObjectNode;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import site.icebang.domain.workflow.model.Task;
import site.icebang.domain.workflow.model.TaskRun;
import site.icebang.domain.workflow.runner.TaskRunner;

@Slf4j
@Service
@RequiredArgsConstructor
public class TaskExecutionService { // 📌 클래스 이름 변경
private static final Logger workflowLogger = LoggerFactory.getLogger("WORKFLOW_HISTORY");
private final Map<String, TaskRunner> taskRunners;

/** RestClientException 발생 시, 5초 간격으로 최대 3번 재시도합니다. */
@Retryable(
value = {RestClientException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 5000))
public TaskRunner.TaskExecutionResult executeWithRetry(
Task task, TaskRun taskRun, ObjectNode requestBody) {
workflowLogger.info("Task 실행 시도: TaskId={}, TaskRunId={}", task.getId(), taskRun.getId());

String runnerBeanName = task.getType().toLowerCase() + "TaskRunner";
TaskRunner runner = taskRunners.get(runnerBeanName);

if (runner == null) {
throw new IllegalArgumentException("지원하지 않는 Task 타입: " + task.getType());
}

return runner.execute(task, taskRun, requestBody);
}

/** 모든 재시도가 실패했을 때 마지막으로 호출될 복구 메소드입니다. */
@Recover
public TaskRunner.TaskExecutionResult recover(
RestClientException e, Task task, TaskRun taskRun, ObjectNode requestBody) {
workflowLogger.error("최종 Task 실행 실패 (모든 재시도 소진): TaskRunId={}", taskRun.getId(), e);
return TaskRunner.TaskExecutionResult.failure("최대 재시도 횟수 초과: " + e.getMessage());
}
}
Loading
Loading