Files
Aether/_deprecated_py_src/services/system/maintenance_scheduler.py

1479 lines
58 KiB
Python
Raw Normal View History

2025-12-10 20:52:44 +08:00
"""
系统维护定时任务调度器
2025-12-10 20:52:44 +08:00
包含以下任务
- 统计聚合每天凌晨聚合前一天的统计数据
- Provider 签到每天凌晨执行所有已配置 Provider 的签到
- 使用记录清理分级清理策略压缩清空删除
- 审计日志清理定期清理过期的审计日志
- 连接池监控定期检查数据库连接池状态
- Pending 状态清理清理异常的 Pending 状态记录
- Gemini 文件映射清理清理过期的 Gemini 文件Key 映射
- 请求候选记录清理定期清理过期的 request_candidates 记录
- 数据库表维护定期 VACUUM ANALYZE 防止表和索引膨胀
2025-12-10 20:52:44 +08:00
使用 APScheduler 进行任务调度支持时区配置
"""
from __future__ import annotations
2025-12-10 20:52:44 +08:00
import asyncio
import os
from datetime import date, datetime, timedelta, timezone
from typing import Any
2025-12-10 20:52:44 +08:00
from sqlalchemy import delete, literal_column, text
2025-12-10 20:52:44 +08:00
from src.clients.http_client import HTTPClientPool
from src.config.settings import config
2025-12-10 20:52:44 +08:00
from src.core.logger import logger
from src.database import create_session
from src.models.database import AuditLog, Provider, RequestCandidate, Usage
from src.services.provider_ops.service import ProviderOpsService
2025-12-10 20:52:44 +08:00
from src.services.system.config import SystemConfigService
from src.services.system.scheduler import get_scheduler
from src.services.system.stats_aggregator import StatsAggregatorService
from src.services.user.apikey import ApiKeyService
from src.services.wallet import WalletDailyUsageLedgerService
2025-12-10 20:52:44 +08:00
from src.utils.compression import compress_json
class MaintenanceScheduler:
"""系统维护任务调度器"""
2025-12-10 20:52:44 +08:00
# 签到任务的 job_id
CHECKIN_JOB_ID = "provider_checkin"
def __init__(self) -> None:
2025-12-10 20:52:44 +08:00
self.running = False
self._interval_tasks = []
self._stats_aggregation_lock = asyncio.Lock()
self._wallet_daily_usage_lock = asyncio.Lock()
self._startup_task: asyncio.Task[Any] | None = None
self._registered_job_ids: list[str] = []
2025-12-10 20:52:44 +08:00
@staticmethod
def _get_http_client_idle_cleanup_interval_minutes() -> int:
"""获取 HTTP 客户端空闲清理调度间隔(分钟)。"""
raw = os.getenv("HTTP_CLIENT_IDLE_CLEANUP_INTERVAL_MINUTES", "5")
try:
minutes = int(raw)
except ValueError:
logger.warning(
"环境变量 HTTP_CLIENT_IDLE_CLEANUP_INTERVAL_MINUTES 非法: {}, 使用默认值 5",
raw,
)
return 5
return max(1, minutes)
@staticmethod
def _has_python_startup_tasks() -> bool:
return bool(
config.pending_cleanup_python_enabled or config.antigravity_ua_refresh_python_enabled
)
def _get_checkin_time(self) -> tuple[int, int]:
"""获取签到任务的执行时间
Returns:
(hour, minute) 元组
"""
db = create_session()
try:
time_str = SystemConfigService.get_config(db, "provider_checkin_time", "01:05")
return self._parse_time_string(time_str)
finally:
db.close()
@staticmethod
def _parse_time_string(time_str: str) -> tuple[int, int]:
"""解析时间字符串为 (hour, minute) 元组
Args:
time_str: HH:MM 格式的时间字符串
Returns:
(hour, minute) 元组解析失败返回默认值 (1, 5)
"""
try:
if not time_str or ":" not in time_str:
return (1, 5)
parts = time_str.split(":")
hour = int(parts[0])
minute = int(parts[1])
# 验证范围
if 0 <= hour <= 23 and 0 <= minute <= 59:
return (hour, minute)
return (1, 5)
except (ValueError, IndexError):
return (1, 5)
def update_checkin_time(self, time_str: str) -> bool:
"""更新签到任务的执行时间
Args:
time_str: HH:MM 格式的时间字符串
Returns:
是否成功更新
"""
hour, minute = self._parse_time_string(time_str)
scheduler = get_scheduler()
success = scheduler.reschedule_cron_job(
self.CHECKIN_JOB_ID,
hour=hour,
minute=minute,
)
if success:
logger.info(f"Provider 签到任务时间已更新为: {hour:02d}:{minute:02d}")
return success
def get_checkin_job_info(self) -> dict | None:
"""获取签到任务的信息
Returns:
任务信息字典
"""
scheduler = get_scheduler()
return scheduler.get_job_info(self.CHECKIN_JOB_ID)
async def start(self) -> Any:
2025-12-10 20:52:44 +08:00
"""启动调度器"""
if self.running:
logger.warning("Maintenance scheduler already running")
2025-12-10 20:52:44 +08:00
return
self.running = True
self._registered_job_ids.clear()
logger.info("系统维护调度器已启动")
2025-12-10 20:52:44 +08:00
scheduler = get_scheduler()
def _add_cron(job_id: str, **kwargs: Any) -> None:
scheduler.add_cron_job(job_id=job_id, **kwargs)
self._registered_job_ids.append(job_id)
def _add_interval(job_id: str, **kwargs: Any) -> None:
scheduler.add_interval_job(job_id=job_id, **kwargs)
self._registered_job_ids.append(job_id)
# 注册定时任务
if config.stats_aggregation_python_enabled:
# 统计聚合任务 - UTC 00:05 执行
_add_cron(
"stats_aggregation",
func=self._scheduled_stats_aggregation,
hour=0,
minute=5,
name="统计数据聚合",
timezone="UTC",
)
else:
logger.info("每日统计聚合已切到 Rust gatewayPython 调度器跳过注册")
if config.stats_hourly_aggregation_python_enabled:
# 小时统计聚合任务 - 每小时 05 分执行UTC
_add_cron(
"stats_hourly_aggregation",
func=self._scheduled_hourly_stats_aggregation,
hour="*",
minute=5,
name="统计小时数据聚合",
timezone="UTC",
)
else:
logger.info("小时统计聚合已切到 Rust gatewayPython 调度器跳过注册")
if config.wallet_daily_usage_aggregation_python_enabled:
_add_cron(
"wallet_daily_usage_aggregation",
func=self._scheduled_wallet_daily_usage_aggregation,
hour=0,
minute=10,
name="钱包每日消费汇总",
)
else:
logger.info("钱包每日消费汇总已切到 Rust gatewayPython 调度器跳过注册")
if config.usage_cleanup_python_enabled:
# 清理任务 - 凌晨 3 点执行
_add_cron(
"usage_cleanup",
func=self._scheduled_cleanup,
hour=3,
minute=0,
name="使用记录清理",
)
else:
logger.info("使用记录清理已切到 Rust gatewayPython 调度器跳过注册")
if config.pool_monitor_python_enabled:
# 连接池监控 - 每 5 分钟
_add_interval(
"pool_monitor",
func=self._scheduled_monitor,
minutes=5,
name="连接池监控",
)
else:
logger.info("连接池监控已切到 Rust gatewayPython 调度器跳过注册")
if config.http_client_idle_cleanup_python_enabled:
# HTTP 代理/Tunnel 客户端空闲清理 - 默认每 5 分钟
_add_interval(
"http_client_idle_cleanup",
func=self._scheduled_http_client_idle_cleanup,
minutes=self._get_http_client_idle_cleanup_interval_minutes(),
name="HTTP客户端空闲清理",
)
else:
logger.info("HTTP 客户端空闲清理默认跳过 Python 调度注册")
if config.pending_cleanup_python_enabled:
# Pending 状态清理 - 每 5 分钟
_add_interval(
"pending_cleanup",
func=self._scheduled_pending_cleanup,
minutes=5,
name="Pending状态清理",
)
else:
logger.info("Pending 状态清理已切到 Rust gatewayPython 调度器跳过注册")
if config.audit_cleanup_python_enabled:
# 审计日志清理 - 凌晨 4 点执行
_add_cron(
"audit_cleanup",
func=self._scheduled_audit_cleanup,
hour=4,
minute=0,
name="审计日志清理",
)
else:
logger.info("审计日志清理已切到 Rust gatewayPython 调度器跳过注册")
if config.gemini_file_mapping_cleanup_python_enabled:
# Gemini 文件映射清理 - 每小时执行
_add_interval(
"gemini_file_mapping_cleanup",
func=self._scheduled_gemini_file_mapping_cleanup,
hours=1,
name="Gemini文件映射清理",
)
else:
logger.info("Gemini 文件映射清理已切到 Rust gatewayPython 调度器跳过注册")
if config.request_candidate_cleanup_python_enabled:
# 请求候选记录清理 - 凌晨 3:30 执行
_add_cron(
"candidate_cleanup",
func=self._scheduled_candidate_cleanup,
hour=3,
minute=30,
name="请求候选记录清理",
)
else:
logger.info("请求候选记录清理已切到 Rust gatewayPython 调度器跳过注册")
if config.db_maintenance_python_enabled:
# 数据库表维护 - 每周日凌晨 5 点执行 VACUUM ANALYZE
_add_cron(
"db_maintenance",
func=self._scheduled_db_maintenance,
day_of_week="sun",
hour=5,
minute=0,
name="数据库表维护",
)
else:
logger.info("数据库维护已切到 Rust gatewayPython 调度器跳过注册")
if config.antigravity_ua_refresh_python_enabled:
# Antigravity User-Agent 版本刷新 - 每 6 小时
_add_interval(
"antigravity_ua_refresh",
func=self._scheduled_antigravity_ua_refresh,
hours=6,
name="Antigravity UA版本刷新",
)
else:
logger.info("Antigravity UA 版本刷新默认跳过 Python 调度注册")
if config.provider_checkin_python_enabled:
# Provider 签到任务 - 根据配置时间执行
checkin_hour, checkin_minute = self._get_checkin_time()
_add_cron(
self.CHECKIN_JOB_ID,
func=self._scheduled_provider_checkin,
hour=checkin_hour,
minute=checkin_minute,
name="Provider签到",
)
else:
logger.info("Provider 签到已切到 Rust gatewayPython 调度器跳过注册")
2025-12-10 20:52:44 +08:00
# 启动时执行一次初始化任务
if config.maintenance_startup_tasks_enabled and self._has_python_startup_tasks():
from src.utils.async_utils import safe_create_task
self._startup_task = safe_create_task(self._run_startup_tasks())
elif config.maintenance_startup_tasks_enabled:
self._startup_task = None
logger.info("维护调度器启动任务已无剩余 Python owner跳过创建")
else:
self._startup_task = None
logger.info("维护调度器启动任务已禁用MAINTENANCE_STARTUP_TASKS_ENABLED=false")
2025-12-10 20:52:44 +08:00
async def _run_startup_tasks(self) -> None:
2025-12-10 20:52:44 +08:00
"""启动时执行的初始化任务"""
# 延迟执行等待系统完全启动Redis 连接、其他后台任务稳定)
# 增加延迟时间避免与 UsageQueueConsumer 等后台任务竞争数据库连接
await asyncio.sleep(10)
2025-12-10 20:52:44 +08:00
if config.antigravity_ua_refresh_python_enabled:
# 刷新 Antigravity User-Agent 版本号(不阻塞其他启动任务)
try:
from src.services.provider.adapters.antigravity.client import refresh_user_agent
await refresh_user_agent()
except Exception as e:
logger.debug("启动时刷新 Antigravity UA 版本失败(不影响运行): {}", e)
else:
logger.info("启动时 Antigravity UA 刷新默认跳过")
if config.pending_cleanup_python_enabled:
try:
logger.info("启动时清理残留的 pending/streaming 请求...")
await self._perform_pending_cleanup()
except Exception as e:
logger.exception(f"启动时 pending 清理执行出错: {e}")
else:
logger.info("启动时 pending 清理已切到 Rust gatewayPython 宿主跳过执行")
async def stop(self) -> Any:
2025-12-10 20:52:44 +08:00
"""停止调度器"""
if not self.running:
return
self.running = False
if self._startup_task and not self._startup_task.done():
self._startup_task.cancel()
try:
await self._startup_task
except asyncio.CancelledError:
pass
self._startup_task = None
2025-12-10 20:52:44 +08:00
scheduler = get_scheduler()
for job_id in self._registered_job_ids:
scheduler.remove_job(job_id)
self._registered_job_ids.clear()
2025-12-10 20:52:44 +08:00
logger.info("系统维护调度器已停止")
2025-12-10 20:52:44 +08:00
# ========== 任务函数APScheduler 直接调用异步函数) ==========
async def _scheduled_stats_aggregation(self, backfill: bool = False) -> None:
2025-12-10 20:52:44 +08:00
"""统计聚合任务(定时调用)"""
if not self.running:
return
await self._perform_stats_aggregation(backfill=backfill)
2025-12-10 20:52:44 +08:00
async def _scheduled_wallet_daily_usage_aggregation(self) -> None:
"""钱包每日消费汇总任务(定时调用)"""
if not self.running:
return
await self._perform_wallet_daily_usage_aggregation()
async def _scheduled_hourly_stats_aggregation(self) -> None:
"""小时统计聚合任务(定时调用)"""
if not self.running:
return
await self._perform_hourly_stats_aggregation()
async def _scheduled_cleanup(self) -> None:
2025-12-10 20:52:44 +08:00
"""清理任务(定时调用)"""
if not self.running:
return
2025-12-10 20:52:44 +08:00
await self._perform_cleanup()
async def _scheduled_monitor(self) -> None:
2025-12-10 20:52:44 +08:00
"""监控任务(定时调用)"""
if not self.running:
return
2025-12-10 20:52:44 +08:00
try:
from src.database import log_pool_status
log_pool_status()
except Exception as e:
logger.exception("连接池监控任务出错: {}", e)
async def _scheduled_http_client_idle_cleanup(self) -> None:
"""HTTP 客户端空闲清理任务(定时调用)。"""
if not self.running:
return
try:
stats = await HTTPClientPool.cleanup_idle_clients()
if stats.get("proxy_closed", 0) or stats.get("tunnel_closed", 0):
logger.info(
"HTTP 客户端空闲清理释放连接: proxy={}, tunnel={}",
stats.get("proxy_closed", 0),
stats.get("tunnel_closed", 0),
)
except Exception as e:
logger.exception("HTTP 客户端空闲清理任务出错: {}", e)
2025-12-10 20:52:44 +08:00
async def _scheduled_pending_cleanup(self) -> None:
2025-12-10 20:52:44 +08:00
"""Pending 清理任务(定时调用)"""
if not self.running:
return
2025-12-10 20:52:44 +08:00
await self._perform_pending_cleanup()
async def _scheduled_audit_cleanup(self) -> None:
"""审计日志清理任务(定时调用)"""
if not self.running:
return
await self._perform_audit_cleanup()
async def _scheduled_candidate_cleanup(self) -> None:
"""请求候选记录清理任务(定时调用)"""
if not self.running:
return
await self._perform_candidate_cleanup()
async def _scheduled_db_maintenance(self) -> None:
"""数据库表维护任务(定时调用)"""
if not self.running:
return
await self._perform_db_maintenance()
async def _scheduled_gemini_file_mapping_cleanup(self) -> None:
"""Gemini 文件映射清理任务(定时调用)"""
if not self.running:
return
await self._perform_gemini_file_mapping_cleanup()
async def _scheduled_antigravity_ua_refresh(self) -> None:
"""Antigravity User-Agent 版本刷新(定时调用)"""
if not self.running:
return
try:
from src.services.provider.adapters.antigravity.client import refresh_user_agent
await refresh_user_agent()
except Exception as e:
logger.debug("定时刷新 Antigravity UA 版本失败: {}", e)
async def _scheduled_provider_checkin(self) -> None:
"""Provider 签到任务(定时调用)"""
if not self.running:
return
await self._perform_provider_checkin()
2025-12-10 20:52:44 +08:00
# ========== 实际任务实现 ==========
async def _perform_stats_aggregation(self, backfill: bool = False) -> None:
2025-12-10 20:52:44 +08:00
"""执行统计聚合任务
Args:
backfill: 是否回填历史数据启动时检查缺失的日期
2025-12-10 20:52:44 +08:00
"""
if self._stats_aggregation_lock.locked():
logger.info("统计聚合任务正在运行,跳过本次触发")
return
async with self._stats_aggregation_lock:
db = create_session()
try:
# 检查是否启用统计聚合
if not SystemConfigService.get_config(db, "enable_stats_aggregation", True):
logger.info("统计聚合已禁用,跳过聚合任务")
2025-12-10 20:52:44 +08:00
return
logger.info("开始执行统计数据聚合...")
from src.models.database import StatsDaily
from src.models.database import User as DBUser
# 使用 UTC 日期,定时任务在 UTC 00:05 触发,聚合 UTC 昨天
now_utc = datetime.now(timezone.utc)
today_utc = now_utc.replace(hour=0, minute=0, second=0, microsecond=0)
if backfill:
# 启动时检查并回填缺失的日期
from src.models.database import StatsSummary
summary = db.query(StatsSummary).first()
if not summary:
# 首次运行,回填所有历史数据
logger.info("检测到首次运行,开始回填历史统计数据...")
days_to_backfill = SystemConfigService.get_config(
db, "stats_backfill_days", 365
)
count = StatsAggregatorService.backfill_historical_data(
db, days=days_to_backfill
)
logger.info(f"历史数据回填完成,共 {count}")
return
# 非首次运行,检查最近是否有缺失的日期需要回填
from src.models.database import StatsDailyModel, StatsDailyProvider
yesterday_utc_date = today_utc.date() - timedelta(days=1)
max_backfill_days: int = (
SystemConfigService.get_config(db, "max_stats_backfill_days", 30) or 30
)
# 计算回填检查的起始日期
check_start_date = yesterday_utc_date - timedelta(days=max_backfill_days - 1)
check_start_dt = datetime.combine(
check_start_date, datetime.min.time(), tzinfo=timezone.utc
)
# 单次查询获取三张统计表中已有数据的日期UNION ALL 合并)
existing_daily_dates: set[date] = set()
existing_model_dates: set[date] = set()
existing_provider_dates: set[date] = set()
q_daily = db.query(
StatsDaily.date.label("dt"),
literal_column("'daily'").label("src"),
).filter(StatsDaily.date >= check_start_dt)
q_model = (
db.query(
StatsDailyModel.date.label("dt"),
literal_column("'model'").label("src"),
)
.filter(StatsDailyModel.date >= check_start_dt)
.distinct()
)
q_provider = (
db.query(
StatsDailyProvider.date.label("dt"),
literal_column("'provider'").label("src"),
)
.filter(StatsDailyProvider.date >= check_start_dt)
.distinct()
)
combined = q_daily.union_all(q_model).union_all(q_provider).all()
for stat_date, src in combined:
if stat_date.tzinfo is None:
stat_date = stat_date.replace(tzinfo=timezone.utc)
d = stat_date.date()
if src == "daily":
existing_daily_dates.add(d)
elif src == "model":
existing_model_dates.add(d)
else:
existing_provider_dates.add(d)
# 找出需要回填的日期
all_dates = set()
current = check_start_date
while current <= yesterday_utc_date:
all_dates.add(current)
current += timedelta(days=1)
# 需要回填 StatsDaily 的日期
missing_daily_dates = all_dates - existing_daily_dates
# 需要回填 StatsDailyModel 的日期
missing_model_dates = all_dates - existing_model_dates
# 需要回填 StatsDailyProvider 的日期
missing_provider_dates = all_dates - existing_provider_dates
# 合并所有需要处理的日期
dates_to_process = (
missing_daily_dates | missing_model_dates | missing_provider_dates
)
if dates_to_process:
sorted_dates = sorted(dates_to_process)
logger.info(
f"检测到 {len(dates_to_process)} 天的统计数据需要回填 "
f"(StatsDaily 缺失 {len(missing_daily_dates)} 天, "
f"StatsDailyModel 缺失 {len(missing_model_dates)} 天, "
f"StatsDailyProvider 缺失 {len(missing_provider_dates)} 天)"
)
users = db.query(DBUser.id).filter(DBUser.is_active.is_(True)).all()
user_ids = [user_id for (user_id,) in users]
failed_dates = 0
for current_date in sorted_dates:
try:
current_date_utc = datetime.combine(
current_date, datetime.min.time(), tzinfo=timezone.utc
)
StatsAggregatorService.aggregate_daily_stats_bundle(
db, current_date_utc, user_ids=user_ids
)
db.expunge_all()
except Exception as e:
failed_dates += 1
logger.warning(f"回填日期 {current_date} 失败: {e}")
try:
db.rollback()
except Exception as rollback_err:
logger.error(f"回滚失败: {rollback_err}")
StatsAggregatorService.update_summary(db)
if failed_dates > 0:
logger.warning(
f"回填完成,共处理 {len(dates_to_process)} 天,"
f"失败: {failed_dates}"
)
else:
logger.info(f"缺失数据回填完成,共处理 {len(dates_to_process)}")
else:
logger.info("统计数据已是最新,无需回填")
return
# 定时任务:聚合昨天 (UTC) 的数据
yesterday_utc = today_utc - timedelta(days=1)
users = db.query(DBUser.id).filter(DBUser.is_active.is_(True)).all()
user_ids = [user_id for (user_id,) in users]
StatsAggregatorService.aggregate_daily_stats_bundle(
db, yesterday_utc, user_ids=user_ids
)
2025-12-10 20:52:44 +08:00
StatsAggregatorService.update_summary(db)
2025-12-10 20:52:44 +08:00
logger.info("统计数据聚合完成")
2025-12-10 20:52:44 +08:00
except Exception as e:
logger.exception(f"统计聚合任务执行失败: {e}")
try:
db.rollback()
except Exception:
pass
finally:
db.close()
2025-12-10 20:52:44 +08:00
async def _perform_wallet_daily_usage_aggregation(self) -> None:
if self._wallet_daily_usage_lock.locked():
logger.info("钱包每日消费汇总任务正在运行,跳过本次触发")
return
async with self._wallet_daily_usage_lock:
def _do() -> None:
db = create_session()
try:
logger.info("开始执行钱包每日消费汇总...")
billing_today = WalletDailyUsageLedgerService.get_today_billing_date()
billing_yesterday = billing_today - timedelta(days=1)
affected = WalletDailyUsageLedgerService.aggregate_day(db, billing_yesterday)
logger.info(
"钱包每日消费汇总完成: date={}, wallets={}",
billing_yesterday.isoformat(),
affected,
)
except Exception as e:
logger.exception("钱包每日消费汇总任务执行失败: {}", e)
try:
db.rollback()
except Exception:
pass
finally:
db.close()
await asyncio.to_thread(_do)
async def _perform_hourly_stats_aggregation(self) -> None:
"""执行小时统计聚合任务"""
def _do() -> None:
db = create_session()
try:
if not SystemConfigService.get_config(db, "enable_stats_aggregation", True):
logger.info("统计聚合已禁用,跳过小时聚合任务")
return
now_utc = datetime.now(timezone.utc)
last_hour = now_utc.replace(minute=0, second=0, microsecond=0) - timedelta(hours=1)
StatsAggregatorService.aggregate_hourly_stats_bundle(db, last_hour)
logger.info("小时统计聚合完成: {}", last_hour.isoformat())
except Exception as e:
logger.exception("小时统计聚合任务执行失败: {}", e)
try:
db.rollback()
except Exception:
pass
finally:
db.close()
await asyncio.to_thread(_do)
async def _perform_pending_cleanup(self) -> None:
2025-12-10 20:52:44 +08:00
"""执行 pending 状态清理"""
def _do_pending_cleanup() -> int:
db = create_session()
try:
from src.services.usage.service import UsageService
2025-12-10 20:52:44 +08:00
timeout_minutes = SystemConfigService.get_config(
db, "pending_request_timeout_minutes", 10
)
# pending 清理涉及 candidate 表关联查询,限制批次大小以控制内存
batch_size = min(
max(SystemConfigService.get_config(db, "cleanup_batch_size", 1000), 1),
200,
)
return UsageService.cleanup_stale_pending_requests(
db,
timeout_minutes=timeout_minutes,
batch_size=batch_size,
)
except Exception as e:
logger.exception(f"清理 pending 请求失败: {e}")
db.rollback()
return 0
finally:
db.close()
2025-12-10 20:52:44 +08:00
loop = asyncio.get_running_loop()
cleaned_count = await loop.run_in_executor(None, _do_pending_cleanup)
if cleaned_count > 0:
logger.info(f"清理了 {cleaned_count} 条超时的 pending/streaming 请求")
2025-12-10 20:52:44 +08:00
async def _perform_audit_cleanup(self) -> None:
"""执行审计日志清理任务"""
def _do_audit_cleanup() -> int:
db = create_session()
try:
if not SystemConfigService.get_config(db, "enable_auto_cleanup", True):
logger.info("自动清理已禁用,跳过审计日志清理")
return 0
audit_retention_days = max(
SystemConfigService.get_config(db, "audit_log_retention_days", 30),
7,
)
batch_size = SystemConfigService.get_config(db, "cleanup_batch_size", 1000)
cutoff_time = datetime.now(timezone.utc) - timedelta(days=audit_retention_days)
logger.info(f"开始清理 {audit_retention_days} 天前的审计日志...")
total_deleted = 0
while True:
batch_db = create_session()
try:
records_to_delete = (
batch_db.query(AuditLog.id)
.filter(AuditLog.created_at < cutoff_time)
.limit(batch_size)
.all()
)
if not records_to_delete:
break
record_ids = [r.id for r in records_to_delete]
result = batch_db.execute(
delete(AuditLog)
.where(AuditLog.id.in_(record_ids))
.execution_options(synchronize_session=False)
)
rows_deleted = result.rowcount
batch_db.commit()
total_deleted += rows_deleted
logger.debug(f"已删除 {rows_deleted} 条审计日志,累计 {total_deleted}")
except Exception as e:
logger.exception(f"删除审计日志批次失败: {e}")
try:
batch_db.rollback()
except Exception:
pass
break
finally:
batch_db.close()
return total_deleted
except Exception as e:
logger.exception(f"审计日志清理失败: {e}")
return 0
finally:
db.close()
loop = asyncio.get_running_loop()
total_deleted = await loop.run_in_executor(None, _do_audit_cleanup)
if total_deleted > 0:
logger.info(f"审计日志清理完成,共删除 {total_deleted} 条记录")
else:
logger.info("无需清理的审计日志")
async def _perform_gemini_file_mapping_cleanup(self) -> None:
"""清理过期的 Gemini 文件映射记录"""
def _do() -> None:
db = create_session()
try:
from src.services.gemini_files_mapping import cleanup_expired_mappings
deleted_count = cleanup_expired_mappings(db)
if deleted_count > 0:
logger.info(f"清理了 {deleted_count} 条过期的 Gemini 文件映射")
except Exception as e:
logger.exception(f"Gemini 文件映射清理失败: {e}")
try:
db.rollback()
except Exception:
pass
finally:
db.close()
await asyncio.to_thread(_do)
async def _perform_provider_checkin(self) -> None:
"""执行 Provider 签到任务
遍历所有已配置 provider_ops Provider触发签到
签到会在余额查询时一起执行先签到再查询余额
"""
def _load_provider_ids() -> list[str]:
db = create_session()
try:
if not SystemConfigService.get_config(db, "enable_provider_checkin", True):
return []
providers = (
db.query(Provider.id, Provider.config)
.filter(Provider.is_active.is_(True))
.all()
)
return [p.id for p in providers if p.config and p.config.get("provider_ops")]
finally:
db.close()
try:
provider_ids = await asyncio.to_thread(_load_provider_ids)
if not provider_ids:
logger.info("无已配置的 Provider跳过签到任务")
return
logger.info(f"开始执行 Provider 签到,共 {len(provider_ids)} 个...")
# 使用信号量限制并发,避免同时发起过多请求
concurrency = 3 # 签到任务并发数
semaphore = asyncio.Semaphore(concurrency)
async def _checkin_provider(provider_id: str) -> tuple[str, bool, str]:
"""执行单个 Provider 的签到"""
async with semaphore:
task_db = create_session()
try:
service = ProviderOpsService(task_db)
# 触发余额查询(会先执行签到)
result = await service.query_balance(provider_id)
# 检查签到结果
checkin_success = None
checkin_message = ""
if result.data and hasattr(result.data, "extra") and result.data.extra:
checkin_success = result.data.extra.get("checkin_success")
checkin_message = result.data.extra.get("checkin_message", "")
if checkin_success is True:
return provider_id, True, checkin_message
elif checkin_success is False:
return provider_id, False, checkin_message
else:
# None 表示未执行签到(可能没配置 Cookie
return provider_id, False, "未执行签到"
except Exception as e:
logger.warning(f"Provider {provider_id} 签到失败: {e}")
return provider_id, False, str(e)
finally:
try:
task_db.close()
except Exception:
pass
# 并行执行签到
tasks = [_checkin_provider(pid) for pid in provider_ids]
results = await asyncio.gather(*tasks)
# 统计结果
success_count = sum(1 for _, success, _ in results if success)
logger.info(f"Provider 签到完成: {success_count}/{len(provider_ids)} 成功")
# 记录详细结果
for provider_id, success, message in results:
if success:
logger.debug(f" - {provider_id}: 签到成功 - {message}")
elif message != "未执行签到":
logger.debug(f" - {provider_id}: 签到失败 - {message}")
except Exception as e:
logger.exception(f"Provider 签到任务执行失败: {e}")
async def _perform_candidate_cleanup(self) -> None:
"""清理过期的 request_candidates 记录"""
def _do_candidate_cleanup() -> int:
db = create_session()
try:
if not SystemConfigService.get_config(db, "enable_auto_cleanup", True):
logger.info("自动清理已禁用,跳过候选记录清理")
return 0
2025-12-10 20:52:44 +08:00
retention_days = max(
SystemConfigService.get_config(
db,
"request_candidates_retention_days",
SystemConfigService.get_config(db, "detail_log_retention_days", 7),
),
3,
)
batch_size = max(
SystemConfigService.get_config(
db,
"request_candidates_cleanup_batch_size",
SystemConfigService.get_config(db, "cleanup_batch_size", 1000),
),
1,
)
cutoff_time = datetime.now(timezone.utc) - timedelta(days=retention_days)
2025-12-10 20:52:44 +08:00
logger.info(
"开始清理 {} 天前的请求候选记录batch_size={}",
retention_days,
batch_size,
)
except Exception as e:
logger.exception(f"候选记录清理配置读取失败: {e}")
return 0
finally:
db.close()
2025-12-10 20:52:44 +08:00
total_deleted = 0
while True:
batch_db = create_session()
try:
records_to_delete = (
batch_db.query(RequestCandidate.id)
.filter(RequestCandidate.created_at < cutoff_time)
.order_by(RequestCandidate.created_at.asc(), RequestCandidate.id.asc())
.limit(batch_size)
.all()
)
2025-12-10 20:52:44 +08:00
if not records_to_delete:
break
2025-12-10 20:52:44 +08:00
record_ids = [r.id for r in records_to_delete]
2025-12-10 20:52:44 +08:00
result = batch_db.execute(
delete(RequestCandidate)
.where(RequestCandidate.id.in_(record_ids))
.execution_options(synchronize_session=False)
)
2025-12-10 20:52:44 +08:00
rows_deleted = result.rowcount
batch_db.commit()
2025-12-10 20:52:44 +08:00
total_deleted += rows_deleted
logger.debug(f"已删除 {rows_deleted} 条候选记录,累计 {total_deleted}")
except Exception as e:
logger.exception(f"删除候选记录批次失败: {e}")
try:
batch_db.rollback()
except Exception:
pass
break
finally:
batch_db.close()
2025-12-10 20:52:44 +08:00
return total_deleted
2025-12-10 20:52:44 +08:00
loop = asyncio.get_running_loop()
total_deleted = await loop.run_in_executor(None, _do_candidate_cleanup)
if total_deleted > 0:
logger.info(f"请求候选记录清理完成,共删除 {total_deleted} 条记录")
else:
logger.info("无需清理的候选记录")
async def _perform_db_maintenance(self) -> None:
"""执行数据库表维护VACUUM ANALYZE
对大表执行 VACUUM ANALYZE防止表和索引膨胀
VACUUM 不能在事务内执行需要使用 autocommit 连接
使用线程池执行避免阻塞事件循环
"""
db = create_session()
try:
if not SystemConfigService.get_config(db, "enable_db_maintenance", True):
logger.info("数据库维护已禁用,跳过")
return
2025-12-10 20:52:44 +08:00
except Exception as e:
logger.exception(f"读取数据库维护配置失败: {e}")
return
2025-12-10 20:52:44 +08:00
finally:
db.close()
tables = ["usage", "request_candidates", "audit_logs"]
logger.info(f"开始数据库表维护VACUUM ANALYZE目标表: {', '.join(tables)}")
from src.database.database import _ensure_engine
engine = _ensure_engine()
def _vacuum_table(table_name: str) -> tuple[str, bool, str]:
"""在线程池中执行 VACUUM ANALYZE同步阻塞操作"""
try:
with engine.connect() as raw_conn:
conn = raw_conn.execution_options(isolation_level="AUTOCOMMIT")
conn.execute(text(f"VACUUM ANALYZE {table_name}"))
return table_name, True, ""
except Exception as e:
return table_name, False, str(e)
loop = asyncio.get_running_loop()
for table in tables:
table_name, success, error = await loop.run_in_executor(None, _vacuum_table, table)
if success:
logger.info(f"VACUUM ANALYZE {table_name} 完成")
else:
logger.warning(f"VACUUM ANALYZE {table_name} 失败: {error}")
logger.info("数据库表维护完成")
async def _perform_cleanup(self) -> None:
"""执行清理任务(在线程池中运行,避免阻塞事件循环)"""
def _do_cleanup() -> None:
db = create_session()
try:
if not SystemConfigService.get_config(db, "enable_auto_cleanup", True):
logger.info("自动清理已禁用,跳过清理任务")
return
logger.info("开始执行使用记录分级清理...")
detail_retention = SystemConfigService.get_config(
db, "detail_log_retention_days", 7
)
compressed_retention = SystemConfigService.get_config(
db, "compressed_log_retention_days", 30
)
header_retention = SystemConfigService.get_config(db, "header_retention_days", 90)
log_retention = SystemConfigService.get_config(db, "log_retention_days", 365)
batch_size = SystemConfigService.get_config(db, "cleanup_batch_size", 1000)
auto_delete = SystemConfigService.get_config(db, "auto_delete_expired_keys", False)
except Exception as e:
logger.exception(f"清理任务配置读取失败: {e}")
return
finally:
db.close()
try:
now = datetime.now(timezone.utc)
detail_cutoff = now - timedelta(days=detail_retention)
compressed_cutoff = now - timedelta(days=compressed_retention)
header_cutoff = now - timedelta(days=header_retention)
log_cutoff = now - timedelta(days=log_retention)
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
# 先删最老的整行,再按窗口处理剩余记录,避免同一行在一轮里被重复改写。
records_deleted = self._delete_old_records(log_cutoff, batch_size)
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
header_cleaned = self._cleanup_header_fields(
header_cutoff, batch_size, newer_than=log_cutoff
)
body_cleaned = self._cleanup_stale_body_fields(
compressed_cutoff, batch_size, newer_than=log_cutoff
)
# 仅压缩 7-30 天窗口内的 body更老记录直接清空 body不再先压缩再清理。
body_compressed = self._cleanup_body_fields(
detail_cutoff, batch_size, newer_than=compressed_cutoff
)
# 5. 清理过期的API Keys
keys_db = create_session()
try:
keys_cleaned = ApiKeyService.cleanup_expired_keys(
keys_db, auto_delete=auto_delete
)
except Exception as e:
logger.exception(f"清理过期 Keys 失败: {e}")
keys_cleaned = 0
finally:
keys_db.close()
logger.info(
f"清理完成: 压缩 {body_compressed} 条, "
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
f"清理body {body_cleaned} 条, "
f"清理header {header_cleaned} 条, "
f"删除记录 {records_deleted} 条, "
f"清理过期Keys {keys_cleaned}"
)
except Exception as e:
logger.exception(f"清理任务执行失败: {e}")
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, _do_cleanup)
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
def _cleanup_body_fields(
self,
cutoff_time: datetime,
batch_size: int,
*,
newer_than: datetime | None = None,
) -> int:
2025-12-10 20:52:44 +08:00
"""压缩 request_body 和 response_body 字段到压缩字段
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
仅处理指定时间窗口内仍保留原始 body 的记录避免对更老记录重复写放大
2025-12-10 20:52:44 +08:00
"""
from sqlalchemy import null, update
total_compressed = 0
no_progress_count = 0
memory_safe_batch_size = max(1, min(batch_size, 25))
2025-12-10 20:52:44 +08:00
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
if newer_than is not None and newer_than >= cutoff_time:
logger.warning(
"压缩 body 字段跳过: 无效时间窗口 newer_than={} cutoff_time={}",
newer_than,
cutoff_time,
)
return 0
2025-12-10 20:52:44 +08:00
while True:
batch_db = create_session()
try:
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
query = batch_db.query(
Usage.id,
Usage.request_body,
Usage.response_body,
Usage.provider_request_body,
Usage.client_response_body,
).filter(Usage.created_at < cutoff_time)
if newer_than is not None:
query = query.filter(Usage.created_at >= newer_than)
records = (
query.filter(
(Usage.request_body.isnot(None))
| (Usage.response_body.isnot(None))
| (Usage.provider_request_body.isnot(None))
| (Usage.client_response_body.isnot(None))
)
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
.order_by(Usage.created_at.asc(), Usage.id.asc())
.limit(memory_safe_batch_size)
.all()
)
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
if not records:
break
2025-12-10 20:52:44 +08:00
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
batch_success = 0
batch_progress = False
for record in records:
result = batch_db.execute(
update(Usage)
.where(Usage.id == record.id)
.values(
request_body=null(),
response_body=null(),
provider_request_body=null(),
client_response_body=null(),
request_body_compressed=(
compress_json(record.request_body) if record.request_body else None
),
response_body_compressed=(
compress_json(record.response_body)
if record.response_body
else None
),
provider_request_body_compressed=(
compress_json(record.provider_request_body)
if record.provider_request_body
else None
),
client_response_body_compressed=(
compress_json(record.client_response_body)
if record.client_response_body
else None
),
2025-12-10 20:52:44 +08:00
)
.execution_options(synchronize_session=False)
)
if result.rowcount > 0:
batch_success += 1
batch_progress = True
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
batch_db.commit()
except Exception as e:
logger.warning("压缩 body 批次失败: {}", e)
try:
batch_db.rollback()
except Exception:
pass
break
finally:
batch_db.close()
if not batch_progress:
no_progress_count += 1
if no_progress_count >= 3:
logger.error(
f"压缩 body 字段连续 {no_progress_count} 批无进展," "终止循环以避免死循环"
)
break
else:
no_progress_count = 0
2025-12-10 20:52:44 +08:00
total_compressed += batch_success
if batch_success > 0:
2025-12-10 20:52:44 +08:00
logger.debug(
f"已压缩 {batch_success} 条记录的 body 字段,累计 {total_compressed}"
)
return total_compressed
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
def _cleanup_stale_body_fields(
self,
cutoff_time: datetime,
batch_size: int,
*,
newer_than: datetime | None = None,
) -> int:
"""清理已超过压缩保留期的 body 字段
2025-12-10 20:52:44 +08:00
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
直接清空 raw/compressed body避免更老记录先压缩再马上被清掉
2025-12-10 20:52:44 +08:00
"""
from sqlalchemy import null, update
total_cleaned = 0
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
if newer_than is not None and newer_than >= cutoff_time:
logger.warning(
"清理 body 字段跳过: 无效时间窗口 newer_than={} cutoff_time={}",
newer_than,
cutoff_time,
)
return 0
2025-12-10 20:52:44 +08:00
while True:
batch_db = create_session()
try:
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
query = batch_db.query(Usage.id).filter(Usage.created_at < cutoff_time)
if newer_than is not None:
query = query.filter(Usage.created_at >= newer_than)
2025-12-10 20:52:44 +08:00
records_to_clean = (
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
query.filter(
(Usage.request_body.isnot(None))
| (Usage.response_body.isnot(None))
| (Usage.provider_request_body.isnot(None))
| (Usage.client_response_body.isnot(None))
| (Usage.request_body_compressed.isnot(None))
2025-12-10 20:52:44 +08:00
| (Usage.response_body_compressed.isnot(None))
| (Usage.provider_request_body_compressed.isnot(None))
| (Usage.client_response_body_compressed.isnot(None))
2025-12-10 20:52:44 +08:00
)
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
.order_by(Usage.created_at.asc(), Usage.id.asc())
2025-12-10 20:52:44 +08:00
.limit(batch_size)
.all()
)
if not records_to_clean:
break
record_ids = [r.id for r in records_to_clean]
result = batch_db.execute(
update(Usage)
.where(Usage.id.in_(record_ids))
.values(
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
request_body=null(),
response_body=null(),
provider_request_body=null(),
client_response_body=null(),
2025-12-10 20:52:44 +08:00
request_body_compressed=null(),
response_body_compressed=null(),
provider_request_body_compressed=null(),
client_response_body_compressed=null(),
2025-12-10 20:52:44 +08:00
)
)
rows_updated = result.rowcount
batch_db.commit()
if rows_updated == 0:
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
logger.warning("清理 body 字段: rowcount=0可能存在问题")
2025-12-10 20:52:44 +08:00
break
total_cleaned += rows_updated
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
logger.debug(f"已清理 {rows_updated} 条记录的 body 字段,累计 {total_cleaned}")
2025-12-10 20:52:44 +08:00
except Exception as e:
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
logger.exception(f"清理 body 字段失败: {e}")
2025-12-10 20:52:44 +08:00
try:
batch_db.rollback()
except Exception:
pass
break
finally:
batch_db.close()
return total_cleaned
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
def _cleanup_header_fields(
self,
cutoff_time: datetime,
batch_size: int,
*,
newer_than: datetime | None = None,
) -> int:
2025-12-10 20:52:44 +08:00
"""清理 request_headers, response_headers 和 provider_request_headers 字段
每批使用短生命周期 session同步方法在线程池中调用
2025-12-10 20:52:44 +08:00
"""
from sqlalchemy import null, update
total_cleaned = 0
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
if newer_than is not None and newer_than >= cutoff_time:
logger.warning(
"清理 header 字段跳过: 无效时间窗口 newer_than={} cutoff_time={}",
newer_than,
cutoff_time,
)
return 0
2025-12-10 20:52:44 +08:00
while True:
batch_db = create_session()
try:
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
query = batch_db.query(Usage.id).filter(Usage.created_at < cutoff_time)
if newer_than is not None:
query = query.filter(Usage.created_at >= newer_than)
2025-12-10 20:52:44 +08:00
records_to_clean = (
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
query.filter(
2025-12-10 20:52:44 +08:00
(Usage.request_headers.isnot(None))
| (Usage.response_headers.isnot(None))
| (Usage.provider_request_headers.isnot(None))
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
| (Usage.client_response_headers.isnot(None))
2025-12-10 20:52:44 +08:00
)
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
.order_by(Usage.created_at.asc(), Usage.id.asc())
2025-12-10 20:52:44 +08:00
.limit(batch_size)
.all()
)
if not records_to_clean:
break
record_ids = [r.id for r in records_to_clean]
result = batch_db.execute(
update(Usage)
.where(Usage.id.in_(record_ids))
.values(
request_headers=null(),
response_headers=null(),
provider_request_headers=null(),
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
client_response_headers=null(),
2025-12-10 20:52:44 +08:00
)
)
rows_updated = result.rowcount
batch_db.commit()
if rows_updated == 0:
logger.warning("清理 header 字段: rowcount=0可能存在问题")
break
total_cleaned += rows_updated
logger.debug(f"已清理 {rows_updated} 条记录的 header 字段,累计 {total_cleaned}")
2025-12-10 20:52:44 +08:00
except Exception as e:
logger.exception(f"清理 header 字段失败: {e}")
try:
batch_db.rollback()
except Exception:
pass
break
finally:
batch_db.close()
return total_cleaned
def _delete_old_records(self, cutoff_time: datetime, batch_size: int) -> int:
"""删除过期的完整记录(同步方法,在线程池中调用)"""
2025-12-10 20:52:44 +08:00
total_deleted = 0
while True:
batch_db = create_session()
2025-12-10 20:52:44 +08:00
try:
records_to_delete = (
batch_db.query(Usage.id)
2025-12-10 20:52:44 +08:00
.filter(Usage.created_at < cutoff_time)
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
.order_by(Usage.created_at.asc(), Usage.id.asc())
2025-12-10 20:52:44 +08:00
.limit(batch_size)
.all()
)
if not records_to_delete:
break
record_ids = [r.id for r in records_to_delete]
result = batch_db.execute(
2025-12-10 20:52:44 +08:00
delete(Usage)
.where(Usage.id.in_(record_ids))
.execution_options(synchronize_session=False)
)
rows_deleted = result.rowcount
batch_db.commit()
2025-12-10 20:52:44 +08:00
total_deleted += rows_deleted
logger.debug(f"已删除 {rows_deleted} 条过期记录,累计 {total_deleted}")
except Exception as e:
logger.exception(f"删除过期记录失败: {e}")
try:
batch_db.rollback()
2025-12-10 20:52:44 +08:00
except Exception:
pass
break
finally:
batch_db.close()
2025-12-10 20:52:44 +08:00
return total_deleted
def should_start_python_maintenance_scheduler() -> bool:
return bool(
config.audit_cleanup_python_enabled
or config.antigravity_ua_refresh_python_enabled
or config.db_maintenance_python_enabled
or config.gemini_file_mapping_cleanup_python_enabled
or config.http_client_idle_cleanup_python_enabled
or config.pending_cleanup_python_enabled
or config.pool_monitor_python_enabled
or config.provider_checkin_python_enabled
or config.request_candidate_cleanup_python_enabled
or config.stats_aggregation_python_enabled
or config.stats_hourly_aggregation_python_enabled
or config.usage_cleanup_python_enabled
or config.wallet_daily_usage_aggregation_python_enabled
)
2025-12-10 20:52:44 +08:00
# 全局单例
_maintenance_scheduler = None
def get_maintenance_scheduler() -> MaintenanceScheduler:
"""获取维护调度器单例"""
global _maintenance_scheduler
if _maintenance_scheduler is None:
_maintenance_scheduler = MaintenanceScheduler()
return _maintenance_scheduler