104 lines
4.2 KiB
Python
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))
|