test(recommendation): 응원 크리에이터 동시성 테스트를 안정화한다
This commit is contained in:
@@ -14,8 +14,8 @@ import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.Future
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class RecommendationSnapshotFallbackServiceTest {
|
||||
@Test
|
||||
@@ -265,10 +265,18 @@ class RecommendationSnapshotFallbackServiceTest {
|
||||
homeWaitMillis = 50
|
||||
)
|
||||
|
||||
val first = service.refreshCheerCreatorIfMissing(offset = 0, limit = 16, nowUtc = LocalDateTime.of(2026, 7, 9, 21, 0))
|
||||
assertEquals(true, refreshStarted.await(1, TimeUnit.SECONDS))
|
||||
allowRefreshComplete.countDown()
|
||||
executor.shutdown()
|
||||
val first = try {
|
||||
val result = service.refreshCheerCreatorIfMissing(
|
||||
offset = 0,
|
||||
limit = 16,
|
||||
nowUtc = LocalDateTime.of(2026, 7, 9, 21, 0)
|
||||
)
|
||||
assertEquals(true, refreshStarted.await(1, TimeUnit.SECONDS))
|
||||
result
|
||||
} finally {
|
||||
allowRefreshComplete.countDown()
|
||||
executor.shutdown()
|
||||
}
|
||||
assertEquals(true, executor.awaitTermination(1, TimeUnit.SECONDS))
|
||||
val second = service.refreshCheerCreatorIfMissing(
|
||||
offset = 0,
|
||||
@@ -283,46 +291,43 @@ class RecommendationSnapshotFallbackServiceTest {
|
||||
@Test
|
||||
@DisplayName("응원 크리에이터 fallback 동시 요청은 하나의 refresh future를 공유한다")
|
||||
fun shouldShareSingleCheerCreatorRefreshFutureForConcurrentRequests() {
|
||||
val existsSnapshotEntered = CountDownLatch(2)
|
||||
val existsSnapshotReturned = CountDownLatch(2)
|
||||
val snapshotPort = FakeRecommendationFallbackSnapshotPort(
|
||||
existsSnapshotEntered = existsSnapshotEntered,
|
||||
existsSnapshotReturned = existsSnapshotReturned
|
||||
)
|
||||
val refreshService = FastCheerRefreshService(snapshotPort)
|
||||
val snapshotPort = FakeRecommendationFallbackSnapshotPort()
|
||||
val refreshStarted = CountDownLatch(1)
|
||||
val allowRefreshComplete = CountDownLatch(1)
|
||||
val refreshService = BlockingCheerRefreshService(snapshotPort, refreshStarted, allowRefreshComplete)
|
||||
val redissonClient = Mockito.mock(RedissonClient::class.java)
|
||||
val lock = Mockito.mock(RLock::class.java)
|
||||
Mockito.`when`(redissonClient.getLock(RecommendationSnapshotFallbackService.CHEER_CREATOR_LOCK_KEY)).thenReturn(lock)
|
||||
Mockito.`when`(lock.tryLock(300, -1, TimeUnit.MILLISECONDS)).thenReturn(true)
|
||||
Mockito.`when`(lock.isHeldByCurrentThread).thenReturn(true)
|
||||
val workerExecutor = CapturingExecutor()
|
||||
val workerTaskCount = AtomicInteger()
|
||||
val workerPool = Executors.newFixedThreadPool(2)
|
||||
val workerExecutor = Executor { command ->
|
||||
workerTaskCount.incrementAndGet()
|
||||
workerPool.execute(command)
|
||||
}
|
||||
val requestExecutor = Executors.newFixedThreadPool(2)
|
||||
val service = RecommendationSnapshotFallbackService(
|
||||
snapshotPort,
|
||||
refreshService,
|
||||
redissonClient,
|
||||
workerExecutor,
|
||||
homeWaitMillis = 1_000
|
||||
homeWaitMillis = 50
|
||||
)
|
||||
val nowUtc = LocalDateTime.of(2026, 7, 9, 21, 0)
|
||||
|
||||
try {
|
||||
val first = requestExecutor.submitCheerRefresh(service, nowUtc)
|
||||
val second = requestExecutor.submitCheerRefresh(service, nowUtc)
|
||||
assertEquals(true, existsSnapshotEntered.await(1, TimeUnit.SECONDS))
|
||||
assertEquals(true, existsSnapshotReturned.await(1, TimeUnit.SECONDS))
|
||||
assertEquals(true, workerExecutor.taskSubmitted.await(1, TimeUnit.SECONDS))
|
||||
assertEquals(false, first.isDone)
|
||||
assertEquals(false, second.isDone)
|
||||
assertEquals(1, workerExecutor.taskCount)
|
||||
|
||||
workerExecutor.runNext()
|
||||
|
||||
assertEquals(listOf(99L), first.get(1, TimeUnit.SECONDS).map { it.targetId })
|
||||
assertEquals(listOf(99L), second.get(1, TimeUnit.SECONDS).map { it.targetId })
|
||||
assertEquals(true, refreshStarted.await(5, TimeUnit.SECONDS))
|
||||
assertEquals(emptyList<RecommendationSnapshotRecord>(), first.get(5, TimeUnit.SECONDS))
|
||||
assertEquals(emptyList<RecommendationSnapshotRecord>(), second.get(5, TimeUnit.SECONDS))
|
||||
assertEquals(1, workerTaskCount.get())
|
||||
assertEquals(1, refreshService.cheerRefreshCount)
|
||||
} finally {
|
||||
allowRefreshComplete.countDown()
|
||||
requestExecutor.shutdownNow()
|
||||
workerPool.shutdownNow()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,23 +413,6 @@ class RecommendationSnapshotFallbackServiceTest {
|
||||
}
|
||||
}
|
||||
|
||||
private class CapturingExecutor : Executor {
|
||||
val taskSubmitted = CountDownLatch(1)
|
||||
private val tasks = LinkedBlockingQueue<Runnable>()
|
||||
|
||||
val taskCount: Int
|
||||
get() = tasks.size
|
||||
|
||||
override fun execute(command: Runnable) {
|
||||
tasks.add(command)
|
||||
taskSubmitted.countDown()
|
||||
}
|
||||
|
||||
fun runNext() {
|
||||
tasks.poll(1, TimeUnit.SECONDS)!!.run()
|
||||
}
|
||||
}
|
||||
|
||||
private class BlockingCheerRefreshService(
|
||||
private val snapshotPort: RecommendationSnapshotPort,
|
||||
private val refreshStarted: CountDownLatch,
|
||||
@@ -438,26 +426,7 @@ private class BlockingCheerRefreshService(
|
||||
override fun refreshCheerCreatorSnapshots(nowUtc: LocalDateTime): Int {
|
||||
cheerRefreshCount += 1
|
||||
refreshStarted.countDown()
|
||||
allowRefreshComplete.await(1, TimeUnit.SECONDS)
|
||||
snapshotPort.replaceSnapshots(
|
||||
RecommendedSectionType.CHEER_CREATOR,
|
||||
LocalDateTime.of(2026, 7, 9, 14, 59, 59),
|
||||
listOf(snapshot(RecommendedSectionType.CHEER_CREATOR, 99L))
|
||||
)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
private class FastCheerRefreshService(
|
||||
private val snapshotPort: RecommendationSnapshotPort
|
||||
) : RecommendationSnapshotRefreshService(
|
||||
snapshotPort,
|
||||
Mockito.mock(kr.co.vividnext.sodalive.v2.recommendation.port.out.HomeRecommendationQueryPort::class.java)
|
||||
) {
|
||||
var cheerRefreshCount: Int = 0
|
||||
|
||||
override fun refreshCheerCreatorSnapshots(nowUtc: LocalDateTime): Int {
|
||||
cheerRefreshCount += 1
|
||||
allowRefreshComplete.await()
|
||||
snapshotPort.replaceSnapshots(
|
||||
RecommendedSectionType.CHEER_CREATOR,
|
||||
LocalDateTime.of(2026, 7, 9, 14, 59, 59),
|
||||
@@ -496,10 +465,7 @@ private class BlockingAiAndFastCheerRefreshService(
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeRecommendationFallbackSnapshotPort(
|
||||
private val existsSnapshotEntered: CountDownLatch? = null,
|
||||
private val existsSnapshotReturned: CountDownLatch? = null
|
||||
) : RecommendationSnapshotPort {
|
||||
private class FakeRecommendationFallbackSnapshotPort : RecommendationSnapshotPort {
|
||||
private val snapshots = mutableListOf<RecommendationSnapshotRecord>()
|
||||
|
||||
override fun findLatestSnapshots(
|
||||
@@ -531,12 +497,7 @@ private class FakeRecommendationFallbackSnapshotPort(
|
||||
}
|
||||
|
||||
override fun existsSnapshot(sectionType: RecommendedSectionType, snapshotAt: LocalDateTime): Boolean {
|
||||
existsSnapshotEntered?.countDown()
|
||||
existsSnapshotEntered?.await(1, TimeUnit.SECONDS)
|
||||
val exists = snapshots.any { it.sectionType == sectionType && it.snapshotAt == snapshotAt }
|
||||
existsSnapshotReturned?.countDown()
|
||||
existsSnapshotReturned?.await(1, TimeUnit.SECONDS)
|
||||
return exists
|
||||
return snapshots.any { it.sectionType == sectionType && it.snapshotAt == snapshotAt }
|
||||
}
|
||||
|
||||
override fun replaceSnapshots(
|
||||
|
||||
Reference in New Issue
Block a user