"""Periodic cleanup of TTL data: expired drafts, old container snapshots and access attempts.""" import asyncio import logging from datetime import datetime, timedelta from typing import Any, cast from sqlalchemy import CursorResult, Delete, delete from sqlalchemy.ext.asyncio import AsyncSession from app.db.models import AccessAttempt, ContainerSnapshot, Draft, utcnow from app.db.session import SessionMaker log = logging.getLogger(__name__) SNAPSHOT_TTL = timedelta(days=1) ATTEMPT_TTL = timedelta(days=1) async def _delete(session: AsyncSession, stmt: Delete) -> int: # DML executes into a CursorResult; the generic annotation of execute() just doesn't say so return cast(CursorResult[Any], await session.execute(stmt)).rowcount async def cleanup_expired(sessions: SessionMaker, now: datetime) -> dict[str, int]: async with sessions.begin() as session: return { "drafts": await _delete(session, delete(Draft).where(Draft.expires_at < now)), "snapshots": await _delete( session, delete(ContainerSnapshot).where(ContainerSnapshot.at < now - SNAPSHOT_TTL) ), "access_attempts": await _delete( session, delete(AccessAttempt).where(AccessAttempt.at < now - ATTEMPT_TTL) ), } async def cleanup_loop(sessions: SessionMaker, interval: float = 3600.0) -> None: while True: try: removed = await cleanup_expired(sessions, utcnow()) if any(removed.values()): log.info("cleanup: %s", removed) except Exception: # a failed cleanup must not kill the bot; retry next round log.exception("cleanup failed") await asyncio.sleep(interval)