feat(usage): 改进请求追踪与缓存预热

- 区分请求重试与故障转移:新增 has_retry 标识亲和缓存重试
- 修正候选 TTFB 计算:记录候选自身的首字节时间而非全局时间
- Streaming 状态同步 rate_multiplier,支持按 API 格式配置
- 新增缓存预热服务:启动时预热仪表盘统计、热力图、每日统计
- 优化缓存 TTL 配置:仪表盘统计 2 分钟、热力图和每日统计 10 分钟
- 自动刷新间隔从 10 秒调整为 5 秒
This commit is contained in:
fawney19
2026-01-15 11:45:46 +08:00
parent 24b65d76b5
commit 514bf7e3ed
15 changed files with 386 additions and 39 deletions

View File

@@ -0,0 +1,159 @@
"""
缓存预热服务
在应用启动时预热关键缓存,避免用户首次访问时的长时间等待。
预热的缓存包括:
- 管理员仪表盘统计数据
- 管理员热力图数据
- 每日统计数据
"""
import asyncio
import time
from dataclasses import dataclass, field
from typing import Any, Dict
from sqlalchemy.orm import Session
from src.core.logger import logger
from src.database import create_session
@dataclass
class WarmupContext:
"""缓存预热专用的简化 Context"""
db: Session
user: Any # User model
audit_metadata: Dict[str, Any] = field(default_factory=dict)
def add_audit_metadata(self, **kwargs: Any) -> None:
"""兼容 ApiRequestContext 接口"""
self.audit_metadata.update(kwargs)
class CacheWarmupService:
"""缓存预热服务"""
@classmethod
async def warmup_all(cls, delay_seconds: float = 3.0) -> None:
"""
预热所有关键缓存
Args:
delay_seconds: 启动后延迟执行的秒数,确保系统完全就绪
"""
await asyncio.sleep(delay_seconds)
logger.info("开始预热关键缓存...")
start_time = time.time()
results = await asyncio.gather(
cls._warmup_admin_dashboard_stats(),
cls._warmup_admin_heatmap(),
cls._warmup_daily_stats(),
return_exceptions=True,
)
success_count = sum(1 for r in results if r is True)
error_count = sum(1 for r in results if isinstance(r, Exception))
elapsed = time.time() - start_time
if error_count > 0:
logger.warning(f"缓存预热完成: {success_count}/3 成功, {error_count} 失败, 耗时 {elapsed:.2f}s")
else:
logger.info(f"缓存预热完成: {success_count}/3 成功, 耗时 {elapsed:.2f}s")
@classmethod
async def _warmup_admin_dashboard_stats(cls) -> bool:
"""预热管理员仪表盘统计缓存"""
db = None
try:
from src.api.dashboard.routes import AdminDashboardStatsAdapter
from src.models.database import User as DBUser
db = create_session()
# 获取一个管理员用户用于构造 context
admin_user = db.query(DBUser).filter(DBUser.role == "admin").first()
if not admin_user:
logger.info("缓存预热: 无管理员用户,跳过仪表盘统计预热")
return True
context = WarmupContext(db=db, user=admin_user)
adapter = AdminDashboardStatsAdapter()
await adapter.handle(context)
logger.debug("缓存预热: 管理员仪表盘统计已预热")
return True
except Exception as e:
logger.warning(f"缓存预热失败 (仪表盘统计): {e}")
return False
finally:
if db:
db.close()
@classmethod
async def _warmup_admin_heatmap(cls) -> bool:
"""预热管理员热力图缓存"""
db = None
try:
from src.services.usage.service import UsageService
db = create_session()
# 预热全局热力图(管理员视角)
await UsageService.get_cached_heatmap(
db=db,
user_id=None,
include_actual_cost=True,
)
logger.debug("缓存预热: 管理员热力图已预热")
return True
except Exception as e:
logger.warning(f"缓存预热失败 (热力图): {e}")
return False
finally:
if db:
db.close()
@classmethod
async def _warmup_daily_stats(cls) -> bool:
"""预热每日统计缓存"""
db = None
try:
from src.api.dashboard.routes import DashboardDailyStatsAdapter
from src.models.database import User as DBUser
db = create_session()
# 获取一个管理员用户
admin_user = db.query(DBUser).filter(DBUser.role == "admin").first()
if not admin_user:
logger.info("缓存预热: 无管理员用户,跳过每日统计预热")
return True
context = WarmupContext(db=db, user=admin_user)
# 预热 7 天的每日统计
adapter = DashboardDailyStatsAdapter(days=7)
await adapter.handle(context)
logger.debug("缓存预热: 每日统计已预热")
return True
except Exception as e:
logger.warning(f"缓存预热失败 (每日统计): {e}")
return False
finally:
if db:
db.close()
async def start_cache_warmup() -> None:
"""启动缓存预热(作为后台任务)"""
asyncio.create_task(CacheWarmupService.warmup_all())