diff --git a/backend-java/build.gradle b/backend-java/build.gradle index 34f9358..8f8a58e 100644 --- a/backend-java/build.gradle +++ b/backend-java/build.gradle @@ -28,6 +28,10 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-validation' + // #335 — 분산 락 (RollingUpdate 시 멀티 파드 공존 중 데몬 중복 실행 차단) + implementation 'net.javacrumbs.shedlock:shedlock-spring:5.16.0' + implementation 'net.javacrumbs.shedlock:shedlock-provider-redis-spring:5.16.0' + // Oracle JDBC + Security (Wallet support for Oracle ADB) implementation 'com.oracle.database.jdbc:ojdbc11:23.7.0.25.01' implementation 'com.oracle.database.security:oraclepki:23.7.0.25.01' diff --git a/backend-java/src/main/java/com/tasteby/TastebyApplication.java b/backend-java/src/main/java/com/tasteby/TastebyApplication.java index cc6b951..5395b5a 100644 --- a/backend-java/src/main/java/com/tasteby/TastebyApplication.java +++ b/backend-java/src/main/java/com/tasteby/TastebyApplication.java @@ -1,5 +1,6 @@ package com.tasteby; +import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableAsync; @@ -8,6 +9,8 @@ import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableAsync @EnableScheduling +// #335 — defaultLockAtMostFor: 어떤 작업이 lockAtMostFor 명시 안 해도 보호 (안전 마진) +@EnableSchedulerLock(defaultLockAtMostFor = "PT15M") public class TastebyApplication { public static void main(String[] args) { SpringApplication.run(TastebyApplication.class, args); diff --git a/backend-java/src/main/java/com/tasteby/config/ShedLockConfig.java b/backend-java/src/main/java/com/tasteby/config/ShedLockConfig.java new file mode 100644 index 0000000..a76ceb0 --- /dev/null +++ b/backend-java/src/main/java/com/tasteby/config/ShedLockConfig.java @@ -0,0 +1,22 @@ +package com.tasteby.config; + +import net.javacrumbs.shedlock.core.LockProvider; +import net.javacrumbs.shedlock.provider.redis.spring.RedisLockProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; + +/** + * #335 — ShedLock LockProvider (Redis 기반). + * + * 데몬 스케줄러가 다중 파드 환경에서 한 번에 하나만 실행되도록 보장. + * key prefix는 ShedLock 기본 ("lock:")을 사용 → Redis 키는 `lock:daemon-runner`. + */ +@Configuration +public class ShedLockConfig { + + @Bean + public LockProvider lockProvider(RedisConnectionFactory connectionFactory) { + return new RedisLockProvider(connectionFactory); + } +} diff --git a/backend-java/src/main/java/com/tasteby/service/DaemonScheduler.java b/backend-java/src/main/java/com/tasteby/service/DaemonScheduler.java index 4e31d39..531b413 100644 --- a/backend-java/src/main/java/com/tasteby/service/DaemonScheduler.java +++ b/backend-java/src/main/java/com/tasteby/service/DaemonScheduler.java @@ -1,6 +1,7 @@ package com.tasteby.service; import com.tasteby.domain.DaemonConfig; +import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -37,6 +38,10 @@ public class DaemonScheduler { } @Scheduled(fixedDelay = 30_000) // Check every 30 seconds + // #335 — 분산 락: 멀티 파드 환경에서 한 인스턴스만 실행. Redis 키 `lock:daemon-runner`. + // lockAtMostFor: 작업이 비정상 종료돼도 15분 후 강제 해제 (다음 cron이 잡을 수 있게) + // lockAtLeastFor: 빨리 끝나도 30초 동안 유지 (즉시 다른 cron이 같은 작업 잡는 것 방지) + @SchedulerLock(name = "daemon-runner", lockAtMostFor = "PT15M", lockAtLeastFor = "PT30S") public void run() { // 인스턴스 차원 차단(dev/prod 동일 DB 공유 환경에서 dev 쪽 동시 폴링 방지). // dev .env: DAEMON_ENABLED=false → 이 인스턴스는 스케줄러 동작 안 함.