Recovery Architecture · 3-Layer

마스킹-스킵 복구 — DB · BullMQ · Redis 3계층 처리

beta · 마스킹으로 skip된 발송 복구 · 2026-06-04

한 줄 결론

3계층 모두 스크립트(+loader)로 처리됩니다.BullMQ는 직접 넣지 않고 DB를 고치면 loader가 자동 전파합니다. 스크립트의 BullMQ/Redis 역할은 "stale 정리"뿐(보통 no-op).

1 계층별 역할

계층해야 할 일처리 주체스크립트 역할
DB SSOTexecution skipped/failed→pending + 누적 재스케줄 · enrollment 리셋 · sequence active스크립트(SQL)✅ 직접
BullMQpending → 잡 적재해 발송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 발송) → 버킷 확장.