diff --git a/src/main/java/com/process/clash/adapter/scheduler/GithubStatsSyncScheduler.java b/src/main/java/com/process/clash/adapter/scheduler/GithubStatsSyncScheduler.java index f8e240744..ec72dc61a 100755 --- a/src/main/java/com/process/clash/adapter/scheduler/GithubStatsSyncScheduler.java +++ b/src/main/java/com/process/clash/adapter/scheduler/GithubStatsSyncScheduler.java @@ -3,6 +3,7 @@ import com.process.clash.application.github.service.GithubDailyStatsSyncService; import com.process.clash.application.ranking.service.ZeroRankingDataInitService; import com.process.clash.application.user.exp.service.GithubExpGrantService; +import java.util.concurrent.atomic.AtomicBoolean; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; @@ -17,28 +18,21 @@ public class GithubStatsSyncScheduler { private final GithubDailyStatsSyncService syncService; private final GithubExpGrantService githubExpGrantService; private final ZeroRankingDataInitService zeroRankingDataInitService; + private final AtomicBoolean syncRunning = new AtomicBoolean(false); // 6시에는 365일 동기화가 작동하기에 30일 동기화는 6시를 제외한 매 시간에 작동하도록 설정했습니다. @Async @Scheduled(cron = "0 0 0-5,7-23 * * *", zone = "${github.sync.timezone:Asia/Seoul}") public void runHourly30DaysSyncExceptMorningSix() { - log.info("GitHub 30일 동기화 스케줄러 시작."); - try { - syncAndGrant(syncService::syncRecent30Days); - } catch (Exception e) { - log.error("GitHub 30일 동기화 스케줄러 실패.", e); - } + runExclusive("GitHub 30일 동기화", () -> syncAndGrant(syncService::syncRecent30Days)); } // 365일 동기화는 매일 오전 6시에만 작동. (이 시각에는 30일 동기화가 중복되기에 작동하지 않음) @Async @Scheduled(cron = "0 0 6 * * *", zone = "${github.sync.timezone:Asia/Seoul}") public void runDaily365DaysSyncAtMorningSix() { - log.info("GitHub 365일 동기화 스케줄러 시작."); - try { - syncAndGrant(syncService::syncRecent365Days); - } catch (Exception e) { - log.error("GitHub 365일 동기화 스케줄러 실패.", e); + if (!runExclusive("GitHub 365일 동기화", () -> syncAndGrant(syncService::syncRecent365Days))) { + return; } try { zeroRankingDataInitService.initZeroExpForToday(); @@ -51,4 +45,23 @@ private void syncAndGrant(Runnable syncAction) { syncAction.run(); githubExpGrantService.grantForToday(); } + + private boolean runExclusive(String jobName, Runnable action) { + if (!syncRunning.compareAndSet(false, true)) { + log.warn("{} 스케줄러를 건너뜁니다. 이전 GitHub 동기화가 아직 실행 중입니다.", jobName); + return false; + } + + long startedAt = System.currentTimeMillis(); + log.info("{} 스케줄러 시작.", jobName); + try { + action.run(); + log.info("{} 스케줄러 완료. elapsedMs={}", jobName, System.currentTimeMillis() - startedAt); + } catch (Exception e) { + log.error("{} 스케줄러 실패. elapsedMs={}", jobName, System.currentTimeMillis() - startedAt, e); + } finally { + syncRunning.set(false); + } + return true; + } } diff --git a/src/main/java/com/process/clash/application/github/service/GithubDailyStatsSyncService.java b/src/main/java/com/process/clash/application/github/service/GithubDailyStatsSyncService.java index 33c901726..0fa061c59 100755 --- a/src/main/java/com/process/clash/application/github/service/GithubDailyStatsSyncService.java +++ b/src/main/java/com/process/clash/application/github/service/GithubDailyStatsSyncService.java @@ -7,7 +7,6 @@ import com.process.clash.application.github.port.out.GithubStatsFetchPort; import com.process.clash.application.github.port.out.GithubSyncTargetPort; import com.process.clash.domain.github.entity.GitHubDailyStats; -import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -21,7 +20,6 @@ import java.util.concurrent.ExecutorService; @Service -@Transactional @RequiredArgsConstructor @Slf4j public class GithubDailyStatsSyncService implements SyncGithubDailyStatsUseCase { diff --git a/src/test/java/com/process/clash/adapter/scheduler/GithubStatsSyncSchedulerTest.java b/src/test/java/com/process/clash/adapter/scheduler/GithubStatsSyncSchedulerTest.java index ac7fbed3e..d92f929ec 100644 --- a/src/test/java/com/process/clash/adapter/scheduler/GithubStatsSyncSchedulerTest.java +++ b/src/test/java/com/process/clash/adapter/scheduler/GithubStatsSyncSchedulerTest.java @@ -3,6 +3,9 @@ import com.process.clash.application.github.service.GithubDailyStatsSyncService; import com.process.clash.application.ranking.service.ZeroRankingDataInitService; import com.process.clash.application.user.exp.service.GithubExpGrantService; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -10,6 +13,10 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -79,4 +86,52 @@ void runDaily365DaysSyncAtMorningSix_callsZeroExpInit() { verify(zeroRankingDataInitService, times(1)).initZeroExpForToday(); } + + @Test + @DisplayName("6시 GitHub 동기화가 실패해도 0 EXP 초기화를 호출한다") + void runDaily365DaysSyncAtMorningSix_callsZeroExpInitWhenSyncFails() { + doThrow(new RuntimeException("github sync failed")).when(syncService).syncRecent365Days(); + + scheduler.runDaily365DaysSyncAtMorningSix(); + + verify(zeroRankingDataInitService, times(1)).initZeroExpForToday(); + } + + @Test + @DisplayName("이전 GitHub 동기화가 실행 중이면 다음 GitHub 동기화를 건너뛴다") + void skipOverlappingGithubSyncJob() throws Exception { + CountDownLatch firstSyncStarted = new CountDownLatch(1); + CountDownLatch releaseFirstSync = new CountDownLatch(1); + + doAnswer(invocation -> { + firstSyncStarted.countDown(); + assertThat(releaseFirstSync.await(1, TimeUnit.SECONDS)).isTrue(); + return null; + }).when(syncService).syncRecent30Days(); + + FutureTask firstJob = new FutureTask<>(() -> { + scheduler.runHourly30DaysSyncExceptMorningSix(); + return null; + }); + Thread firstJobThread = new Thread(firstJob, "github-sync-test"); + firstJobThread.start(); + + assertThat(firstSyncStarted.await(1, TimeUnit.SECONDS)).isTrue(); + + scheduler.runDaily365DaysSyncAtMorningSix(); + + verify(syncService, never()).syncRecent365Days(); + verify(zeroRankingDataInitService, never()).initZeroExpForToday(); + + releaseFirstSync.countDown(); + firstJob.get(1, TimeUnit.SECONDS); + + verify(githubExpGrantService).grantForToday(); + + scheduler.runDaily365DaysSyncAtMorningSix(); + + verify(syncService).syncRecent365Days(); + verify(githubExpGrantService, times(2)).grantForToday(); + verify(zeroRankingDataInitService).initZeroExpForToday(); + } }