마스킹-스킵 복구 — DB · BullMQ · Redis 3계층 처리
beta · 마스킹으로 skip된 발송 복구 · 2026-06-04
한 줄 결론
3계층 모두 스크립트(+loader)로 처리됩니다. 단 BullMQ는 직접 넣지 않고 DB를 고치면 loader가 자동 전파합니다. 스크립트의 BullMQ/Redis 역할은 "stale 정리"뿐(보통 no-op).
1 계층별 역할
| 계층 | 해야 할 일 | 처리 주체 | 스크립트 역할 |
|---|---|---|---|
| DB SSOT | execution skipped/failed→pending + 누적 재스케줄 · enrollment 리셋 · sequence active | 스크립트(SQL) | ✅ 직접 |
| BullMQ 큐 | pending → 잡 적재해 발송 | loader 워커가 DB 보고 자동 생성 | ⚠️ 직접 push X · stale 잡만 제거 |
| Redis 마커 | seq-email:sent:<id>(2h) 재발송 차단 | — | ⚠️ stale 마커만 제거(보통 없음) |
2 핵심 원리 — loader가 DB→BullMQ 다리
스크립트: DB(execution pending + scheduled_at) 수정 ← SSOT
↓ loader 30초 tick (s.status=active & e.status=active & scheduled_at<cutoff)
BullMQ: seq-email-<execId> 잡 자동 생성 ← 스크립트가 직접 push 안 함
↓ worker 처리 → cascade 재검증 → 발송 (나쁜 주소 자동 skip)
스크립트가 BullMQ에 직접 enqueue 하면 loader 우회 = 깨짐. DB(SSOT)만 바꾸면 loader가 전파. 이게 올바른 아키텍처.
3 계층별 상세
DB 스크립트가 직접 (authoritative)
-- (1) 스냅샷 (2) 누적 재스케줄 WITH s AS ( SELECT e.id, (date_trunc('day',now()) + (sum(st.delay_days) OVER(ORDER BY e.step_order))*interval '1 day' + coalesce(st.scheduled_hour,10)*interval '1h') AS sched FROM sequence_step_executions e JOIN sequence_steps st ON st.id=e.step_id WHERE e.enrollment_id=$1) UPDATE sequence_step_executions e SET status='pending', error_message=NULL, executed_at=NULL, scheduled_at=s.sched FROM s WHERE e.id=s.id; -- (3) enrollment 리셋 UPDATE sequence_enrollments SET current_step_order=0, status='active', first_email_sent_at=NULL, stopped_at=NULL WHERE id=$1; -- (5) sequence resume UPDATE sequences SET status='active' WHERE id=$seq;
BullMQ loader 자동 · 스크립트는 stale만 제거(조건부)
마스킹-스킵 건은 skip 시 잡 완료+removeOnComplete(1h)로 이미 제거됨(파일럿 0 확인). 잔존 시에만 제거:
-- 조건부: 대상 execId 잔존 잡 제거 (있을 때만) EVAL "redis.call('DEL','bull:sequence-email:seq-email-'..id)" ...
Redis stale 마커 제거(보통 없음)
스킵 건은 발송된 적 없어 seq-email:sent:<id> 마커 없음. 최근 발송 재시도 시에만 clearSentMarker 필요.
4 정지/재개 — sequence-level만 (enrollment 금지)
| 작업 | 방식 | 부작용 |
|---|---|---|
| 시퀀스 일시정지/재개 | UPDATE sequences SET status | 없음 ✅ 순수 SQL (worker self-handle) |
| enrollment status 변경 | 앱 updateEnrollmentStatusWithSync | ⚠️ BullMQ 잡취소 + pending→skip → raw SQL 부적합 |
정지는 반드시 sequence 단위. enrollment-level을 raw SQL로 흉내내면 앱과 달라 잡·pending 불일치 발생.
5 복구 대상 & 안전망
| 대상 | 마스킹 skipped 6,130 + failed 954 중 복구 이메일 보유분 |
| cadence | 누적 재스케줄(reEnroll의 now+delay 평면 버그 회피) |
| 안전망 | 발송 직전 cascade 재검증(나쁜 주소 자동 skip) + 계정별 throttle |
| 롤백 | execution 스냅샷 + lead_contacts_mask_backup_20260604 |
결론
DB·BullMQ·Redis 3계층 모두 스크립트로 처리. ① BullMQ는 DB 수정→loader 자동 전파(직접 push X) ② 정지/재개는 sequence-level ③ 누적 재스케줄 + 발송직전 재검증.
실행: 파일럿 1건(스테이징=시퀀스 paused 유지 → 검증 → active 발송) → 버킷 확장.
실행: 파일럿 1건(스테이징=시퀀스 paused 유지 → 검증 → active 발송) → 버킷 확장.