"""Daily APScheduler cron for the auto-pin scan. Single global job at 03:00 UTC. Runs scan_all_users_for_auto_pins so the system promotes stable note versions before they get aged out of the rolling cap. Off-hours by design — the scan is cheap but not time- critical and doesn't need to interrupt regular activity. Mirrors the BackgroundScheduler + threadsafe-async-call pattern used by journal_scheduler.py. """ from __future__ import annotations import asyncio import logging from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.cron import CronTrigger from scribe.services.version_pinning import scan_all_users_for_auto_pins logger = logging.getLogger(__name__) _scheduler: BackgroundScheduler | None = None _loop: asyncio.AbstractEventLoop | None = None def _run_scan_threadsafe() -> None: """APScheduler invokes this from a worker thread; bridge into the asyncio loop so the scan can await its DB operations.""" if _loop is None: logger.warning("version_pinning scheduler: no loop registered") return async def _runner(): try: results = await scan_all_users_for_auto_pins() total = sum(results.values()) if total > 0: logger.info( "auto-pin scan: pinned %d version(s) across %d user(s)", total, len(results), ) else: logger.debug("auto-pin scan: no new pins") except Exception: logger.exception("auto-pin scan run failed") asyncio.run_coroutine_threadsafe(_runner(), _loop) def start_version_pinning_scheduler(loop: asyncio.AbstractEventLoop) -> None: global _scheduler, _loop if _scheduler is not None: return _loop = loop _scheduler = BackgroundScheduler() _scheduler.add_job( _run_scan_threadsafe, trigger=CronTrigger(hour=3, minute=0, timezone="UTC"), id="version_pinning_auto_scan", replace_existing=True, ) _scheduler.start() logger.info("Version pinning scheduler started (daily 03:00 UTC)") def stop_version_pinning_scheduler() -> None: global _scheduler if _scheduler is not None: _scheduler.shutdown(wait=False) _scheduler = None logger.info("Version pinning scheduler stopped")