마스킹 이메일 — 종합 조치계획 (누락 없이)
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 exec | pending (미래 예약) | 9,971 (future 9,459) | 복구 시 자동 발송 대상 |
| DB exec | skipped (마스킹) | 6,124 | terminal · 자동 재발송 안 됨 |
| DB exec | failed (마스킹) | 954 | reEnroll 공식경로 가능 |
| BullMQ | delayed ZSET | 216,066 | 미래 예약 잡(우리 9,459 포함) — 손대지 않음 |
| BullMQ | wait / active / failed | 0 / 1 / 0 | 적체 없음 · 정상 |
| Redis | sent marker (2h TTL) | 598 | 옛 skip엔 무관 · 자연만료 |
| Redis | 옛 skip job | 제거됨 | removeOnComplete(1h) → dedup 차단 없음 |
"다시 예약 걸어야 하나?" — 버킷별 결론
| 버킷 | 규모 | 재예약? | 처리 |
|---|---|---|---|
| 미래 예약메일 (active future pending) | 9,459 | 불필요·자동 | DB pending + BullMQ delayed에 살아있음 → 이메일만 복구하면 scheduled_at에 정상 발송 |
| stopped/unreachable | 2,139 (복구가능 1,628) | 재활성화 | status stopped→active + 미래 step 복구 / reEnroll |
| skip된 과거 발송 | 6,124 (step1 4,133) | 정책결정 | 기본 재발송 안 함 권장(스테일·버스트). 보내려면 scheduled_at 미래로 stagger 재예약 |
| failed (마스킹) | 954 | reEnroll | reEnrollFailedEnrollments() 공식 경로 (failed 전용) |
| completed 52 / bounced 442 / account_removed 280 | 774 | 제외 | 마스킹 무관(completed=0 masked-skip) → 조치 없음 |
발송 임박도 = 복구 시급도
| 구간 | overdue/즉시 | ~6h | ~24h | ~7d | 7d+ |
|---|---|---|---|---|---|
| active 마스킹 예약메일 | 512 | 141 | 384 | 3,267 | 5,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)
- DB —
masked=0· overdue pending 정상 발송 모니터 · 중복 sent 0 - BullMQ — wait/active 적체 없는지 · delayed가 자연 소진되는지 (재적재한 잡 fire 확인)
- Redis — sent marker 2h 자연만료 · 재발송 시 marker 잔존으로 막히면
clearSentMarker(execId) - 영속 방어 —
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는 복구 최우선으로 손실 방지.