Sequence Email Worker · Step 2 (verify-email)
2026-04 ~ 2026-05 사이 sequence-email-worker 의 발송 직전 검증 단계는 MV 단발 → 3-tier cascade → cheap 게이트만 세 형태로 두 번 크게 바뀌었다. 각 시기의 개념·호출 흐름·캐시 정책·실패 처리·도입 배경·제거 배경을 한 곳에 정리.
⓪ 2025-12-30 ~ 2026-03-09 · 워커-내 검증 호출의 진짜 최초 — Mohamed Magdy (PR #532) 가 email-sequence-worker-v2.ts 에 verifyEmail() 호출을 처음 넣음. Hunter.io 단일 verify + Hunter.io domain search fallback.
① 2026-03-09 ~ 2026-05-06 · Cheolhee Lee (PR #1978) 가 Hunter.io → MillionVerifier multi-signal pipeline 으로 전면 교체. Redis ev:cache + in-process MX 24h + DB history 30d + KR 도메인 보정 + Option B (API 실패 시 MX fallback). 실패 시 step skip (lead-level dead-end 아님).
② 2026-05-06 ~ 2026-05-21 · Ahmed Mansy (PR #6594) 가 MV → Findymail → Hunter 3-tier cascade 도입 + fail-CLOSED 으로 enrollment 비가역 stop. 이게 사고의 직접 원인.
③ 2026-05-21 이후 (현재) · 이예인 (PR #7787) 가 유료 cascade 전체 제거. 무료 게이트(format/role/dummy/disposable/blacklist/dedup)만 잔존. 책임은 upstream(buyer-search·enrichment·CSV import)으로 이동.
④ 제안 — Hybrid 디자인 · ① 시기 그대로 복원은 비추천. SSOT(lead_contacts.is_verified) 신뢰 + Phase 0~5 보강 + ① 정신(MV multi-signal · fail-OPEN) 을 합친 4-Tier 게이트. 자세히 ↓
이 단계가 sequence 발송 워커에 검증 호출이 처음 들어간 시점. 도입자는 Mohamed Magdy. elysia-server/src/workers/email-sequence-worker-v2.ts (당시 워커 파일명) 에 verifyEmail(leadContact.email) 호출 + Hunter.io domain search fallback 을 같이 넣었다.
Hunter.io 의 단일 verify API 호출. multi-signal pipeline 없음 — DB 발송 이력도 MX 캐시도 KR 도메인 보정도 없다. 이 시기 가장 흥미로운 점은 undeliverable 판정 시 같은 도메인의 다른 주소를 찾아 자동 교체하는 fallback 분기가 있었다는 것.
processSequenceEmailJob
1. leadContact.email 획득
2. verifyEmail(leadContact.email) // hunterio-email-verifier.service
3. result === "undeliverable"
├─ Gemini enrichment 에서 받은 대체 주소 → verifyEmail 재실행 → ok 면 채택
└─ 그래도 없으면: searchDomainAllEmails(domain, 5) (Hunter.io domain search)
└─ 각 후보를 verifyEmail() 로 재검증 → ok 첫 후보 채택
4. 모두 실패 → step skip
5. ok → 발송 진행
WORKER_CONCURRENCY = 10.verifyEmail() 이 null/undefined 를 반환해도 발송이 진행되는 결함이 있었음 (PR #1978 메시지에 명시)..kr, .한국) SMTP 거부율 높아 invalid 오판 다수.76ece44e4 / 8a729c0a0 (2025-12-27, PR #529) · Magdy — hunterio-email-verifier.service 신규 (verify 서비스 자체 도입).283cd0bb6 / 37f4055e2 (2025-12-30, PR #532) · Magdy — email-sequence-worker-v2.ts 에 verifyEmail 호출 + Hunter domain search fallback 도입. ← 워커-내 검증의 최초.13e5dce1b (2026-01-02, PR #561) · chlee — BullMQ 마이그레이션 (sequence-email.worker.ts 로 옮김). 검증 로직 그대로 이관.4d33fae20 (2026-03-03) · chlee — 시퀀스 이메일 발송 전 이메일 포맷 pre-validation 추가.a3b264b92 (2026-03-26, PR #3163) · chlee — "enrichment 이메일 교체 로직 제거 — 반송률 55.5% 원인 차단". ⓪ 시기의 도메인-스왑 fallback 분기 제거.도입자 Cheolhee Lee, PR #1978 (커밋 06e3c47ce). 커밋 메시지 발췌:
"Replace simple Hunter.io-based verification with a robust multi-signal pipeline — MillionVerifier API + DB delivery/bounce history + MX record + Korean domain FP correction + Option B (API failures return MX-based fallback instead of null) + Fix null-passes-as-valid bugs in worker."
Step 2 의 verification 은 MillionVerifier 한 곳만 호출했다. 단, MV 호출 자체가 multi-signal pipeline 으로 래핑되어 있어 호출 전에 Redis 캐시 → DB 발송/바운스 이력 → MX 레코드 순서로 단서를 모으고, 시그널이 충분하면 MV API 를 스킵한다. API 장애 시에는 null 을 던지지 않고 MX-기반 fallback result 를 만들어 downstream 분기를 통일.
verifyAndDedup(toEmail)
└─► verifyEmail(toEmail) // million-verifier.service
1. isValidEmailFormat → null 반환 = format error
2. Redis cache ev:<email> → 히트 시 즉시 반환
3. parallel:
├─ checkDeliveryHistory(email) // emails 테이블 30d 윈도우
└─ checkMxRecords(domain) // DNS, in-process Map 24h
4. history.bounced → undeliverable result (API skip)
history.delivered → deliverable score=99 (API skip, MV 95 보다 높음)
5. !hasMx → undeliverable result (API skip)
6. callMillionVerifierApi(email) // PQueue 144/sec
├─ ok → transformMvResponse
├─ credits ≤ 0 → CreditsExhaustedError throw
├─ 401/403/429/5xx → buildApiFallbackResult(MX 기반)
└─ schema fail / null → buildApiFallbackResult
7. applyKoreanDomainCorrection (.kr/.한국 → invalid 면 risky 완화)
8. cache.set(result)
9. return EmailVerificationResult { status, result, score, … }
└─► verifyAndDedup 의 분기:
result === null → failed (format)
result.result === "undeliverable" → failed
result.result === "risky" && score≤20 → skipped
DUMMY_PATTERNS 매칭 → skipped
!isValidEmailFormat → failed
existingSend dedup → skipped
→ ok
| 레이어 | 저장소 | 키 | TTL | 역할 |
|---|---|---|---|---|
| L1 · 검증 결과 캐시 | Redis | ev:<email> |
config.cache.emailVerification |
verifyEmail 의 최종 result 캐싱. 같은 주소 재발송 시 MV API 호출 0회. |
| L2 · MX 캐시 | 워커 in-process Map |
도메인 (lowercase) | 24h | DNS resolveMx 결과. 워커 재시작마다 콜드. |
| L3 · DB 발송 이력 시그널 | Postgres emails |
to_email |
30d 윈도우 | bounced 1건이라도 있으면 즉시 undeliverable. delivered 있으면 score 99 로 API skip. |
| 요청 throttle | in-process PQueue |
— | — | 144 req/s (MV 한도 160/s 대비 10% safety margin). |
step-level skip 만 한다 — lead-level dead-end 아님. 한 step 이 undeliverable/risky/dummy 로 빠지면
step_executions.status='skipped'/'failed' 만 찍고 다음 step 으로 넘어간다. sequence_enrollments 의 상태에는 손대지 않음.
10minutemail 류도 MV 호출 후에야 차단.// million-verifier.service.ts
export async function verifyEmail(email: string): Promise<EmailVerificationResult | null>
// verify-email.ts (worker step)
export async function verifyAndDedup(
ctx: JobContext,
toEmail: string,
): Promise<{ ok: true } | { ok: false; result: SequenceEmailResult }>
PR #6594 (Ahmed Mansy, lead-discovery 리워크에 묶여서 들어옴) 가 MV → Findymail → Hunter 3단 cascade 를 도입.
각 tier 가 send / catch_all / inconclusive 판정을 내리고, 마지막 tier 까지 합의 실패면 verdict = "block".
가장 큰 변화는 verdict 가 block 일 때 단순 step skip 이 아니라 sequence_enrollments.status = 'stopped', stop_reason = 'unreachable' 으로 enrollment 자체를 비가역 종료한다는 점.
verifyAndDedup(toEmail)
1. isValidEmailFormat (format pre-check, cascade 호출 전)
2. cascade = verifyEmailCascade(toEmail, { caller: "send_time" })
├─ Tier 1: MillionVerifier (consensus cache 우선)
├─ Tier 2: Findymail (catch_all 의심 시 escalate)
└─ Tier 3: Hunter (마지막 합의 시도)
→ verdict = "send" | "skip" | "block"
3. verdict === "block"
├─ step_executions.status = 'skipped'
├─ sequence_enrollments.status = 'stopped'
│ stop_reason = 'unreachable'
│ stop_note = 'verification_cascade:<blockReason>'
└─ 동일 enrollment 다시 못 깨움 ← fail-CLOSED 비가역
4. dummy / dedup gate (기존 그대로)
도입 2일 만에 Hojin Kang 이 cascade 위에 무료 보강 5단계를 올림. 유료 API 추가 없이 약점 제거 목적.
| Phase | 내용 | 위치 | Default |
|---|---|---|---|
| 0 | Shadow telemetry — pre-gate 적중률 + inconclusive-block 비율 측정 | verification-cascade.service | always on |
| 1 | pre-cascade 게이트 재정렬 (role/dummy/blacklist defense-in-depth) | verify-email.ts | dummy/blacklist on · role off |
| 2 | 정적 disposable 블록리스트 (vendored 5,438 도메인 + suffix match) | email-validation.util | off (shadow 후 flip) |
| 3 | cascade single-flight + 30s total timeout | verification-cascade.service | on |
| 4 | MX 캐시를 Redis 로 이관 (24h+/1h-) + A/AAAA implicit-MX fallback + LRU front-cache | million-verifier.service | on |
| 5 | circuit breaker 분산화 (Redis hydrate / async write-through) | verification-cascade.service | off |
| 레이어 | 저장소 | 키 | TTL | 비고 |
|---|---|---|---|---|
| 검증 결과 캐시 | Redis | ev:<email> | config | MV 단발 시기와 동일 |
| Cascade consensus 캐시 | Redis | cascade-level 키 | — | provider 합의 결과 재사용 (재방문 시 0 quota) |
| MX 캐시 | Redis (Phase 4) + LRU front-cache | 도메인 | 24h hit / 1h miss | 워커 재시작 콜드 해소 |
| Circuit breaker 상태 | 워커 in-process (Phase 5 미flip) | provider | — | 분산화 deferred |
| Rate limit | provider 별 PQueue (in-process) | — | — | distributed limiter deferred |
Symptom. 캠페인이 자동으로 stopped + 모든 step skipped. DB 에 stop_reason='unreachable', stop_note='verification_cascade:all_tiers_inconclusive_or_unavailable'.
| 증거 | 내용 |
|---|---|
| Hunter | HTTP 429 "billing period limit 소진". 콘솔 11,877 / 10,000 (연간 quota, reset 2027-05-15) |
| Findymail | HTTP 402 "Not enough credits" (verifier_credits: 0) |
| 폭주 주범 | beta 워크스페이스 "YS Medi" 가 5/15 하루 20,242건 발송 (평소 1,000~1,500/일). 21,498건 중 고유 수신자 21,204 → 98.6% 캐시 미스 → cascade 거의 전부 풀 실행 → Findymail 소진 → Hunter 로 쏠려 연 한도를 하루에 소진 |
| 키 공유 | Hunter/Findymail/MV 키가 alpha · beta 공용 → 한도 합산 차감, 양쪽 모두 fail-CLOSED |
| valid 인데 막힘 | 한 enrollment 가 07:39:01 stop → 3분 52초 뒤 같은 이메일이 다른 enrollment 에서도 동일 차단. quota 회복 전까지 valid 도 영구 차단 |
PR #7787 (이예인) 가 send-time cascade 호출 자체를 제거. 발송 직전에는 무료·결정론적 휴리스틱만 돌고, paid verification 책임은 buyer-search · on-demand enrichment · CSV import 의 upstream 으로 이동. 실패 처리는 다시 step-level skip으로 회귀 — enrollment 비가역 stop 분기 완전 제거.
verifyAndDedup(toEmail)
1. isValidEmailFormat → failed
2. roleBlock (isUndeliverableEmail) → skipped
3. dummyBlock (jane.doe/test/...) → skipped
4. disposableBlock (isDisposableDomain) → skipped
5. blacklistRedundant (Redis/DB) → skipped
// step 1.5 와 step 2 사이 ms-window 방어
6. existingSend dedup → skipped
→ ok
// paid cascade 호출 없음
| 검증 시점 | 주체 | 대상 | 실패 처리 |
|---|---|---|---|
| buyer-search | lead-discovery cascade | discovery 결과 | row drop, lead 미생성 |
| on-demand enrichment | lead-on-demand-enrich.worker.ts | enrich 결과 | block-and-drop, lead 미persist |
| CSV import | lead-import.service.ts | 업로드 배치 | MV batch 로 undeliverable/risky row 사전 reject |
| send-time (worker step 2) | verifyAndDedup | 모든 outbound | 무료 게이트만. step-level skip. |
| 레이어 | 저장소 | 키 | TTL | 역할 |
|---|---|---|---|---|
| Bounce blacklist | Redis + DB fallback | — | step 1.5 (resolve-lead) + step 2 (verify-email) 양쪽에서 조회 | |
| Bounce record | Postgres email_bounces | — | step 1.5 의 보조 시그널 | |
| Disposable domains | vendored JSON | 도메인 | 분기별 수동 갱신 | Phase 2 산물 — cascade 제거 후에도 잔존 |
| Dedup index | Postgres emails unique on (sequence_id, step_id, to_email) | — | — | 같은 step 안 중복 발송 방지 |
verifyAndDedup(_isContactVerified = true, _contactMetadata = null) — PR #7760 에서 추가된 verified-skip 분기의 underscore 처리된 인자. cascade 호출 자체가 없어져 무의미.index.ts:6 헤더 주석에 여전히 "MillionVerifier multi-signal email verification (DB history + MX + MV API)" 라고 적혀 있음 — out-of-date.resolve-lead.ts:32 주석 "extra send-time MV gate (see verify-email.ts)" — out-of-date.| 항목 | ⓪ Hunter.io (12-30 ~ 03-09) | ① MV 단발 (03-09 ~ 05-06) | ② 3-tier cascade (05-06 ~ 05-21) | ③ 현재 (05-21~) |
|---|---|---|---|---|
| 도입자 | Mohamed Magdy (#529/#532) | Cheolhee Lee (#1978) | Ahmed Mansy (#6594) | 이예인 (#7787) |
| Paid provider | Hunter.io 1개 | MV 1개 | MV → Findymail → Hunter | 없음 |
| Send-time API 호출 | 매번 1회 (캐시 없음) | 최대 1회 (cache miss 시) | 최대 3회 (tier escalation) | 0회 |
| DB 발송 이력 시그널 | ✕ | ✓ 30d 윈도우 | ✓ (계승) | (N/A — cascade 자체 없음) |
| MX 캐시 | ✕ | 워커 in-process Map · 24h · 콜드 스타트 위험 | Phase 4: Redis 이관 · 24h hit / 1h miss + LRU | (cascade 제거로 미사용) |
| Result 캐시 | ✕ | Redis ev:<email> |
Redis + cascade consensus | (cascade 제거로 미사용) |
| KR 도메인 보정 | ✕ | ✓ .kr/.한국 → risky 완화 | ✓ (계승) | (N/A) |
| API 장애 시 | null (silent send-through 버그) | Option B — MX-based fallback result | provider escalation + circuit | (N/A) |
| undeliverable 시 이메일 교체 | ✓ 같은 도메인 다른 주소 swap (반송률↑ 원인) | ✕ (PR #3163 에서 제거) | ✕ | ✕ |
| Format check 위치 | cascade 뒤 | cascade 뒤 | cascade 앞 (Ahmed 가 옮김) | 앞 |
| Role 차단 | ✕ | ✕ | Phase 1 (default off) | ✓ roleBlock flag |
| Disposable 차단 | ✕ | ✕ | Phase 2 vendored 5,438 도메인 (default off) | ✓ disposableBlock flag |
| Dummy 차단 | ✕ | inline 상수 (PR #3951) | Phase 1 flag 화 | ✓ dummyBlock flag |
| Blacklist 재검사 | ✕ | step 1.5 1회만 | step 1.5 1회만 | ✓ blacklistRedundant (1.5 + 2) |
| Single-flight | ✕ | ✕ (중복 호출 가능) | Phase 3 | (N/A) |
| Total timeout | ✕ | ✕ (워커 stall 가능) | Phase 3 — 30s | (N/A) |
| Circuit breaker | Hunter 429 (chlee 2026-02-26 추가) | MV credits-exhausted throw | Phase 5 (default off, in-process) | (N/A) |
| 실패 시 enrollment | step skip + email 교체 시도 | step skip만 (lead-level dead-end 아님) | stopped (unreachable, 비가역) | step skip만 |
| 검증 책임 위치 | send-time | send-time | send-time + upstream 일부 | upstream (buyer-search · enrich · import) |
① 시기에는 캐시 미스 페널티가 API 호출 1회 + KR 도메인 보정 수준이라 단순했다. ② 시기에는 미스가 최악 3회 호출로 폭증하고, 각 provider 가 독립 quota / rate limit / circuit / consensus 를 가지면서 캐시도 그에 맞게 다층화됐다:
그런데 캐시 다층화가 인시던트를 막진 못했다. YS Medi 가 하루 20k 신규 수신자 (98.6% cache miss) 를 보낸 시점에 모든 캐시는 무용지물 — 그게 발송 패턴의 자연스러운 특성이라는 게 핵심. 캐시는 재방문을 싸게 하는 도구지, 신규 폭주를 막는 도구가 아니다.
현재는 send-time cache 가 거의 사라졌다. 이유: 검증 자체가 send-time 에서 사라졌기 때문. 대신 upstream 의 검증 결과를 SSOT 인 DB(lead_contacts.is_verified) 에 영속시키고, send-time 은 그 신뢰를 그대로 받는 구조. 즉
이로 인해 send-time 의 cache pollution / single-flight / circuit-breaker / quota 관리 부담이 모두 사라졌다.
질문: "phase 1 (MV 단발, 2026-03-09 ~ 2026-05-06) 의 형태로 발송워커에 다시 추가하는 게 맞나?"
결론: ① 시기 그대로 복원은 비추천. 그 시기에도 6주 뒤 #6947 가 정리한 약점이 있었고, 현재는 더 좋은 자산(Phase 4 Redis MX · disposable JSON · single-flight · SSOT)이 이미 있다. Hybrid (D) 가 최적.
| 약점 (#6947 회고) | 그대로 복원 시 영향 |
|---|---|
| 워커 재시작 시 in-process MX 캐시 손실 | DNS 폭주 — 재시작 후 첫 N분 모든 도메인 cold lookup |
| dummy/role 주소가 cache miss 때 MV 까지 진입 | 무료로 막을 걸 quota 낭비 |
| 정적 disposable 블록리스트 없음 | 10minutemail 류도 MV API 호출 |
| MX → A/AAAA RFC 5321 fallback 누락 | MX 없는 정상 도메인 false-negative |
| 동일 이메일 동시 검증 single-flight 부재 | 같은 이메일 N개 enrollment 동시 처리 시 N회 중복 호출 |
| API wallclock 무제한 | MV hang 시 워커 stall |
| SSOT 부재 | upstream 검증 결과(lead_contacts.is_verified)를 신뢰할 채널 없음 — 매 발송이 MV 진입 (그 시기엔 upstream 자체가 없었으니 자연스러움) |
| 자산 | 출처 | 베타 상태 (2026-05-30 실측) |
|---|---|---|
| Phase 4 Redis MX 캐시 (24h hit / 1h miss + LRU) | #6947 | mx:v1:* 396 키 활성 · enrich-side 호출이 채우는 중 |
| Phase 2 disposable 블록리스트 (5,438 도메인) | #6947 | code 잔존, default flag true |
| Phase 3 single-flight + 30s wallclock timeout | #6947 | flag default on, verifyCascade.singleFlight=true |
| Cheap 게이트 4종 (role/dummy/disposable/blacklist) | #6947 + #7787 | default true 운영 중 (verifyPreflight.*) |
lead_contacts.is_verified SSOT | enrichment 워커 | upstream 이 채우는 중 |
verifyAndDedup(_isContactVerified, _contactMetadata) legacy 인자 | PR #7760 | underscore 인자 그대로 — 무수정 wiring 자리 |
| Format pre-check 위치 (cascade 앞) | PR #6594 | 이미 정리됨 |
bounce:bl:* blacklist | bounce-check.service | 3,953 키 활성 |
| cascade 인프라 (MV / Findymail / Hunter) | PR #6594 + #6947 | 전부 import 가능 · verifyEmail() export 유효 |
호출 비용 중 · 안전성 중 · 사고 재발 낮 · bounce ↓
SSOT 미활용. 매 발송 MV 진입.
호출 비용 매우 높 · 안전성 매우 낮
사고 재발 위험 매우 높음 — YS Medi 패턴.
호출 비용 0 · 안전성 높
upstream 검증 정확도에 100% 의존.
호출 비용 저-중 · 안전성 매우 높 · 사고 재발 매우 낮
SSOT skip + ① 정신 + Phase 0~5 보강.
설계 원칙 3개:
is_verified=true 이면 send-time MV 호출 자체 스킵.verifyAndDedup(toEmail, isContactVerified, contactMetadata)
─── Tier 0 (free, instant) ─────────────────────── ← 이미 운영 중
1. isValidEmailFormat → failed
2. roleBlock → skipped (flag)
3. dummyBlock → skipped (flag)
4. disposableBlock → skipped (flag, vendored 5,438 도메인)
5. blacklistRedundant → skipped (Redis bounce:bl:*)
6. existingSend dedup → skipped
─── Tier 1 (SSOT trust) ────────────────────────── ← 신규 · PR #7760 분기 활용
7. isContactVerified === true
&& !contactMetadata?.isLastResort
&& verifiedAt > now - VERIFY_TRUST_TTL_DAYS (기본 30d)
→ MV 호출 SKIP, 발송 진행
─── Tier 2 (MV multi-signal, fail-OPEN) ────────── ← ① 정신 + Phase 0~5 보강
8. verifyEmail(toEmail) // million-verifier.service
├─ Redis ev:cache (①)
├─ DB delivery/bounce history 30d (①)
├─ Redis mx:v1: 24h/1h + A/AAAA fallback (Phase 4)
├─ MV API + KR 도메인 보정 + Option B MX (①)
├─ single-flight + 30s wallclock (Phase 3)
│
├─ CreditsExhausted / MV_AUTH_ERROR
│ → fail-OPEN: 발송 진행 + 알람만
├─ result.undeliverable → step skip (enrollment 보존)
└─ result.risky && score ≤ 20 → step skip
→ ok → 발송
| 항목 | A · ① 그대로 | D · Hybrid |
|---|---|---|
| Tier 1 SSOT skip | ✕ 모든 발송이 MV 진입 | ✓ verified contact 는 호출 자체 안 함 |
| MX 캐시 영속성 | in-process Map 24h (cold-start 위험) | Redis 24h/1h + LRU + A/AAAA fallback |
| Disposable 사전 차단 | ✕ MV 도달 후 차단 | ✓ vendored 5,438 도메인 |
| Single-flight | ✕ 중복 호출 가능 | ✓ Phase 3 |
| Total timeout | ✕ 워커 stall 가능 | ✓ 30s |
| Stale 검증 정책 | (해당 없음) | ✓ VERIFY_TRUST_TTL_DAYS 경과 시 재검증 |
| 환경별 API 키 분리 | (해당 없음) | ✓ alpha/beta 키 분리 (사고 직접 원인 회피) |
| Workspace quota | (해당 없음) | ✓ 워크스페이스별 MV 호출 quota |
| Shadow / canary | (해당 없음) | ✓ Phase 0 telemetry + workspace allowlist flag |
infisical 에 환경별 secret 등록.VERIFY_TRUST_TTL_DAYS (기본 30) — enrichment 이후 30일 경과 시 재검증.VERIFY_SEND_TIME_ENABLED flag — 워크스페이스 allowlist 로 카나리.CreditsExhaustedError 가 throw 가 아니라 catch + warn + 발송 진행이어야 함 (① 시기엔 throw — 워커 발송 중단). 이 한 줄이 가장 중요한 사고 방지 코드.중요한 자기 점검: W1 단계에서 verified=true contact 의 bounce 율이 이미 충분히 낮다면 Tier 2 자체를 추가하지 않는 게 정답이다. 즉 "MV 단발 복원" 은 가능 하지만 필요 한지가 데이터로 검증돼야 함. shadow 가 그 검증 도구.
bull:sequence-email delayed 큐 24,229 jobs 대기. 최악 가정 (모두 verified=false + cache miss + Tier 1 skip 무력) 도 MV PQueue 144/s 한도로 168초 분량 — 부담 작음.ev:* 0개 → 활성화 시 첫 1주는 cold-start 비용 발생. 이후 동일 수신자 재발송은 cache hit 으로 free.mx:v1:* 396개 이미 채워져 있어 MX lookup 비용 ≈ 0.is_verified=true 를 잘 채우는 한.| 날짜 | 커밋 | 작성자 | 변경 |
|---|---|---|---|
| 2025-12-27 | 76ece44e4 · 8a729c0a0 (PR #529) | Mohamed Magdy | hunterio-email-verifier.service 신규 — Hunter.io verify 서비스 도입 |
| 2025-12-30 | 283cd0bb6 · 37f4055e2 (PR #532) | Mohamed Magdy | 워커-내 검증 호출의 최초 — email-sequence-worker-v2.ts 에 verifyEmail() + Hunter domain search fallback |
| 2026-01-02 | 13e5dce1b (PR #561) | Cheolhee Lee | BullMQ 마이그레이션 — sequence-email.worker.ts 로 이관. 검증 로직 따라옴 |
| 2026-02-26 | 5b37ed554 | Cheolhee Lee | Hunter.io 429 circuit breaker 도입 |
| 2026-03-03 | 4d33fae20 | CheolheeLee0 | 시퀀스 이메일 발송 전 이메일 포맷 pre-validation 추가 |
| 2026-03-09 | 9b866b960 (PR #1976) | Cheolhee Lee | MillionVerifier API key config 추가 |
| 2026-03-09 | 06e3c47ce (PR #1978) | Cheolhee Lee | Hunter.io → MillionVerifier multi-signal pipeline 전면 교체 (① 단계 시작점). DB history + MX + KR 보정 + Option B fallback + null-as-format-error 픽스 |
| 2026-03-25 | aba15ded7 (PR #3021) | Cheolhee Lee | MV 크레딧 고갈 시 에러 전파 — 워커 무한 fallback 방지 |
| 2026-03-26 | a3b264b92 (PR #3163) | Cheolhee Lee | enrichment 이메일 교체 로직 제거 — 반송률 55.5% 원인 차단 (⓪ 시기 도메인-스왑 fallback 종료) |
| 2026-03-31 | e8fb6b400 | Cheolhee Lee | refactor — sequence-email worker 1641줄 → 12개 모듈 분리. steps/verify-email.ts 가 이 시점에 생김 |
| 2026-04-06 | e32527ae5 (PR #3951) | Cheolhee Lee | risky 저점수 이메일 + DUMMY_PATTERNS 발송 사전 차단 |
| 2026-04-09 | 0bc3abc7e | jaykim-cmd | risky threshold 30 → 20 |
| 2026-04-22 | 55943b980 | Cheolhee Lee | skipped/failed 종료 경로 정리 |
| 2026-05-06 | 8abafd828 (PR #6594) | Ahmed Mansy | 3-tier cascade 도입 + fail-CLOSED stop (② 단계 시작점) |
| 2026-05-08 | 355774476 (PR #6947) | Hojin Kang | Phase 0~5 신뢰성 강화 |
| 2026-05-21 | f99670d4f (PR #7760) | lsuminl | verified contact send-time skip 분기 |
| 2026-05-21 | 8485f96b3 (PR #7787) | 이예인 | cascade 호출 자체 제거 (③ 단계 — 현재) |