feat(ranking): 스냅샷 갱신 관측 로그를 추가한다

This commit is contained in:
2026-06-09 00:09:09 +09:00
parent c032d7750a
commit 5f08165239
4 changed files with 206 additions and 14 deletions

View File

@@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence
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.port.out.CreatorRankingAggregationPort
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingAggregationResult
import org.springframework.stereotype.Repository
import java.time.LocalDateTime
import javax.persistence.EntityManager
@@ -16,16 +17,35 @@ class DefaultCreatorRankingAggregationRepository(
override fun aggregateCandidates(
startInclusiveUtc: LocalDateTime,
endExclusiveUtc: LocalDateTime
): List<CreatorRankingSnapshotCandidate> {
return aggregateCandidateResult(startInclusiveUtc, endExclusiveUtc).candidates
}
override fun aggregateCandidateResult(
startInclusiveUtc: LocalDateTime,
endExclusiveUtc: LocalDateTime
): CreatorRankingAggregationResult {
val candidates = aggregateAllCandidates(startInclusiveUtc, endExclusiveUtc)
val includedCandidates = candidates
.filter { candidate -> candidate.finalScore >= MINIMUM_FINAL_SCORE }
.sortedWith(compareByDescending<CreatorRankingSnapshotCandidate> { it.finalScore }.thenBy { it.creatorId })
return CreatorRankingAggregationResult(
candidates = includedCandidates,
lowScoreExcludedCount = candidates.size - includedCandidates.size
)
}
private fun aggregateAllCandidates(
startInclusiveUtc: LocalDateTime,
endExclusiveUtc: LocalDateTime
): List<CreatorRankingSnapshotCandidate> {
val rows = entityManager.createNativeQuery(AGGREGATION_SQL)
.setParameter("startInclusiveUtc", startInclusiveUtc)
.setParameter("endExclusiveUtc", endExclusiveUtc)
.resultList
return rows
.map { row -> (row as Array<*>).toCandidate() }
.filter { candidate -> candidate.finalScore >= MINIMUM_FINAL_SCORE }
.sortedWith(compareByDescending<CreatorRankingSnapshotCandidate> { it.finalScore }.thenBy { it.creatorId })
return rows.map { row -> (row as Array<*>).toCandidate() }
}
private fun Array<*>.toCandidate(): CreatorRankingSnapshotCandidate {

View File

@@ -5,10 +5,14 @@ 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.CreatorRankingUtcRange
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingAggregationPort
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingAggregationResult
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotPort
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotRecord
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.springframework.transaction.support.TransactionSynchronization
import org.springframework.transaction.support.TransactionSynchronizationManager
import java.time.ZonedDateTime
@Service
@@ -16,6 +20,7 @@ class CreatorRankingSnapshotRefreshService(
private val aggregationPort: CreatorRankingAggregationPort,
private val snapshotPort: CreatorRankingSnapshotPort
) {
private val log = LoggerFactory.getLogger(javaClass)
private val periodPolicy = CreatorRankingPeriodPolicy()
private val scorePolicy = CreatorRankingScorePolicy()
@@ -26,19 +31,75 @@ class CreatorRankingSnapshotRefreshService(
@Transactional
fun refreshLastCompletedWeek(now: ZonedDateTime) {
val startedAt = System.currentTimeMillis()
val period = periodPolicy.resolveLastCompletedWeek(now)
val utcRange = periodPolicy.toUtcRange(period)
val snapshots = aggregationPort.aggregateCandidates(
startInclusiveUtc = utcRange.startInclusiveUtc,
endExclusiveUtc = utcRange.endExclusiveUtc
).map { it.toSnapshotRecord(utcRange) }
.sortedByDescending { it.finalScore }
.takeRankedBoundary(limit = SNAPSHOT_LIMIT)
runCatching {
val aggregationResult = aggregationPort.aggregateCandidateResult(
startInclusiveUtc = utcRange.startInclusiveUtc,
endExclusiveUtc = utcRange.endExclusiveUtc
)
val snapshots = aggregationResult.candidates.map { it.toSnapshotRecord(utcRange) }
.sortedByDescending { it.finalScore }
.takeRankedBoundary(limit = SNAPSHOT_LIMIT)
snapshotPort.replaceSnapshots(
aggregationStartAtUtc = utcRange.startInclusiveUtc,
aggregationEndAtUtc = utcRange.endExclusiveUtc,
newSnapshots = snapshots
snapshotPort.replaceSnapshots(
aggregationStartAtUtc = utcRange.startInclusiveUtc,
aggregationEndAtUtc = utcRange.endExclusiveUtc,
newSnapshots = snapshots
)
aggregationResult.toLogCounts(storedCount = snapshots.size)
}.onSuccess { counts ->
afterCommit {
log.info(
"event=creator_ranking_snapshot_refresh_success " +
"aggregationStartAtUtc={} aggregationEndAtUtc={} " +
"candidateCount={} storedCount={} lowScoreExcludedCount={} elapsedMs={}",
utcRange.startInclusiveUtc,
utcRange.endExclusiveUtc,
counts.candidateCount,
counts.storedCount,
counts.lowScoreExcludedCount,
System.currentTimeMillis() - startedAt
)
}
}.onFailure { ex ->
log.warn(
"event=creator_ranking_snapshot_refresh_failure " +
"aggregationStartAtUtc={} aggregationEndAtUtc={} elapsedMs={} error={}",
utcRange.startInclusiveUtc,
utcRange.endExclusiveUtc,
System.currentTimeMillis() - startedAt,
ex.message,
ex
)
throw ex
}
}
private fun CreatorRankingAggregationResult.toLogCounts(storedCount: Int): RefreshLogCounts {
return RefreshLogCounts(
candidateCount = candidates.size,
storedCount = storedCount,
lowScoreExcludedCount = lowScoreExcludedCount
)
}
private data class RefreshLogCounts(
val candidateCount: Int,
val storedCount: Int,
val lowScoreExcludedCount: Int
)
private fun afterCommit(action: () -> Unit) {
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
action()
return
}
TransactionSynchronizationManager.registerSynchronization(
object : TransactionSynchronization {
override fun afterCommit() = action()
}
)
}

View File

@@ -8,4 +8,19 @@ interface CreatorRankingAggregationPort {
startInclusiveUtc: LocalDateTime,
endExclusiveUtc: LocalDateTime
): List<CreatorRankingSnapshotCandidate>
fun aggregateCandidateResult(
startInclusiveUtc: LocalDateTime,
endExclusiveUtc: LocalDateTime
): CreatorRankingAggregationResult {
return CreatorRankingAggregationResult(
candidates = aggregateCandidates(startInclusiveUtc, endExclusiveUtc),
lowScoreExcludedCount = 0
)
}
}
data class CreatorRankingAggregationResult(
val candidates: List<CreatorRankingSnapshotCandidate>,
val lowScoreExcludedCount: Int
)