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())