Files
bakery-ia/services/forecasting/app/consumers/forecast_event_consumer.py
2025-11-30 09:12:40 +01:00

178 lines
7.3 KiB
Python

"""
Forecast event consumer for the forecasting service
Handles events that should trigger cache invalidation for aggregated forecasts
"""
import logging
from typing import Dict, Any, Optional
import json
import redis.asyncio as redis
logger = logging.getLogger(__name__)
class ForecastEventConsumer:
"""
Consumer for forecast events that may trigger cache invalidation
"""
def __init__(self, redis_client: redis.Redis):
self.redis_client = redis_client
async def handle_forecast_updated(self, event_data: Dict[str, Any]):
"""
Handle forecast updated event
Invalidate parent tenant's aggregated forecast cache if this tenant is a child
"""
try:
logger.info(f"Handling forecast updated event: {event_data}")
tenant_id = event_data.get('tenant_id')
forecast_date = event_data.get('forecast_date')
product_id = event_data.get('product_id')
updated_at = event_data.get('updated_at', None)
if not tenant_id:
logger.error("Missing tenant_id in forecast event")
return
# Check if this tenant is a child tenant (has parent)
# In a real implementation, this would call the tenant service to check hierarchy
parent_tenant_id = await self._get_parent_tenant_id(tenant_id)
if parent_tenant_id:
# Invalidate parent's aggregated forecast cache
await self._invalidate_parent_aggregated_cache(
parent_tenant_id=parent_tenant_id,
child_tenant_id=tenant_id,
forecast_date=forecast_date,
product_id=product_id
)
logger.info(f"Forecast updated event processed for tenant {tenant_id}")
except Exception as e:
logger.error(f"Error handling forecast updated event: {e}", exc_info=True)
raise
async def handle_forecast_created(self, event_data: Dict[str, Any]):
"""
Handle forecast created event
Similar to update, may affect parent tenant's aggregated forecasts
"""
await self.handle_forecast_updated(event_data)
async def handle_forecast_deleted(self, event_data: Dict[str, Any]):
"""
Handle forecast deleted event
Similar to update, may affect parent tenant's aggregated forecasts
"""
try:
logger.info(f"Handling forecast deleted event: {event_data}")
tenant_id = event_data.get('tenant_id')
forecast_date = event_data.get('forecast_date')
product_id = event_data.get('product_id')
if not tenant_id:
logger.error("Missing tenant_id in forecast delete event")
return
# Check if this tenant is a child tenant
parent_tenant_id = await self._get_parent_tenant_id(tenant_id)
if parent_tenant_id:
# Invalidate parent's aggregated forecast cache
await self._invalidate_parent_aggregated_cache(
parent_tenant_id=parent_tenant_id,
child_tenant_id=tenant_id,
forecast_date=forecast_date,
product_id=product_id
)
logger.info(f"Forecast deleted event processed for tenant {tenant_id}")
except Exception as e:
logger.error(f"Error handling forecast deleted event: {e}", exc_info=True)
raise
async def _get_parent_tenant_id(self, tenant_id: str) -> Optional[str]:
"""
Get parent tenant ID for a child tenant
In a real implementation, this would call the tenant service
"""
# This is a placeholder implementation
# In real implementation, this would use TenantServiceClient to get tenant hierarchy
try:
# Simulate checking tenant hierarchy
# In real implementation: return await self.tenant_client.get_parent_tenant_id(tenant_id)
# For now, we'll return a placeholder implementation that would check the database
# This is just a simulation of the actual implementation needed
return None # Placeholder - real implementation needed
except Exception as e:
logger.error(f"Error getting parent tenant ID for {tenant_id}: {e}")
return None
async def _invalidate_parent_aggregated_cache(
self,
parent_tenant_id: str,
child_tenant_id: str,
forecast_date: Optional[str] = None,
product_id: Optional[str] = None
):
"""
Invalidate parent tenant's aggregated forecast cache
"""
try:
# Pattern to match all aggregated forecast cache keys for this parent
# Format: agg_forecast:{parent_tenant_id}:{start_date}:{end_date}:{product_id}
pattern = f"agg_forecast:{parent_tenant_id}:*:*:*"
# Find all matching keys and delete them
keys_to_delete = []
async for key in self.redis_client.scan_iter(match=pattern):
if isinstance(key, bytes):
key = key.decode('utf-8')
keys_to_delete.append(key)
if keys_to_delete:
await self.redis_client.delete(*keys_to_delete)
logger.info(f"Invalidated {len(keys_to_delete)} aggregated forecast cache entries for parent tenant {parent_tenant_id}")
else:
logger.info(f"No aggregated forecast cache entries found to invalidate for parent tenant {parent_tenant_id}")
except Exception as e:
logger.error(f"Error invalidating parent aggregated cache: {e}", exc_info=True)
raise
async def handle_tenant_hierarchy_changed(self, event_data: Dict[str, Any]):
"""
Handle tenant hierarchy change event
This could be when a tenant becomes a child of another, or when the hierarchy changes
"""
try:
logger.info(f"Handling tenant hierarchy change event: {event_data}")
tenant_id = event_data.get('tenant_id')
parent_tenant_id = event_data.get('parent_tenant_id')
action = event_data.get('action') # 'added', 'removed', 'changed'
# Invalidate any cached aggregated forecasts that might be affected
if parent_tenant_id:
# If this child tenant changed, invalidate parent's cache
await self._invalidate_parent_aggregated_cache(
parent_tenant_id=parent_tenant_id,
child_tenant_id=tenant_id
)
# If this was a former parent tenant that's no longer a parent,
# its aggregated cache might need to be invalidated differently
if action == 'removed' and event_data.get('was_parent'):
# Invalidate its own aggregated cache since it's no longer a parent
# This would be handled by tenant service events
pass
except Exception as e:
logger.error(f"Error handling tenant hierarchy change event: {e}", exc_info=True)
raise