feat(i18n): 번역 작업을 그룹 단위로 처리한다

This commit is contained in:
2026-05-06 20:21:29 +09:00
parent 3a0c30e340
commit 13ca6a97b9
4 changed files with 181 additions and 54 deletions

View File

@@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.i18n.translation
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.mockito.ArgumentCaptor
import org.mockito.Mockito
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.transaction.support.AbstractPlatformTransactionManager
@@ -21,7 +22,7 @@ class TranslationJobWorkerTest {
}
@Test
fun shouldClaimPendingJobByLockedRepositoryMethod() {
fun shouldClaimPendingJobGroupByLockedRepositoryMethod() {
val jobRepository = Mockito.mock(TranslationJobRepository::class.java)
val memoryRepository = Mockito.mock(TranslationMemoryRepository::class.java)
val provider = successfulProvider()
@@ -36,17 +37,111 @@ class TranslationJobWorkerTest {
val job = translationJob()
job.id = 100L
Mockito.`when`(jobRepository.findNextPendingJobIdForUpdate(anyLocalDateTime())).thenReturn(100L)
Mockito.`when`(jobRepository.findNextPendingGroupJobIdsForUpdate(anyLocalDateTime())).thenReturn(listOf(100L))
Mockito.`when`(jobRepository.findById(100L)).thenReturn(Optional.of(job))
worker.processNextJob()
worker.processNextGroup()
Mockito.verify(jobRepository).findNextPendingJobIdForUpdate(anyLocalDateTime())
Mockito.verify(jobRepository, Mockito.never())
.findFirstByStatusAndNextRetryAtLessThanEqualOrderByCreatedAtAsc(
anyTranslationJobStatus(),
anyLocalDateTime()
)
Mockito.verify(jobRepository).findNextPendingGroupJobIdsForUpdate(anyLocalDateTime())
}
@Test
fun shouldProcessAllJobsInClaimedGroupBeforeMaterializing() {
val jobRepository = Mockito.mock(TranslationJobRepository::class.java)
val memoryRepository = Mockito.mock(TranslationMemoryRepository::class.java)
val provider = successfulProvider()
val materializer = Mockito.mock(TranslationReadModelMaterializer::class.java)
val worker = TranslationJobWorker(
translationJobRepository = jobRepository,
translationMemoryRepository = memoryRepository,
translationProvider = provider,
materializer = materializer,
transactionManager = TestTransactionManager()
)
val titleJob = translationJob(fieldKey = "title", sourceText = "제목")
titleJob.id = 100L
val detailJob = translationJob(fieldKey = "detail", sourceText = "설명")
detailJob.id = 101L
Mockito.`when`(jobRepository.findNextPendingGroupJobIdsForUpdate(anyLocalDateTime()))
.thenReturn(listOf(100L, 101L))
Mockito.`when`(jobRepository.findById(100L)).thenReturn(Optional.of(titleJob))
Mockito.`when`(jobRepository.findById(101L)).thenReturn(Optional.of(detailJob))
worker.processNextGroup()
assertEquals(TranslationJobStatus.COMPLETED, titleJob.status)
assertEquals(TranslationJobStatus.COMPLETED, detailJob.status)
Mockito.verify(materializer, Mockito.times(1)).materialize(LanguageTranslationTargetType.CONTENT, 10L, "en")
}
@Test
fun shouldRetryGroupWhenMaterializationFails() {
val jobRepository = Mockito.mock(TranslationJobRepository::class.java)
val memoryRepository = Mockito.mock(TranslationMemoryRepository::class.java)
val provider = successfulProvider()
val materializer = Mockito.mock(TranslationReadModelMaterializer::class.java)
val worker = TranslationJobWorker(
translationJobRepository = jobRepository,
translationMemoryRepository = memoryRepository,
translationProvider = provider,
materializer = materializer,
transactionManager = TestTransactionManager()
)
val titleJob = translationJob(fieldKey = "title", sourceText = "제목")
titleJob.id = 100L
val detailJob = translationJob(fieldKey = "detail", sourceText = "설명")
detailJob.id = 101L
val beforeRetryAt = titleJob.nextRetryAt
Mockito.`when`(jobRepository.findNextPendingGroupJobIdsForUpdate(anyLocalDateTime()))
.thenReturn(listOf(100L, 101L))
Mockito.`when`(jobRepository.findById(100L)).thenReturn(Optional.of(titleJob))
Mockito.`when`(jobRepository.findById(101L)).thenReturn(Optional.of(detailJob))
Mockito.`when`(materializer.materialize(LanguageTranslationTargetType.CONTENT, 10L, "en"))
.thenThrow(IllegalStateException("materialize down"))
worker.processNextGroup()
assertEquals(TranslationJobStatus.PENDING, titleJob.status)
assertEquals(TranslationJobStatus.PENDING, detailJob.status)
assertEquals(1, titleJob.retryCount)
assertEquals(1, detailJob.retryCount)
assertTrue(titleJob.nextRetryAt.isAfter(beforeRetryAt))
assertTrue(detailJob.nextRetryAt.isAfter(beforeRetryAt))
}
@Test
fun shouldLimitRunToFiveGroupsPerTick() {
val jobRepository = Mockito.mock(TranslationJobRepository::class.java)
val memoryRepository = Mockito.mock(TranslationMemoryRepository::class.java)
val provider = successfulProvider()
val materializer = Mockito.mock(TranslationReadModelMaterializer::class.java)
val worker = TranslationJobWorker(
translationJobRepository = jobRepository,
translationMemoryRepository = memoryRepository,
translationProvider = provider,
materializer = materializer,
transactionManager = TestTransactionManager()
)
val jobs = (1L..6L).map { id ->
translationJob(resourceId = id).also { it.id = id }
}
Mockito.`when`(jobRepository.findNextPendingGroupJobIdsForUpdate(anyLocalDateTime()))
.thenReturn(listOf(1L), listOf(2L), listOf(3L), listOf(4L), listOf(5L), listOf(6L))
jobs.forEach { job ->
Mockito.`when`(jobRepository.findById(job.id!!)).thenReturn(Optional.of(job))
}
worker.runPendingJobs()
val savedJobCaptor = ArgumentCaptor.forClass(TranslationJob::class.java)
Mockito.verify(jobRepository, Mockito.atLeastOnce()).save(savedJobCaptor.capture())
val completedResourceIds = savedJobCaptor.allValues
.filter { it.status == TranslationJobStatus.COMPLETED }
.map { it.resourceId }
.toSet()
assertEquals(setOf(1L, 2L, 3L, 4L, 5L), completedResourceIds)
}
@Test
@@ -66,10 +161,10 @@ class TranslationJobWorkerTest {
job.id = 200L
val beforeRetryAt = job.nextRetryAt
Mockito.`when`(jobRepository.findNextPendingJobIdForUpdate(anyLocalDateTime())).thenReturn(200L)
Mockito.`when`(jobRepository.findNextPendingGroupJobIdsForUpdate(anyLocalDateTime())).thenReturn(listOf(200L))
Mockito.`when`(jobRepository.findById(200L)).thenReturn(Optional.of(job))
worker.processNextJob()
worker.processNextGroup()
assertEquals(TranslationJobStatus.PENDING, job.status)
assertEquals(1, job.retryCount)
@@ -77,13 +172,17 @@ class TranslationJobWorkerTest {
assertTrue(job.nextRetryAt.isAfter(beforeRetryAt))
}
private fun translationJob(): TranslationJob {
private fun translationJob(
resourceId: Long = 10L,
fieldKey: String = "title",
sourceText: String = "제목"
): TranslationJob {
return TranslationJob(
resourceType = LanguageTranslationTargetType.CONTENT,
resourceId = 10L,
fieldKey = "title",
sourceHash = SourceTextNormalizer.hash("제목"),
sourceText = "제목",
resourceId = resourceId,
fieldKey = fieldKey,
sourceHash = SourceTextNormalizer.hash(sourceText),
sourceText = sourceText,
sourceLanguage = "ko",
targetLanguage = "en"
)
@@ -114,10 +213,6 @@ class TranslationJobWorkerTest {
private fun anyLocalDateTime(): LocalDateTime {
return Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now()
}
private fun anyTranslationJobStatus(): TranslationJobStatus {
return Mockito.any(TranslationJobStatus::class.java) ?: TranslationJobStatus.PENDING
}
}
private class TestTransactionManager : AbstractPlatformTransactionManager() {