"""Scheduler for recurring-task spawning. Every 15 minutes, creates the next occurrence of any recurring task whose spawn time has arrived — draining `recurrence_next_spawn_at`, which is armed on task completion. Without this job, recurring tasks would never recur. Uses the BackgroundScheduler pattern shared with the other *_scheduler modules. (Formerly event_scheduler.py, which also ran event reminders + CalDAV sync; those were removed when the calendar surface was retired.) """ from __future__ import annotations import asyncio import logging from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.interval import IntervalTrigger logger = logging.getLogger(__name__) _scheduler: BackgroundScheduler | None = None _loop: asyncio.AbstractEventLoop | None = None async def _run_recurrence_spawn() -> None: from scribe.services.recurrence import spawn_recurring_tasks # noqa: PLC0415 try: await spawn_recurring_tasks() except Exception: logger.warning("Recurring-task spawn job failed", exc_info=True) def _run_recurrence_spawn_threadsafe(loop: asyncio.AbstractEventLoop) -> None: asyncio.run_coroutine_threadsafe(_run_recurrence_spawn(), loop) def start_recurrence_scheduler(loop: asyncio.AbstractEventLoop) -> None: global _scheduler, _loop if _scheduler is not None: return _loop = loop _scheduler = BackgroundScheduler() # Spawn the next occurrence of due recurring tasks every 15 minutes. # Without this job, recurrence_next_spawn_at is armed on completion but # never drained, so recurring tasks never recur. _scheduler.add_job( _run_recurrence_spawn_threadsafe, trigger=IntervalTrigger(minutes=15), args=[loop], id="recurrence_spawn", replace_existing=True, ) _scheduler.start() logger.info("Recurrence scheduler started (recurring-task spawn every 15m)") def stop_recurrence_scheduler() -> None: global _scheduler if _scheduler is not None: _scheduler.shutdown(wait=False) _scheduler = None logger.info("Recurrence scheduler stopped")