"""Daily APScheduler cron that purges expired trash. Mirrors version_pinning_scheduler.py: a single global BackgroundScheduler job at 03:30 UTC bridges into the asyncio loop to run the async purge. Iterates every user and applies that user's own `trash_retention_days` setting; 0 disables auto-purge for that user. """ from __future__ import annotations import asyncio import logging from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.cron import CronTrigger from fabledassistant.services import trash as trash_svc from fabledassistant.services.settings import get_setting logger = logging.getLogger(__name__) _scheduler: BackgroundScheduler | None = None _loop: asyncio.AbstractEventLoop | None = None def _run_purge_threadsafe() -> None: """APScheduler invokes this from a worker thread; bridge into the loop.""" if _loop is None: logger.warning("trash scheduler: no loop registered") return async def _runner(): try: from sqlalchemy import select from fabledassistant.models import async_session from fabledassistant.models.user import User async with async_session() as session: user_ids = (await session.execute(select(User.id))).scalars().all() purged = 0 for uid in user_ids: raw = await get_setting(uid, "trash_retention_days", "90") try: days = int(raw) except (TypeError, ValueError): days = 90 purged += await trash_svc.purge_expired(uid, days) if purged: logger.info("trash purge: removed %d expired row(s)", purged) else: logger.debug("trash purge: nothing expired") except Exception: logger.exception("trash purge run failed") asyncio.run_coroutine_threadsafe(_runner(), _loop) def start_trash_scheduler(loop: asyncio.AbstractEventLoop) -> None: global _scheduler, _loop if _scheduler is not None: return _loop = loop _scheduler = BackgroundScheduler() _scheduler.add_job( _run_purge_threadsafe, trigger=CronTrigger(hour=3, minute=30, timezone="UTC"), id="trash_retention_purge", replace_existing=True, ) _scheduler.start() logger.info("Trash retention scheduler started (daily 03:30 UTC)") def stop_trash_scheduler() -> None: global _scheduler if _scheduler is not None: _scheduler.shutdown(wait=False) _scheduler = None logger.info("Trash retention scheduler stopped")