demo seed change

This commit is contained in:
Urtzi Alfaro
2025-12-13 23:57:54 +01:00
parent f3688dfb04
commit ff830a3415
299 changed files with 20328 additions and 19485 deletions

View File

@@ -4,14 +4,21 @@ Handles automatic cleanup of expired sessions
"""
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update
from datetime import datetime, timezone
from typing import List
from sqlalchemy import select
from datetime import datetime, timezone, timedelta
import structlog
import httpx
import asyncio
import os
from app.models import DemoSession, DemoSessionStatus
from app.services.data_cloner import DemoDataCloner
from datetime import datetime, timezone, timedelta
from app.core.redis_wrapper import DemoRedisWrapper
from app.monitoring.metrics import (
demo_sessions_deleted_total,
demo_session_cleanup_duration_seconds,
demo_sessions_active
)
logger = structlog.get_logger()
@@ -22,7 +29,199 @@ class DemoCleanupService:
def __init__(self, db: AsyncSession, redis: DemoRedisWrapper):
self.db = db
self.redis = redis
self.data_cloner = DemoDataCloner(db, redis)
from app.core.config import settings
self.internal_api_key = settings.INTERNAL_API_KEY
# Service URLs for cleanup
self.services = [
("tenant", os.getenv("TENANT_SERVICE_URL", "http://tenant-service:8000")),
("auth", os.getenv("AUTH_SERVICE_URL", "http://auth-service:8000")),
("inventory", os.getenv("INVENTORY_SERVICE_URL", "http://inventory-service:8000")),
("recipes", os.getenv("RECIPES_SERVICE_URL", "http://recipes-service:8000")),
("suppliers", os.getenv("SUPPLIERS_SERVICE_URL", "http://suppliers-service:8000")),
("production", os.getenv("PRODUCTION_SERVICE_URL", "http://production-service:8000")),
("procurement", os.getenv("PROCUREMENT_SERVICE_URL", "http://procurement-service:8000")),
("sales", os.getenv("SALES_SERVICE_URL", "http://sales-service:8000")),
("orders", os.getenv("ORDERS_SERVICE_URL", "http://orders-service:8000")),
("forecasting", os.getenv("FORECASTING_SERVICE_URL", "http://forecasting-service:8000")),
("orchestrator", os.getenv("ORCHESTRATOR_SERVICE_URL", "http://orchestrator-service:8000")),
]
async def cleanup_session(self, session: DemoSession) -> dict:
"""
Delete all data for a demo session across all services.
Returns:
{
"success": bool,
"total_deleted": int,
"duration_ms": int,
"details": {service: {records_deleted, duration_ms}},
"errors": []
}
"""
start_time = datetime.now(timezone.utc)
virtual_tenant_id = str(session.virtual_tenant_id)
session_id = session.session_id
logger.info(
"Starting demo session cleanup",
session_id=session_id,
virtual_tenant_id=virtual_tenant_id,
demo_account_type=session.demo_account_type
)
# Delete from all services in parallel
tasks = [
self._delete_from_service(name, url, virtual_tenant_id)
for name, url in self.services
]
service_results = await asyncio.gather(*tasks, return_exceptions=True)
# Aggregate results
total_deleted = 0
details = {}
errors = []
for (service_name, _), result in zip(self.services, service_results):
if isinstance(result, Exception):
errors.append(f"{service_name}: {str(result)}")
details[service_name] = {"status": "error", "error": str(result)}
else:
total_deleted += result.get("records_deleted", {}).get("total", 0)
details[service_name] = result
# Delete from Redis
await self._delete_redis_cache(virtual_tenant_id)
# Delete child tenants if enterprise
if session.demo_account_type == "enterprise":
child_metadata = session.session_metadata.get("children", [])
for child in child_metadata:
child_tenant_id = child["virtual_tenant_id"]
await self._delete_from_all_services(child_tenant_id)
duration_ms = int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000)
success = len(errors) == 0
logger.info(
"Demo session cleanup completed",
session_id=session_id,
virtual_tenant_id=virtual_tenant_id,
success=success,
total_deleted=total_deleted,
duration_ms=duration_ms,
error_count=len(errors)
)
return {
"success": success,
"total_deleted": total_deleted,
"duration_ms": duration_ms,
"details": details,
"errors": errors
}
async def _delete_from_service(
self,
service_name: str,
service_url: str,
virtual_tenant_id: str
) -> dict:
"""Delete all data from a single service"""
try:
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.delete(
f"{service_url}/internal/demo/tenant/{virtual_tenant_id}",
headers={"X-Internal-API-Key": self.internal_api_key}
)
if response.status_code == 200:
return response.json()
elif response.status_code == 404:
# Already deleted or never existed - idempotent
return {
"service": service_name,
"status": "not_found",
"records_deleted": {"total": 0}
}
else:
raise Exception(f"HTTP {response.status_code}: {response.text}")
except Exception as e:
logger.error(
"Failed to delete from service",
service=service_name,
virtual_tenant_id=virtual_tenant_id,
error=str(e)
)
raise
async def _delete_redis_cache(self, virtual_tenant_id: str):
"""Delete all Redis keys for a virtual tenant"""
try:
client = await self.redis.get_client()
pattern = f"*:{virtual_tenant_id}:*"
keys = await client.keys(pattern)
if keys:
await client.delete(*keys)
logger.debug("Deleted Redis cache", tenant_id=virtual_tenant_id, keys_deleted=len(keys))
except Exception as e:
logger.warning("Failed to delete Redis cache", error=str(e), tenant_id=virtual_tenant_id)
async def _delete_from_all_services(self, virtual_tenant_id: str):
"""Delete data from all services for a tenant"""
tasks = [
self._delete_from_service(name, url, virtual_tenant_id)
for name, url in self.services
]
return await asyncio.gather(*tasks, return_exceptions=True)
async def _delete_tenant_data(self, tenant_id: str, session_id: str) -> dict:
"""Delete demo data for a tenant across all services"""
logger.info("Deleting tenant data", tenant_id=tenant_id, session_id=session_id)
results = {}
async def delete_from_service(service_name: str, service_url: str):
try:
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.delete(
f"{service_url}/internal/demo/tenant/{tenant_id}",
headers={"X-Internal-API-Key": self.internal_api_key}
)
if response.status_code == 200:
logger.debug(f"Deleted data from {service_name}", tenant_id=tenant_id)
return {"service": service_name, "status": "deleted"}
else:
logger.warning(
f"Failed to delete from {service_name}",
status_code=response.status_code,
tenant_id=tenant_id
)
return {"service": service_name, "status": "failed", "error": f"HTTP {response.status_code}"}
except Exception as e:
logger.warning(
f"Exception deleting from {service_name}",
error=str(e),
tenant_id=tenant_id
)
return {"service": service_name, "status": "failed", "error": str(e)}
# Delete from all services in parallel
tasks = [delete_from_service(name, url) for name, url in self.services]
service_results = await asyncio.gather(*tasks, return_exceptions=True)
for result in service_results:
if isinstance(result, Exception):
logger.error("Service deletion failed", error=str(result))
elif isinstance(result, dict):
results[result["service"]] = result
return results
async def cleanup_expired_sessions(self) -> dict:
"""
@@ -32,9 +231,9 @@ class DemoCleanupService:
Returns:
Cleanup statistics
"""
from datetime import timedelta
logger.info("Starting demo session cleanup")
start_time = datetime.now(timezone.utc)
now = datetime.now(timezone.utc)
stuck_threshold = now - timedelta(minutes=5) # Sessions pending > 5 min are stuck
@@ -97,10 +296,7 @@ class DemoCleanupService:
)
for child_id in child_tenant_ids:
try:
await self.data_cloner.delete_session_data(
str(child_id),
session.session_id
)
await self._delete_tenant_data(child_id, session.session_id)
except Exception as child_error:
logger.error(
"Failed to delete child tenant",
@@ -109,11 +305,14 @@ class DemoCleanupService:
)
# Delete parent/main session data
await self.data_cloner.delete_session_data(
await self._delete_tenant_data(
str(session.virtual_tenant_id),
session.session_id
)
# Delete Redis data
await self.redis.delete_session_data(session.session_id)
stats["cleaned_up"] += 1
logger.info(
@@ -137,6 +336,19 @@ class DemoCleanupService:
)
logger.info("Demo session cleanup completed", stats=stats)
# Update Prometheus metrics
duration_ms = int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000)
demo_session_cleanup_duration_seconds.labels(tier="all").observe(duration_ms / 1000)
# Update deleted sessions metrics by tier (we need to determine tiers from sessions)
for session in all_sessions_to_cleanup:
demo_sessions_deleted_total.labels(
tier=session.demo_account_type,
status="success"
).inc()
demo_sessions_active.labels(tier=session.demo_account_type).dec()
return stats
async def cleanup_old_destroyed_sessions(self, days: int = 7) -> int:
@@ -149,8 +361,6 @@ class DemoCleanupService:
Returns:
Number of deleted records
"""
from datetime import timedelta
cutoff_date = datetime.now(timezone.utc) - timedelta(days=days)
result = await self.db.execute(