feat(ranking): 스냅샷 스케줄러 lock을 적용한다

This commit is contained in:
2026-06-08 20:19:46 +09:00
parent 8ab4d0ae84
commit f384ee0dd5
2 changed files with 67 additions and 7 deletions

View File

@@ -1,15 +1,29 @@
package kr.co.vividnext.sodalive.v2.ranking.adapter.out.scheduler package kr.co.vividnext.sodalive.v2.ranking.adapter.out.scheduler
import kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshService import kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshService
import org.redisson.api.RedissonClient
import org.springframework.scheduling.annotation.Scheduled import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import java.util.concurrent.TimeUnit
@Component @Component
class CreatorRankingSnapshotScheduler( 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() { fun refreshLastCompletedWeek() {
val lockName = "lock:creator-ranking-snapshot-refresh"
val lock = redissonClient.getLock(lockName)
try {
if (lock.tryLock(0, -1, TimeUnit.SECONDS)) {
refreshService.refreshLastCompletedWeek() refreshService.refreshLastCompletedWeek()
} }
} finally {
if (lock.isHeldByCurrentThread) {
lock.unlock()
}
}
}
} }

View File

@@ -9,10 +9,13 @@ import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.mockito.Mockito import org.mockito.Mockito
import org.redisson.api.RLock
import org.redisson.api.RedissonClient
import org.springframework.scheduling.annotation.Scheduled import org.springframework.scheduling.annotation.Scheduled
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneId import java.time.ZoneId
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.util.concurrent.TimeUnit
class CreatorRankingSnapshotRefreshServiceTest { class CreatorRankingSnapshotRefreshServiceTest {
@Test @Test
@@ -86,21 +89,64 @@ class CreatorRankingSnapshotRefreshServiceTest {
} }
@Test @Test
@DisplayName("주간 스냅샷 스케줄러는 매주 월요일 06:00 KST cron으로 갱신 서비스를 호출한다") @DisplayName("주간 스냅샷 스케줄러는 매주 월요일 07:30 KST cron으로 갱신 서비스를 호출한다")
fun shouldScheduleWeeklySnapshotRefreshAtKstMondaySix() { fun shouldScheduleWeeklySnapshotRefreshAtKstMondaySevenThirty() {
val scheduled = CreatorRankingSnapshotScheduler::class.java val scheduled = CreatorRankingSnapshotScheduler::class.java
.getDeclaredMethod("refreshLastCompletedWeek") .getDeclaredMethod("refreshLastCompletedWeek")
.getAnnotation(Scheduled::class.java) .getAnnotation(Scheduled::class.java)
val service = Mockito.mock(CreatorRankingSnapshotRefreshService::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() scheduler.refreshLastCompletedWeek()
assertEquals("0 0 6 * * MON", scheduled.cron) assertEquals("0 30 7 * * MON", scheduled.cron)
assertEquals("Asia/Seoul", scheduled.zone) assertEquals("Asia/Seoul", scheduled.zone)
Mockito.verify(service).refreshLastCompletedWeek() 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( private fun service(
aggregationPort: CreatorRankingAggregationPort = FakeCreatorRankingAggregationPort(), aggregationPort: CreatorRankingAggregationPort = FakeCreatorRankingAggregationPort(),
snapshotPort: CreatorRankingSnapshotPort = FakeCreatorRankingSnapshotPort() snapshotPort: CreatorRankingSnapshotPort = FakeCreatorRankingSnapshotPort()