diff --git a/build.gradle b/build.gradle index 6aa7bb7..7edd1d2 100644 --- a/build.gradle +++ b/build.gradle @@ -49,10 +49,16 @@ dependencies { testImplementation "org.testcontainers:junit-jupiter" // JUnit 5 연동 testImplementation 'org.testcontainers:jdbc' testImplementation 'de.flapdoodle.embed:de.flapdoodle.embed.mongo:4.11.0' + testImplementation "org.testcontainers:mongodb" + //Slack implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'com.slack.api:slack-api-client:1.43.0' + + //AWS + implementation "software.amazon.awssdk:eventbridge" + implementation platform('software.amazon.awssdk:bom:2.20.83') } tasks.named('test') { diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/store/slackjudge/batch/SlackJudgeBatchRunner.java b/src/main/java/store/slackjudge/batch/SlackJudgeBatchRunner.java index 4c177ec..7294d88 100644 --- a/src/main/java/store/slackjudge/batch/SlackJudgeBatchRunner.java +++ b/src/main/java/store/slackjudge/batch/SlackJudgeBatchRunner.java @@ -7,6 +7,7 @@ import org.springframework.batch.core.JobParametersBuilder; import org.springframework.batch.core.launch.JobLauncher; import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; import store.slackjudge.batch.common.CalculateSnapShotDate; @@ -16,6 +17,7 @@ @Slf4j @Component @RequiredArgsConstructor +@Profile("!test") public class SlackJudgeBatchRunner implements CommandLineRunner { private final JobLauncher jobLauncher; private final Job slackJudgeBatch; diff --git a/src/main/java/store/slackjudge/batch/config/BatchConfig.java b/src/main/java/store/slackjudge/batch/config/BatchConfig.java index ffdaba4..6306003 100644 --- a/src/main/java/store/slackjudge/batch/config/BatchConfig.java +++ b/src/main/java/store/slackjudge/batch/config/BatchConfig.java @@ -8,6 +8,7 @@ import org.springframework.batch.core.step.builder.StepBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; import org.springframework.transaction.PlatformTransactionManager; import store.slackjudge.batch.tasklet.*; diff --git a/src/main/java/store/slackjudge/batch/config/BatchJobListener.java b/src/main/java/store/slackjudge/batch/config/BatchJobListener.java index 2adc94c..c21d63a 100644 --- a/src/main/java/store/slackjudge/batch/config/BatchJobListener.java +++ b/src/main/java/store/slackjudge/batch/config/BatchJobListener.java @@ -6,8 +6,10 @@ import org.springframework.batch.core.annotation.BeforeJob; import org.springframework.batch.item.ExecutionContext; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; import store.slackjudge.batch.common.CalculateSnapShotDate; +import store.slackjudge.batch.infra.aws.EventBridgePublisher; import store.slackjudge.batch.infra.slack.SlackNotificationService; import java.time.Duration; @@ -23,6 +25,7 @@ public class BatchJobListener { private final BatchLogger logger; private final SlackNotificationService notificationService; private final CalculateSnapShotDate calculateSnapShotDate; + private final EventBridgePublisher eventBridgePublisher; /*========================== * @@ -57,7 +60,7 @@ public void beforeJob(JobExecution jobExecution) { * @parm jobExecution : Job 실행 중에 발생 정보 저장 객체 * @return * @author kimdoyeon - * @version 1.0.0 + * @version 1.1.0 * @date 25. 12. 17. * ==========================**/ @@ -88,6 +91,7 @@ public void afterJob(JobExecution jobExecution) { ); LocalDateTime occurredTime=calculateSnapShotDate.now(); + String status=""; //배치 종료 slack 알림 추가 if (jobExecution.getStatus().isUnsuccessful()) { //fail 예외 객체 없으면 기본 값 => Batch failed 출력 @@ -97,9 +101,14 @@ public void afterJob(JobExecution jobExecution) { //배치 실패 slack 알림 전송 notificationService.notifyBatchFailed(occurredTime, reason); + status="FAILED"; } else { //배치 성공 slack 알림 전송 notificationService.notifyBatchSuccess(durationMs, total, newUser, updated, failedUser,occurredTime); + status="SUCCESS"; } + + //배치 종료 시 AWS EventBridge 전송 + eventBridgePublisher.publishBatchSuccessCompleteEvent(String.valueOf(jobExecution.getJobInstance().getInstanceId()),status); } } diff --git a/src/main/java/store/slackjudge/batch/config/EventBridgeConfig.java b/src/main/java/store/slackjudge/batch/config/EventBridgeConfig.java new file mode 100644 index 0000000..00e6e6e --- /dev/null +++ b/src/main/java/store/slackjudge/batch/config/EventBridgeConfig.java @@ -0,0 +1,14 @@ +package store.slackjudge.batch.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import software.amazon.awssdk.services.eventbridge.EventBridgeClient; + +@Configuration +public class EventBridgeConfig { + @Bean + public EventBridgeClient eventBridgeClient(){ + return EventBridgeClient.builder().build(); + } +} diff --git a/src/main/java/store/slackjudge/batch/infra/aws/BatchEventDetail.java b/src/main/java/store/slackjudge/batch/infra/aws/BatchEventDetail.java new file mode 100644 index 0000000..b835858 --- /dev/null +++ b/src/main/java/store/slackjudge/batch/infra/aws/BatchEventDetail.java @@ -0,0 +1,9 @@ +package store.slackjudge.batch.infra.aws; + +import java.io.Serializable; + +public record BatchEventDetail( + String jobId, + String status +) implements Serializable { +} diff --git a/src/main/java/store/slackjudge/batch/infra/aws/EventBridgePublisher.java b/src/main/java/store/slackjudge/batch/infra/aws/EventBridgePublisher.java new file mode 100644 index 0000000..265d278 --- /dev/null +++ b/src/main/java/store/slackjudge/batch/infra/aws/EventBridgePublisher.java @@ -0,0 +1,46 @@ +package store.slackjudge.batch.infra.aws; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import software.amazon.awssdk.services.eventbridge.EventBridgeClient; +import software.amazon.awssdk.services.eventbridge.EventBridgeClientBuilder; +import software.amazon.awssdk.services.eventbridge.model.PutEventsRequest; +import software.amazon.awssdk.services.eventbridge.model.PutEventsRequestEntry; + +@Service +@RequiredArgsConstructor +public class EventBridgePublisher { + private final EventBridgeClient bridgeClientBuilder; + private final ObjectMapper mapper; + + @Value("${aws.eventbridge.source}") + private String source; + + @Value("${aws.eventbridge.detail-type}") + private String detailType; + + public void publishBatchSuccessCompleteEvent(String jobId, String status) { + try { + String detail = mapper.writeValueAsString(new BatchEventDetail(jobId, status)); + PutEventsRequestEntry eventsRequestEntry = PutEventsRequestEntry.builder() + .source(source) + .detailType(detailType) + .detail(detail) + .build(); + + PutEventsRequest request = PutEventsRequest.builder() + .entries(eventsRequestEntry) + .build(); + + bridgeClientBuilder.putEvents(request); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + + } + +} diff --git a/src/main/resources/batch.yml b/src/main/resources/batch.yml index cebdcfd..0a7d059 100644 --- a/src/main/resources/batch.yml +++ b/src/main/resources/batch.yml @@ -4,4 +4,9 @@ spring: initialize-schema: always job: enabled: false - name: 'SlackJudge Batch Worker #1' \ No newline at end of file + name: 'SlackJudge Batch Worker #1' + +aws: + eventbridge: + source: com.slackJudge.batch + detail-type: Batch Finished \ No newline at end of file diff --git a/src/test/java/store/slackjudge/batch/MongoContainer.java b/src/test/java/store/slackjudge/batch/MongoContainer.java new file mode 100644 index 0000000..5b829c8 --- /dev/null +++ b/src/test/java/store/slackjudge/batch/MongoContainer.java @@ -0,0 +1,24 @@ +package store.slackjudge.batch; + +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers +@ActiveProfiles("test") +public class MongoContainer { + @Container + static final MongoDBContainer mongo=new MongoDBContainer("mongo:6.0") + .withReuse(true); + + @DynamicPropertySource + static void mongoProperties(DynamicPropertyRegistry registry){ + registry.add( + "spring.data.mongodb.uri", + mongo::getReplicaSetUrl + ); + } +} diff --git a/src/test/java/store/slackjudge/batch/SlackjudgeApplicationTests.java b/src/test/java/store/slackjudge/batch/SlackjudgeApplicationTests.java deleted file mode 100644 index 80e7581..0000000 --- a/src/test/java/store/slackjudge/batch/SlackjudgeApplicationTests.java +++ /dev/null @@ -1,15 +0,0 @@ -package store.slackjudge.batch; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; - -@SpringBootTest -@ActiveProfiles("test") -class SlackjudgeApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/src/test/java/store/slackjudge/batch/infra/mongo/repository/UserSolvedSnapShotRepositoryTest.java b/src/test/java/store/slackjudge/batch/infra/mongo/repository/UserSolvedSnapShotRepositoryTest.java index 9aaf110..ec5c27d 100644 --- a/src/test/java/store/slackjudge/batch/infra/mongo/repository/UserSolvedSnapShotRepositoryTest.java +++ b/src/test/java/store/slackjudge/batch/infra/mongo/repository/UserSolvedSnapShotRepositoryTest.java @@ -6,12 +6,16 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; +import org.springframework.context.annotation.Import; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.testcontainers.containers.MongoDBContainer; +import store.slackjudge.batch.MongoContainer; import store.slackjudge.batch.SlackjudgeApplication; import store.slackjudge.batch.infra.mongo.document.SnapShotId; import store.slackjudge.batch.infra.mongo.document.UserSolvedSnapShotDocument; @@ -26,10 +30,9 @@ @ContextConfiguration(classes = SlackjudgeApplication.class) @DataMongoTest -@ExtendWith(SpringExtension.class) -@DirtiesContext +@Import(MongoAutoConfiguration.class) @ActiveProfiles("test") -class UserSolvedSnapShotRepositoryTest { +class UserSolvedSnapShotRepositoryTest extends MongoContainer { @Autowired private UserSolvedSnapShotRepository repository; diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index df73f85..68030b9 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -50,3 +50,8 @@ slack: logging: level: root: WARN + +aws: + eventbridge: + enabled: false + region: ap-northeast-2