fix(content): 오디오 스냅샷 일 배치 lock을 보강한다
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.content.recommendation.adapter.out.scheduler
|
package kr.co.vividnext.sodalive.v2.content.recommendation.adapter.out.scheduler
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationSnapshotFallbackService
|
||||||
import kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationSnapshotRefreshService
|
import kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationSnapshotRefreshService
|
||||||
|
import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType
|
||||||
|
import org.redisson.api.RLock
|
||||||
import org.redisson.api.RedissonClient
|
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
|
||||||
@@ -14,19 +17,43 @@ class AudioRecommendationSnapshotScheduler(
|
|||||||
@Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul")
|
@Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul")
|
||||||
fun refreshDailySnapshots() {
|
fun refreshDailySnapshots() {
|
||||||
val lock = redissonClient.getLock(LOCK_KEY)
|
val lock = redissonClient.getLock(LOCK_KEY)
|
||||||
|
val sectionLocks = mutableListOf<RLock>()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (lock.tryLock(0, -1, TimeUnit.SECONDS)) {
|
if (lock.tryLock(0, -1, TimeUnit.SECONDS)) {
|
||||||
|
if (!tryLockSections(sectionLocks)) return
|
||||||
refreshService.refreshDailySnapshots()
|
refreshService.refreshDailySnapshots()
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
sectionLocks.asReversed().forEach { sectionLock ->
|
||||||
|
if (sectionLock.isHeldByCurrentThread) {
|
||||||
|
sectionLock.unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
if (lock.isHeldByCurrentThread) {
|
if (lock.isHeldByCurrentThread) {
|
||||||
lock.unlock()
|
lock.unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun tryLockSections(acquiredLocks: MutableList<RLock>): Boolean {
|
||||||
|
return AUDIO_SECTION_TYPES.all { sectionType ->
|
||||||
|
val lock = redissonClient.getLock(AudioRecommendationSnapshotFallbackService.lockKey(sectionType))
|
||||||
|
if (!lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) return false
|
||||||
|
acquiredLocks.add(lock)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val LOCK_KEY = "lock:audio-recommendation-snapshot-refresh"
|
const val LOCK_KEY = "lock:audio-recommendation-snapshot-refresh"
|
||||||
|
private val AUDIO_SECTION_TYPES = listOf(
|
||||||
|
RecommendedSectionType.NEW_AND_HOT_AUDIO_SAFE,
|
||||||
|
RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL,
|
||||||
|
RecommendedSectionType.MOST_COMMENTED_AUDIO_SAFE,
|
||||||
|
RecommendedSectionType.MOST_COMMENTED_AUDIO_ALL,
|
||||||
|
RecommendedSectionType.RECOMMENDED_AUDIO_SAFE,
|
||||||
|
RecommendedSectionType.RECOMMENDED_AUDIO_ALL
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.content.recommendation.adapter.out.scheduler
|
package kr.co.vividnext.sodalive.v2.content.recommendation.adapter.out.scheduler
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationSnapshotFallbackService
|
||||||
import kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationSnapshotRefreshService
|
import kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationSnapshotRefreshService
|
||||||
|
import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
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
|
||||||
@@ -14,6 +16,15 @@ class AudioRecommendationSnapshotSchedulerTest {
|
|||||||
private val refreshService = Mockito.mock(AudioRecommendationSnapshotRefreshService::class.java)
|
private val refreshService = Mockito.mock(AudioRecommendationSnapshotRefreshService::class.java)
|
||||||
private val redissonClient = Mockito.mock(RedissonClient::class.java)
|
private val redissonClient = Mockito.mock(RedissonClient::class.java)
|
||||||
private val lock = Mockito.mock(RLock::class.java)
|
private val lock = Mockito.mock(RLock::class.java)
|
||||||
|
private val sectionLocks = listOf(
|
||||||
|
RecommendedSectionType.NEW_AND_HOT_AUDIO_SAFE,
|
||||||
|
RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL,
|
||||||
|
RecommendedSectionType.MOST_COMMENTED_AUDIO_SAFE,
|
||||||
|
RecommendedSectionType.MOST_COMMENTED_AUDIO_ALL,
|
||||||
|
RecommendedSectionType.RECOMMENDED_AUDIO_SAFE,
|
||||||
|
RecommendedSectionType.RECOMMENDED_AUDIO_ALL
|
||||||
|
)
|
||||||
|
.associateWith { Mockito.mock(RLock::class.java) }
|
||||||
private val scheduler = AudioRecommendationSnapshotScheduler(refreshService, redissonClient)
|
private val scheduler = AudioRecommendationSnapshotScheduler(refreshService, redissonClient)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -33,10 +44,40 @@ class AudioRecommendationSnapshotSchedulerTest {
|
|||||||
Mockito.doReturn(lock).`when`(redissonClient).getLock(AudioRecommendationSnapshotScheduler.LOCK_KEY)
|
Mockito.doReturn(lock).`when`(redissonClient).getLock(AudioRecommendationSnapshotScheduler.LOCK_KEY)
|
||||||
Mockito.doReturn(true).`when`(lock).tryLock(0, -1, TimeUnit.SECONDS)
|
Mockito.doReturn(true).`when`(lock).tryLock(0, -1, TimeUnit.SECONDS)
|
||||||
Mockito.doReturn(true).`when`(lock).isHeldByCurrentThread
|
Mockito.doReturn(true).`when`(lock).isHeldByCurrentThread
|
||||||
|
sectionLocks.forEach { (sectionType, sectionLock) ->
|
||||||
|
Mockito.doReturn(sectionLock).`when`(redissonClient)
|
||||||
|
.getLock(AudioRecommendationSnapshotFallbackService.lockKey(sectionType))
|
||||||
|
Mockito.doReturn(true).`when`(sectionLock).tryLock(0, -1, TimeUnit.MILLISECONDS)
|
||||||
|
Mockito.doReturn(true).`when`(sectionLock).isHeldByCurrentThread
|
||||||
|
}
|
||||||
|
|
||||||
scheduler.refreshDailySnapshots()
|
scheduler.refreshDailySnapshots()
|
||||||
|
|
||||||
Mockito.verify(refreshService).refreshDailySnapshots()
|
Mockito.verify(refreshService).refreshDailySnapshots()
|
||||||
|
sectionLocks.forEach { (_, sectionLock) ->
|
||||||
|
Mockito.verify(sectionLock).unlock()
|
||||||
|
}
|
||||||
|
Mockito.verify(lock).unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("section lock 획득 실패 시 일 배치를 실행하지 않는다")
|
||||||
|
fun shouldSkipWhenSectionLockNotAcquired() {
|
||||||
|
Mockito.doReturn(lock).`when`(redissonClient).getLock(AudioRecommendationSnapshotScheduler.LOCK_KEY)
|
||||||
|
Mockito.doReturn(true).`when`(lock).tryLock(0, -1, TimeUnit.SECONDS)
|
||||||
|
Mockito.doReturn(true).`when`(lock).isHeldByCurrentThread
|
||||||
|
val missedSectionType = RecommendedSectionType.MOST_COMMENTED_AUDIO_SAFE
|
||||||
|
sectionLocks.forEach { (sectionType, sectionLock) ->
|
||||||
|
Mockito.doReturn(sectionLock).`when`(redissonClient)
|
||||||
|
.getLock(AudioRecommendationSnapshotFallbackService.lockKey(sectionType))
|
||||||
|
Mockito.doReturn(sectionType != missedSectionType).`when`(sectionLock).tryLock(0, -1, TimeUnit.MILLISECONDS)
|
||||||
|
Mockito.doReturn(sectionType != missedSectionType).`when`(sectionLock).isHeldByCurrentThread
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduler.refreshDailySnapshots()
|
||||||
|
|
||||||
|
Mockito.verify(refreshService, Mockito.never()).refreshDailySnapshots()
|
||||||
|
Mockito.verify(sectionLocks.getValue(missedSectionType), Mockito.never()).unlock()
|
||||||
Mockito.verify(lock).unlock()
|
Mockito.verify(lock).unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user