New alert service
This commit is contained in:
191
shared/messaging/README.md
Normal file
191
shared/messaging/README.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# Unified Messaging Architecture
|
||||
|
||||
This document describes the standardized messaging system used across all bakery-ia microservices.
|
||||
|
||||
## Overview
|
||||
|
||||
The unified messaging architecture provides a consistent approach for:
|
||||
- Publishing business events (inventory changes, user actions, etc.)
|
||||
- Publishing user-facing alerts, notifications, and recommendations
|
||||
- Consuming events from other services
|
||||
- Maintaining service-to-service communication patterns
|
||||
|
||||
## Core Components
|
||||
|
||||
### 1. UnifiedEventPublisher
|
||||
The main publisher for all event types, located in `shared/messaging/messaging_client.py`:
|
||||
|
||||
```python
|
||||
from shared.messaging import UnifiedEventPublisher, EVENT_TYPES, RabbitMQClient
|
||||
|
||||
# Initialize
|
||||
rabbitmq_client = RabbitMQClient(settings.RABBITMQ_URL, service_name="my-service")
|
||||
await rabbitmq_client.connect()
|
||||
event_publisher = UnifiedEventPublisher(rabbitmq_client, "my-service")
|
||||
|
||||
# Publish business events
|
||||
await event_publisher.publish_business_event(
|
||||
event_type=EVENT_TYPES.INVENTORY.STOCK_ADDED,
|
||||
tenant_id=tenant_id,
|
||||
data={"ingredient_id": "123", "quantity": 100.0}
|
||||
)
|
||||
|
||||
# Publish alerts (action required)
|
||||
await event_publisher.publish_alert(
|
||||
event_type="procurement.po_approval_needed",
|
||||
tenant_id=tenant_id,
|
||||
severity="high", # urgent, high, medium, low
|
||||
data={"po_id": "456", "supplier_name": "ABC Corp"}
|
||||
)
|
||||
|
||||
# Publish notifications (informational)
|
||||
await event_publisher.publish_notification(
|
||||
event_type="production.batch_completed",
|
||||
tenant_id=tenant_id,
|
||||
data={"batch_id": "789", "product_name": "Bread"}
|
||||
)
|
||||
|
||||
# Publish recommendations (suggestions)
|
||||
await event_publisher.publish_recommendation(
|
||||
event_type="forecasting.demand_surge_predicted",
|
||||
tenant_id=tenant_id,
|
||||
data={"product_name": "Croissants", "surge_percentage": 25.0}
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Event Types Constants
|
||||
Use predefined event types for consistency:
|
||||
|
||||
```python
|
||||
from shared.messaging import EVENT_TYPES
|
||||
|
||||
# Inventory events
|
||||
EVENT_TYPES.INVENTORY.INGREDIENT_CREATED
|
||||
EVENT_TYPES.INVENTORY.STOCK_ADDED
|
||||
EVENT_TYPES.INVENTORY.LOW_STOCK_ALERT
|
||||
|
||||
# Production events
|
||||
EVENT_TYPES.PRODUCTION.BATCH_CREATED
|
||||
EVENT_TYPES.PRODUCTION.BATCH_COMPLETED
|
||||
|
||||
# Procurement events
|
||||
EVENT_TYPES.PROCUREMENT.PO_APPROVED
|
||||
EVENT_TYPES.PROCUREMENT.DELIVERY_SCHEDULED
|
||||
```
|
||||
|
||||
### 3. Service Integration Pattern
|
||||
|
||||
#### In Service Main.py:
|
||||
```python
|
||||
from shared.messaging import UnifiedEventPublisher, ServiceMessagingManager
|
||||
|
||||
class MyService(StandardFastAPIService):
|
||||
def __init__(self):
|
||||
self.messaging_manager = None
|
||||
self.event_publisher = None # For alerts/notifications
|
||||
self.unified_publisher = None # For business events
|
||||
|
||||
super().__init__(
|
||||
service_name="my-service",
|
||||
# ... other params
|
||||
enable_messaging=True
|
||||
)
|
||||
|
||||
async def _setup_messaging(self):
|
||||
try:
|
||||
self.messaging_manager = ServiceMessagingManager("my-service", settings.RABBITMQ_URL)
|
||||
success = await self.messaging_manager.setup()
|
||||
if success:
|
||||
self.event_publisher = self.messaging_manager.publisher
|
||||
self.unified_publisher = self.messaging_manager.publisher
|
||||
|
||||
self.logger.info("Messaging setup completed")
|
||||
else:
|
||||
raise Exception("Failed to setup messaging")
|
||||
except Exception as e:
|
||||
self.logger.error("Messaging setup failed", error=str(e))
|
||||
raise
|
||||
|
||||
async def on_startup(self, app: FastAPI):
|
||||
await super().on_startup(app)
|
||||
|
||||
# Pass publishers to services
|
||||
my_service = MyAlertService(self.event_publisher)
|
||||
my_event_service = MyEventService(self.unified_publisher)
|
||||
|
||||
# Store in app state if needed
|
||||
app.state.my_service = my_service
|
||||
app.state.my_event_service = my_event_service
|
||||
|
||||
async def on_shutdown(self, app: FastAPI):
|
||||
if self.messaging_manager:
|
||||
await self.messaging_manager.cleanup()
|
||||
await super().on_shutdown(app)
|
||||
```
|
||||
|
||||
#### In Service Implementation:
|
||||
```python
|
||||
from shared.messaging import UnifiedEventPublisher
|
||||
|
||||
class MyEventService:
|
||||
def __init__(self, event_publisher: UnifiedEventPublisher):
|
||||
self.publisher = event_publisher
|
||||
|
||||
async def handle_business_logic(self, tenant_id: UUID, data: Dict[str, Any]):
|
||||
# Publish business events
|
||||
await self.publisher.publish_business_event(
|
||||
event_type="mydomain.action_performed",
|
||||
tenant_id=tenant_id,
|
||||
data=data
|
||||
)
|
||||
```
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Old Pattern (Deprecated):
|
||||
```python
|
||||
# OLD - Don't use this anymore
|
||||
from shared.alerts.base_service import BaseAlertService
|
||||
|
||||
class MyService(BaseAlertService):
|
||||
def __init__(self, config):
|
||||
super().__init__(config)
|
||||
|
||||
async def send_alert(self, tenant_id, data):
|
||||
await self.publish_item(tenant_id, data, item_type="alert")
|
||||
```
|
||||
|
||||
### New Pattern (Recommended):
|
||||
```python
|
||||
# NEW - Use UnifiedEventPublisher for all event types
|
||||
from shared.messaging import UnifiedEventPublisher
|
||||
|
||||
class MyService:
|
||||
def __init__(self, event_publisher: UnifiedEventPublisher):
|
||||
self.publisher = event_publisher
|
||||
|
||||
async def send_alert(self, tenant_id: UUID, data: Dict[str, Any]):
|
||||
await self.publisher.publish_alert(
|
||||
event_type="mydomain.alert_type",
|
||||
tenant_id=tenant_id,
|
||||
severity="high",
|
||||
data=data
|
||||
)
|
||||
```
|
||||
|
||||
## Event Routing
|
||||
|
||||
Events are routed using the following patterns:
|
||||
- **Alerts**: `alert.{domain}.{severity}` (e.g., `alert.inventory.high`)
|
||||
- **Notifications**: `notification.{domain}.info` (e.g., `notification.production.info`)
|
||||
- **Recommendations**: `recommendation.{domain}.medium` (e.g., `recommendation.forecasting.medium`)
|
||||
- **Business Events**: `business.{event_type}` (e.g., `business.inventory_stock_added`)
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Consistent Naming**: Use lowercase, dot-separated event types (e.g., `inventory.stock.added`)
|
||||
2. **Tenant Awareness**: Always include tenant_id for multi-tenant operations
|
||||
3. **Data Minimization**: Include only essential data in events
|
||||
4. **Error Handling**: Always wrap event publishing in try-catch blocks
|
||||
5. **Service Names**: Use consistent service names matching your service definition
|
||||
6. **Lifecycle Management**: Always clean up messaging resources during service shutdown
|
||||
@@ -0,0 +1,21 @@
|
||||
from .messaging_client import (
|
||||
RabbitMQClient,
|
||||
UnifiedEventPublisher,
|
||||
ServiceMessagingManager,
|
||||
initialize_service_publisher,
|
||||
cleanup_service_publisher,
|
||||
EventMessage,
|
||||
EventType,
|
||||
EVENT_TYPES
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'RabbitMQClient',
|
||||
'UnifiedEventPublisher',
|
||||
'ServiceMessagingManager',
|
||||
'initialize_service_publisher',
|
||||
'cleanup_service_publisher',
|
||||
'EventMessage',
|
||||
'EventType',
|
||||
'EVENT_TYPES'
|
||||
]
|
||||
@@ -1,141 +0,0 @@
|
||||
"""
|
||||
shared/messaging/events.py
|
||||
Event definitions for microservices communication
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, Any, Optional
|
||||
import uuid
|
||||
|
||||
class BaseEvent:
|
||||
"""Base event class - FIXED"""
|
||||
def __init__(self, service_name: str, data: Dict[str, Any], event_type: str = "", correlation_id: Optional[str] = None):
|
||||
self.service_name = service_name
|
||||
self.data = data
|
||||
self.event_type = event_type
|
||||
self.event_id = str(uuid.uuid4())
|
||||
self.timestamp = datetime.now(timezone.utc)
|
||||
self.correlation_id = correlation_id
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Converts the event object to a dictionary for JSON serialization - FIXED"""
|
||||
return {
|
||||
"service_name": self.service_name,
|
||||
"data": self.data,
|
||||
"event_type": self.event_type,
|
||||
"event_id": self.event_id,
|
||||
"timestamp": self.timestamp.isoformat(), # Convert datetime to ISO string
|
||||
"correlation_id": self.correlation_id
|
||||
}
|
||||
|
||||
# Auth Events - FIXED
|
||||
class UserRegisteredEvent(BaseEvent):
|
||||
def __init__(self, service_name: str, data: Dict[str, Any], correlation_id: Optional[str] = None):
|
||||
super().__init__(
|
||||
service_name=service_name,
|
||||
data=data,
|
||||
event_type="user.registered",
|
||||
correlation_id=correlation_id
|
||||
)
|
||||
|
||||
class UserLoginEvent(BaseEvent):
|
||||
def __init__(self, service_name: str, data: Dict[str, Any], correlation_id: Optional[str] = None):
|
||||
super().__init__(
|
||||
service_name=service_name,
|
||||
data=data,
|
||||
event_type="user.login",
|
||||
correlation_id=correlation_id
|
||||
)
|
||||
|
||||
class UserLogoutEvent(BaseEvent):
|
||||
def __init__(self, service_name: str, data: Dict[str, Any], correlation_id: Optional[str] = None):
|
||||
super().__init__(
|
||||
service_name=service_name,
|
||||
data=data,
|
||||
event_type="user.logout",
|
||||
correlation_id=correlation_id
|
||||
)
|
||||
|
||||
# Training Events
|
||||
class TrainingStartedEvent(BaseEvent):
|
||||
def __init__(self, service_name: str, data: Dict[str, Any], correlation_id: Optional[str] = None):
|
||||
super().__init__(
|
||||
service_name=service_name,
|
||||
data=data,
|
||||
event_type="training.started",
|
||||
correlation_id=correlation_id
|
||||
)
|
||||
|
||||
class TrainingCompletedEvent(BaseEvent):
|
||||
def __init__(self, service_name: str, data: Dict[str, Any], correlation_id: Optional[str] = None):
|
||||
super().__init__(
|
||||
service_name=service_name,
|
||||
data=data,
|
||||
event_type="training.completed",
|
||||
correlation_id=correlation_id
|
||||
)
|
||||
|
||||
class TrainingFailedEvent(BaseEvent):
|
||||
def __init__(self, service_name: str, data: Dict[str, Any], correlation_id: Optional[str] = None):
|
||||
super().__init__(
|
||||
service_name=service_name,
|
||||
data=data,
|
||||
event_type="training.failed",
|
||||
correlation_id=correlation_id
|
||||
)
|
||||
|
||||
# Forecasting Events
|
||||
class ForecastGeneratedEvent(BaseEvent):
|
||||
def __init__(self, service_name: str, data: Dict[str, Any], correlation_id: Optional[str] = None):
|
||||
super().__init__(
|
||||
service_name=service_name,
|
||||
data=data,
|
||||
event_type="forecast.generated",
|
||||
correlation_id=correlation_id
|
||||
)
|
||||
|
||||
# Data Events
|
||||
class DataImportedEvent(BaseEvent):
|
||||
def __init__(self, service_name: str, data: Dict[str, Any], correlation_id: Optional[str] = None):
|
||||
super().__init__(
|
||||
service_name=service_name,
|
||||
data=data,
|
||||
event_type="data.imported",
|
||||
correlation_id=correlation_id
|
||||
)
|
||||
|
||||
# Procurement Events
|
||||
class PurchaseOrderApprovedEvent(BaseEvent):
|
||||
def __init__(self, service_name: str, data: Dict[str, Any], correlation_id: Optional[str] = None):
|
||||
super().__init__(
|
||||
service_name=service_name,
|
||||
data=data,
|
||||
event_type="po.approved",
|
||||
correlation_id=correlation_id
|
||||
)
|
||||
|
||||
class PurchaseOrderRejectedEvent(BaseEvent):
|
||||
def __init__(self, service_name: str, data: Dict[str, Any], correlation_id: Optional[str] = None):
|
||||
super().__init__(
|
||||
service_name=service_name,
|
||||
data=data,
|
||||
event_type="po.rejected",
|
||||
correlation_id=correlation_id
|
||||
)
|
||||
|
||||
class PurchaseOrderSentToSupplierEvent(BaseEvent):
|
||||
def __init__(self, service_name: str, data: Dict[str, Any], correlation_id: Optional[str] = None):
|
||||
super().__init__(
|
||||
service_name=service_name,
|
||||
data=data,
|
||||
event_type="po.sent_to_supplier",
|
||||
correlation_id=correlation_id
|
||||
)
|
||||
|
||||
class DeliveryReceivedEvent(BaseEvent):
|
||||
def __init__(self, service_name: str, data: Dict[str, Any], correlation_id: Optional[str] = None):
|
||||
super().__init__(
|
||||
service_name=service_name,
|
||||
data=data,
|
||||
event_type="delivery.received",
|
||||
correlation_id=correlation_id
|
||||
)
|
||||
642
shared/messaging/messaging_client.py
Normal file
642
shared/messaging/messaging_client.py
Normal file
@@ -0,0 +1,642 @@
|
||||
"""
|
||||
Unified RabbitMQ Client and Publisher for Bakery-IA Services
|
||||
|
||||
This module provides a standardized approach for all services to connect to RabbitMQ,
|
||||
publish messages, and handle messaging lifecycle. It combines all messaging
|
||||
functionality into a single, unified interface.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Dict, Any, Callable, Optional, Union
|
||||
from datetime import datetime, date, timezone
|
||||
import uuid
|
||||
import structlog
|
||||
from contextlib import suppress
|
||||
from enum import Enum
|
||||
|
||||
try:
|
||||
import aio_pika
|
||||
from aio_pika import connect_robust, Message, DeliveryMode, ExchangeType
|
||||
AIO_PIKA_AVAILABLE = True
|
||||
except ImportError:
|
||||
AIO_PIKA_AVAILABLE = False
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class EventType(Enum):
|
||||
"""Event type enum for consistent event classification"""
|
||||
BUSINESS = "business" # Business events like inventory changes, user actions
|
||||
ALERT = "alert" # User-facing alerts requiring action
|
||||
NOTIFICATION = "notification" # User-facing informational notifications
|
||||
RECOMMENDATION = "recommendation" # User-facing recommendations
|
||||
SYSTEM = "system" # System-level events
|
||||
|
||||
|
||||
class EVENT_TYPES:
|
||||
"""Static class for event type constants"""
|
||||
class INVENTORY:
|
||||
INGREDIENT_CREATED = "inventory.ingredient.created"
|
||||
STOCK_ADDED = "inventory.stock.added"
|
||||
STOCK_CONSUMED = "inventory.stock.consumed"
|
||||
LOW_STOCK_ALERT = "inventory.alert.low_stock"
|
||||
EXPIRATION_ALERT = "inventory.alert.expiration"
|
||||
STOCK_UPDATED = "inventory.stock.updated"
|
||||
STOCK_TRANSFERRED = "inventory.stock.transferred"
|
||||
STOCK_WASTED = "inventory.stock.wasted"
|
||||
|
||||
class PRODUCTION:
|
||||
BATCH_CREATED = "production.batch.created"
|
||||
BATCH_STARTED = "production.batch.started"
|
||||
BATCH_COMPLETED = "production.batch.completed"
|
||||
EQUIPMENT_STATUS_CHANGED = "production.equipment.status_changed"
|
||||
|
||||
class PROCUREMENT:
|
||||
PO_CREATED = "procurement.po.created"
|
||||
PO_APPROVED = "procurement.po.approved"
|
||||
PO_REJECTED = "procurement.po.rejected"
|
||||
DELIVERY_SCHEDULED = "procurement.delivery.scheduled"
|
||||
DELIVERY_RECEIVED = "procurement.delivery.received"
|
||||
DELIVERY_OVERDUE = "procurement.delivery.overdue"
|
||||
|
||||
class FORECASTING:
|
||||
FORECAST_GENERATED = "forecasting.forecast.generated"
|
||||
FORECAST_UPDATED = "forecasting.forecast.updated"
|
||||
DEMAND_SPIKE_DETECTED = "forecasting.demand.spike_detected"
|
||||
WEATHER_IMPACT_FORECAST = "forecasting.weather.impact_forecast"
|
||||
|
||||
class NOTIFICATION:
|
||||
NOTIFICATION_SENT = "notification.sent"
|
||||
NOTIFICATION_FAILED = "notification.failed"
|
||||
NOTIFICATION_DELIVERED = "notification.delivered"
|
||||
NOTIFICATION_OPENED = "notification.opened"
|
||||
|
||||
class TENANT:
|
||||
TENANT_CREATED = "tenant.created"
|
||||
TENANT_UPDATED = "tenant.updated"
|
||||
TENANT_DELETED = "tenant.deleted"
|
||||
TENANT_MEMBER_ADDED = "tenant.member.added"
|
||||
TENANT_MEMBER_REMOVED = "tenant.member.removed"
|
||||
|
||||
|
||||
def json_serializer(obj):
|
||||
"""JSON serializer for objects not serializable by default json code"""
|
||||
if isinstance(obj, (datetime, date)):
|
||||
return obj.isoformat()
|
||||
elif isinstance(obj, uuid.UUID):
|
||||
return str(obj)
|
||||
elif hasattr(obj, '__class__') and obj.__class__.__name__ == 'Decimal':
|
||||
# Handle Decimal objects from SQLAlchemy without importing decimal
|
||||
return float(obj)
|
||||
raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
|
||||
|
||||
|
||||
class RabbitMQHeartbeatMonitor:
|
||||
"""Monitor to ensure heartbeats are processed during heavy operations"""
|
||||
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
self._monitor_task = None
|
||||
self._should_monitor = False
|
||||
|
||||
async def start_monitoring(self):
|
||||
"""Start heartbeat monitoring task"""
|
||||
if self._monitor_task and not self._monitor_task.done():
|
||||
return
|
||||
|
||||
self._should_monitor = True
|
||||
self._monitor_task = asyncio.create_task(self._monitor_loop())
|
||||
|
||||
async def stop_monitoring(self):
|
||||
"""Stop heartbeat monitoring task"""
|
||||
self._should_monitor = False
|
||||
if self._monitor_task and not self._monitor_task.done():
|
||||
self._monitor_task.cancel()
|
||||
with suppress(asyncio.CancelledError):
|
||||
await self._monitor_task
|
||||
|
||||
async def _monitor_loop(self):
|
||||
"""Monitor loop that periodically yields control for heartbeat processing"""
|
||||
while self._should_monitor:
|
||||
# Yield control to allow heartbeat processing
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# Verify connection is still alive
|
||||
if self.client.connection and not self.client.connection.is_closed:
|
||||
# Check if connection is still responsive
|
||||
try:
|
||||
# This is a lightweight check to ensure the connection is responsive
|
||||
pass # The heartbeat mechanism in aio_pika handles this internally
|
||||
except Exception as e:
|
||||
logger.warning("Connection check failed", error=str(e))
|
||||
self.client.connected = False
|
||||
break
|
||||
else:
|
||||
logger.warning("Connection is closed, stopping monitor")
|
||||
break
|
||||
|
||||
|
||||
class RabbitMQClient:
|
||||
"""
|
||||
Universal RabbitMQ client for all bakery-ia microservices
|
||||
Handles all messaging patterns with proper fallbacks
|
||||
"""
|
||||
|
||||
def __init__(self, connection_url: str, service_name: str = "unknown"):
|
||||
self.connection_url = connection_url
|
||||
self.service_name = service_name
|
||||
self.connection = None
|
||||
self.channel = None
|
||||
self.connected = False
|
||||
self._reconnect_attempts = 0
|
||||
self._max_reconnect_attempts = 5
|
||||
self.heartbeat_monitor = RabbitMQHeartbeatMonitor(self)
|
||||
|
||||
async def connect(self):
|
||||
"""Connect to RabbitMQ with retry logic"""
|
||||
if not AIO_PIKA_AVAILABLE:
|
||||
logger.warning("aio-pika not available, messaging disabled", service=self.service_name)
|
||||
return False
|
||||
|
||||
try:
|
||||
self.connection = await connect_robust(
|
||||
self.connection_url,
|
||||
heartbeat=600 # Increase heartbeat to 600 seconds (10 minutes) to prevent timeouts
|
||||
)
|
||||
self.channel = await self.connection.channel()
|
||||
await self.channel.set_qos(prefetch_count=100) # Performance optimization
|
||||
|
||||
self.connected = True
|
||||
self._reconnect_attempts = 0
|
||||
|
||||
# Start heartbeat monitoring
|
||||
await self.heartbeat_monitor.start_monitoring()
|
||||
|
||||
logger.info("Connected to RabbitMQ", service=self.service_name)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.connected = False
|
||||
self._reconnect_attempts += 1
|
||||
logger.warning(
|
||||
"Failed to connect to RabbitMQ",
|
||||
service=self.service_name,
|
||||
error=str(e),
|
||||
attempt=self._reconnect_attempts
|
||||
)
|
||||
return False
|
||||
|
||||
async def disconnect(self):
|
||||
"""Disconnect from RabbitMQ with proper channel cleanup"""
|
||||
try:
|
||||
# Stop heartbeat monitoring first
|
||||
await self.heartbeat_monitor.stop_monitoring()
|
||||
|
||||
# Close channel before connection to avoid "unexpected close" warnings
|
||||
if self.channel and not self.channel.is_closed:
|
||||
await self.channel.close()
|
||||
logger.debug("RabbitMQ channel closed", service=self.service_name)
|
||||
|
||||
# Then close connection
|
||||
if self.connection and not self.connection.is_closed:
|
||||
await self.connection.close()
|
||||
logger.info("Disconnected from RabbitMQ", service=self.service_name)
|
||||
|
||||
self.connected = False
|
||||
|
||||
except Exception as e:
|
||||
logger.warning("Error during RabbitMQ disconnect",
|
||||
service=self.service_name,
|
||||
error=str(e))
|
||||
self.connected = False
|
||||
|
||||
async def ensure_connected(self) -> bool:
|
||||
"""Ensure connection is active, reconnect if needed"""
|
||||
if self.connected and self.connection and not self.connection.is_closed:
|
||||
return True
|
||||
|
||||
if self._reconnect_attempts >= self._max_reconnect_attempts:
|
||||
logger.error("Max reconnection attempts reached", service=self.service_name)
|
||||
return False
|
||||
|
||||
return await self.connect()
|
||||
|
||||
async def publish_event(self, exchange_name: str, routing_key: str, event_data: Dict[str, Any],
|
||||
persistent: bool = True) -> bool:
|
||||
"""
|
||||
Universal event publisher with automatic fallback
|
||||
Returns True if published successfully, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Ensure we're connected
|
||||
if not await self.ensure_connected():
|
||||
logger.debug("Event not published - RabbitMQ unavailable",
|
||||
service=self.service_name, routing_key=routing_key)
|
||||
return False
|
||||
|
||||
# Declare exchange
|
||||
exchange = await self.channel.declare_exchange(
|
||||
exchange_name,
|
||||
ExchangeType.TOPIC,
|
||||
durable=True
|
||||
)
|
||||
|
||||
# Prepare message with proper JSON serialization
|
||||
message_body = json.dumps(event_data, default=json_serializer)
|
||||
message = Message(
|
||||
message_body.encode(),
|
||||
delivery_mode=DeliveryMode.PERSISTENT if persistent else DeliveryMode.NOT_PERSISTENT,
|
||||
content_type="application/json",
|
||||
timestamp=datetime.now(),
|
||||
headers={
|
||||
"source_service": self.service_name,
|
||||
"event_id": event_data.get("event_id", str(uuid.uuid4()))
|
||||
}
|
||||
)
|
||||
|
||||
# Publish message
|
||||
await exchange.publish(message, routing_key=routing_key)
|
||||
|
||||
logger.debug("Event published successfully",
|
||||
service=self.service_name,
|
||||
exchange=exchange_name,
|
||||
routing_key=routing_key,
|
||||
size=len(message_body))
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to publish event",
|
||||
service=self.service_name,
|
||||
exchange=exchange_name,
|
||||
routing_key=routing_key,
|
||||
error=str(e))
|
||||
self.connected = False # Force reconnection on next attempt
|
||||
return False
|
||||
|
||||
async def consume_events(self, exchange_name: str, queue_name: str,
|
||||
routing_key: str, callback: Callable) -> bool:
|
||||
"""Universal event consumer"""
|
||||
try:
|
||||
if not await self.ensure_connected():
|
||||
return False
|
||||
|
||||
# Declare exchange
|
||||
exchange = await self.channel.declare_exchange(
|
||||
exchange_name,
|
||||
ExchangeType.TOPIC,
|
||||
durable=True
|
||||
)
|
||||
|
||||
# Declare queue
|
||||
queue = await self.channel.declare_queue(
|
||||
queue_name,
|
||||
durable=True
|
||||
)
|
||||
|
||||
# Bind queue to exchange
|
||||
await queue.bind(exchange, routing_key)
|
||||
|
||||
# Set up consumer
|
||||
await queue.consume(callback)
|
||||
|
||||
logger.info("Started consuming events",
|
||||
service=self.service_name,
|
||||
queue=queue_name,
|
||||
routing_key=routing_key)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to start consuming events",
|
||||
service=self.service_name,
|
||||
error=str(e))
|
||||
return False
|
||||
|
||||
# High-level convenience methods for common patterns
|
||||
async def publish_user_event(self, event_type: str, user_data: Dict[str, Any]) -> bool:
|
||||
"""Publish user-related events"""
|
||||
return await self.publish_event("user.events", f"user.{event_type}", user_data)
|
||||
|
||||
async def publish_training_event(self, event_type: str, training_data: Dict[str, Any]) -> bool:
|
||||
"""Publish training-related events"""
|
||||
return await self.publish_event("training.events", f"training.{event_type}", training_data)
|
||||
|
||||
async def publish_data_event(self, event_type: str, data: Dict[str, Any]) -> bool:
|
||||
"""Publish data-related events"""
|
||||
return await self.publish_event("data.events", f"data.{event_type}", data)
|
||||
|
||||
async def publish_forecast_event(self, event_type: str, forecast_data: Dict[str, Any]) -> bool:
|
||||
"""Publish forecast-related events"""
|
||||
return await self.publish_event("forecast.events", f"forecast.{event_type}", forecast_data)
|
||||
|
||||
|
||||
class EventMessage:
|
||||
"""Standardized event message structure"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
event_type: str,
|
||||
tenant_id: Union[str, uuid.UUID],
|
||||
service_name: str,
|
||||
data: Dict[str, Any],
|
||||
event_class: str = "business", # business, alert, notification, recommendation
|
||||
correlation_id: Optional[str] = None,
|
||||
trace_id: Optional[str] = None,
|
||||
severity: Optional[str] = None, # For alerts: urgent, high, medium, low
|
||||
source: Optional[str] = None
|
||||
):
|
||||
self.event_type = event_type
|
||||
self.tenant_id = str(tenant_id) if isinstance(tenant_id, uuid.UUID) else tenant_id
|
||||
self.service_name = service_name
|
||||
self.data = data
|
||||
self.event_class = event_class
|
||||
self.correlation_id = correlation_id or str(uuid.uuid4())
|
||||
self.trace_id = trace_id or str(uuid.uuid4())
|
||||
self.severity = severity
|
||||
self.source = source or service_name
|
||||
self.timestamp = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for message publishing"""
|
||||
result = {
|
||||
"event_type": self.event_type,
|
||||
"tenant_id": self.tenant_id,
|
||||
"service_name": self.service_name,
|
||||
"data": self.data,
|
||||
"event_class": self.event_class,
|
||||
"correlation_id": self.correlation_id,
|
||||
"trace_id": self.trace_id,
|
||||
"timestamp": self.timestamp
|
||||
}
|
||||
|
||||
if self.severity:
|
||||
result["severity"] = self.severity
|
||||
if self.source:
|
||||
result["source"] = self.source
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class UnifiedEventPublisher:
|
||||
"""Unified publisher for all event types - business events, alerts, notifications, recommendations"""
|
||||
|
||||
def __init__(self, rabbitmq_client: RabbitMQClient, service_name: str):
|
||||
self.rabbitmq = rabbitmq_client
|
||||
self.service_name = service_name
|
||||
self.exchange = "events.exchange"
|
||||
|
||||
async def publish_event(
|
||||
self,
|
||||
event_type: str,
|
||||
tenant_id: Union[str, uuid.UUID],
|
||||
data: Dict[str, Any],
|
||||
event_class: str = "business",
|
||||
severity: Optional[str] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Publish a standardized event using the unified messaging pattern.
|
||||
|
||||
Args:
|
||||
event_type: Type of event (e.g., 'inventory.ingredient.created')
|
||||
tenant_id: Tenant identifier
|
||||
data: Event payload data
|
||||
event_class: One of 'business', 'alert', 'notification', 'recommendation'
|
||||
severity: Alert severity (for alert events only)
|
||||
"""
|
||||
# Determine event domain and event type separately for alert processor
|
||||
# The event_type should be just the specific event name, domain should be extracted separately
|
||||
if '.' in event_type and event_class in ["alert", "notification", "recommendation"]:
|
||||
# For events like "inventory.critical_stock_shortage", split into domain and event
|
||||
parts = event_type.split('.', 1) # Split only on first dot
|
||||
event_domain = parts[0]
|
||||
actual_event_type = parts[1]
|
||||
else:
|
||||
# For simple event types or business events, use as-is
|
||||
event_domain = "general" if event_class == "business" else self.service_name
|
||||
actual_event_type = event_type
|
||||
|
||||
# For the message payload that goes to alert processor, use the expected MinimalEvent format
|
||||
if event_class in ["alert", "notification", "recommendation"]:
|
||||
# Format for alert processor (uses MinimalEvent schema)
|
||||
event_payload = {
|
||||
"tenant_id": str(tenant_id),
|
||||
"event_class": event_class,
|
||||
"event_domain": event_domain,
|
||||
"event_type": actual_event_type, # Just the specific event name, not domain.event_name
|
||||
"service": self.service_name, # Changed from service_name to service
|
||||
"metadata": data, # Changed from data to metadata
|
||||
"timestamp": datetime.now(timezone.utc).isoformat()
|
||||
}
|
||||
if severity:
|
||||
event_payload["severity"] = severity # Include severity for alerts
|
||||
else:
|
||||
# Format for business events (standard format)
|
||||
event_payload = {
|
||||
"event_type": event_type,
|
||||
"tenant_id": str(tenant_id),
|
||||
"service_name": self.service_name,
|
||||
"data": data,
|
||||
"event_class": event_class,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat()
|
||||
}
|
||||
if severity:
|
||||
event_payload["severity"] = severity
|
||||
|
||||
# Determine routing key based on event class
|
||||
# For routing, we can still use the original event_type format since it's for routing purposes
|
||||
if event_class == "alert":
|
||||
routing_key = f"alert.{event_domain}.{severity or 'medium'}"
|
||||
elif event_class == "notification":
|
||||
routing_key = f"notification.{event_domain}.info"
|
||||
elif event_class == "recommendation":
|
||||
routing_key = f"recommendation.{event_domain}.medium"
|
||||
else: # business events
|
||||
routing_key = f"business.{event_type.replace('.', '_')}"
|
||||
|
||||
try:
|
||||
success = await self.rabbitmq.publish_event(
|
||||
exchange_name=self.exchange,
|
||||
routing_key=routing_key,
|
||||
event_data=event_payload
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info(
|
||||
"event_published",
|
||||
tenant_id=str(tenant_id),
|
||||
event_type=event_type,
|
||||
event_class=event_class,
|
||||
severity=severity
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
"event_publish_failed",
|
||||
tenant_id=str(tenant_id),
|
||||
event_type=event_type
|
||||
)
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"event_publish_error",
|
||||
tenant_id=str(tenant_id),
|
||||
event_type=event_type,
|
||||
error=str(e)
|
||||
)
|
||||
return False
|
||||
|
||||
# Business event methods
|
||||
async def publish_business_event(
|
||||
self,
|
||||
event_type: str,
|
||||
tenant_id: Union[str, uuid.UUID],
|
||||
data: Dict[str, Any]
|
||||
) -> bool:
|
||||
"""Publish a business event (inventory changes, user actions, etc.)"""
|
||||
return await self.publish_event(
|
||||
event_type=event_type,
|
||||
tenant_id=tenant_id,
|
||||
data=data,
|
||||
event_class="business"
|
||||
)
|
||||
|
||||
# Alert methods
|
||||
async def publish_alert(
|
||||
self,
|
||||
event_type: str,
|
||||
tenant_id: Union[str, uuid.UUID],
|
||||
severity: str, # urgent, high, medium, low
|
||||
data: Dict[str, Any]
|
||||
) -> bool:
|
||||
"""Publish an alert (actionable by user)"""
|
||||
return await self.publish_event(
|
||||
event_type=event_type,
|
||||
tenant_id=tenant_id,
|
||||
data=data,
|
||||
event_class="alert",
|
||||
severity=severity
|
||||
)
|
||||
|
||||
# Notification methods
|
||||
async def publish_notification(
|
||||
self,
|
||||
event_type: str,
|
||||
tenant_id: Union[str, uuid.UUID],
|
||||
data: Dict[str, Any]
|
||||
) -> bool:
|
||||
"""Publish a notification (informational to user)"""
|
||||
return await self.publish_event(
|
||||
event_type=event_type,
|
||||
tenant_id=tenant_id,
|
||||
data=data,
|
||||
event_class="notification"
|
||||
)
|
||||
|
||||
# Recommendation methods
|
||||
async def publish_recommendation(
|
||||
self,
|
||||
event_type: str,
|
||||
tenant_id: Union[str, uuid.UUID],
|
||||
data: Dict[str, Any]
|
||||
) -> bool:
|
||||
"""Publish a recommendation (suggestion to user)"""
|
||||
return await self.publish_event(
|
||||
event_type=event_type,
|
||||
tenant_id=tenant_id,
|
||||
data=data,
|
||||
event_class="recommendation"
|
||||
)
|
||||
|
||||
|
||||
class ServiceMessagingManager:
|
||||
"""Manager class to handle messaging lifecycle for services"""
|
||||
|
||||
def __init__(self, service_name: str, rabbitmq_url: str):
|
||||
self.service_name = service_name
|
||||
self.rabbitmq_url = rabbitmq_url
|
||||
self.rabbitmq_client = None
|
||||
self.publisher = None
|
||||
|
||||
async def setup(self):
|
||||
"""Setup the messaging system for the service"""
|
||||
try:
|
||||
self.rabbitmq_client = RabbitMQClient(self.rabbitmq_url, self.service_name)
|
||||
success = await self.rabbitmq_client.connect()
|
||||
if success:
|
||||
self.publisher = UnifiedEventPublisher(self.rabbitmq_client, self.service_name)
|
||||
logger.info(f"{self.service_name} messaging manager setup completed")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"{self.service_name} messaging manager setup failed")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error during {self.service_name} messaging manager setup", error=str(e))
|
||||
return False
|
||||
|
||||
async def cleanup(self):
|
||||
"""Cleanup the messaging system for the service"""
|
||||
try:
|
||||
if self.rabbitmq_client:
|
||||
await self.rabbitmq_client.disconnect()
|
||||
logger.info(f"{self.service_name} messaging manager cleanup completed")
|
||||
return True
|
||||
return True # If no client to clean up, consider it successful
|
||||
except Exception as e:
|
||||
logger.error(f"Error during {self.service_name} messaging manager cleanup", error=str(e))
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_ready(self):
|
||||
"""Check if the messaging system is ready for use"""
|
||||
return (self.publisher is not None and
|
||||
self.rabbitmq_client is not None and
|
||||
self.rabbitmq_client.connected)
|
||||
|
||||
|
||||
# Utility functions for easy service integration
|
||||
async def initialize_service_publisher(service_name: str, rabbitmq_url: str):
|
||||
"""
|
||||
Initialize a service-specific publisher using the unified messaging system.
|
||||
|
||||
Args:
|
||||
service_name: Name of the service (e.g., 'notification-service', 'forecasting-service')
|
||||
rabbitmq_url: RabbitMQ connection URL
|
||||
|
||||
Returns:
|
||||
UnifiedEventPublisher instance or None if initialization failed
|
||||
"""
|
||||
try:
|
||||
rabbitmq_client = RabbitMQClient(rabbitmq_url, service_name)
|
||||
success = await rabbitmq_client.connect()
|
||||
if success:
|
||||
publisher = UnifiedEventPublisher(rabbitmq_client, service_name)
|
||||
logger.info(f"{service_name} unified messaging publisher initialized")
|
||||
return rabbitmq_client, publisher
|
||||
else:
|
||||
logger.warning(f"{service_name} unified messaging publisher failed to connect")
|
||||
return None, None
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize {service_name} unified messaging publisher", error=str(e))
|
||||
return None, None
|
||||
|
||||
|
||||
async def cleanup_service_publisher(rabbitmq_client):
|
||||
"""
|
||||
Cleanup messaging for a service.
|
||||
|
||||
Args:
|
||||
rabbitmq_client: The RabbitMQ client to disconnect
|
||||
|
||||
Returns:
|
||||
True if cleanup was successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
if rabbitmq_client:
|
||||
await rabbitmq_client.disconnect()
|
||||
logger.info("Service messaging cleanup completed")
|
||||
return True
|
||||
return True # If no client to clean up, consider it successful
|
||||
except Exception as e:
|
||||
logger.error("Error during service messaging cleanup", error=str(e))
|
||||
return False
|
||||
@@ -1,266 +0,0 @@
|
||||
"""
|
||||
RabbitMQ messaging client for microservices - FIXED VERSION
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Dict, Any, Callable, Optional
|
||||
from datetime import datetime, date
|
||||
import uuid
|
||||
import structlog
|
||||
from contextlib import suppress
|
||||
|
||||
try:
|
||||
import aio_pika
|
||||
from aio_pika import connect_robust, Message, DeliveryMode, ExchangeType
|
||||
AIO_PIKA_AVAILABLE = True
|
||||
except ImportError:
|
||||
AIO_PIKA_AVAILABLE = False
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
class HeartbeatMonitor:
|
||||
"""Monitor to ensure heartbeats are processed during heavy operations"""
|
||||
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
self._monitor_task = None
|
||||
self._should_monitor = False
|
||||
|
||||
async def start_monitoring(self):
|
||||
"""Start heartbeat monitoring task"""
|
||||
if self._monitor_task and not self._monitor_task.done():
|
||||
return
|
||||
|
||||
self._should_monitor = True
|
||||
self._monitor_task = asyncio.create_task(self._monitor_loop())
|
||||
|
||||
async def stop_monitoring(self):
|
||||
"""Stop heartbeat monitoring task"""
|
||||
self._should_monitor = False
|
||||
if self._monitor_task and not self._monitor_task.done():
|
||||
self._monitor_task.cancel()
|
||||
with suppress(asyncio.CancelledError):
|
||||
await self._monitor_task
|
||||
|
||||
async def _monitor_loop(self):
|
||||
"""Monitor loop that periodically yields control for heartbeat processing"""
|
||||
while self._should_monitor:
|
||||
# Yield control to allow heartbeat processing
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# Verify connection is still alive
|
||||
if self.client.connection and not self.client.connection.is_closed:
|
||||
# Check if connection is still responsive
|
||||
try:
|
||||
# This is a lightweight check to ensure the connection is responsive
|
||||
pass # The heartbeat mechanism in aio_pika handles this internally
|
||||
except Exception as e:
|
||||
logger.warning("Connection check failed", error=str(e))
|
||||
self.client.connected = False
|
||||
break
|
||||
else:
|
||||
logger.warning("Connection is closed, stopping monitor")
|
||||
break
|
||||
|
||||
def json_serializer(obj):
|
||||
"""JSON serializer for objects not serializable by default json code"""
|
||||
if isinstance(obj, (datetime, date)):
|
||||
return obj.isoformat()
|
||||
elif isinstance(obj, uuid.UUID):
|
||||
return str(obj)
|
||||
elif hasattr(obj, '__class__') and obj.__class__.__name__ == 'Decimal':
|
||||
# Handle Decimal objects from SQLAlchemy without importing decimal
|
||||
return float(obj)
|
||||
raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
|
||||
|
||||
class RabbitMQClient:
|
||||
"""
|
||||
Universal RabbitMQ client for all microservices
|
||||
Handles all messaging patterns with proper fallbacks
|
||||
"""
|
||||
|
||||
def __init__(self, connection_url: str, service_name: str = "unknown"):
|
||||
self.connection_url = connection_url
|
||||
self.service_name = service_name
|
||||
self.connection = None
|
||||
self.channel = None
|
||||
self.connected = False
|
||||
self._reconnect_attempts = 0
|
||||
self._max_reconnect_attempts = 5
|
||||
self.heartbeat_monitor = HeartbeatMonitor(self)
|
||||
|
||||
async def connect(self):
|
||||
"""Connect to RabbitMQ with retry logic"""
|
||||
if not AIO_PIKA_AVAILABLE:
|
||||
logger.warning("aio-pika not available, messaging disabled", service=self.service_name)
|
||||
return False
|
||||
|
||||
try:
|
||||
self.connection = await connect_robust(
|
||||
self.connection_url,
|
||||
heartbeat=600 # Increase heartbeat to 600 seconds (10 minutes) to prevent timeouts
|
||||
)
|
||||
self.channel = await self.connection.channel()
|
||||
await self.channel.set_qos(prefetch_count=100) # Performance optimization
|
||||
|
||||
self.connected = True
|
||||
self._reconnect_attempts = 0
|
||||
|
||||
# Start heartbeat monitoring
|
||||
await self.heartbeat_monitor.start_monitoring()
|
||||
|
||||
logger.info("Connected to RabbitMQ", service=self.service_name)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.connected = False
|
||||
self._reconnect_attempts += 1
|
||||
logger.warning(
|
||||
"Failed to connect to RabbitMQ",
|
||||
service=self.service_name,
|
||||
error=str(e),
|
||||
attempt=self._reconnect_attempts
|
||||
)
|
||||
return False
|
||||
|
||||
async def disconnect(self):
|
||||
"""Disconnect from RabbitMQ with proper channel cleanup"""
|
||||
try:
|
||||
# Stop heartbeat monitoring first
|
||||
await self.heartbeat_monitor.stop_monitoring()
|
||||
|
||||
# Close channel before connection to avoid "unexpected close" warnings
|
||||
if self.channel and not self.channel.is_closed:
|
||||
await self.channel.close()
|
||||
logger.debug("RabbitMQ channel closed", service=self.service_name)
|
||||
|
||||
# Then close connection
|
||||
if self.connection and not self.connection.is_closed:
|
||||
await self.connection.close()
|
||||
logger.info("Disconnected from RabbitMQ", service=self.service_name)
|
||||
|
||||
self.connected = False
|
||||
|
||||
except Exception as e:
|
||||
logger.warning("Error during RabbitMQ disconnect",
|
||||
service=self.service_name,
|
||||
error=str(e))
|
||||
self.connected = False
|
||||
|
||||
async def ensure_connected(self) -> bool:
|
||||
"""Ensure connection is active, reconnect if needed"""
|
||||
if self.connected and self.connection and not self.connection.is_closed:
|
||||
return True
|
||||
|
||||
if self._reconnect_attempts >= self._max_reconnect_attempts:
|
||||
logger.error("Max reconnection attempts reached", service=self.service_name)
|
||||
return False
|
||||
|
||||
return await self.connect()
|
||||
|
||||
async def publish_event(self, exchange_name: str, routing_key: str, event_data: Dict[str, Any],
|
||||
persistent: bool = True) -> bool:
|
||||
"""
|
||||
Universal event publisher with automatic fallback
|
||||
Returns True if published successfully, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Ensure we're connected
|
||||
if not await self.ensure_connected():
|
||||
logger.debug("Event not published - RabbitMQ unavailable",
|
||||
service=self.service_name, routing_key=routing_key)
|
||||
return False
|
||||
|
||||
# Declare exchange
|
||||
exchange = await self.channel.declare_exchange(
|
||||
exchange_name,
|
||||
ExchangeType.TOPIC,
|
||||
durable=True
|
||||
)
|
||||
|
||||
# Prepare message with proper JSON serialization
|
||||
message_body = json.dumps(event_data, default=json_serializer)
|
||||
message = Message(
|
||||
message_body.encode(),
|
||||
delivery_mode=DeliveryMode.PERSISTENT if persistent else DeliveryMode.NOT_PERSISTENT,
|
||||
content_type="application/json",
|
||||
timestamp=datetime.now(),
|
||||
headers={
|
||||
"source_service": self.service_name,
|
||||
"event_id": event_data.get("event_id", str(uuid.uuid4()))
|
||||
}
|
||||
)
|
||||
|
||||
# Publish message
|
||||
await exchange.publish(message, routing_key=routing_key)
|
||||
|
||||
logger.debug("Event published successfully",
|
||||
service=self.service_name,
|
||||
exchange=exchange_name,
|
||||
routing_key=routing_key,
|
||||
size=len(message_body))
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to publish event",
|
||||
service=self.service_name,
|
||||
exchange=exchange_name,
|
||||
routing_key=routing_key,
|
||||
error=str(e))
|
||||
self.connected = False # Force reconnection on next attempt
|
||||
return False
|
||||
|
||||
async def consume_events(self, exchange_name: str, queue_name: str,
|
||||
routing_key: str, callback: Callable) -> bool:
|
||||
"""Universal event consumer"""
|
||||
try:
|
||||
if not await self.ensure_connected():
|
||||
return False
|
||||
|
||||
# Declare exchange
|
||||
exchange = await self.channel.declare_exchange(
|
||||
exchange_name,
|
||||
ExchangeType.TOPIC,
|
||||
durable=True
|
||||
)
|
||||
|
||||
# Declare queue
|
||||
queue = await self.channel.declare_queue(
|
||||
queue_name,
|
||||
durable=True
|
||||
)
|
||||
|
||||
# Bind queue to exchange
|
||||
await queue.bind(exchange, routing_key)
|
||||
|
||||
# Set up consumer
|
||||
await queue.consume(callback)
|
||||
|
||||
logger.info("Started consuming events",
|
||||
service=self.service_name,
|
||||
queue=queue_name,
|
||||
routing_key=routing_key)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to start consuming events",
|
||||
service=self.service_name,
|
||||
error=str(e))
|
||||
return False
|
||||
|
||||
# High-level convenience methods for common patterns
|
||||
async def publish_user_event(self, event_type: str, user_data: Dict[str, Any]) -> bool:
|
||||
"""Publish user-related events"""
|
||||
return await self.publish_event("user.events", f"user.{event_type}", user_data)
|
||||
|
||||
async def publish_training_event(self, event_type: str, training_data: Dict[str, Any]) -> bool:
|
||||
"""Publish training-related events"""
|
||||
return await self.publish_event("training.events", f"training.{event_type}", training_data)
|
||||
|
||||
async def publish_data_event(self, event_type: str, data: Dict[str, Any]) -> bool:
|
||||
"""Publish data-related events"""
|
||||
return await self.publish_event("data.events", f"data.{event_type}", data)
|
||||
|
||||
async def publish_forecast_event(self, event_type: str, forecast_data: Dict[str, Any]) -> bool:
|
||||
"""Publish forecast-related events"""
|
||||
return await self.publish_event("forecast.events", f"forecast.{event_type}", forecast_data)
|
||||
Reference in New Issue
Block a user