"""#4408: beat takes each job's last run from task_run, not a shelve file. The default scheduler forgot everything on each container recreate and waited a full interval before any job, so with several redeploys a day no daily or weekly job had run since 2026-09-21. """ from datetime import UTC, datetime, timedelta import pytest from celery.beat import ScheduleEntry from celery.schedules import schedule from backend.app.beat_scheduler import NEVER, last_runs, seed from backend.app.celery_app import celery from backend.app.models import TaskRun DAY = 86400.0 def _entry(task, every=DAY): return ScheduleEntry(name=task, task=task, schedule=schedule(every, app=celery), app=celery) def test_a_job_that_ran_recently_waits_only_the_remainder(): entry = _entry("t.daily") seed([entry], {"t.daily": datetime.now(UTC) - timedelta(hours=20)}) due, next_in = entry.is_due() assert not due assert 3 * 3600 < next_in <= 4 * 3600 + 5 def test_an_overdue_job_is_due_at_once(): """The live case: daily jobs last ran three days before the restart.""" entry = _entry("t.daily") seed([entry], {"t.daily": datetime.now(UTC) - timedelta(days=3)}) assert entry.is_due()[0] def test_a_job_with_no_recorded_run_is_due_now(): entry = _entry("t.never") seed([entry], {}) assert entry.last_run_at == NEVER assert entry.is_due()[0] def test_the_scheduler_is_the_one_celery_uses(): assert celery.conf.beat_scheduler == "backend.app.beat_scheduler:TaskRunScheduler" @pytest.mark.integration def test_last_runs_reads_the_newest_start_per_task(db_sync): now = datetime.now(UTC) for name, ago in [("t.a", 30), ("t.a", 2), ("t.b", 5)]: db_sync.add(TaskRun( celery_task_id="x", queue="maintenance", task_name=name, started_at=now - timedelta(hours=ago), status="ok", )) db_sync.flush() got = last_runs(db_sync, ["t.a", "t.b", "t.c"]) assert set(got) == {"t.a", "t.b"} assert abs((got["t.a"] - (now - timedelta(hours=2))).total_seconds()) < 1