test #426
@@ -1,5 +1,6 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.ranking.application
|
package kr.co.vividnext.sodalive.v2.ranking.application
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishService
|
||||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingPeriodPolicy
|
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingPeriodPolicy
|
||||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingScorePolicy
|
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingScorePolicy
|
||||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingSnapshotCandidate
|
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingSnapshotCandidate
|
||||||
@@ -19,7 +20,8 @@ import java.time.ZonedDateTime
|
|||||||
@Service
|
@Service
|
||||||
class CreatorRankingSnapshotRefreshService(
|
class CreatorRankingSnapshotRefreshService(
|
||||||
private val aggregationPort: CreatorRankingAggregationPort,
|
private val aggregationPort: CreatorRankingAggregationPort,
|
||||||
private val snapshotPort: CreatorRankingSnapshotPort
|
private val snapshotPort: CreatorRankingSnapshotPort,
|
||||||
|
private val homeFollowingNewsPublishService: HomeFollowingNewsPublishService
|
||||||
) {
|
) {
|
||||||
private val log = LoggerFactory.getLogger(javaClass)
|
private val log = LoggerFactory.getLogger(javaClass)
|
||||||
private val periodPolicy = CreatorRankingPeriodPolicy()
|
private val periodPolicy = CreatorRankingPeriodPolicy()
|
||||||
@@ -47,6 +49,28 @@ class CreatorRankingSnapshotRefreshService(
|
|||||||
visibleFromAtUtc = visibleFromAtUtc,
|
visibleFromAtUtc = visibleFromAtUtc,
|
||||||
newSnapshots = snapshots
|
newSnapshots = snapshots
|
||||||
)
|
)
|
||||||
|
afterCommit {
|
||||||
|
snapshots.forEachIndexed { index, snapshot ->
|
||||||
|
runCatching {
|
||||||
|
homeFollowingNewsPublishService.publishCreatorRankingVisible(
|
||||||
|
creatorId = snapshot.creatorId,
|
||||||
|
creatorNickname = snapshot.nickname,
|
||||||
|
creatorProfileImagePath = snapshot.profileImageUrl,
|
||||||
|
aggregationStartAtUtc = utcRange.startInclusiveUtc,
|
||||||
|
visibleFromAtUtc = visibleFromAtUtc,
|
||||||
|
rank = index + 1
|
||||||
|
)
|
||||||
|
}.onFailure { ex ->
|
||||||
|
log.warn(
|
||||||
|
"event=home_following_creator_ranking_news_publish_failure creatorId={} rank={} error={}",
|
||||||
|
snapshot.creatorId,
|
||||||
|
index + 1,
|
||||||
|
ex.message,
|
||||||
|
ex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
aggregationResult.toLogCounts(storedCount = snapshots.size)
|
aggregationResult.toLogCounts(storedCount = snapshots.size)
|
||||||
}.onSuccess { counts ->
|
}.onSuccess { counts ->
|
||||||
afterCommit {
|
afterCommit {
|
||||||
@@ -92,12 +116,18 @@ class CreatorRankingSnapshotRefreshService(
|
|||||||
|
|
||||||
private fun afterCommit(action: () -> Unit) {
|
private fun afterCommit(action: () -> Unit) {
|
||||||
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
|
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||||
action()
|
runCatching(action).onFailure { ex ->
|
||||||
|
log.warn("event=creator_ranking_after_commit_failure error={}", ex.message, ex)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
TransactionSynchronizationManager.registerSynchronization(
|
TransactionSynchronizationManager.registerSynchronization(
|
||||||
object : TransactionSynchronization {
|
object : TransactionSynchronization {
|
||||||
override fun afterCommit() = action()
|
override fun afterCommit() {
|
||||||
|
runCatching(action).onFailure { ex ->
|
||||||
|
log.warn("event=creator_ranking_after_commit_failure error={}", ex.message, ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.ranking.application
|
package kr.co.vividnext.sodalive.v2.ranking.application
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishService
|
||||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingSnapshotCandidate
|
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingSnapshotCandidate
|
||||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingType
|
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingType
|
||||||
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingAggregationPort
|
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingAggregationPort
|
||||||
@@ -11,6 +12,7 @@ import org.junit.jupiter.api.Assertions.assertThrows
|
|||||||
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.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
|
import org.mockito.Mockito
|
||||||
import org.springframework.boot.test.system.CapturedOutput
|
import org.springframework.boot.test.system.CapturedOutput
|
||||||
import org.springframework.boot.test.system.OutputCaptureExtension
|
import org.springframework.boot.test.system.OutputCaptureExtension
|
||||||
import org.springframework.transaction.support.TransactionSynchronizationManager
|
import org.springframework.transaction.support.TransactionSynchronizationManager
|
||||||
@@ -165,16 +167,125 @@ class CreatorRankingSnapshotRefreshServiceTest {
|
|||||||
assertEquals(true, output.out.contains("error=aggregate failed"))
|
assertEquals(true, output.out.contains("error=aggregate failed"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("주간 스냅샷 저장 성공 후 크리에이터 랭킹 최근 소식을 순위와 함께 발행한다")
|
||||||
|
fun shouldPublishCreatorRankingNewsAfterSnapshotsAreReplaced() {
|
||||||
|
val aggregationPort = FakeCreatorRankingAggregationPort()
|
||||||
|
val snapshotPort = FakeCreatorRankingSnapshotPort()
|
||||||
|
val publishService = Mockito.mock(HomeFollowingNewsPublishService::class.java)
|
||||||
|
val service = service(
|
||||||
|
aggregationPort = aggregationPort,
|
||||||
|
snapshotPort = snapshotPort,
|
||||||
|
publishService = publishService
|
||||||
|
)
|
||||||
|
aggregationPort.candidates = listOf(
|
||||||
|
candidate(creatorId = 1L, liveCanAmount = 200),
|
||||||
|
candidate(creatorId = 2L, liveCanAmount = 100)
|
||||||
|
)
|
||||||
|
|
||||||
|
service.refreshLastCompletedWeek(ZonedDateTime.of(2026, 6, 8, 6, 0, 0, 0, ZoneId.of("Asia/Seoul")))
|
||||||
|
|
||||||
|
Mockito.verify(publishService).publishCreatorRankingVisible(
|
||||||
|
creatorId = 1L,
|
||||||
|
creatorNickname = "creator-1",
|
||||||
|
creatorProfileImagePath = "profile-1.png",
|
||||||
|
aggregationStartAtUtc = LocalDateTime.of(2026, 5, 31, 15, 0),
|
||||||
|
visibleFromAtUtc = LocalDateTime.of(2026, 6, 8, 0, 0),
|
||||||
|
rank = 1
|
||||||
|
)
|
||||||
|
Mockito.verify(publishService).publishCreatorRankingVisible(
|
||||||
|
creatorId = 2L,
|
||||||
|
creatorNickname = "creator-2",
|
||||||
|
creatorProfileImagePath = "profile-2.png",
|
||||||
|
aggregationStartAtUtc = LocalDateTime.of(2026, 5, 31, 15, 0),
|
||||||
|
visibleFromAtUtc = LocalDateTime.of(2026, 6, 8, 0, 0),
|
||||||
|
rank = 2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("주간 스냅샷 저장 실패 시 크리에이터 랭킹 최근 소식을 발행하지 않는다")
|
||||||
|
fun shouldNotPublishCreatorRankingNewsWhenReplaceSnapshotsFails() {
|
||||||
|
val aggregationPort = FakeCreatorRankingAggregationPort()
|
||||||
|
val snapshotPort = FakeCreatorRankingSnapshotPort()
|
||||||
|
val publishService = Mockito.mock(HomeFollowingNewsPublishService::class.java)
|
||||||
|
val service = service(
|
||||||
|
aggregationPort = aggregationPort,
|
||||||
|
snapshotPort = snapshotPort,
|
||||||
|
publishService = publishService
|
||||||
|
)
|
||||||
|
aggregationPort.candidates = listOf(candidate(creatorId = 1L, liveCanAmount = 100))
|
||||||
|
snapshotPort.failure = IllegalStateException("replace failed")
|
||||||
|
|
||||||
|
assertThrows(IllegalStateException::class.java) {
|
||||||
|
service.refreshLastCompletedWeek(ZonedDateTime.of(2026, 6, 8, 6, 0, 0, 0, ZoneId.of("Asia/Seoul")))
|
||||||
|
}
|
||||||
|
|
||||||
|
Mockito.verifyNoInteractions(publishService)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("일부 크리에이터 랭킹 최근 소식 발행 실패는 스냅샷 갱신을 실패시키지 않는다")
|
||||||
|
fun shouldNotFailSnapshotRefreshWhenCreatorRankingNewsPublishFails() {
|
||||||
|
val aggregationPort = FakeCreatorRankingAggregationPort()
|
||||||
|
val snapshotPort = FakeCreatorRankingSnapshotPort()
|
||||||
|
val publishService = Mockito.mock(HomeFollowingNewsPublishService::class.java)
|
||||||
|
val service = service(
|
||||||
|
aggregationPort = aggregationPort,
|
||||||
|
snapshotPort = snapshotPort,
|
||||||
|
publishService = publishService
|
||||||
|
)
|
||||||
|
aggregationPort.candidates = listOf(
|
||||||
|
candidate(creatorId = 1L, liveCanAmount = 200),
|
||||||
|
candidate(creatorId = 2L, liveCanAmount = 100)
|
||||||
|
)
|
||||||
|
Mockito.doAnswer { invocation ->
|
||||||
|
if (invocation.getArgument<Long>(0) == 1L) {
|
||||||
|
throw IllegalStateException("publish failed")
|
||||||
|
}
|
||||||
|
0
|
||||||
|
}.`when`(publishService)
|
||||||
|
.publishCreatorRankingVisible(
|
||||||
|
creatorId = Mockito.anyLong(),
|
||||||
|
creatorNickname = anyStringValue(),
|
||||||
|
creatorProfileImagePath = Mockito.anyString(),
|
||||||
|
aggregationStartAtUtc = anyLocalDateTime(),
|
||||||
|
visibleFromAtUtc = anyLocalDateTime(),
|
||||||
|
rank = Mockito.anyInt()
|
||||||
|
)
|
||||||
|
|
||||||
|
service.refreshLastCompletedWeek(ZonedDateTime.of(2026, 6, 8, 6, 0, 0, 0, ZoneId.of("Asia/Seoul")))
|
||||||
|
|
||||||
|
Mockito.verify(publishService).publishCreatorRankingVisible(
|
||||||
|
creatorId = 2L,
|
||||||
|
creatorNickname = "creator-2",
|
||||||
|
creatorProfileImagePath = "profile-2.png",
|
||||||
|
aggregationStartAtUtc = LocalDateTime.of(2026, 5, 31, 15, 0),
|
||||||
|
visibleFromAtUtc = LocalDateTime.of(2026, 6, 8, 0, 0),
|
||||||
|
rank = 2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun service(
|
private fun service(
|
||||||
aggregationPort: CreatorRankingAggregationPort = FakeCreatorRankingAggregationPort(),
|
aggregationPort: CreatorRankingAggregationPort = FakeCreatorRankingAggregationPort(),
|
||||||
snapshotPort: CreatorRankingSnapshotPort = FakeCreatorRankingSnapshotPort()
|
snapshotPort: CreatorRankingSnapshotPort = FakeCreatorRankingSnapshotPort(),
|
||||||
|
publishService: HomeFollowingNewsPublishService = Mockito.mock(HomeFollowingNewsPublishService::class.java)
|
||||||
): CreatorRankingSnapshotRefreshService {
|
): CreatorRankingSnapshotRefreshService {
|
||||||
return CreatorRankingSnapshotRefreshService(
|
return CreatorRankingSnapshotRefreshService(
|
||||||
aggregationPort = aggregationPort,
|
aggregationPort = aggregationPort,
|
||||||
snapshotPort = snapshotPort
|
snapshotPort = snapshotPort,
|
||||||
|
homeFollowingNewsPublishService = publishService
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun anyStringValue(): String {
|
||||||
|
return Mockito.anyString() ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun anyLocalDateTime(): LocalDateTime {
|
||||||
|
return Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.MIN
|
||||||
|
}
|
||||||
|
|
||||||
private fun candidate(
|
private fun candidate(
|
||||||
creatorId: Long,
|
creatorId: Long,
|
||||||
finalScore: Double = 0.0,
|
finalScore: Double = 0.0,
|
||||||
@@ -247,6 +358,7 @@ private class FakeCreatorRankingSnapshotPort : CreatorRankingSnapshotPort {
|
|||||||
var aggregationStartAtUtc: LocalDateTime? = null
|
var aggregationStartAtUtc: LocalDateTime? = null
|
||||||
var aggregationEndAtUtc: LocalDateTime? = null
|
var aggregationEndAtUtc: LocalDateTime? = null
|
||||||
var visibleFromAtUtc: LocalDateTime? = null
|
var visibleFromAtUtc: LocalDateTime? = null
|
||||||
|
var failure: RuntimeException? = null
|
||||||
|
|
||||||
override fun findSnapshotsByAggregationPeriod(
|
override fun findSnapshotsByAggregationPeriod(
|
||||||
aggregationStartAtUtc: LocalDateTime,
|
aggregationStartAtUtc: LocalDateTime,
|
||||||
@@ -281,6 +393,7 @@ private class FakeCreatorRankingSnapshotPort : CreatorRankingSnapshotPort {
|
|||||||
visibleFromAtUtc: LocalDateTime,
|
visibleFromAtUtc: LocalDateTime,
|
||||||
newSnapshots: List<CreatorRankingSnapshotRecord>
|
newSnapshots: List<CreatorRankingSnapshotRecord>
|
||||||
) {
|
) {
|
||||||
|
failure?.let { throw it }
|
||||||
this.rankingType = rankingType
|
this.rankingType = rankingType
|
||||||
this.aggregationStartAtUtc = aggregationStartAtUtc
|
this.aggregationStartAtUtc = aggregationStartAtUtc
|
||||||
this.aggregationEndAtUtc = aggregationEndAtUtc
|
this.aggregationEndAtUtc = aggregationEndAtUtc
|
||||||
|
|||||||
Reference in New Issue
Block a user