INCIDENT · BETA 배포 실패

RecipientSendTimeLearning Worker Freeze

2026-06-15 · PR #8598 (설정 UI) · Actions run 27517617074 · 작성자 분석

배포 실패는 코드 버그가 아니라, RecipientSendTimeLearning 데일리 잡이 worker 프로세스를 frozen(멈춤) 시켜 unhealthy로 2일 방치 → FE-only 배포가 그 worker를 recreate 하지 않아 헬스 게이트가 막힌 것. worker 재생성 후 재배포 성공 (CD Pipeline Beta = success) ✅
조치 완료 (2026-06-15) — 데일리 cron 임시 비활성화 머지 완료 (alpha #8601 beta #8603). ensureRecipientSendTimeLearningCronremoveJobScheduler 로 바꿔 Redis 데일리 스케줄러 제거 → 재발 차단. 기존 추천 데이터·조회 경로는 유지(학습 갱신만 중단).

1배포 실패 인과 (확정)

단계사실
worker 상태bullmq-worker-139 "Up 2 days (unhealthy)", 마지막 로그 약 54시간 전 = frozen
헬스체크/livez curl 이 2일째 exceeded timeout (2s) — 이벤트 루프 블록으로 HTTP 무응답
왜 방치되나restart: alwaysexit 시에만 작동. frozen(살아있지만 멈춤)은 재시작 트리거 안 됨
왜 배포 실패FE-only 변경 → worker 이미지 불변 → compose 가 recreate 안 함 → unhealthy worker 잔존 → "모든 서비스 healthy" 게이트 240s 타임아웃 → 롤백

2Worker 가 Frozen 된 코드 원인

도입: 04387be37 (lsuminl, 2026-05-27, #7915 "회사별 추천 발송 시간") · cron: 매일 KST 03:20 전역 실행 (workspaceId 없이 호출 → 전 워크스페이스 90일치 전량 재계산)

1 이벤트 루프 동기 블로킹

recipient-send-time-learning.service.ts:369
// 110만 행을 동기 map + 각 행 new Date()
const stats = rowsOf<StatRow>(raw).map(mapStatRow)
// chunk(stats, 500) 도 110만 항목 동기 슬라이스
수 초간 이벤트 루프 정지 → heartbeat setInterval(5s)/livez HTTP(:3010) 응답 불가 → docker 2s 타임아웃 초과.

2 메모리 스파이크 → JSC GC thrash → frozen

recipient-send-time-learning.service.ts:369–385
110만 매핑 객체 + chunk 복사본을 한 잡에서 보유. mem_limit 4g + 무 swap (memswap_limit 4g, mem_swappiness 0). Bun(JavaScriptCore)은 V8식 heap cap 도 없음 → RSS 급등 시 GC 가 계속 돌며 프로세스가 exit 없이 멈춤 → restart: always 도 안 걸려 2일 방치.

3 무경계 전역 풀스캔

queues.ts:3653 → service.ts:379
cron 이 workspaceId 없이 호출 → 서비스가 전역 경로(DELETE ALL 후 전체 재삽입)로 90일치 전 워크스페이스 재계산. 데이터 증가에 선형 악화 (현재 statCount 1,126,658). 증분이 아니라 매일 전량 재구축.

4 직렬 삽입

recipient-send-time-learning.service.ts:383
for … await db.insert(chunk) 2,253회 순차 round-trip.

3이 기능이 하는 일 (상세)

회사(수신자)별 최적 발송 시간 추천 — 시퀀스 이메일을 "받는 사람이 가장 잘 여는 현지 시간대"에 보내려는 기능. 과거 발송·반응 데이터를 학습해 리드/도메인/국가 단위로 추천 시각을 만든다.

구성요소역할
학습 (learning.service)최근 90일 emails+email_events(open/click/reply)를 단일 거대 CTE로 집계 → recipient_send_time_stats(요일×시간 버킷별 발송·오픈·클릭·답장 카운트) 재구축. 매일 KST 03:20 전역 실행.
추천 (recommendation.service)버킷 통계를 점수화해 scope별 최적 시각 1개 선정 → recipient_send_time_recommendations. 점수 = 발송성과 0.6 + 반응활동 0.4, 업무시간(평일 8–18시)만 후보.
스코프 우선순위lead(샘플≥20) → email_domain(≥100) → country_timezone(≥200) → 부족하면 기본 10:00. 좁은 스코프 우선.
소비처sequences.routes / bulk-enrollment-schedulinggetSendTimeRecommendationsForLeads 로 리드별 추천 시각 조회 → 발송 스케줄에 반영. 미리보기는 previewSendTimeRecommendations.

점수식: 오픈율×0.35 + 클릭율×0.25 + 답장율×0.40 (답장 가중 최대). confidence = 샘플신뢰 0.7 + 점수신뢰 0.3. 도입 #7915 (lsuminl, 2026-05-27). 비활성화 영향: 기존 추천은 계속 사용되고 일일 재학습만 멈춤 — 데이터가 점차 오래될 뿐 발송은 정상.

4권장 수정

근본 해결: worker 분할 + INSERT…SELECT