90 lines
3.1 KiB
Python
90 lines
3.1 KiB
Python
import asyncio
|
|
import json
|
|
import logging
|
|
from typing import Any, cast
|
|
|
|
import aio_pika
|
|
from aio_pika.abc import AbstractIncomingMessage
|
|
from redis.asyncio import Redis
|
|
|
|
from gapido_auth.config import get_settings
|
|
from gapido_auth.domain.entities import SmsJob
|
|
from gapido_auth.domain.ports import SmsClient
|
|
from gapido_auth.infrastructure.rabbitmq import (
|
|
SMS_ROUTING_KEY,
|
|
connect_robust,
|
|
declare_sms_topology,
|
|
)
|
|
from gapido_auth.infrastructure.sms_provider import create_sms_client
|
|
|
|
logger = logging.getLogger(__name__)
|
|
MAX_RETRIES = 3
|
|
|
|
|
|
async def handle_message(
|
|
message: AbstractIncomingMessage,
|
|
client: SmsClient,
|
|
exchange: Any,
|
|
dlx: Any,
|
|
) -> None:
|
|
"""Process one SMS job, retry bounded failures, and dead-letter permanent failures."""
|
|
|
|
async with message.process(ignore_processed=True, requeue=False):
|
|
payload = json.loads(message.body.decode())
|
|
job = SmsJob(**payload)
|
|
retry_count = int(cast(int | str, (message.headers or {}).get("x-retry-count", 0)))
|
|
|
|
try:
|
|
await client.send_otp(job.mobile, job.code, job.template)
|
|
except Exception:
|
|
if retry_count >= MAX_RETRIES:
|
|
logger.exception("SMS job failed permanently for mobile=%s", job.mobile)
|
|
await dlx.publish(
|
|
aio_pika.Message(
|
|
body=message.body,
|
|
content_type="application/json",
|
|
headers={"x-retry-count": retry_count},
|
|
delivery_mode=aio_pika.DeliveryMode.PERSISTENT,
|
|
),
|
|
routing_key=SMS_ROUTING_KEY,
|
|
)
|
|
return
|
|
|
|
# Re-publish instead of requeueing indefinitely so retries remain bounded.
|
|
logger.warning(
|
|
"SMS job failed for mobile=%s retry=%s", job.mobile, retry_count + 1, exc_info=True
|
|
)
|
|
await exchange.publish(
|
|
aio_pika.Message(
|
|
body=message.body,
|
|
content_type="application/json",
|
|
headers={"x-retry-count": retry_count + 1},
|
|
delivery_mode=aio_pika.DeliveryMode.PERSISTENT,
|
|
),
|
|
routing_key=SMS_ROUTING_KEY,
|
|
)
|
|
|
|
|
|
async def main() -> None:
|
|
"""Start the RabbitMQ consumer and bind it to the configured SMS strategy."""
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
settings = get_settings()
|
|
connection = await connect_robust(settings.rabbitmq_url)
|
|
async with connection:
|
|
channel = await connection.channel()
|
|
await channel.set_qos(prefetch_count=20)
|
|
exchange, queue, dlx = await declare_sms_topology(channel)
|
|
redis = Redis.from_url(settings.redis_url, decode_responses=True)
|
|
try:
|
|
client = create_sms_client(settings, debug_store=redis)
|
|
await queue.consume(lambda message: handle_message(message, client, exchange, dlx))
|
|
logger.info("SMS worker started with provider=%s", settings.sms_provider)
|
|
await asyncio.Future()
|
|
finally:
|
|
await redis.aclose()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|