feat(ranking): cold-start 스냅샷 생성을 위임한다

This commit is contained in:
2026-06-09 16:10:40 +09:00
parent e147847a2d
commit 597b7f26b9
4 changed files with 268 additions and 14 deletions

View File

@@ -20,6 +20,7 @@ class CreatorRankingQueryService(
private val snapshotPort: CreatorRankingSnapshotPort,
private val blockPort: CreatorRankingBlockPort,
private val aggregationPort: CreatorRankingAggregationPort,
private val snapshotJobService: CreatorRankingSnapshotJobService,
private val nowProvider: () -> ZonedDateTime = { ZonedDateTime.now() },
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
@@ -36,6 +37,9 @@ class CreatorRankingQueryService(
if (latestItems.isEmpty()) {
if (snapshotPort.isSnapshotTableEmpty()) {
val fallbackItems = aggregateColdStartFallback().toRankedItems()
if (fallbackItems.isNotEmpty()) {
delegateColdStartSnapshotRefresh()
}
val blockedCreatorIds = findBlockedCreatorIds(viewerMemberId = viewerMemberId, items = fallbackItems)
return@runCatching QueryLogResult(
result = CreatorRankingResult(
@@ -127,6 +131,18 @@ class CreatorRankingQueryService(
}.getOrThrow()
}
private fun delegateColdStartSnapshotRefresh() {
runCatching {
snapshotJobService.ensureLastCompletedWeekSnapshotForColdStart()
}.onFailure { ex ->
log.warn(
"event=creator_ranking_query_cold_start_snapshot_refresh_failure error={}",
ex.message,
ex
)
}
}
private fun List<CreatorRankingSnapshotRecord>.toRankedItems(): List<CreatorRankingItem> {
return groupBy { it.finalScore }
.toSortedMap(compareByDescending { it })

View File

@@ -1,31 +1,49 @@
package kr.co.vividnext.sodalive.v2.ranking.application
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingPeriodPolicy
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingUtcRange
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotJobPort
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotJobRecord
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotJobStatus
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotJobTrigger
import org.redisson.api.RedissonClient
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.TransactionDefinition
import org.springframework.transaction.annotation.Transactional
import org.springframework.transaction.support.TransactionTemplate
import java.time.LocalDateTime
import java.time.ZonedDateTime
import java.util.concurrent.TimeUnit
@Service
@Transactional(readOnly = true)
class CreatorRankingSnapshotJobService(
private val refreshService: CreatorRankingSnapshotRefreshService,
private val jobPort: CreatorRankingSnapshotJobPort,
private val redissonClient: RedissonClient,
transactionManager: PlatformTransactionManager,
private val nowProvider: () -> ZonedDateTime = { ZonedDateTime.now() }
) {
private val log = LoggerFactory.getLogger(javaClass)
private val periodPolicy = CreatorRankingPeriodPolicy()
private val transactionTemplate = TransactionTemplate(transactionManager).also { template ->
template.propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRES_NEW
}
@Transactional
fun refreshLastCompletedWeekByScheduledJob() {
val now = nowProvider()
val period = periodPolicy.resolveLastCompletedWeek(now)
val utcRange = periodPolicy.toUtcRange(period)
withLastCompletedWeekPeriodLock { now, utcRange ->
transactionTemplate.executeWithoutResult {
refreshLastCompletedWeekByScheduledJob(now, utcRange)
}
}
}
private fun refreshLastCompletedWeekByScheduledJob(
now: ZonedDateTime,
utcRange: CreatorRankingUtcRange
) {
val job = jobPort.save(
CreatorRankingSnapshotJobRecord(
aggregationStartAtUtc = utcRange.startInclusiveUtc,
@@ -89,6 +107,32 @@ class CreatorRankingSnapshotJobService(
jobPort.markPending(jobId)
}
fun ensureLastCompletedWeekSnapshotForColdStart() {
withLastCompletedWeekPeriodLock { now, _ ->
transactionTemplate.executeWithoutResult {
refreshService.refreshLastCompletedWeek(now)
}
}
}
private fun withLastCompletedWeekPeriodLock(action: (ZonedDateTime, CreatorRankingUtcRange) -> Unit) {
val now = nowProvider()
val period = periodPolicy.resolveLastCompletedWeek(now)
val utcRange = periodPolicy.toUtcRange(period)
val lockName = "lock:creator-ranking-snapshot-refresh:${utcRange.startInclusiveUtc}:${utcRange.endExclusiveUtc}"
val lock = redissonClient.getLock(lockName)
try {
if (lock.tryLock(0, -1, TimeUnit.SECONDS)) {
action(now, utcRange)
}
} finally {
if (lock.isHeldByCurrentThread) {
lock.unlock()
}
}
}
private fun logJobStatusChanged(
job: CreatorRankingSnapshotJobRecord,
status: CreatorRankingSnapshotJobStatus,