rmi-backend/app/core/lifespan.py

104 lines
4.2 KiB
Python

"""RMI Backend - Application lifespan (startup/shutdown events)."""
import asyncio
import os
import httpx
from fastapi import FastAPI
from app.core.logging import get_logger
logger = get_logger(__name__)
# Background task imports (lazy, at startup time)
async def lifespan(app: FastAPI):
"""Application lifespan: startup checks, background tasks, graceful shutdown."""
vault_pw = os.getenv("WALLET_VAULT_PASSWORD", "").strip()
if not vault_pw:
raise RuntimeError(
"CRITICAL: WALLET_VAULT_PASSWORD environment variable is missing or empty. "
"The backend will not start without it to prevent silent wallet key loss."
)
app.state.http_client = httpx.AsyncClient(
limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), timeout=10.0
)
await _verify_indexes(app)
await _start_background_tasks(app)
yield # App runs here
await app.state.http_client.aclose()
logger.info("shutdown_complete")
async def _start_background_tasks(app: FastAPI) -> None:
"""Start all background monitoring / cleanup tasks."""
tasks = [
("facilitator_health", "app.routers.facilitator_health", "health_check_loop", "60s"),
("status_page", "app.routers.status_page", "status_monitor_loop", "30s"),
("webhook_dispatcher", "app.routers.webhook_dispatcher", "webhook_dispatcher_loop", "5s"),
("x402_trial_cleanup", "app.routers.x402_enforcement", "trial_cleanup_loop", "hourly"),
("auto_sweep", "app.wallet_manager_v2", "auto_sweep_loop", ""),
]
for name, module_path, func_name, interval in tasks:
try:
mod = __import__(module_path, fromlist=[func_name])
fn = getattr(mod, func_name)
if name == "auto_sweep":
asyncio.create_task(fn())
else:
asyncio.create_task(fn())
logger.info("background_task_started", task=name, interval=interval)
except Exception as e:
logger.warning("background_task_failed", task=name, error=str(e))
# Cache warmer (passes app instance)
try:
from app.domains.databus.core import cache_warm_loop
asyncio.create_task(cache_warm_loop(app))
logger.info("background_task_started", task="databus_cache_warmer", interval="")
except Exception as e:
logger.warning("background_task_failed", task="databus_cache_warmer", error=str(e))
async def _verify_indexes(app: FastAPI) -> None:
"""Verify and create database indexes on startup."""
try:
supabase_url = os.getenv("SUPABASE_URL", "")
supabase_key = os.getenv("SUPABASE_SERVICE_KEY", "") or os.getenv("SUPABASE_KEY", "")
if not supabase_url or not supabase_key:
return
indexes = [
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_scan_results_created_at ON scan_results(created_at DESC);",
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_scan_results_token_address ON scan_results(token_address);",
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_whale_alerts_created_at ON whale_alerts(created_at DESC);",
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_security_alerts_created_at ON security_alerts(created_at DESC);",
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_x402_payments_tx_hash ON x402_payments(tx_hash);",
]
for sql in indexes:
try:
res = await app.state.http_client.post(
f"{supabase_url}/rest/v1/rpc/exec_sql",
json={"query": sql},
headers={
"apikey": supabase_key,
"Authorization": f"Bearer {supabase_key}",
"Content-Type": "application/json",
},
)
if res.status_code in [200, 204]:
logger.info("index_verified", table=sql.split("ON ")[1].split("(")[0].strip())
else:
logger.warning("index_skipped", status=res.status_code, sql=sql[:50])
except Exception as e:
logger.warning("index_verify_failed", error=str(e))
except Exception as e:
logger.warning("index_verification_skipped", error=str(e))