feat(home): AI 캐릭터 스냅샷 갱신 경로를 분리한다
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
package kr.co.vividnext.sodalive.v2.recommendation.application
|
||||
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationSnapshotWindowPolicy
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.HomeRecommendationQueryPort
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapshotPort
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapshotRecord
|
||||
import org.redisson.api.RedissonClient
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
@@ -11,13 +13,17 @@ 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
|
||||
|
||||
@Service
|
||||
class RecommendationSnapshotRefreshService(
|
||||
open class RecommendationSnapshotRefreshService(
|
||||
private val snapshotPort: RecommendationSnapshotPort,
|
||||
private val queryPort: HomeRecommendationQueryPort
|
||||
private val queryPort: HomeRecommendationQueryPort,
|
||||
private val redissonClient: RedissonClient? = null
|
||||
) {
|
||||
private val log = LoggerFactory.getLogger(javaClass)
|
||||
private val windowPolicy = RecommendationSnapshotWindowPolicy()
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun getLatestSnapshots(sectionType: RecommendedSectionType): List<RecommendationSnapshotRecord> {
|
||||
@@ -26,7 +32,7 @@ class RecommendationSnapshotRefreshService(
|
||||
|
||||
@Transactional
|
||||
fun refreshDailySnapshots() {
|
||||
refreshDailySnapshots(LocalDateTime.now())
|
||||
refreshDailySnapshots(LocalDateTime.now(ZoneOffset.UTC))
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -41,7 +47,7 @@ class RecommendationSnapshotRefreshService(
|
||||
val windowStart = snapshotAt.toLocalDate().minusDays(6).atStartOfDay()
|
||||
|
||||
runCatching {
|
||||
val aiCharacterCount = replaceAiCharacterSnapshots(windowStart, snapshotAt)
|
||||
val aiCharacterCount = refreshAiCharacterSnapshotsWithSectionLock(now)
|
||||
val cheerCreatorCount = replaceCheerCreatorSnapshots(windowStart, snapshotAt)
|
||||
val popularCommunityCount = replacePopularCommunitySnapshots(windowStart, snapshotAt)
|
||||
RefreshCounts(aiCharacterCount, cheerCreatorCount, popularCommunityCount)
|
||||
@@ -69,12 +75,61 @@ class RecommendationSnapshotRefreshService(
|
||||
}
|
||||
}
|
||||
|
||||
private fun replaceAiCharacterSnapshots(windowStart: LocalDateTime, snapshotAt: LocalDateTime): Int {
|
||||
val snapshots = queryPort.findAiCharacterSnapshots(windowStart, snapshotAt, AI_CHARACTER_SNAPSHOT_LIMIT)
|
||||
snapshotPort.replaceSnapshots(RecommendedSectionType.AI_CHARACTER, snapshotAt, snapshots)
|
||||
@Transactional
|
||||
open fun refreshAiCharacterSnapshots(nowUtc: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)): Int {
|
||||
val startedAt = System.currentTimeMillis()
|
||||
val window = windowPolicy.previousKstDayUtcWindow(nowUtc)
|
||||
val snapshots = queryPort.findAiCharacterSnapshots(window.startUtc, window.endExclusiveUtc, AI_CHARACTER_SNAPSHOT_LIMIT)
|
||||
snapshotPort.replaceSnapshots(RecommendedSectionType.AI_CHARACTER, window.snapshotAt, snapshots)
|
||||
afterCommit {
|
||||
log.info(
|
||||
"event=ai_character_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(AiCharacterSnapshotFallbackService.LOCK_KEY)
|
||||
|
||||
if (!lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
|
||||
log.info(
|
||||
"event=ai_character_recommendation_snapshot_refresh_lock_missed lockKey={}",
|
||||
AiCharacterSnapshotFallbackService.LOCK_KEY
|
||||
)
|
||||
return 0
|
||||
}
|
||||
|
||||
unlockAfterTransaction(lock)
|
||||
return refreshAiCharacterSnapshots(nowUtc)
|
||||
}
|
||||
|
||||
private fun unlockAfterTransaction(lock: org.redisson.api.RLock) {
|
||||
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
if (lock.isHeldByCurrentThread) {
|
||||
lock.unlock()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
TransactionSynchronizationManager.registerSynchronization(
|
||||
object : TransactionSynchronization {
|
||||
override fun afterCompletion(status: Int) {
|
||||
if (lock.isHeldByCurrentThread) {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun replaceCheerCreatorSnapshots(windowStart: LocalDateTime, snapshotAt: LocalDateTime): Int {
|
||||
val snapshots = queryPort.findCheerCreatorSnapshots(windowStart, snapshotAt, CHEER_CREATOR_SNAPSHOT_LIMIT)
|
||||
snapshotPort.replaceSnapshots(RecommendedSectionType.CHEER_CREATOR, snapshotAt, snapshots)
|
||||
|
||||
Reference in New Issue
Block a user