demo seed change
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user