| name | batch |
| description | Use when working in the ssolv-batch module โ periodic schedulers, `CoroutineWatchdogManager` for watchdog-protected long-running jobs, Redis Streams dead-letter reprocessing, and place data cleanup tasks. Independent from API modules. Trigger on files under ssolv-batch/ or when adding a new scheduled/batch task. |
Batch / Scheduler Patterns
ssolv-batch ๋ชจ๋: ์ฃผ๊ธฐ์ ๋ฐฐ๊ฒฝ ์์
๋ด๋น.
Redis Streams Dead-Letter ์ฒ๋ฆฌ, ์ฅ์ ๋ฐ์ดํฐ ์ ๋ฆฌ ๋ฑ API ๋ชจ๋๊ณผ ๋
๋ฆฝ ์คํ.
๋ชจ๋ ๊ตฌ์กฐ
ssolv-batch
โโโ scheduler/
โโโ PendingMessageScheduler โ Redis Streams dead-letter (XACK ํ๊ธฐ)
โโโ PlaceDataCleanupScheduler โ 30์ผ ์ด๊ณผ ์ฅ์ ๋ฐ์ดํฐ ์ญ์ (Google ToS)
โโโ CoroutineWatchdogManager โ ๋ถ์ฐ ๋ฝ + TTL ์๋ ์ฐ์ฅ
ํ์ ๊ท์น
@Component
@Profile("!test")
class SomeScheduler(...)
@Scheduled(fixedDelay = 60_000)
fun run() {
runBlocking {
}
}
runBlocking {
watchdogManager.executeWithLock(LOCK_KEY, initialTtlMillis = 10_000, extensionMillis = 10_000) {
}
}
CoroutineWatchdogManager
Redis ๋ถ์ฐ ๋ฝ + ์์
์ค TTL ์๋ ์ฐ์ฅ ํจํด.
@Component
class SomeScheduler(
private val watchdogManager: CoroutineWatchdogManager,
) {
companion object {
private const val LOCK_KEY = "lock:<scheduler-name>"
}
@Scheduled(fixedDelay = 60_000)
fun run() {
runBlocking {
watchdogManager.executeWithLock(
lockKey = LOCK_KEY,
initialTtlMillis = 10_000,
extensionMillis = 10_000,
) {
doWork()
}
}
}
}
๋ด๋ถ ๋์:
- ๋ฝ ํ๋ โ ์ฝ๋ฃจํด Watchdog์ด ์ฃผ๊ธฐ์ ์ผ๋ก TTL ์ฐ์ฅ
- ์์
์๋ฃ โ Lua ์คํฌ๋ฆฝํธ๋ก ์์ ํ๊ฒ ๋ฝ ํด์ (๋ด๊ฐ ์ก์ ๋ฝ๋ง ํด์ )
- ๋ฝ ํ๋ ์คํจ ์ โ ์์
๊ฑด๋๋ (๋ค๋ฅธ ์ธ์คํด์ค๊ฐ ์ฒ๋ฆฌ ์ค)
PendingMessageScheduler โ Dead-Letter ์ฒ๋ฆฌ
deliveryCount ์ฆ๊ฐ ์์ :
์ต์ด XREADGROUP ๋ฐฐ๋ฌ: +1
API ๋ชจ๋ RecoveryScheduler XCLAIM ๋ง๋ค: +1
MAX_DELIVERY_COUNT = 4 โ ์ต์ด 1ํ + ๋ณต๊ตฌ 3ํ ํ XACK ํ๊ธฐ + Sentry ์บก์ฒ
์คํธ๋ฆผ ๋ชฉ๋ก: meeting_calculation_stream, meeting_notification_stream
์ Redis Stream ์ถ๊ฐ ์ PendingMessageScheduler.streams์ ๋ฑ๋ก:
private val streams = listOf(
RedisStreamConstants.MEETING_CALCULATION_STREAM to RedisStreamConstants.MEETING_CALCULATION_GROUP,
RedisStreamConstants.MEETING_NOTIFICATION_STREAM to RedisStreamConstants.MEETING_NOTIFICATION_GROUP,
RedisStreamConstants.NEW_STREAM to RedisStreamConstants.NEW_GROUP,
)
PlaceDataCleanupScheduler โ ์ฅ์ ๋ฐ์ดํฐ ์ ๋ฆฌ
Google Places API ์ฝ๊ด(ToS) ์ค์: 30์ผ ์ด๊ณผ ์บ์ ๋ฐ์ดํฐ ์ญ์ .
@Scheduled(cron = "0 0 3 * * *")
fun cleanupStalePlaceData() {
runBlocking {
watchdogManager.executeWithLock(lockKey, 10_000, 10_000) {
transactionTemplate.execute {
val deletedCount = placeJpaRepository.deleteByUpdatedAtBefore(
LocalDateTime.now().minusDays(30)
)
logger.info("30์ผ ๊ฒฝ๊ณผ ์ฅ์ ๋ฐ์ดํฐ ์ญ์ : {}๊ฑด", deletedCount)
}
}
}
}
์ฃผ์: ํธ๋์ญ์
์ด ํ์ํ DB ์์
์ transactionTemplate.execute {} ์์์ ์คํ.
์ค์ผ์ค๋ฌ ๋ฉ์๋ ์์ฒด์ @Transactional ์ฌ์ฉ ๊ธ์ง (์ฝ๋ฃจํด ์ปจํ
์คํธ ๋ฌธ์ ).
์ ์ค์ผ์ค๋ฌ ์ถ๊ฐ ์ฒดํฌ๋ฆฌ์คํธ
MDC ํธ๋ ์ด์ฑ ํจํด
@Scheduled(fixedDelay = 60_000)
fun run() {
val requestId = "scheduler-" + UUID.randomUUID().toString().substring(0, 8)
MDC.put(MdcLoggingFilter.REQUEST_ID, requestId)
try {
runBlocking { }
} finally {
MDC.clear()
}
}