""" 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