feat(home): 인기 커뮤니티 스냅샷 갱신 경로를 분리한다
This commit is contained in:
@@ -32,7 +32,7 @@ class RecommendationSnapshotFallbackService(
|
||||
executor: Executor? = null,
|
||||
private val homeWaitMillis: Long = HOME_WAIT_MILLIS,
|
||||
transactionManager: PlatformTransactionManager? = null
|
||||
) : AiCharacterSnapshotFallbackPort, CheerCreatorSnapshotFallbackPort {
|
||||
) : AiCharacterSnapshotFallbackPort, CheerCreatorSnapshotFallbackPort, PopularCommunitySnapshotFallbackPort {
|
||||
private val log = LoggerFactory.getLogger(javaClass)
|
||||
private val ownedExecutor: ExecutorService? = if (executor == null) {
|
||||
Executors.newFixedThreadPool(DEFAULT_WORKER_THREADS)
|
||||
@@ -65,6 +65,19 @@ class RecommendationSnapshotFallbackService(
|
||||
return refreshIfMissing(target(RecommendedSectionType.CHEER_CREATOR, CHEER_CREATOR_LOCK_KEY), offset, limit, nowUtc)
|
||||
}
|
||||
|
||||
override fun refreshPopularCommunityIfMissing(
|
||||
offset: Long,
|
||||
limit: Int,
|
||||
nowUtc: LocalDateTime
|
||||
): List<RecommendationSnapshotRecord> {
|
||||
return refreshIfMissing(
|
||||
target(RecommendedSectionType.POPULAR_COMMUNITY, POPULAR_COMMUNITY_LOCK_KEY),
|
||||
offset,
|
||||
limit,
|
||||
nowUtc
|
||||
)
|
||||
}
|
||||
|
||||
private fun refreshIfMissing(
|
||||
target: FallbackTarget,
|
||||
offset: Long,
|
||||
@@ -186,6 +199,7 @@ class RecommendationSnapshotFallbackService(
|
||||
return when (sectionType) {
|
||||
RecommendedSectionType.AI_CHARACTER -> refreshService.refreshAiCharacterSnapshots(nowUtc)
|
||||
RecommendedSectionType.CHEER_CREATOR -> refreshService.refreshCheerCreatorSnapshots(nowUtc)
|
||||
RecommendedSectionType.POPULAR_COMMUNITY -> refreshService.refreshPopularCommunitySnapshots(nowUtc)
|
||||
else -> error("Unsupported fallback sectionType: $sectionType")
|
||||
}
|
||||
}
|
||||
@@ -207,6 +221,7 @@ class RecommendationSnapshotFallbackService(
|
||||
companion object {
|
||||
const val AI_CHARACTER_LOCK_KEY = "lock:recommendation-snapshot-refresh:AI_CHARACTER"
|
||||
const val CHEER_CREATOR_LOCK_KEY = "lock:recommendation-snapshot-refresh:CHEER_CREATOR"
|
||||
const val POPULAR_COMMUNITY_LOCK_KEY = "lock:recommendation-snapshot-refresh:POPULAR_COMMUNITY"
|
||||
private const val LOCK_WAIT_MILLIS = 300L
|
||||
private const val HOME_WAIT_MILLIS = 1_500L
|
||||
private const val DEFAULT_WORKER_THREADS = 2
|
||||
@@ -220,3 +235,11 @@ interface CheerCreatorSnapshotFallbackPort {
|
||||
nowUtc: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)
|
||||
): List<RecommendationSnapshotRecord>
|
||||
}
|
||||
|
||||
interface PopularCommunitySnapshotFallbackPort {
|
||||
fun refreshPopularCommunityIfMissing(
|
||||
offset: Long,
|
||||
limit: Int,
|
||||
nowUtc: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)
|
||||
): List<RecommendationSnapshotRecord>
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import org.springframework.transaction.annotation.Transactional
|
||||
import org.springframework.transaction.support.TransactionSynchronization
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@@ -38,18 +37,12 @@ open class RecommendationSnapshotRefreshService(
|
||||
@Transactional
|
||||
fun refreshDailySnapshots(now: LocalDateTime) {
|
||||
val startedAt = System.currentTimeMillis()
|
||||
val snapshotAt = now
|
||||
.atZone(UTC_ZONE)
|
||||
.withZoneSameInstant(KST_ZONE)
|
||||
.toLocalDate()
|
||||
.minusDays(1)
|
||||
.atTime(23, 59, 59)
|
||||
val windowStart = snapshotAt.toLocalDate().minusDays(6).atStartOfDay()
|
||||
val snapshotAt = windowPolicy.previousKstDayUtcWindow(now).snapshotAt
|
||||
|
||||
runCatching {
|
||||
val aiCharacterCount = refreshAiCharacterSnapshotsWithSectionLock(now)
|
||||
val cheerCreatorCount = refreshCheerCreatorSnapshotsWithSectionLock(now)
|
||||
val popularCommunityCount = replacePopularCommunitySnapshots(windowStart, snapshotAt)
|
||||
val popularCommunityCount = refreshPopularCommunitySnapshotsWithSectionLock(now)
|
||||
RefreshCounts(aiCharacterCount, cheerCreatorCount, popularCommunityCount)
|
||||
}.onSuccess { counts ->
|
||||
afterCommit {
|
||||
@@ -119,6 +112,30 @@ open class RecommendationSnapshotRefreshService(
|
||||
return snapshots.size
|
||||
}
|
||||
|
||||
@Transactional
|
||||
open fun refreshPopularCommunitySnapshots(nowUtc: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)): Int {
|
||||
val startedAt = System.currentTimeMillis()
|
||||
val window = windowPolicy.previousKstSevenDayUtcWindow(nowUtc)
|
||||
val snapshots = queryPort.findPopularCommunitySnapshots(
|
||||
window.startUtc,
|
||||
window.endExclusiveUtc,
|
||||
POPULAR_COMMUNITY_SNAPSHOT_LIMIT
|
||||
)
|
||||
snapshotPort.replaceSnapshots(RecommendedSectionType.POPULAR_COMMUNITY, window.snapshotAt, snapshots)
|
||||
afterCommit {
|
||||
log.info(
|
||||
"event=popular_community_recommendation_snapshot_refresh_success " +
|
||||
"snapshotAt={} windowStartUtc={} windowEndExclusiveUtc={} savedCount={} elapsedMs={}",
|
||||
window.snapshotAt,
|
||||
window.startUtc,
|
||||
window.endExclusiveUtc,
|
||||
snapshots.size,
|
||||
System.currentTimeMillis() - startedAt
|
||||
)
|
||||
}
|
||||
return snapshots.size
|
||||
}
|
||||
|
||||
private fun refreshAiCharacterSnapshotsWithSectionLock(nowUtc: LocalDateTime): Int {
|
||||
val client = redissonClient ?: return refreshAiCharacterSnapshots(nowUtc)
|
||||
val lock = client.getLock(RecommendationSnapshotFallbackService.AI_CHARACTER_LOCK_KEY)
|
||||
@@ -151,6 +168,22 @@ open class RecommendationSnapshotRefreshService(
|
||||
return refreshCheerCreatorSnapshots(nowUtc)
|
||||
}
|
||||
|
||||
private fun refreshPopularCommunitySnapshotsWithSectionLock(nowUtc: LocalDateTime): Int {
|
||||
val client = redissonClient ?: return refreshPopularCommunitySnapshots(nowUtc)
|
||||
val lock = client.getLock(RecommendationSnapshotFallbackService.POPULAR_COMMUNITY_LOCK_KEY)
|
||||
|
||||
if (!lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
|
||||
log.info(
|
||||
"event=popular_community_recommendation_snapshot_refresh_lock_missed lockKey={}",
|
||||
RecommendationSnapshotFallbackService.POPULAR_COMMUNITY_LOCK_KEY
|
||||
)
|
||||
return 0
|
||||
}
|
||||
|
||||
unlockAfterTransaction(lock)
|
||||
return refreshPopularCommunitySnapshots(nowUtc)
|
||||
}
|
||||
|
||||
private fun unlockAfterTransaction(lock: org.redisson.api.RLock) {
|
||||
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
if (lock.isHeldByCurrentThread) {
|
||||
@@ -170,12 +203,6 @@ open class RecommendationSnapshotRefreshService(
|
||||
)
|
||||
}
|
||||
|
||||
private fun replacePopularCommunitySnapshots(windowStart: LocalDateTime, snapshotAt: LocalDateTime): Int {
|
||||
val snapshots = queryPort.findPopularCommunitySnapshots(windowStart, snapshotAt, POPULAR_COMMUNITY_SNAPSHOT_LIMIT)
|
||||
snapshotPort.replaceSnapshots(RecommendedSectionType.POPULAR_COMMUNITY, snapshotAt, snapshots)
|
||||
return snapshots.size
|
||||
}
|
||||
|
||||
private data class RefreshCounts(
|
||||
val aiCharacterCount: Int,
|
||||
val cheerCreatorCount: Int,
|
||||
@@ -198,7 +225,5 @@ open class RecommendationSnapshotRefreshService(
|
||||
private const val AI_CHARACTER_SNAPSHOT_LIMIT = 20
|
||||
private const val CHEER_CREATOR_SNAPSHOT_LIMIT = 16
|
||||
private const val POPULAR_COMMUNITY_SNAPSHOT_LIMIT = 20
|
||||
private val UTC_ZONE: ZoneId = ZoneId.of("UTC")
|
||||
private val KST_ZONE: ZoneId = ZoneId.of("Asia/Seoul")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,57 @@ class RecommendationSnapshotFallbackServiceTest {
|
||||
Mockito.verify(lock).unlock()
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("인기 커뮤니티 fallback은 section lock을 잡고 공통 refresh 후 대상일 스냅샷을 다시 조회한다")
|
||||
fun shouldRefreshMissingPopularCommunitySnapshotsWithSectionLock() {
|
||||
val snapshotPort = FakeRecommendationFallbackSnapshotPort()
|
||||
val refreshService = Mockito.mock(RecommendationSnapshotRefreshService::class.java)
|
||||
val redissonClient = Mockito.mock(RedissonClient::class.java)
|
||||
val lock = Mockito.mock(RLock::class.java)
|
||||
val nowUtc = LocalDateTime.of(2026, 7, 9, 21, 0)
|
||||
Mockito.`when`(redissonClient.getLock(RecommendationSnapshotFallbackService.POPULAR_COMMUNITY_LOCK_KEY)).thenReturn(lock)
|
||||
Mockito.`when`(lock.tryLock(300, -1, TimeUnit.MILLISECONDS)).thenReturn(true)
|
||||
Mockito.`when`(lock.isHeldByCurrentThread).thenReturn(true)
|
||||
Mockito.doAnswer {
|
||||
snapshotPort.replaceSnapshots(
|
||||
RecommendedSectionType.POPULAR_COMMUNITY,
|
||||
LocalDateTime.of(2026, 7, 9, 14, 59, 59),
|
||||
listOf(snapshot(RecommendedSectionType.POPULAR_COMMUNITY, 7L))
|
||||
)
|
||||
1
|
||||
}.`when`(refreshService).refreshPopularCommunitySnapshots(nowUtc)
|
||||
val service = RecommendationSnapshotFallbackService(snapshotPort, refreshService, redissonClient, directExecutor())
|
||||
|
||||
val snapshots = service.refreshPopularCommunityIfMissing(offset = 0, limit = 20, nowUtc = nowUtc)
|
||||
|
||||
assertEquals(listOf(7L), snapshots.map { it.targetId })
|
||||
Mockito.verify(redissonClient).getLock(RecommendationSnapshotFallbackService.POPULAR_COMMUNITY_LOCK_KEY)
|
||||
Mockito.verify(lock).tryLock(300, -1, TimeUnit.MILLISECONDS)
|
||||
Mockito.verify(refreshService).refreshPopularCommunitySnapshots(nowUtc)
|
||||
Mockito.verify(lock).unlock()
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("인기 커뮤니티 fallback은 빈 스냅샷 marker가 있으면 refresh를 반복하지 않는다")
|
||||
fun shouldSkipPopularCommunityRefreshWhenEmptySnapshotMarkerExists() {
|
||||
val snapshotPort = FakeRecommendationFallbackSnapshotPort()
|
||||
val nowUtc = LocalDateTime.of(2026, 7, 9, 21, 0)
|
||||
snapshotPort.replaceSnapshots(
|
||||
RecommendedSectionType.POPULAR_COMMUNITY,
|
||||
LocalDateTime.of(2026, 7, 9, 14, 59, 59),
|
||||
emptyList()
|
||||
)
|
||||
val refreshService = Mockito.mock(RecommendationSnapshotRefreshService::class.java)
|
||||
val redissonClient = Mockito.mock(RedissonClient::class.java)
|
||||
val service = RecommendationSnapshotFallbackService(snapshotPort, refreshService, redissonClient, directExecutor())
|
||||
|
||||
val snapshots = service.refreshPopularCommunityIfMissing(offset = 0, limit = 20, nowUtc = nowUtc)
|
||||
|
||||
assertEquals(emptyList<RecommendationSnapshotRecord>(), snapshots)
|
||||
Mockito.verifyNoInteractions(redissonClient)
|
||||
Mockito.verifyNoInteractions(refreshService)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("응원 크리에이터 fallback은 오래된 스냅샷만 있으면 refresh를 실행한다")
|
||||
fun shouldRefreshCheerCreatorWhenOnlyStaleSnapshotsExist() {
|
||||
@@ -371,7 +422,9 @@ private class FakeRecommendationFallbackSnapshotPort : RecommendationSnapshotPor
|
||||
}
|
||||
|
||||
private fun supportsEmptySnapshotMarker(sectionType: RecommendedSectionType): Boolean {
|
||||
return sectionType == RecommendedSectionType.AI_CHARACTER || sectionType == RecommendedSectionType.CHEER_CREATOR
|
||||
return sectionType == RecommendedSectionType.AI_CHARACTER ||
|
||||
sectionType == RecommendedSectionType.CHEER_CREATOR ||
|
||||
sectionType == RecommendedSectionType.POPULAR_COMMUNITY
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,8 +53,9 @@ class RecommendationSnapshotRefreshServiceTest {
|
||||
val queryPort = Mockito.mock(HomeRecommendationQueryPort::class.java)
|
||||
val service = service(snapshotPort = snapshotPort, queryPort = queryPort)
|
||||
val now = LocalDateTime.of(2026, 5, 29, 15, 0, 0)
|
||||
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
|
||||
val windowStart = LocalDateTime.of(2026, 5, 23, 0, 0, 0)
|
||||
val snapshotAt = LocalDateTime.of(2026, 5, 29, 14, 59, 59)
|
||||
val windowStart = LocalDateTime.of(2026, 5, 22, 15, 0, 0)
|
||||
val windowEndExclusive = LocalDateTime.of(2026, 5, 29, 15, 0, 0)
|
||||
val aiWindowStart = LocalDateTime.of(2026, 5, 28, 15, 0, 0)
|
||||
val aiSnapshotAt = LocalDateTime.of(2026, 5, 29, 14, 59, 59)
|
||||
val aiWindowEndExclusive = LocalDateTime.of(2026, 5, 29, 15, 0, 0)
|
||||
@@ -81,7 +82,7 @@ class RecommendationSnapshotRefreshServiceTest {
|
||||
)
|
||||
)
|
||||
)
|
||||
Mockito.`when`(queryPort.findPopularCommunitySnapshots(windowStart, snapshotAt, 20)).thenReturn(
|
||||
Mockito.`when`(queryPort.findPopularCommunitySnapshots(windowStart, windowEndExclusive, 20)).thenReturn(
|
||||
listOf(
|
||||
RecommendationSnapshotRecord(
|
||||
sectionType = RecommendedSectionType.POPULAR_COMMUNITY,
|
||||
@@ -112,7 +113,7 @@ class RecommendationSnapshotRefreshServiceTest {
|
||||
|
||||
Mockito.verify(queryPort).findAiCharacterSnapshots(aiWindowStart, aiWindowEndExclusive, 20)
|
||||
Mockito.verify(queryPort).findCheerCreatorSnapshots(aiWindowStart, aiWindowEndExclusive, 16)
|
||||
Mockito.verify(queryPort).findPopularCommunitySnapshots(windowStart, snapshotAt, 20)
|
||||
Mockito.verify(queryPort).findPopularCommunitySnapshots(windowStart, windowEndExclusive, 20)
|
||||
assertEquals(true, output.out.contains("event=recommendation_snapshot_refresh_success"))
|
||||
assertEquals(true, output.out.contains("aiCharacterCount=1"))
|
||||
}
|
||||
@@ -123,8 +124,8 @@ class RecommendationSnapshotRefreshServiceTest {
|
||||
val queryPort = Mockito.mock(HomeRecommendationQueryPort::class.java)
|
||||
val service = service(queryPort = queryPort)
|
||||
val now = LocalDateTime.of(2026, 5, 29, 15, 0, 0)
|
||||
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
|
||||
val windowStart = LocalDateTime.of(2026, 5, 23, 0, 0, 0)
|
||||
val windowStart = LocalDateTime.of(2026, 5, 22, 15, 0, 0)
|
||||
val windowEndExclusive = LocalDateTime.of(2026, 5, 29, 15, 0, 0)
|
||||
Mockito.`when`(
|
||||
queryPort.findAiCharacterSnapshots(
|
||||
LocalDateTime.of(2026, 5, 28, 15, 0, 0),
|
||||
@@ -139,7 +140,7 @@ class RecommendationSnapshotRefreshServiceTest {
|
||||
16
|
||||
)
|
||||
).thenReturn(emptyList())
|
||||
Mockito.`when`(queryPort.findPopularCommunitySnapshots(windowStart, snapshotAt, 20)).thenReturn(emptyList())
|
||||
Mockito.`when`(queryPort.findPopularCommunitySnapshots(windowStart, windowEndExclusive, 20)).thenReturn(emptyList())
|
||||
|
||||
TransactionSynchronizationManager.initSynchronization()
|
||||
try {
|
||||
@@ -161,8 +162,9 @@ class RecommendationSnapshotRefreshServiceTest {
|
||||
val queryPort = Mockito.mock(HomeRecommendationQueryPort::class.java)
|
||||
val service = service(snapshotPort = snapshotPort, queryPort = queryPort)
|
||||
val now = LocalDateTime.of(2026, 5, 29, 15, 0, 0)
|
||||
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
|
||||
val windowStart = LocalDateTime.of(2026, 5, 23, 0, 0, 0)
|
||||
val snapshotAt = LocalDateTime.of(2026, 5, 29, 14, 59, 59)
|
||||
val windowStart = LocalDateTime.of(2026, 5, 22, 15, 0, 0)
|
||||
val windowEndExclusive = LocalDateTime.of(2026, 5, 29, 15, 0, 0)
|
||||
val aiSnapshotAt = LocalDateTime.of(2026, 5, 29, 14, 59, 59)
|
||||
val aiWindowEndExclusive = LocalDateTime.of(2026, 5, 29, 15, 0, 0)
|
||||
|
||||
@@ -184,7 +186,7 @@ class RecommendationSnapshotRefreshServiceTest {
|
||||
).thenReturn(
|
||||
listOf(snapshot(RecommendedSectionType.CHEER_CREATOR, targetId = 120L, score = 120.0, snapshotAt = snapshotAt))
|
||||
)
|
||||
Mockito.`when`(queryPort.findPopularCommunitySnapshots(windowStart, snapshotAt, 20)).thenReturn(
|
||||
Mockito.`when`(queryPort.findPopularCommunitySnapshots(windowStart, windowEndExclusive, 20)).thenReturn(
|
||||
listOf(snapshot(RecommendedSectionType.POPULAR_COMMUNITY, targetId = 225L, score = 225.0, snapshotAt = snapshotAt))
|
||||
)
|
||||
|
||||
@@ -203,16 +205,21 @@ class RecommendationSnapshotRefreshServiceTest {
|
||||
val redissonClient = Mockito.mock(RedissonClient::class.java)
|
||||
val lock = Mockito.mock(RLock::class.java)
|
||||
val cheerLock = Mockito.mock(RLock::class.java)
|
||||
val popularLock = Mockito.mock(RLock::class.java)
|
||||
val service = service(snapshotPort = snapshotPort, queryPort = queryPort, redissonClient = redissonClient)
|
||||
val now = LocalDateTime.of(2026, 5, 29, 15, 0, 0)
|
||||
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
|
||||
val windowStart = LocalDateTime.of(2026, 5, 23, 0, 0, 0)
|
||||
val windowStart = LocalDateTime.of(2026, 5, 22, 15, 0, 0)
|
||||
val windowEndExclusive = LocalDateTime.of(2026, 5, 29, 15, 0, 0)
|
||||
Mockito.`when`(redissonClient.getLock(RecommendationSnapshotFallbackService.AI_CHARACTER_LOCK_KEY)).thenReturn(lock)
|
||||
Mockito.`when`(redissonClient.getLock(RecommendationSnapshotFallbackService.CHEER_CREATOR_LOCK_KEY)).thenReturn(cheerLock)
|
||||
Mockito.`when`(redissonClient.getLock(RecommendationSnapshotFallbackService.POPULAR_COMMUNITY_LOCK_KEY))
|
||||
.thenReturn(popularLock)
|
||||
Mockito.`when`(lock.tryLock(0, -1, TimeUnit.MILLISECONDS)).thenReturn(true)
|
||||
Mockito.`when`(lock.isHeldByCurrentThread).thenReturn(true)
|
||||
Mockito.`when`(cheerLock.tryLock(0, -1, TimeUnit.MILLISECONDS)).thenReturn(true)
|
||||
Mockito.`when`(cheerLock.isHeldByCurrentThread).thenReturn(true)
|
||||
Mockito.`when`(popularLock.tryLock(0, -1, TimeUnit.MILLISECONDS)).thenReturn(true)
|
||||
Mockito.`when`(popularLock.isHeldByCurrentThread).thenReturn(true)
|
||||
Mockito.`when`(
|
||||
queryPort.findAiCharacterSnapshots(
|
||||
LocalDateTime.of(2026, 5, 28, 15, 0, 0),
|
||||
@@ -227,7 +234,7 @@ class RecommendationSnapshotRefreshServiceTest {
|
||||
16
|
||||
)
|
||||
).thenReturn(emptyList())
|
||||
Mockito.`when`(queryPort.findPopularCommunitySnapshots(windowStart, snapshotAt, 20)).thenReturn(emptyList())
|
||||
Mockito.`when`(queryPort.findPopularCommunitySnapshots(windowStart, windowEndExclusive, 20)).thenReturn(emptyList())
|
||||
|
||||
service.refreshDailySnapshots(now)
|
||||
|
||||
@@ -240,6 +247,7 @@ class RecommendationSnapshotRefreshServiceTest {
|
||||
)
|
||||
Mockito.verify(lock).unlock()
|
||||
Mockito.verify(cheerLock).unlock()
|
||||
Mockito.verify(popularLock).unlock()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -250,22 +258,28 @@ class RecommendationSnapshotRefreshServiceTest {
|
||||
val redissonClient = Mockito.mock(RedissonClient::class.java)
|
||||
val aiLock = Mockito.mock(RLock::class.java)
|
||||
val cheerLock = Mockito.mock(RLock::class.java)
|
||||
val popularLock = Mockito.mock(RLock::class.java)
|
||||
val service = service(snapshotPort = snapshotPort, queryPort = queryPort, redissonClient = redissonClient)
|
||||
val now = LocalDateTime.of(2026, 5, 29, 15, 0, 0)
|
||||
val windowStart = LocalDateTime.of(2026, 5, 23, 0, 0, 0)
|
||||
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
|
||||
val windowStart = LocalDateTime.of(2026, 5, 22, 15, 0, 0)
|
||||
val windowEndExclusive = LocalDateTime.of(2026, 5, 29, 15, 0, 0)
|
||||
val snapshotAt = LocalDateTime.of(2026, 5, 29, 14, 59, 59)
|
||||
val cheerWindowStart = LocalDateTime.of(2026, 5, 28, 15, 0, 0)
|
||||
val cheerWindowEndExclusive = LocalDateTime.of(2026, 5, 29, 15, 0, 0)
|
||||
Mockito.`when`(redissonClient.getLock(RecommendationSnapshotFallbackService.AI_CHARACTER_LOCK_KEY)).thenReturn(aiLock)
|
||||
Mockito.`when`(redissonClient.getLock(RecommendationSnapshotFallbackService.CHEER_CREATOR_LOCK_KEY)).thenReturn(cheerLock)
|
||||
Mockito.`when`(redissonClient.getLock(RecommendationSnapshotFallbackService.POPULAR_COMMUNITY_LOCK_KEY))
|
||||
.thenReturn(popularLock)
|
||||
Mockito.`when`(aiLock.tryLock(0, -1, TimeUnit.MILLISECONDS)).thenReturn(false)
|
||||
Mockito.`when`(aiLock.isHeldByCurrentThread).thenReturn(false)
|
||||
Mockito.`when`(cheerLock.tryLock(0, -1, TimeUnit.MILLISECONDS)).thenReturn(true)
|
||||
Mockito.`when`(cheerLock.isHeldByCurrentThread).thenReturn(true)
|
||||
Mockito.`when`(popularLock.tryLock(0, -1, TimeUnit.MILLISECONDS)).thenReturn(true)
|
||||
Mockito.`when`(popularLock.isHeldByCurrentThread).thenReturn(true)
|
||||
Mockito.`when`(queryPort.findCheerCreatorSnapshots(cheerWindowStart, cheerWindowEndExclusive, 16)).thenReturn(
|
||||
listOf(snapshot(RecommendedSectionType.CHEER_CREATOR, targetId = 10L, score = 10.0, snapshotAt = snapshotAt))
|
||||
)
|
||||
Mockito.`when`(queryPort.findPopularCommunitySnapshots(windowStart, snapshotAt, 20)).thenReturn(emptyList())
|
||||
Mockito.`when`(queryPort.findPopularCommunitySnapshots(windowStart, windowEndExclusive, 20)).thenReturn(emptyList())
|
||||
|
||||
service.refreshDailySnapshots(now)
|
||||
|
||||
@@ -274,6 +288,7 @@ class RecommendationSnapshotRefreshServiceTest {
|
||||
Mockito.verify(queryPort).findCheerCreatorSnapshots(cheerWindowStart, cheerWindowEndExclusive, 16)
|
||||
assertEquals(listOf(10L), snapshotPort.findLatestSnapshots(RecommendedSectionType.CHEER_CREATOR).map { it.targetId })
|
||||
Mockito.verify(cheerLock).unlock()
|
||||
Mockito.verify(popularLock).unlock()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -284,17 +299,23 @@ class RecommendationSnapshotRefreshServiceTest {
|
||||
val redissonClient = Mockito.mock(RedissonClient::class.java)
|
||||
val aiLock = Mockito.mock(RLock::class.java)
|
||||
val cheerLock = Mockito.mock(RLock::class.java)
|
||||
val popularLock = Mockito.mock(RLock::class.java)
|
||||
val service = service(snapshotPort = snapshotPort, queryPort = queryPort, redissonClient = redissonClient)
|
||||
val now = LocalDateTime.of(2026, 5, 29, 15, 0, 0)
|
||||
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
|
||||
val windowStart = LocalDateTime.of(2026, 5, 23, 0, 0, 0)
|
||||
val snapshotAt = LocalDateTime.of(2026, 5, 29, 14, 59, 59)
|
||||
val windowStart = LocalDateTime.of(2026, 5, 22, 15, 0, 0)
|
||||
val windowEndExclusive = LocalDateTime.of(2026, 5, 29, 15, 0, 0)
|
||||
Mockito.`when`(redissonClient.getLock(RecommendationSnapshotFallbackService.AI_CHARACTER_LOCK_KEY)).thenReturn(aiLock)
|
||||
Mockito.`when`(redissonClient.getLock(RecommendationSnapshotFallbackService.CHEER_CREATOR_LOCK_KEY)).thenReturn(cheerLock)
|
||||
Mockito.`when`(redissonClient.getLock(RecommendationSnapshotFallbackService.POPULAR_COMMUNITY_LOCK_KEY))
|
||||
.thenReturn(popularLock)
|
||||
Mockito.`when`(aiLock.tryLock(0, -1, TimeUnit.MILLISECONDS)).thenReturn(false)
|
||||
Mockito.`when`(aiLock.isHeldByCurrentThread).thenReturn(false)
|
||||
Mockito.`when`(cheerLock.tryLock(0, -1, TimeUnit.MILLISECONDS)).thenReturn(false)
|
||||
Mockito.`when`(cheerLock.isHeldByCurrentThread).thenReturn(false)
|
||||
Mockito.`when`(queryPort.findPopularCommunitySnapshots(windowStart, snapshotAt, 20)).thenReturn(
|
||||
Mockito.`when`(popularLock.tryLock(0, -1, TimeUnit.MILLISECONDS)).thenReturn(true)
|
||||
Mockito.`when`(popularLock.isHeldByCurrentThread).thenReturn(true)
|
||||
Mockito.`when`(queryPort.findPopularCommunitySnapshots(windowStart, windowEndExclusive, 20)).thenReturn(
|
||||
listOf(snapshot(RecommendedSectionType.POPULAR_COMMUNITY, targetId = 20L, score = 20.0, snapshotAt = snapshotAt))
|
||||
)
|
||||
|
||||
@@ -311,6 +332,7 @@ class RecommendationSnapshotRefreshServiceTest {
|
||||
)
|
||||
assertEquals(listOf(20L), snapshotPort.findLatestSnapshots(RecommendedSectionType.POPULAR_COMMUNITY).map { it.targetId })
|
||||
Mockito.verify(cheerLock, Mockito.never()).unlock()
|
||||
Mockito.verify(popularLock).unlock()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -321,16 +343,22 @@ class RecommendationSnapshotRefreshServiceTest {
|
||||
val redissonClient = Mockito.mock(RedissonClient::class.java)
|
||||
val lock = Mockito.mock(RLock::class.java)
|
||||
val cheerLock = Mockito.mock(RLock::class.java)
|
||||
val popularLock = Mockito.mock(RLock::class.java)
|
||||
val service = service(snapshotPort = snapshotPort, queryPort = queryPort, redissonClient = redissonClient)
|
||||
val now = LocalDateTime.of(2026, 5, 29, 15, 0, 0)
|
||||
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
|
||||
val windowStart = LocalDateTime.of(2026, 5, 23, 0, 0, 0)
|
||||
val snapshotAt = LocalDateTime.of(2026, 5, 29, 14, 59, 59)
|
||||
val windowStart = LocalDateTime.of(2026, 5, 22, 15, 0, 0)
|
||||
val windowEndExclusive = LocalDateTime.of(2026, 5, 29, 15, 0, 0)
|
||||
Mockito.`when`(redissonClient.getLock(RecommendationSnapshotFallbackService.AI_CHARACTER_LOCK_KEY)).thenReturn(lock)
|
||||
Mockito.`when`(redissonClient.getLock(RecommendationSnapshotFallbackService.CHEER_CREATOR_LOCK_KEY)).thenReturn(cheerLock)
|
||||
Mockito.`when`(redissonClient.getLock(RecommendationSnapshotFallbackService.POPULAR_COMMUNITY_LOCK_KEY))
|
||||
.thenReturn(popularLock)
|
||||
Mockito.`when`(lock.tryLock(0, -1, TimeUnit.MILLISECONDS)).thenReturn(false)
|
||||
Mockito.`when`(lock.isHeldByCurrentThread).thenReturn(false)
|
||||
Mockito.`when`(cheerLock.tryLock(0, -1, TimeUnit.MILLISECONDS)).thenReturn(true)
|
||||
Mockito.`when`(cheerLock.isHeldByCurrentThread).thenReturn(true)
|
||||
Mockito.`when`(popularLock.tryLock(0, -1, TimeUnit.MILLISECONDS)).thenReturn(true)
|
||||
Mockito.`when`(popularLock.isHeldByCurrentThread).thenReturn(true)
|
||||
Mockito.`when`(
|
||||
queryPort.findCheerCreatorSnapshots(
|
||||
LocalDateTime.of(2026, 5, 28, 15, 0, 0),
|
||||
@@ -340,7 +368,7 @@ class RecommendationSnapshotRefreshServiceTest {
|
||||
).thenReturn(
|
||||
listOf(snapshot(RecommendedSectionType.CHEER_CREATOR, targetId = 1L, score = 1.0, snapshotAt = snapshotAt))
|
||||
)
|
||||
Mockito.`when`(queryPort.findPopularCommunitySnapshots(windowStart, snapshotAt, 20)).thenReturn(emptyList())
|
||||
Mockito.`when`(queryPort.findPopularCommunitySnapshots(windowStart, windowEndExclusive, 20)).thenReturn(emptyList())
|
||||
|
||||
service.refreshDailySnapshots(now)
|
||||
|
||||
@@ -356,6 +384,7 @@ class RecommendationSnapshotRefreshServiceTest {
|
||||
assertEquals(listOf(1L), snapshotPort.findLatestSnapshots(RecommendedSectionType.CHEER_CREATOR).map { it.targetId })
|
||||
Mockito.verify(lock, Mockito.never()).unlock()
|
||||
Mockito.verify(cheerLock).unlock()
|
||||
Mockito.verify(popularLock).unlock()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user