Action Plan · DB · BullMQ · Redis

마스킹 이메일 — 종합 조치계획 (누락 없이)

beta · 마스킹 연락처 11,958 · 영향 enrollment ~5,797 · 2026-06-04

현재 상태 스냅샷

Enrollment (마스킹 primary)

2,851
active
2,419
stopped
442
bounced
52
completed
33
paused

Execution / BullMQ / Redis

레이어지표의미
DB execpending (미래 예약)9,971 (future 9,459)복구 시 자동 발송 대상
DB execskipped (마스킹)6,124terminal · 자동 재발송 안 됨
DB execfailed (마스킹)954reEnroll 공식경로 가능
BullMQdelayed ZSET216,066미래 예약 잡(우리 9,459 포함) — 손대지 않음
BullMQwait / active / failed0 / 1 / 0적체 없음 · 정상
Redissent marker (2h TTL)598옛 skip엔 무관 · 자연만료
Redis옛 skip job제거됨removeOnComplete(1h) → dedup 차단 없음

"다시 예약 걸어야 하나?" — 버킷별 결론

버킷규모재예약?처리
미래 예약메일 (active future pending)9,459불필요·자동DB pending + BullMQ delayed에 살아있음 → 이메일만 복구하면 scheduled_at에 정상 발송
stopped/unreachable2,139
(복구가능 1,628)
재활성화status stopped→active + 미래 step 복구 / reEnroll
skip된 과거 발송6,124
(step1 4,133)
정책결정기본 재발송 안 함 권장(스테일·버스트). 보내려면 scheduled_at 미래로 stagger 재예약
failed (마스킹)954reEnrollreEnrollFailedEnrollments() 공식 경로 (failed 전용)
completed 52 / bounced 442 / account_removed 280774제외마스킹 무관(completed=0 masked-skip) → 조치 없음

발송 임박도 = 복구 시급도

구간overdue/즉시~6h~24h~7d7d+
active 마스킹 예약메일5121413843,2675,747
overdue 512건은 다음 loader tick(30초)에 발송 시도 → 복구 전이면 또 skip(영구 손실). 따라서 ① 복구를 최우선으로 하거나 ② 이 512건 enrollment를 잠시 paused로 두고 복구 후 재개. 나머지 8,998건(>24h)은 여유.

0 누수 멈춤 확정 (선행 게이트)

#8138(beta) 배포 완료(05:03 UTC). 복구 전 신규 마스킹 유입 0 확인 — 안 그러면 복구분이 재오염.

-- 배포 후 신규 마스킹 = 0 이어야 진행
SELECT count(*) FROM lead_contacts
WHERE contact_type='email' AND contact_value LIKE '%***%'
  AND created_at > '2026-06-04 05:03:00+00';

1 백업 ✅ 완료

lead_contacts_mask_backup_20260604 (11,958행). 추가 권장: 변경 전 enrollment·execution 상태도 스냅샷.

CREATE TABLE seq_recovery_backup_20260604 AS
SELECT en.id enr_id, en.status enr_status, en.stop_reason, en.current_step_order,
       e.id exec_id, e.status exec_status, e.scheduled_at, e.error_message
FROM sequence_enrollments en
JOIN sequence_step_executions e ON e.enrollment_id=en.id
WHERE en.lead_id IN (SELECT lead_id FROM lead_contacts WHERE contact_value LIKE '%***%');

2 이메일 원본 복구

워터폴 — T1 원본공존 6,903(삭제+primary승격) · T2 discovery유일 1,935(제자리교체) · T2b 모호 366 · T3 원본없음 2,739(재enrich/비활성). 상세 SQL = 복구계획 페이지.

복구 후 검증: contact_value LIKE '%***%' 가 0(또는 T3 비활성 잔량만). primary 유일성 재확인.

3 예약메일 — 자동 정상화 조치 최소

이메일 복구 → execution.status pending 그대로 → loader 30초 tick → WHERE status='pending' AND scheduled_at < cutoff 감지 → BullMQ 적재(jobId seq-email-<execId>, idempotent) → 정상 발송

BullMQ delayed 216k은 손대지 않음 — 각 잡이 scheduled_at에 fire하며 그때 복구된 원본으로 검증 통과. 재예약/재적재 불필요.

overdue 512 + 6h 141 = 653건은 복구 전에 또 skip될 수 있음 → 복구 최우선 또는 해당 enrollment 임시 pause.

4 재활성화 — stopped/unreachable

2,139건 중 원본 복구가능 1,628만 대상. stop은 skip 게이트가 아니라 업스트림 unreachable 누적이 트리거 — 복구로 contactable 해지면 재개 안전.

-- 복구된 lead 의 stopped/unreachable → active 복귀
UPDATE sequence_enrollments SET status='active', stop_reason=NULL, stopped_at=NULL
WHERE status='stopped' AND stop_reason='unreachable'
  AND lead_id IN (-- 복구되어 비마스킹 email 보유한 lead
    SELECT lead_id FROM lead_contacts WHERE contact_type='email' AND contact_value NOT LIKE '%***%');
-- 중단 시 skipped 된 미래 step 은 Phase 5 패턴으로 pending 복구(또는 reEnroll)

5 재발송 — skip된 과거발송 정책 결정

일괄 재발송 비권장. 6,124건(step1 4,133)을 한 번에 pending으로 되돌리면 과거 콜드메일이 즉시 버스트 발송 → 발송속도 폭증·평판 하락·스테일 메시지.

보내야 한다면 (안전 패턴)

-- ① scheduled_at 을 미래로 stagger 재설정(버스트 방지) + status 복구
UPDATE sequence_step_executions SET
  status='pending', error_message=NULL,
  scheduled_at = now() + (row_number() OVER(ORDER BY id) * interval '20 seconds')
WHERE status='skipped' AND error_message LIKE '%***%'
  AND enrollment_id IN (/* 선별: 최근 1~2일 + 복구완료 */);
-- 옛 BullMQ job 제거됨 + sent marker 없음 → 재적재 정상. currentStepOrder 손대지 말 것(워커 자동진행)
-- ② failed(마스킹) 954건은 공식 reEnrollFailedEnrollments() 사용

권장: 과거 skip은 재발송하지 말고, 복구로 미래 발송만 정상화. 필요 시 최근 건만 선별 stagger.

6 정합·검증 (DB·BullMQ·Redis)

  1. DBmasked=0 · overdue pending 정상 발송 모니터 · 중복 sent 0
  2. BullMQ — wait/active 적체 없는지 · delayed가 자연 소진되는지 (재적재한 잡 fire 확인)
  3. Redis — sent marker 2h 자연만료 · 재발송 시 marker 잔존으로 막히면 clearSentMarker(execId)
  4. 영속 방어ALTER TABLE lead_contacts ADD CONSTRAINT … CHECK(contact_value NOT LIKE '%***%') NOT VALID (복구 후 VALIDATE)

실행 순서 요약

0 누수멈춤 확정(신규=0) → 1 백업(완료) + exec/enr 스냅샷 → 2 이메일 복구(T1·T2) ──┬─→ 3 예약메일 9,459 자동 정상화 (재예약 불필요) ├─→ 4 재활성화 1,628 (stopped→active) └─→ 5 재발송 = 정책결정 (기본 안 함 / 보내면 stagger) → 6 DB·BullMQ·Redis 검증 + CHECK 제약
핵심: 미래 예약메일은 재예약 불필요 — 살아있어 이메일만 고치면 자동. 별도 조치가 필요한 건 stopped 재활성화(1,628)선택적 과거 재발송뿐. overdue 512는 복구 최우선으로 손실 방지.