feat(core): 初始化 Key-IP Sentinel 服务与部署骨架
- 搭建 FastAPI、Redis、PostgreSQL、Nginx 与 Docker Compose 基础结构 - 实现反向代理、首用绑定、拦截告警、归档任务和管理接口 - 提供 Vue3 管理后台初版,以及 uv/requirements 双依赖配置
This commit is contained in:
84
app/services/archive_service.py
Normal file
84
app/services/archive_service.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import Callable
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from sqlalchemy import delete, select
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
|
||||
from app.config import RuntimeSettings, Settings
|
||||
from app.models.token_binding import TokenBinding
|
||||
from app.services.binding_service import BindingService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ArchiveService:
|
||||
def __init__(
|
||||
self,
|
||||
settings: Settings,
|
||||
session_factory: async_sessionmaker[AsyncSession],
|
||||
binding_service: BindingService,
|
||||
runtime_settings_getter: Callable[[], RuntimeSettings],
|
||||
) -> None:
|
||||
self.settings = settings
|
||||
self.session_factory = session_factory
|
||||
self.binding_service = binding_service
|
||||
self.runtime_settings_getter = runtime_settings_getter
|
||||
self.scheduler = AsyncIOScheduler(timezone="UTC")
|
||||
|
||||
async def start(self) -> None:
|
||||
if self.scheduler.running:
|
||||
return
|
||||
self.scheduler.add_job(
|
||||
self.archive_inactive_bindings,
|
||||
trigger="interval",
|
||||
minutes=self.settings.archive_job_interval_minutes,
|
||||
id="archive-inactive-bindings",
|
||||
replace_existing=True,
|
||||
max_instances=1,
|
||||
coalesce=True,
|
||||
)
|
||||
self.scheduler.start()
|
||||
|
||||
async def stop(self) -> None:
|
||||
if self.scheduler.running:
|
||||
self.scheduler.shutdown(wait=False)
|
||||
|
||||
async def archive_inactive_bindings(self) -> int:
|
||||
runtime_settings = self.runtime_settings_getter()
|
||||
cutoff = datetime.now(UTC) - timedelta(days=runtime_settings.archive_days)
|
||||
total_archived = 0
|
||||
|
||||
while True:
|
||||
async with self.session_factory() as session:
|
||||
try:
|
||||
result = await session.execute(
|
||||
select(TokenBinding.token_hash)
|
||||
.where(TokenBinding.last_used_at < cutoff)
|
||||
.order_by(TokenBinding.last_used_at.asc())
|
||||
.limit(self.settings.archive_batch_size)
|
||||
)
|
||||
token_hashes = list(result.scalars())
|
||||
if not token_hashes:
|
||||
break
|
||||
|
||||
await session.execute(delete(TokenBinding).where(TokenBinding.token_hash.in_(token_hashes)))
|
||||
await session.commit()
|
||||
except SQLAlchemyError:
|
||||
await session.rollback()
|
||||
logger.exception("Failed to archive inactive bindings.")
|
||||
break
|
||||
|
||||
await self.binding_service.invalidate_many(token_hashes)
|
||||
total_archived += len(token_hashes)
|
||||
|
||||
if len(token_hashes) < self.settings.archive_batch_size:
|
||||
break
|
||||
|
||||
if total_archived:
|
||||
logger.info("Archived inactive bindings.", extra={"count": total_archived})
|
||||
return total_archived
|
||||
Reference in New Issue
Block a user