diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/scheduler/CreatorRankingSnapshotScheduler.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/scheduler/CreatorRankingSnapshotScheduler.kt index e6220f08..cfcdcf38 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/scheduler/CreatorRankingSnapshotScheduler.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/scheduler/CreatorRankingSnapshotScheduler.kt @@ -1,15 +1,29 @@ package kr.co.vividnext.sodalive.v2.ranking.adapter.out.scheduler import kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshService +import org.redisson.api.RedissonClient import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Component +import java.util.concurrent.TimeUnit @Component class CreatorRankingSnapshotScheduler( - private val refreshService: CreatorRankingSnapshotRefreshService + private val refreshService: CreatorRankingSnapshotRefreshService, + private val redissonClient: RedissonClient ) { - @Scheduled(cron = "0 0 6 * * MON", zone = "Asia/Seoul") + @Scheduled(cron = "0 30 7 * * MON", zone = "Asia/Seoul") fun refreshLastCompletedWeek() { - refreshService.refreshLastCompletedWeek() + val lockName = "lock:creator-ranking-snapshot-refresh" + val lock = redissonClient.getLock(lockName) + + try { + if (lock.tryLock(0, -1, TimeUnit.SECONDS)) { + refreshService.refreshLastCompletedWeek() + } + } finally { + if (lock.isHeldByCurrentThread) { + lock.unlock() + } + } } } diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotRefreshServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotRefreshServiceTest.kt index 3caf3960..a123e24a 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotRefreshServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotRefreshServiceTest.kt @@ -9,10 +9,13 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.mockito.Mockito +import org.redisson.api.RLock +import org.redisson.api.RedissonClient import org.springframework.scheduling.annotation.Scheduled import java.time.LocalDateTime import java.time.ZoneId import java.time.ZonedDateTime +import java.util.concurrent.TimeUnit class CreatorRankingSnapshotRefreshServiceTest { @Test @@ -86,21 +89,64 @@ class CreatorRankingSnapshotRefreshServiceTest { } @Test - @DisplayName("주간 스냅샷 스케줄러는 매주 월요일 06:00 KST cron으로 갱신 서비스를 호출한다") - fun shouldScheduleWeeklySnapshotRefreshAtKstMondaySix() { + @DisplayName("주간 스냅샷 스케줄러는 매주 월요일 07:30 KST cron으로 갱신 서비스를 호출한다") + fun shouldScheduleWeeklySnapshotRefreshAtKstMondaySevenThirty() { val scheduled = CreatorRankingSnapshotScheduler::class.java .getDeclaredMethod("refreshLastCompletedWeek") .getAnnotation(Scheduled::class.java) val service = Mockito.mock(CreatorRankingSnapshotRefreshService::class.java) - val scheduler = CreatorRankingSnapshotScheduler(service) + val redissonClient = Mockito.mock(RedissonClient::class.java) + val lock = Mockito.mock(RLock::class.java) + Mockito.`when`(redissonClient.getLock("lock:creator-ranking-snapshot-refresh")).thenReturn(lock) + Mockito.`when`(lock.tryLock(0, -1, TimeUnit.SECONDS)).thenReturn(true) + Mockito.`when`(lock.isHeldByCurrentThread).thenReturn(true) + val scheduler = CreatorRankingSnapshotScheduler(service, redissonClient) scheduler.refreshLastCompletedWeek() - assertEquals("0 0 6 * * MON", scheduled.cron) + assertEquals("0 30 7 * * MON", scheduled.cron) assertEquals("Asia/Seoul", scheduled.zone) Mockito.verify(service).refreshLastCompletedWeek() } + @Test + @DisplayName("주간 스냅샷 스케줄러는 Redisson lock을 획득한 인스턴스만 갱신을 실행한다") + fun shouldRefreshLastCompletedWeekOnlyWhenRedissonLockAcquired() { + val service = Mockito.mock(CreatorRankingSnapshotRefreshService::class.java) + val redissonClient = Mockito.mock(RedissonClient::class.java) + val lock = Mockito.mock(RLock::class.java) + Mockito.`when`(redissonClient.getLock("lock:creator-ranking-snapshot-refresh")).thenReturn(lock) + Mockito.`when`(lock.tryLock(0, -1, TimeUnit.SECONDS)).thenReturn(true) + Mockito.`when`(lock.isHeldByCurrentThread).thenReturn(true) + val scheduler = CreatorRankingSnapshotScheduler(service, redissonClient) + + scheduler.refreshLastCompletedWeek() + + Mockito.verify(redissonClient).getLock("lock:creator-ranking-snapshot-refresh") + Mockito.verify(lock).tryLock(0, -1, TimeUnit.SECONDS) + Mockito.verify(service).refreshLastCompletedWeek() + Mockito.verify(lock).unlock() + } + + @Test + @DisplayName("주간 스냅샷 스케줄러는 Redisson lock 획득 실패 시 갱신을 건너뛴다") + fun shouldSkipLastCompletedWeekRefreshWhenRedissonLockNotAcquired() { + val service = Mockito.mock(CreatorRankingSnapshotRefreshService::class.java) + val redissonClient = Mockito.mock(RedissonClient::class.java) + val lock = Mockito.mock(RLock::class.java) + Mockito.`when`(redissonClient.getLock("lock:creator-ranking-snapshot-refresh")).thenReturn(lock) + Mockito.`when`(lock.tryLock(0, -1, TimeUnit.SECONDS)).thenReturn(false) + Mockito.`when`(lock.isHeldByCurrentThread).thenReturn(false) + val scheduler = CreatorRankingSnapshotScheduler(service, redissonClient) + + scheduler.refreshLastCompletedWeek() + + Mockito.verify(redissonClient).getLock("lock:creator-ranking-snapshot-refresh") + Mockito.verify(lock).tryLock(0, -1, TimeUnit.SECONDS) + Mockito.verify(service, Mockito.never()).refreshLastCompletedWeek() + Mockito.verify(lock, Mockito.never()).unlock() + } + private fun service( aggregationPort: CreatorRankingAggregationPort = FakeCreatorRankingAggregationPort(), snapshotPort: CreatorRankingSnapshotPort = FakeCreatorRankingSnapshotPort()