239 lines
7.2 KiB
Python
239 lines
7.2 KiB
Python
"""
|
|
RabbitMQ client module.
|
|
|
|
This module provides a RabbitMQ client with connection management
|
|
and queue declaration functionality.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
from datetime import datetime
|
|
from typing import Dict, Optional, Callable
|
|
import pika
|
|
from pika.exceptions import AMQPConnectionError, AMQPChannelError
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class RabbitMQClient:
|
|
"""
|
|
RabbitMQ client with connection management and queue declaration.
|
|
|
|
Features:
|
|
- Connection management with reconnection logic
|
|
- Queue declaration with durability
|
|
- Message publishing with standard event format
|
|
- Task consumption with error handling
|
|
- Automatic acknowledgment management
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
rabbitmq_url: str = "amqp://guest:guest@localhost:5672",
|
|
prefetch_count: int = 1
|
|
):
|
|
"""
|
|
Initialize RabbitMQ client.
|
|
|
|
Args:
|
|
rabbitmq_url: RabbitMQ connection URL
|
|
prefetch_count: Number of unacknowledged messages to prefetch
|
|
"""
|
|
self.rabbitmq_url = rabbitmq_url
|
|
self.prefetch_count = prefetch_count
|
|
self.connection: Optional[pika.BlockingConnection] = None
|
|
self.channel: Optional[pika.adapters.blocking_connection.BlockingChannel] = None
|
|
|
|
# Queue names
|
|
self.queues = {
|
|
'scraping_tasks': 'scraping_tasks',
|
|
'sentiment_analysis_tasks': 'sentiment_analysis_tasks',
|
|
'energy_calculation_tasks': 'energy_calculation_tasks',
|
|
'results': 'results'
|
|
}
|
|
|
|
def connect(self) -> None:
|
|
"""
|
|
Establish connection to RabbitMQ server.
|
|
|
|
Raises:
|
|
AMQPConnectionError: If connection fails
|
|
"""
|
|
try:
|
|
logger.info(f"🔗 Connecting to RabbitMQ at {self.rabbitmq_url}")
|
|
|
|
# Create connection
|
|
self.connection = pika.BlockingConnection(
|
|
pika.URLParameters(self.rabbitmq_url)
|
|
)
|
|
self.channel = self.connection.channel()
|
|
|
|
# Set prefetch count for fair dispatch
|
|
self.channel.basic_qos(prefetch_count=self.prefetch_count)
|
|
|
|
# Declare queues
|
|
self._declare_queues()
|
|
|
|
logger.info("✅ Connected to RabbitMQ successfully")
|
|
|
|
except AMQPConnectionError as e:
|
|
logger.error(f"❌ Failed to connect to RabbitMQ: {e}")
|
|
raise
|
|
except AMQPChannelError as e:
|
|
logger.error(f"❌ Failed to create RabbitMQ channel: {e}")
|
|
raise
|
|
|
|
def _declare_queues(self) -> None:
|
|
"""
|
|
Declare all required queues with durability.
|
|
"""
|
|
for queue_name in self.queues.values():
|
|
self.channel.queue_declare(
|
|
queue=queue_name,
|
|
durable=True
|
|
)
|
|
logger.debug(f"✅ Declared queue: {queue_name}")
|
|
|
|
def close(self) -> None:
|
|
"""Close connection to RabbitMQ server."""
|
|
if self.channel:
|
|
self.channel.close()
|
|
if self.connection:
|
|
self.connection.close()
|
|
logger.info("🔌 Closed RabbitMQ connection")
|
|
|
|
def publish_message(
|
|
self,
|
|
queue_name: str,
|
|
data: Dict,
|
|
event_type: str = "task.created",
|
|
version: str = "1.0"
|
|
) -> None:
|
|
"""
|
|
Publish a message to a queue with standard event format.
|
|
|
|
Args:
|
|
queue_name: Target queue name
|
|
data: Message payload (will be JSON serialized)
|
|
event_type: Event type for message header
|
|
version: Event version
|
|
"""
|
|
if not self.channel:
|
|
raise RuntimeError("RabbitMQ channel not initialized. Call connect() first.")
|
|
|
|
# Create standard event message format
|
|
message = {
|
|
"event": event_type,
|
|
"version": version,
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
"data": data,
|
|
"metadata": {
|
|
"source": "api",
|
|
"queue": queue_name
|
|
}
|
|
}
|
|
|
|
try:
|
|
self.channel.basic_publish(
|
|
exchange='',
|
|
routing_key=queue_name,
|
|
body=json.dumps(message, ensure_ascii=False, default=str),
|
|
properties=pika.BasicProperties(
|
|
delivery_mode=2, # Make message persistent
|
|
content_type='application/json'
|
|
)
|
|
)
|
|
logger.debug(f"📤 Published message to {queue_name}: {event_type}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ Failed to publish message to {queue_name}: {e}")
|
|
raise
|
|
|
|
def consume_messages(
|
|
self,
|
|
queue_name: str,
|
|
callback: Callable
|
|
) -> None:
|
|
"""
|
|
Start consuming messages from a queue.
|
|
|
|
Args:
|
|
queue_name: Queue to consume from
|
|
callback: Callback function to process messages
|
|
"""
|
|
if not self.channel:
|
|
raise RuntimeError("RabbitMQ channel not initialized. Call connect() first.")
|
|
|
|
try:
|
|
logger.info(f"👂 Starting to consume from queue: {queue_name}")
|
|
|
|
self.channel.basic_consume(
|
|
queue=queue_name,
|
|
on_message_callback=callback,
|
|
auto_ack=False
|
|
)
|
|
|
|
self.channel.start_consuming()
|
|
|
|
except KeyboardInterrupt:
|
|
logger.info("⏹️ Stopping consumer...")
|
|
self.channel.stop_consuming()
|
|
except Exception as e:
|
|
logger.error(f"❌ Error consuming from {queue_name}: {e}")
|
|
raise
|
|
|
|
def ack_message(self, delivery_tag: int) -> None:
|
|
"""
|
|
Acknowledge a message as processed.
|
|
|
|
Args:
|
|
delivery_tag: Message delivery tag
|
|
"""
|
|
if not self.channel:
|
|
raise RuntimeError("RabbitMQ channel not initialized.")
|
|
|
|
self.channel.basic_ack(delivery_tag=delivery_tag)
|
|
|
|
def reject_message(
|
|
self,
|
|
delivery_tag: int,
|
|
requeue: bool = False
|
|
) -> None:
|
|
"""
|
|
Reject a message (e.g., on processing failure).
|
|
|
|
Args:
|
|
delivery_tag: Message delivery tag
|
|
requeue: Whether to requeue the message
|
|
"""
|
|
if not self.channel:
|
|
raise RuntimeError("RabbitMQ channel not initialized.")
|
|
|
|
self.channel.basic_reject(delivery_tag=delivery_tag, requeue=requeue)
|
|
|
|
|
|
def create_rabbitmq_client(
|
|
rabbitmq_url: Optional[str] = None,
|
|
prefetch_count: int = 1
|
|
) -> RabbitMQClient:
|
|
"""
|
|
Factory function to create a RabbitMQ client.
|
|
|
|
Args:
|
|
rabbitmq_url: Optional RabbitMQ connection URL (defaults to localhost)
|
|
prefetch_count: Number of unacknowledged messages to prefetch
|
|
|
|
Returns:
|
|
Configured RabbitMQClient instance
|
|
"""
|
|
if rabbitmq_url is None:
|
|
# TODO: Load from environment variables or config file
|
|
rabbitmq_url = "amqp://guest:guest@localhost:5672"
|
|
|
|
client = RabbitMQClient(
|
|
rabbitmq_url=rabbitmq_url,
|
|
prefetch_count=prefetch_count
|
|
)
|
|
|
|
return client
|