| 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()
}
}