test(recommendation): 응원 크리에이터 동시성 테스트를 안정화한다
This commit is contained in:
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
| 항목 | 내용 |
|
| 항목 | 내용 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 상태 | 3차 Phase별 리뷰 완료 |
|
| 상태 | Jenkins 회귀 수정 완료 |
|
||||||
| 확정일 | 2026-07-31 |
|
| 확정일 | 2026-08-03 |
|
||||||
| 요구사항 기준 | `docs/20260710_메인_홈_추천_응원크리에이터_스냅샷/prd.md` 전체 |
|
| 요구사항 기준 | `docs/20260710_메인_홈_추천_응원크리에이터_스냅샷/prd.md` 전체 |
|
||||||
| 현재 Phase | Phase 1~7 3차 리뷰 완료 |
|
| 현재 Phase | Phase 1~7 및 `P5-R2` 완료 |
|
||||||
| 현재 활성 Goal | 없음 |
|
| 현재 활성 Goal | 없음 |
|
||||||
| 다음 Goal | 없음 |
|
| 다음 Goal | 없음 |
|
||||||
|
|
||||||
@@ -318,6 +318,20 @@
|
|||||||
- [x] **GREEN:** 새 테스트가 구현 결함을 드러낼 때만 `refreshFutures` 또는 lock 내부 존재 확인 경로를 최소 수정한다.
|
- [x] **GREEN:** 새 테스트가 구현 결함을 드러낼 때만 `refreshFutures` 또는 lock 내부 존재 확인 경로를 최소 수정한다.
|
||||||
- [x] **REFACTOR/GATE:** sleep 없이 `RecommendationSnapshotFallbackServiceTest` focused test와 `git diff --check`를 실행해 결과를 누적한다.
|
- [x] **REFACTOR/GATE:** sleep 없이 `RecommendationSnapshotFallbackServiceTest` focused test와 `git diff --check`를 실행해 결과를 누적한다.
|
||||||
|
|
||||||
|
#### Task R5.2 Jenkins single-flight 테스트 scheduling race 제거
|
||||||
|
|
||||||
|
**Goal 실행 `P5-R2`:** 두 동시 요청이 pending refresh future를 공유하는 계약을 worker 완료 순서에 의존하지 않고 검증한다.
|
||||||
|
|
||||||
|
- **시작 조건:** Jenkins에서 `shouldShareSingleCheerCreatorRefreshFutureForConcurrentRequests`의 두 번째 request `Future.get(...)`이 `TimeoutException`으로 실패하고, `REV-P5-002`가 테스트 scheduling race로 확정됨.
|
||||||
|
- **완료 증거:** Jenkins 실패 원인 기록, 결정적 테스트 최소 수정, fallback focused test 반복 통과, 직접 영향 회귀·format·문서 명령·diff 검증 결과 누적.
|
||||||
|
- **범위 밖:** `RecommendationSnapshotFallbackService` production 동작, worker 수, 300ms lock 대기와 1,500ms 홈 대기, 다른 fallback 섹션 변경.
|
||||||
|
|
||||||
|
- [x] **RED:** Jenkins 실패의 `RecommendationSnapshotFallbackServiceTest.kt:322 TimeoutException`과 로컬 focused test 통과를 함께 기록해 worker scheduling에 따라 결과가 달라지는 기존 테스트를 확인한다.
|
||||||
|
- [x] **RED 확인:** `./gradlew cleanTest test --tests 'kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotFallbackServiceTest.shouldShareSingleCheerCreatorRefreshFutureForConcurrentRequests'`의 로컬 통과와 Jenkins 실패를 대조해 비결정성을 확인한다.
|
||||||
|
- [x] **GREEN:** 수동 `CapturingExecutor.runNext()` 완료 경쟁 대신 refresh worker를 latch로 pending 상태에 유지한다. 두 요청이 홈 대기 timeout으로 반환할 때까지 refresh를 완료하지 않고 `refreshCheerCreatorSnapshots(...)` 호출이 1회인지 검증한다.
|
||||||
|
- [x] **GREEN 확인:** 같은 focused test를 반복 실행하고 `RecommendationSnapshotFallbackServiceTest` 전체를 실행해 single-flight, timeout 후 worker 지속, lock 내부 double-check가 모두 통과하는지 확인한다.
|
||||||
|
- [x] **REFACTOR/GATE:** 사용하지 않는 test helper/import만 제거하고 `ktlintCheck`, `./gradlew tasks --all`, `git diff --check` 결과를 전체 검증 기록에 누적한다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Phase 6: API 회귀와 최종 검증
|
### Phase 6: API 회귀와 최종 검증
|
||||||
@@ -440,6 +454,8 @@ git diff --check
|
|||||||
- Non-Goals: Task 4.1, Task 6.1, Task 6.3, Task 7.1에서 다른 스냅샷 섹션 marker 확장 없음, 공개 API URL/응답 필드 변경 없음, 16명 밖 후보 보충 없음, 신규 DDL 없음, 관리자/ML/A-B 제외를 확인한다.
|
- Non-Goals: Task 4.1, Task 6.1, Task 6.3, Task 7.1에서 다른 스냅샷 섹션 marker 확장 없음, 공개 API URL/응답 필드 변경 없음, 16명 밖 후보 보충 없음, 신규 DDL 없음, 관리자/ML/A-B 제외를 확인한다.
|
||||||
|
|
||||||
## 전체 검증 기록
|
## 전체 검증 기록
|
||||||
|
- 2026-08-03: `P5-R2` GREEN/GATE로 수동 `CapturingExecutor` 완료 경쟁을 제거하고, 실제 worker를 latch로 pending 상태에 유지한 채 두 요청의 worker task 제출과 `refreshCheerCreatorSnapshots(...)` 호출이 각각 1회인지 검증하도록 수정했다. focused test는 최초 1회와 연속 10회 모두 통과했고, `RecommendationSnapshotFallbackServiceTest` 전체, `./gradlew ktlintCheck`, `./gradlew tasks --all`, `./gradlew cleanTest test`, `git diff --check`가 모두 `BUILD SUCCESSFUL` 또는 출력 없음으로 통과했다. 전체 테스트는 7분 14초 소요됐다. Oracle 고강도 리뷰도 correctness·결정성·thread cleanup·production 무변경 판단에 blocker 없이 승인했다.
|
||||||
|
- 2026-08-03: `P5-R2` RED 확인으로 Jenkins의 `RecommendationSnapshotFallbackServiceTest.kt:322 TimeoutException`과 로컬 `./gradlew cleanTest test --tests 'kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotFallbackServiceTest.shouldShareSingleCheerCreatorRefreshFutureForConcurrentRequests'` `BUILD SUCCESSFUL`을 대조했다. 같은 코드가 worker scheduling에 따라 실패·성공하여 기존 테스트의 `taskSubmitted` latch가 두 요청의 동일 future 대기를 보장하지 않는 비결정성을 확인했다.
|
||||||
- 2026-07-31: Phase 1~7 3차 리뷰로 PRD·plan-task·현재 코드·테스트를 정적으로 대조했다. 이전 리뷰에서 보완한 문서 정합성, 종료 라이브 데뷔 이력, `CHEER_CREATOR` refresh 실패 로그, fallback single-flight·double-check, 공개 응답 3개 필드, 본인·활성 팔로우 제외가 현재 구현과 회귀 테스트에 유지됨을 확인했다. 각 결과는 기존 `reviews/phase-1-review.md`~`reviews/phase-7-review.md`에 3차 리뷰로 별도 누적했다. 추가 확정 발견 사항이 없어 신규 회귀 Task/Goal은 추가하지 않았다. 사용자 지시에 따라 Gradle compile/test는 실행하지 않았다. `git diff --check`는 출력 없이 통과했다. 문서 명령 유효성 확인용 `./gradlew tasks --all`은 일반 sandbox에서 `~/.gradle` wrapper lock 권한으로 실패한 뒤 승인된 동일 명령에서 `BUILD SUCCESSFUL`로 통과했다.
|
- 2026-07-31: Phase 1~7 3차 리뷰로 PRD·plan-task·현재 코드·테스트를 정적으로 대조했다. 이전 리뷰에서 보완한 문서 정합성, 종료 라이브 데뷔 이력, `CHEER_CREATOR` refresh 실패 로그, fallback single-flight·double-check, 공개 응답 3개 필드, 본인·활성 팔로우 제외가 현재 구현과 회귀 테스트에 유지됨을 확인했다. 각 결과는 기존 `reviews/phase-1-review.md`~`reviews/phase-7-review.md`에 3차 리뷰로 별도 누적했다. 추가 확정 발견 사항이 없어 신규 회귀 Task/Goal은 추가하지 않았다. 사용자 지시에 따라 Gradle compile/test는 실행하지 않았다. `git diff --check`는 출력 없이 통과했다. 문서 명령 유효성 확인용 `./gradlew tasks --all`은 일반 sandbox에서 `~/.gradle` wrapper lock 권한으로 실패한 뒤 승인된 동일 명령에서 `BUILD SUCCESSFUL`로 통과했다.
|
||||||
- 2026-07-31: `P3-R2`로 `CHEER_CREATOR` 데뷔 CTE가 채널명이 있는 종료 라이브를 데뷔 이력으로 인정하도록 복구했다. RED 확인으로 `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest`를 실행해 `shouldIncludeEndedLiveWithChannelNameAsCheerCreatorDebut`가 `AssertionFailedError`로 실패하는 것을 확인했다. 이후 `findCheerCreatorSnapshots(...)` 라이브 branch에서 `lr.is_active = true`만 제거했고, 같은 repository focused test는 `BUILD SUCCESSFUL`로 통과했다. `./gradlew ktlintCheck`도 `BUILD SUCCESSFUL`로 통과했으며, `git diff --check`는 출력 없이 통과했다.
|
- 2026-07-31: `P3-R2`로 `CHEER_CREATOR` 데뷔 CTE가 채널명이 있는 종료 라이브를 데뷔 이력으로 인정하도록 복구했다. RED 확인으로 `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest`를 실행해 `shouldIncludeEndedLiveWithChannelNameAsCheerCreatorDebut`가 `AssertionFailedError`로 실패하는 것을 확인했다. 이후 `findCheerCreatorSnapshots(...)` 라이브 branch에서 `lr.is_active = true`만 제거했고, 같은 repository focused test는 `BUILD SUCCESSFUL`로 통과했다. `./gradlew ktlintCheck`도 `BUILD SUCCESSFUL`로 통과했으며, `git diff --check`는 출력 없이 통과했다.
|
||||||
- 2026-07-31: `P1-R2`로 상단 후속 변경 상태를 현재 PRD 전체, Phase 1~7 판정, 실제 미완료 Goal `P3-R2` 기준으로 정리했다. 문서 정합성 복구만 수행해 TDD 예외로 처리했다. `git diff --check -- docs/20260710_메인_홈_추천_응원크리에이터_스냅샷/plan-task.md`는 출력 없이 통과했고, `./gradlew tasks --all`은 `BUILD SUCCESSFUL`로 통과했다.
|
- 2026-07-31: `P1-R2`로 상단 후속 변경 상태를 현재 PRD 전체, Phase 1~7 판정, 실제 미완료 Goal `P3-R2` 기준으로 정리했다. 문서 정합성 복구만 수행해 TDD 예외로 처리했다. `git diff --check -- docs/20260710_메인_홈_추천_응원크리에이터_스냅샷/plan-task.md`는 출력 없이 통과했고, `./gradlew tasks --all`은 `BUILD SUCCESSFUL`로 통과했다.
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| `REV-P5-001` | Low | 보완 완료 | 동일 섹션 single-flight와 lock 내부 double-check의 직접 회귀 테스트가 없다 | Task R5.1 | `P5-R1` |
|
| `REV-P5-001` | Low | 보완 완료 | 동일 섹션 single-flight와 lock 내부 double-check의 직접 회귀 테스트가 없다 | Task R5.1 | `P5-R1` |
|
||||||
|
| `REV-P5-002` | Medium | 보완 완료 | single-flight 테스트가 worker 완료 순서에 의존해 Jenkins에서 timeout된다 | Task R5.2 | `P5-R2` |
|
||||||
|
|
||||||
## 6. 발견 사항 상세
|
## 6. 발견 사항 상세
|
||||||
|
|
||||||
@@ -102,3 +103,16 @@ sleep 대신 latch와 결정적 fake를 사용해 두 경쟁 조건을 고정하
|
|||||||
- **plan·goal 전환:** 전환 항목 없음.
|
- **plan·goal 전환:** 전환 항목 없음.
|
||||||
- **최종 결론:** 확정 발견 사항 없음.
|
- **최종 결론:** 확정 발견 사항 없음.
|
||||||
- **남은 항목:** 없음.
|
- **남은 항목:** 없음.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 4차 리뷰 기록 — 2026-08-03
|
||||||
|
|
||||||
|
- **리뷰 대상:** Phase 5 / Task R5.1의 `shouldShareSingleCheerCreatorRefreshFutureForConcurrentRequests` Jenkins 실패.
|
||||||
|
- **검토 근거:** Jenkins `RecommendationSnapshotFallbackServiceTest.kt:322 TimeoutException`, `RecommendationSnapshotFallbackService.kt:141-149`, 테스트의 `CapturingExecutor`와 latch 흐름, 로컬 `cleanTest` focused test 실행, JDK 17 `CompletableFuture`·`CountDownLatch` 계약.
|
||||||
|
- **확정 발견 사항:** `REV-P5-002`. `taskSubmitted` latch는 worker task가 executor에 전달됐다는 사실만 보장하며, refresh future가 `refreshFutures`에 게시됐거나 두 요청이 같은 future에서 대기 중임을 보장하지 않는다. Jenkins에서 첫 worker가 먼저 완료되면 reference가 제거되고 두 번째 요청은 실행되지 않는 새 captured task를 기다려 바깥 `get(1s)`에서 timeout된다.
|
||||||
|
- **판정:** production single-flight 결함 근거는 없다. timeout 확대는 경쟁을 숨기므로 제외하고, refresh를 latch로 pending 상태에 유지한 채 두 요청의 refresh 호출 횟수 1회를 검증하도록 테스트만 수정한다.
|
||||||
|
- **plan·goal 전환:** `plan-task.md` Phase 5에 Task R5.2 / `P5-R2`를 추가했다.
|
||||||
|
- **검증 결과:** focused test 최초 1회와 연속 10회, `RecommendationSnapshotFallbackServiceTest` 전체, `ktlintCheck`, `tasks --all`, 전체 `cleanTest test`, `git diff --check`가 모두 통과했다. Oracle 고강도 리뷰도 blocker 없이 승인했다.
|
||||||
|
- **최종 결론:** `P5-R2` 보완 완료.
|
||||||
|
- **남은 항목:** 없음.
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import java.util.concurrent.CountDownLatch
|
|||||||
import java.util.concurrent.Executor
|
import java.util.concurrent.Executor
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.Future
|
import java.util.concurrent.Future
|
||||||
import java.util.concurrent.LinkedBlockingQueue
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
class RecommendationSnapshotFallbackServiceTest {
|
class RecommendationSnapshotFallbackServiceTest {
|
||||||
@Test
|
@Test
|
||||||
@@ -265,10 +265,18 @@ class RecommendationSnapshotFallbackServiceTest {
|
|||||||
homeWaitMillis = 50
|
homeWaitMillis = 50
|
||||||
)
|
)
|
||||||
|
|
||||||
val first = service.refreshCheerCreatorIfMissing(offset = 0, limit = 16, nowUtc = LocalDateTime.of(2026, 7, 9, 21, 0))
|
val first = try {
|
||||||
|
val result = service.refreshCheerCreatorIfMissing(
|
||||||
|
offset = 0,
|
||||||
|
limit = 16,
|
||||||
|
nowUtc = LocalDateTime.of(2026, 7, 9, 21, 0)
|
||||||
|
)
|
||||||
assertEquals(true, refreshStarted.await(1, TimeUnit.SECONDS))
|
assertEquals(true, refreshStarted.await(1, TimeUnit.SECONDS))
|
||||||
|
result
|
||||||
|
} finally {
|
||||||
allowRefreshComplete.countDown()
|
allowRefreshComplete.countDown()
|
||||||
executor.shutdown()
|
executor.shutdown()
|
||||||
|
}
|
||||||
assertEquals(true, executor.awaitTermination(1, TimeUnit.SECONDS))
|
assertEquals(true, executor.awaitTermination(1, TimeUnit.SECONDS))
|
||||||
val second = service.refreshCheerCreatorIfMissing(
|
val second = service.refreshCheerCreatorIfMissing(
|
||||||
offset = 0,
|
offset = 0,
|
||||||
@@ -283,46 +291,43 @@ class RecommendationSnapshotFallbackServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
@DisplayName("응원 크리에이터 fallback 동시 요청은 하나의 refresh future를 공유한다")
|
@DisplayName("응원 크리에이터 fallback 동시 요청은 하나의 refresh future를 공유한다")
|
||||||
fun shouldShareSingleCheerCreatorRefreshFutureForConcurrentRequests() {
|
fun shouldShareSingleCheerCreatorRefreshFutureForConcurrentRequests() {
|
||||||
val existsSnapshotEntered = CountDownLatch(2)
|
val snapshotPort = FakeRecommendationFallbackSnapshotPort()
|
||||||
val existsSnapshotReturned = CountDownLatch(2)
|
val refreshStarted = CountDownLatch(1)
|
||||||
val snapshotPort = FakeRecommendationFallbackSnapshotPort(
|
val allowRefreshComplete = CountDownLatch(1)
|
||||||
existsSnapshotEntered = existsSnapshotEntered,
|
val refreshService = BlockingCheerRefreshService(snapshotPort, refreshStarted, allowRefreshComplete)
|
||||||
existsSnapshotReturned = existsSnapshotReturned
|
|
||||||
)
|
|
||||||
val refreshService = FastCheerRefreshService(snapshotPort)
|
|
||||||
val redissonClient = Mockito.mock(RedissonClient::class.java)
|
val redissonClient = Mockito.mock(RedissonClient::class.java)
|
||||||
val lock = Mockito.mock(RLock::class.java)
|
val lock = Mockito.mock(RLock::class.java)
|
||||||
Mockito.`when`(redissonClient.getLock(RecommendationSnapshotFallbackService.CHEER_CREATOR_LOCK_KEY)).thenReturn(lock)
|
Mockito.`when`(redissonClient.getLock(RecommendationSnapshotFallbackService.CHEER_CREATOR_LOCK_KEY)).thenReturn(lock)
|
||||||
Mockito.`when`(lock.tryLock(300, -1, TimeUnit.MILLISECONDS)).thenReturn(true)
|
Mockito.`when`(lock.tryLock(300, -1, TimeUnit.MILLISECONDS)).thenReturn(true)
|
||||||
Mockito.`when`(lock.isHeldByCurrentThread).thenReturn(true)
|
Mockito.`when`(lock.isHeldByCurrentThread).thenReturn(true)
|
||||||
val workerExecutor = CapturingExecutor()
|
val workerTaskCount = AtomicInteger()
|
||||||
|
val workerPool = Executors.newFixedThreadPool(2)
|
||||||
|
val workerExecutor = Executor { command ->
|
||||||
|
workerTaskCount.incrementAndGet()
|
||||||
|
workerPool.execute(command)
|
||||||
|
}
|
||||||
val requestExecutor = Executors.newFixedThreadPool(2)
|
val requestExecutor = Executors.newFixedThreadPool(2)
|
||||||
val service = RecommendationSnapshotFallbackService(
|
val service = RecommendationSnapshotFallbackService(
|
||||||
snapshotPort,
|
snapshotPort,
|
||||||
refreshService,
|
refreshService,
|
||||||
redissonClient,
|
redissonClient,
|
||||||
workerExecutor,
|
workerExecutor,
|
||||||
homeWaitMillis = 1_000
|
homeWaitMillis = 50
|
||||||
)
|
)
|
||||||
val nowUtc = LocalDateTime.of(2026, 7, 9, 21, 0)
|
val nowUtc = LocalDateTime.of(2026, 7, 9, 21, 0)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val first = requestExecutor.submitCheerRefresh(service, nowUtc)
|
val first = requestExecutor.submitCheerRefresh(service, nowUtc)
|
||||||
val second = requestExecutor.submitCheerRefresh(service, nowUtc)
|
val second = requestExecutor.submitCheerRefresh(service, nowUtc)
|
||||||
assertEquals(true, existsSnapshotEntered.await(1, TimeUnit.SECONDS))
|
assertEquals(true, refreshStarted.await(5, TimeUnit.SECONDS))
|
||||||
assertEquals(true, existsSnapshotReturned.await(1, TimeUnit.SECONDS))
|
assertEquals(emptyList<RecommendationSnapshotRecord>(), first.get(5, TimeUnit.SECONDS))
|
||||||
assertEquals(true, workerExecutor.taskSubmitted.await(1, TimeUnit.SECONDS))
|
assertEquals(emptyList<RecommendationSnapshotRecord>(), second.get(5, TimeUnit.SECONDS))
|
||||||
assertEquals(false, first.isDone)
|
assertEquals(1, workerTaskCount.get())
|
||||||
assertEquals(false, second.isDone)
|
|
||||||
assertEquals(1, workerExecutor.taskCount)
|
|
||||||
|
|
||||||
workerExecutor.runNext()
|
|
||||||
|
|
||||||
assertEquals(listOf(99L), first.get(1, TimeUnit.SECONDS).map { it.targetId })
|
|
||||||
assertEquals(listOf(99L), second.get(1, TimeUnit.SECONDS).map { it.targetId })
|
|
||||||
assertEquals(1, refreshService.cheerRefreshCount)
|
assertEquals(1, refreshService.cheerRefreshCount)
|
||||||
} finally {
|
} finally {
|
||||||
|
allowRefreshComplete.countDown()
|
||||||
requestExecutor.shutdownNow()
|
requestExecutor.shutdownNow()
|
||||||
|
workerPool.shutdownNow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,23 +413,6 @@ class RecommendationSnapshotFallbackServiceTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CapturingExecutor : Executor {
|
|
||||||
val taskSubmitted = CountDownLatch(1)
|
|
||||||
private val tasks = LinkedBlockingQueue<Runnable>()
|
|
||||||
|
|
||||||
val taskCount: Int
|
|
||||||
get() = tasks.size
|
|
||||||
|
|
||||||
override fun execute(command: Runnable) {
|
|
||||||
tasks.add(command)
|
|
||||||
taskSubmitted.countDown()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun runNext() {
|
|
||||||
tasks.poll(1, TimeUnit.SECONDS)!!.run()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class BlockingCheerRefreshService(
|
private class BlockingCheerRefreshService(
|
||||||
private val snapshotPort: RecommendationSnapshotPort,
|
private val snapshotPort: RecommendationSnapshotPort,
|
||||||
private val refreshStarted: CountDownLatch,
|
private val refreshStarted: CountDownLatch,
|
||||||
@@ -438,26 +426,7 @@ private class BlockingCheerRefreshService(
|
|||||||
override fun refreshCheerCreatorSnapshots(nowUtc: LocalDateTime): Int {
|
override fun refreshCheerCreatorSnapshots(nowUtc: LocalDateTime): Int {
|
||||||
cheerRefreshCount += 1
|
cheerRefreshCount += 1
|
||||||
refreshStarted.countDown()
|
refreshStarted.countDown()
|
||||||
allowRefreshComplete.await(1, TimeUnit.SECONDS)
|
allowRefreshComplete.await()
|
||||||
snapshotPort.replaceSnapshots(
|
|
||||||
RecommendedSectionType.CHEER_CREATOR,
|
|
||||||
LocalDateTime.of(2026, 7, 9, 14, 59, 59),
|
|
||||||
listOf(snapshot(RecommendedSectionType.CHEER_CREATOR, 99L))
|
|
||||||
)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class FastCheerRefreshService(
|
|
||||||
private val snapshotPort: RecommendationSnapshotPort
|
|
||||||
) : RecommendationSnapshotRefreshService(
|
|
||||||
snapshotPort,
|
|
||||||
Mockito.mock(kr.co.vividnext.sodalive.v2.recommendation.port.out.HomeRecommendationQueryPort::class.java)
|
|
||||||
) {
|
|
||||||
var cheerRefreshCount: Int = 0
|
|
||||||
|
|
||||||
override fun refreshCheerCreatorSnapshots(nowUtc: LocalDateTime): Int {
|
|
||||||
cheerRefreshCount += 1
|
|
||||||
snapshotPort.replaceSnapshots(
|
snapshotPort.replaceSnapshots(
|
||||||
RecommendedSectionType.CHEER_CREATOR,
|
RecommendedSectionType.CHEER_CREATOR,
|
||||||
LocalDateTime.of(2026, 7, 9, 14, 59, 59),
|
LocalDateTime.of(2026, 7, 9, 14, 59, 59),
|
||||||
@@ -496,10 +465,7 @@ private class BlockingAiAndFastCheerRefreshService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class FakeRecommendationFallbackSnapshotPort(
|
private class FakeRecommendationFallbackSnapshotPort : RecommendationSnapshotPort {
|
||||||
private val existsSnapshotEntered: CountDownLatch? = null,
|
|
||||||
private val existsSnapshotReturned: CountDownLatch? = null
|
|
||||||
) : RecommendationSnapshotPort {
|
|
||||||
private val snapshots = mutableListOf<RecommendationSnapshotRecord>()
|
private val snapshots = mutableListOf<RecommendationSnapshotRecord>()
|
||||||
|
|
||||||
override fun findLatestSnapshots(
|
override fun findLatestSnapshots(
|
||||||
@@ -531,12 +497,7 @@ private class FakeRecommendationFallbackSnapshotPort(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun existsSnapshot(sectionType: RecommendedSectionType, snapshotAt: LocalDateTime): Boolean {
|
override fun existsSnapshot(sectionType: RecommendedSectionType, snapshotAt: LocalDateTime): Boolean {
|
||||||
existsSnapshotEntered?.countDown()
|
return snapshots.any { it.sectionType == sectionType && it.snapshotAt == snapshotAt }
|
||||||
existsSnapshotEntered?.await(1, TimeUnit.SECONDS)
|
|
||||||
val exists = snapshots.any { it.sectionType == sectionType && it.snapshotAt == snapshotAt }
|
|
||||||
existsSnapshotReturned?.countDown()
|
|
||||||
existsSnapshotReturned?.await(1, TimeUnit.SECONDS)
|
|
||||||
return exists
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun replaceSnapshots(
|
override fun replaceSnapshots(
|
||||||
|
|||||||
Reference in New Issue
Block a user