Skip to content

Commit

Permalink
Feat/#91. Batch 모듈 추가, Job(질문발행알림) 추가 (#99)
Browse files Browse the repository at this point in the history
  • Loading branch information
RokwonK authored May 21, 2024
1 parent 796cb5e commit bf64e31
Show file tree
Hide file tree
Showing 54 changed files with 803 additions and 133 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,8 @@ build
### Configuration ###
**/src/**/*.yml
**/src/**/*.json
!src/**/application.yml
!**/*/application.yml
!**/*/*-test.yml
!src/test/**/*.yml

.aws-sam/
2 changes: 1 addition & 1 deletion adevspoon-api/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
FROM openjdk:17-jdk-slim
ARG JAR_FILE=./build/libs/mentos-api-0.0.1-SNAPSHOT.jar
ARG JAR_FILE=./build/libs/adevspoon-api-0.0.1-SNAPSHOT.jar
EXPOSE 80

COPY ${JAR_FILE} app.jar
Expand Down
2 changes: 1 addition & 1 deletion adevspoon-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ dependencies {
tasks.register("copyConfig", Copy::class) {
copy {
from("../adevspoon-config/backend/api")
include("*.yml", "*.xml")
include("*.yml", "*.xml", "*.json")
into("src/main/resources")
}
}
5 changes: 4 additions & 1 deletion adevspoon-api/src/main/resources/application-api-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@ jwt:

custom:
image:
temp-dir: /Users/rokwon/Downloads/image-temp
temp-dir: /Users/rokwon/Downloads/image-temp

firebase:
key-path: firebaseKey.json
1 change: 1 addition & 0 deletions adevspoon-api/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ spring:
server:
servlet:
context-path: /api
shutdown: graceful

springdoc:
api-docs:
Expand Down
20 changes: 20 additions & 0 deletions adevspoon-batch/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# 빌드 파일 옮기기
FROM public.ecr.aws/sam/build-java17:1.116.0-20240430173307 as artifact-image
WORKDIR "/task"

ARG JAR_FILE=./build/libs/adevspoon-batch-0.0.1-SNAPSHOT.jar
COPY ${JAR_FILE} app.jar

# Lambda Web Adatper 추가 및 실행
FROM public.ecr.aws/docker/library/amazoncorretto:17-al2023-headless
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.8.3-x86_64 /lambda-adapter /opt/extensions/lambda-adapter

ARG PROFILE
ENV PORT=8080
ENV ACTIVE_PROFILE=${PROFILE}

RUN echo "Active Profile: ${ACTIVE_PROFILE}"

WORKDIR /opt
COPY --from=artifact-image /task/app.jar /opt
CMD ["java", "-jar", "-Duser.timezone=Asia/Seoul", "-Dspring.profiles.active=${ACTIVE_PROFILE}", "app.jar", "--server.port=${PORT}"]
33 changes: 33 additions & 0 deletions adevspoon-batch/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@

plugins {
id("com.adevspoon.kotlin-common-conventions")
}

dependencies {
implementation(project(":adevspoon-common"))
implementation(project(":adevspoon-domain"))
implementation(project(":adevspoon-infrastructure"))

implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-batch")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")

testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.batch:spring-batch-test")
}

tasks.getByName("bootJar") {
enabled = true
}

tasks.getByName("jar") {
enabled = false
}

tasks.register("copyConfig", Copy::class) {
copy {
from("../adevspoon-config/backend/batch")
include("*.yml", "*.xml", "*.json")
into("src/main/resources")
}
}
23 changes: 23 additions & 0 deletions adevspoon-batch/deploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash

REGISTRY=$1
REPOSITORY=$2
TAG=$3

DOCKER_TAG="$REGISTRY/$REPOSITORY:$TAG"

# 새로운 태그로 Build
docker build -t $DOCKER_TAG --build-arg PROFILE=prod --platform linux/x86_64 .

# ECR 로그인 - 필요하다면(--profile adevspoon)
aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin $REGISTRY

# ECR에 Push
docker push $DOCKER_TAG

# template.yaml의 ImageUri를 변경해준다. (& sg, subnet 변경)
sed -i '' -e "s/\${ImagUri}/$DOCKER_TAG|g" template.yml

# SAM Build & Deploy - https://docs.aws.amazon.com/ko_kr/serverless-application-model/latest/developerguide/deploying-using-github.html
sam build --use-container
sam deploy --no-confirm-changeset --no-fail-on-empty-changeset
10 changes: 10 additions & 0 deletions adevspoon-batch/samconfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version = 0.1
[default.deploy.parameters]
stack_name = "adevspoon-batch"
resolve_s3 = true
s3_prefix = "adevspoon-batch"
region = "ap-northeast-2"
profile = "adevspoon"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
image_repositories = []
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.adevspoon.batch

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication(scanBasePackages = ["com.adevspoon"])
class AdevspoonBatchServerApplication

fun main(args: Array<String>) {
runApplication<AdevspoonBatchServerApplication>(*args)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.adevspoon.batch.config

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Primary
import org.springframework.core.task.TaskExecutor
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor


@Configuration
class BatchTaskConfig {
@Bean
@Primary
fun batchTaskExecutor(): TaskExecutor = ThreadPoolTaskExecutor()
.apply {
corePoolSize = 5
maxPoolSize = 10
setThreadNamePrefix("batch-task-")
setWaitForTasksToCompleteOnShutdown(true)
initialize()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.adevspoon.batch.controller

import com.adevspoon.batch.job.JobExecutionFacade
import org.slf4j.LoggerFactory
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/events")
class EventProcessController(
private val jobExecutionFacade: JobExecutionFacade
) {
private val logger = LoggerFactory.getLogger(this.javaClass)!!
@PostMapping
fun processEvents(@RequestBody req: Map<String, Any>): String {
logger.info("Batch Job 실행 요청")
jobExecutionFacade.executeJob(req.toString())

return "Events processed"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.adevspoon.batch.job

import com.adevspoon.infrastructure.alarm.dto.AlarmType
import com.adevspoon.infrastructure.alarm.service.AlarmAdapter
import org.springframework.batch.core.Job
import org.springframework.batch.core.JobParametersBuilder
import org.springframework.batch.core.launch.JobLauncher
import org.springframework.context.ApplicationContext
import org.springframework.stereotype.Service
import java.time.LocalDateTime

@Service
class JobExecutionFacade(
private val jobLauncher: JobLauncher,
// private val batchTaskExecutor: TaskExecutor,
private val applicationContext: ApplicationContext,
private val alarmAdapter: AlarmAdapter,
) {
fun executeJob(event: String) {
alarmAdapter.sendAlarm(AlarmType.CHECK, mapOf("Event" to event))
// (jobLauncher as TaskExecutorJobLauncher).setTaskExecutor(batchTaskExecutor)

// TODO: 추후 Event Type에 따라 Job, JobParameter 나누기
val jobParameters = JobParametersBuilder()
.addString("date", LocalDateTime.now().toString())
.toJobParameters()
val job = applicationContext.getBean(QuestionPublishedNotification.JOB_NAME, Job::class.java)
jobLauncher.run(job, jobParameters)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package com.adevspoon.batch.job

import com.adevspoon.infrastructure.alarm.dto.AlarmType
import com.adevspoon.infrastructure.alarm.service.AlarmAdapter
import com.adevspoon.infrastructure.notification.dto.GroupNotificationInfo
import com.adevspoon.infrastructure.notification.dto.NotificationType
import com.adevspoon.infrastructure.notification.service.PushNotificationAdapter
import jakarta.persistence.EntityManagerFactory
import org.slf4j.LoggerFactory
import org.springframework.batch.core.*
import org.springframework.batch.core.configuration.annotation.JobScope
import org.springframework.batch.core.configuration.annotation.StepScope
import org.springframework.batch.core.job.builder.JobBuilder
import org.springframework.batch.core.repository.JobRepository
import org.springframework.batch.core.step.builder.StepBuilder
import org.springframework.batch.item.ExecutionContext
import org.springframework.batch.item.ItemWriter
import org.springframework.batch.item.database.JpaPagingItemReader
import org.springframework.batch.item.database.builder.JpaPagingItemReaderBuilder
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.task.TaskExecutor
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
import org.springframework.transaction.PlatformTransactionManager


@Configuration
class QuestionPublishedNotification(
private val transactionManager: PlatformTransactionManager,
private val entityManagerFactory: EntityManagerFactory,
private val pushNotificationAdapter: PushNotificationAdapter,
private val alarmAdapter: AlarmAdapter,
){
private val logger = LoggerFactory.getLogger(this.javaClass)!!
private val chunkSize = 500
private val successCountKey = "successCount"
private val failCountKey = "failCount"

companion object {
const val JOB_NAME = "질문발급_푸시알림"
}

@Bean(JOB_NAME)
fun job(jobRepository: JobRepository): Job {
return JobBuilder(JOB_NAME, jobRepository)
.preventRestart()
.start(notificationStep(jobRepository))
.listener(jobListener())
.build()
}

@Bean(JOB_NAME + "_step")
@JobScope
fun notificationStep(jobRepository: JobRepository): Step {
return StepBuilder(JOB_NAME + "_step", jobRepository)
.chunk<String, String>(chunkSize, transactionManager)
.reader(reader())
.writer(pushWriter(null))
.listener(stepListener())
.build()
}

@Bean(JOB_NAME + "_taskPool")
fun executor(): TaskExecutor {
val executor = ThreadPoolTaskExecutor()
executor.corePoolSize = 5
executor.maxPoolSize = 5
executor.setThreadNamePrefix("task-step-")
executor.setWaitForTasksToCompleteOnShutdown(true)
executor.initialize()
return executor
}

@Bean(JOB_NAME + "_reader")
@StepScope
fun reader(): JpaPagingItemReader<String> {
return JpaPagingItemReaderBuilder<String>()
.name(JOB_NAME + "_reader")
.queryString("SELECT u.fcmToken FROM UserEntity u WHERE u.fcmToken IS NOT NULL")
.pageSize(chunkSize)
.entityManagerFactory(entityManagerFactory)
.saveState(false)
.build()
}

@Bean
@StepScope
fun pushWriter(@Value("#{stepExecution.jobExecution.executionContext}") jobExecutionContext: ExecutionContext?): ItemWriter<String> {
return ItemWriter {
val notificationResponse = pushNotificationAdapter.sendMessageSync(
GroupNotificationInfo(NotificationType.QUESTION_OPENED, it.items)
)

jobExecutionContext?.let {
synchronized(jobExecutionContext) {
val successCount = jobExecutionContext.getInt(successCountKey, 0)
val failCount = jobExecutionContext.getInt(failCountKey, 0)
jobExecutionContext.put(successCountKey, successCount + notificationResponse.success)
jobExecutionContext.put(failCountKey, failCount + notificationResponse.failure)
}
}
}
}

@Bean(JOB_NAME + "_listener")
fun jobListener(): JobExecutionListener {
return object : JobExecutionListener {
override fun beforeJob(jobExecution: JobExecution) {
logger.info("$JOB_NAME Batch JOB Start")
}

override fun afterJob(jobExecution: JobExecution) {
val pushSuccess = jobExecution.executionContext.getInt("successCount", -1)
val pushFail = jobExecution.executionContext.getInt("failCount", -1)
val jobInfo = mapOf<String, Any>(
"Batch Name" to JOB_NAME,
"Push Success" to pushSuccess,
"Push Fail" to pushFail,
)

if (jobExecution.status == BatchStatus.COMPLETED) {
logger.info("$JOB_NAME Batch JOB Finished - 성공: $pushSuccess, 실패: $pushFail")
alarmAdapter.sendAlarm(AlarmType.BATCH_COMPLETE, jobInfo)
} else {
logger.error("$JOB_NAME Batch JOB Failed - 성공: $pushSuccess, 실패: $pushFail")
alarmAdapter.sendAlarm(AlarmType.BATCH_ERROR, jobInfo)
}
}
}
}

@Bean(JOB_NAME + "_step_execution_listener")
fun stepListener(): StepExecutionListener {
return object : StepExecutionListener {
override fun beforeStep(stepExecution: StepExecution) {
logger.info("$JOB_NAME Batch Step Start")
}

override fun afterStep(stepExecution: StepExecution): ExitStatus {
logger.info("$JOB_NAME Batch Step Finished")
return stepExecution.exitStatus
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.adevspoon.batch.job

import jakarta.persistence.EntityManagerFactory
import org.springframework.context.annotation.Configuration

// TODO: 추후 예정
@Configuration
class QuestionReminderNotification(
private val entityManagerFactory: EntityManagerFactory
) {
private val chunkSize = 500

companion object {
const val JOB_NAME = "questionReminderNotificationJob"
}

}
Loading

0 comments on commit bf64e31

Please sign in to comment.