From 18efe1b91098f795f660a5f8e6cc1b748c8820ea Mon Sep 17 00:00:00 2001 From: chaeyn Date: Fri, 12 Jun 2026 23:10:00 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20GitHub=20=EB=8F=99=EA=B8=B0=ED=99=94?= =?UTF-8?q?=20=EC=A4=91=EB=B3=B5=20=EC=8B=A4=ED=96=89=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scheduler/GithubStatsSyncScheduler.java | 36 +++++++++++++------ .../service/GithubDailyStatsSyncService.java | 2 -- 2 files changed, 25 insertions(+), 13 deletions(-) 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..911a30d7b 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,24 @@ 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); + return true; + } catch (Exception e) { + log.error("{} 스케줄러 실패. elapsedMs={}", jobName, System.currentTimeMillis() - startedAt, e); + return false; + } finally { + syncRunning.set(false); + } + } } 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 { From 4c450c565261506e31e460694c131c0d760beacf Mon Sep 17 00:00:00 2001 From: chaeyn Date: Fri, 12 Jun 2026 23:10:05 +0900 Subject: [PATCH 2/4] =?UTF-8?q?test:=20GitHub=20=EB=8F=99=EA=B8=B0?= =?UTF-8?q?=ED=99=94=20=EC=A4=91=EB=B3=B5=20=EC=8B=A4=ED=96=89=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GithubStatsSyncSchedulerTest.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) 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..356b7815b 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,9 @@ 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.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -79,4 +85,42 @@ void runDaily365DaysSyncAtMorningSix_callsZeroExpInit() { 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(); + } } From 7c9e3bed4354862caaa565b8141b99ce86289ecb Mon Sep 17 00:00:00 2001 From: chaeyn Date: Fri, 12 Jun 2026 23:18:01 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20GitHub=20=EB=8F=99=EA=B8=B0=ED=99=94?= =?UTF-8?q?=20=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=200=20EXP=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=ED=99=94=20=EC=9C=A0=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clash/adapter/scheduler/GithubStatsSyncScheduler.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 911a30d7b..ec72dc61a 100755 --- a/src/main/java/com/process/clash/adapter/scheduler/GithubStatsSyncScheduler.java +++ b/src/main/java/com/process/clash/adapter/scheduler/GithubStatsSyncScheduler.java @@ -57,12 +57,11 @@ private boolean runExclusive(String jobName, Runnable action) { try { action.run(); log.info("{} 스케줄러 완료. elapsedMs={}", jobName, System.currentTimeMillis() - startedAt); - return true; } catch (Exception e) { log.error("{} 스케줄러 실패. elapsedMs={}", jobName, System.currentTimeMillis() - startedAt, e); - return false; } finally { syncRunning.set(false); } + return true; } } From d62f0944ed03c9277af0a4f3481b7c1570da5b90 Mon Sep 17 00:00:00 2001 From: chaeyn Date: Fri, 12 Jun 2026 23:18:07 +0900 Subject: [PATCH 4/4] =?UTF-8?q?test:=20GitHub=20=EB=8F=99=EA=B8=B0?= =?UTF-8?q?=ED=99=94=20=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=200=20EXP=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=ED=99=94=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scheduler/GithubStatsSyncSchedulerTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) 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 356b7815b..d92f929ec 100644 --- a/src/test/java/com/process/clash/adapter/scheduler/GithubStatsSyncSchedulerTest.java +++ b/src/test/java/com/process/clash/adapter/scheduler/GithubStatsSyncSchedulerTest.java @@ -15,6 +15,7 @@ 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; @@ -86,6 +87,16 @@ 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 {