본문으로 건너뛰기

Solapi Dispatch Webhook State

SMS send가 provider boundary를 넘었고, 실패 지점이 dispatch claim, provider result persistence, webhook binding, tenant-scoped history update일 때 이 가이드를 사용합니다.

더 넓은 rollout 서사는 Solapi SMS 회고에 있습니다. 이 문서는 운영 runbook입니다.

증상

  • SMS는 Solapi를 통해 발송된 것처럼 보이지만 Lumie history가 PENDING 또는 DISPATCHED에 머뭅니다.
  • webhook request는 200을 반환하지만 recipient delivery state가 갱신되지 않습니다.
  • webhook event가 ignored로 기록됩니다.
  • retry attempt가 같은 message를 계속 claim하지만 durable terminal state가 없습니다.
  • mock provider에서는 되던 live-provider send가 전환 후 실패합니다.

알려진 실패 이력

Commit실패 모드지속되는 수정
afab0183live provider dispatch에 provider id, recipient binding, webhook hardening이 필요했습니다.provider group/message tracking, binding validation, test를 추가했습니다.
4de6a6c7REQUIRES_NEW dispatch transaction이 RLS tenant binding을 잃었습니다.dispatch transaction 내부에서 RlsTenantTransactionBinder를 호출합니다.
01ad30b2Solapi webhook report가 provider delivery state를 완전히 반영하지 못했습니다.webhook application이 dispatch state와 recipient status를 갱신합니다.
e73277f2이미 적용된 SMS migration이 실수로 편집됐습니다.historical migration을 복원하고 forward-only로 이동했습니다.
04aae48c후속 SMS index migration이 이미 배포된 환경에 안전해야 했습니다.V77은 transactional no-op이 되었고 V76이 provider column과 index를 소유합니다.

현재 런타임 계약

표면소스 경로필요한 동작
Dispatch claimmodules/notification/src/main/java/com/lumie/notification/application/service/SmsDispatchService.java짧은 tenant-bound transaction에서 pending message를 claim합니다.
Provider sendSolapiSmsProviderAdapterdatabase transaction 밖에서 send하고 provider group/message id를 반환합니다.
Webhook entrySolapiSmsWebhookControllerX-Solapi-Secret을 검증하고 Lumie custom field를 parse하며, 알려진 invalid event만 ignore합니다.
Webhook applicationSmsWebhookService, SmsDispatchService.applyWebhookReport(...)tenant context를 복원하고 message, provider group, recipient index, provider message id를 검증합니다.
Migration stateV76__sms_provider_dispatch_tracking.sql, V77__sms_provider_dispatch_tracking_indexes.sqlprovider tracking은 V76에 있고 V77은 의도적인 no-op입니다.

진단

  1. 실패가 provider send 전인지, provider result 뒤인지, webhook application 중인지 식별합니다.
  2. SmsMessage aggregate state를 확인합니다. provider, provider group id, dispatch attempts, recipients, per-recipient provider message id를 봅니다.
  3. log에서 SMS after-commit dispatch failed, provider send error, Ignored SOLAPI webhook event를 확인합니다.
  4. webhook에 lumieTenant, lumieSmsId, lumieRecipient custom field가 있는지 확인합니다.
  5. webhook messageId가 recipient의 provider message id와 일치하고, group id가 있으면 message provider group id와 일치하는지 확인합니다.
  6. dispatch transaction이 RlsTenantTransactionBinder.bindRequiredTenantToCurrentTransaction()을 호출하는지 확인합니다.
  7. schema drift가 의심되면 V76V77이 현재 forward-only 계약과 일치하는지 확인합니다.

살펴볼 database field에는 provider_group_id, dispatch_attempt_count, last_provider_status, last_provider_error, dispatched_at, confirmed_at, recipients payload 안의 recipient-level provider id가 있습니다.

유용한 소스 확인:

cd lumie-backend
rg -n "applyWebhookReport|bindRequiredTenantToCurrentTransaction|lumieTenant|providerGroupId|claimDispatchAttempt" \
modules/notification app/src/main/resources/db/migration/public

수정

  • dispatch가 commit 뒤에 실행된다면 dispatchMessage(...) 주위의 TenantContextHolder.withinContext(...)를 유지합니다.
  • TransactionTemplate을 쓴다면 repository read/write 전에 transaction 내부에서 required tenant를 bind합니다.
  • webhook event가 ignored라면 custom field 또는 provider id binding을 고칩니다. missing provider group id는 Lumie message id로 fallback할 수 있지만 conflicting id는 계속 거부해야 합니다.
  • provider send가 partial recipient result를 반환하면 recipient index와 provider message id 기준으로 merge합니다.
  • migration drift가 보이면 historical migration을 정확히 복원하고 새 forward migration을 추가합니다.

검증 증거

  • pending message가 한 번 claim되고 provider response 뒤 retry state를 벗어납니다.
  • provider group id와 per-recipient provider message id가 저장됩니다.
  • valid Solapi webhook이 의도한 recipient만 한 번 갱신합니다.
  • tenant, group id, message id, recipient index가 맞지 않는 webhook은 history를 변경하지 않고 ignored됩니다.
  • dispatch와 webhook write가 올바른 tenant의 RLS 아래에서 통과합니다.
  • SMS history가 operator가 기대한 sender number와 final delivery status를 노출합니다.

예방

  • provider callback은 Lumie id custom field와 provider id로 묶습니다. provider status만 신뢰하지 않습니다.
  • live-provider write는 명시적인 tenant-bound transaction 안에 둡니다.
  • webhook 200을 state mutation의 증거로 보지 말고 aggregate를 확인합니다.
  • provider tracking column과 index는 특히 migration fix를 forward-only로 유지합니다.
  • mock-provider behavior와 live Solapi binding rule을 분리해 유지합니다.

관련 페이지