Source code for cata_log_hub.scheduler

# SPDX-License-Identifier: AGPL-3.0-or-later
#
# Cata-Log - the central hub for digital flyers
# Copyright (C) 2026 David Aderbauer & The Cata-Log Contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

import logging
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from datetime import UTC, timedelta

from apscheduler.events import EVENT_JOB_ERROR, JobExecutionEvent
from apscheduler.executors.pool import ThreadPoolExecutor
from apscheduler.job import Job
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.date import DateTrigger
from fastapi import FastAPI

from cata_log_hub.exceptions import NetworkError
from cata_log_hub.settings import get_settings

logger = logging.getLogger(__name__)
settings = get_settings()

JOB_DATABASE_URL = (
    get_settings().external_database_url
    or f"sqlite+pysqlite:///{settings.database_path / 'jobs.sqlite3'}"
)

scheduler = BackgroundScheduler(
    jobstores={"default": SQLAlchemyJobStore(url=JOB_DATABASE_URL)},
    executors={"default": ThreadPoolExecutor(max_workers=1)},
    timezone=UTC,
    job_defaults={
        "coalesce": True,
        "misfire_grace_time": 3600,
        "jobstore": "default",
        "executor": "default",
    },
)


[docs] def retry_on_network_error_listener(event: JobExecutionEvent) -> None: """Listener for job errors that schedules a retry job on networkerror. Args: event: The jobevent. """ if event.exception and isinstance(event.exception, NetworkError): logger.debug("Retry listener triggered by network error.") job: Job | None = scheduler.get_job(event.job_id) if job: scheduler.add_job( job.func, args=job.args, id=job.id + "-retry", trigger=DateTrigger( event.scheduled_run_time + timedelta(seconds=settings.retry_delay) ), replace_existing=True, ) logger.info( "Job retry scheduled", extra={"job_id": job.id, "job_retry_time": job.next_run_time}, )
scheduler.add_listener(retry_on_network_error_listener, EVENT_JOB_ERROR)
[docs] @asynccontextmanager async def run_scheduler(app: FastAPI) -> AsyncGenerator[None]: # noqa: ARG001 # required for lifespan """Controls the scheduler.""" scheduler.start() yield scheduler.shutdown()