"""The scheduler: one tick runs the jobs and records each run (AD-12).""" import threading from datetime import timedelta import pytest from django.utils import timezone from portal.mailer import services as mailer from portal.scheduler import services from portal.scheduler.models import JobRun from portal.scheduler.runner import Runner pytestmark = pytest.mark.django_db(transaction=True) def test_a_tick_runs_the_mail_job_and_records_the_run(adapters): mailer.enqueue("test", "lena@example.test") services.tick() assert len(adapters.mailer.sent) == 1 run = JobRun.objects.get(job="mail.flush") assert run.status == JobRun.Status.OK and run.finished_at assert run.detail == "sent=1 retried=0 failed=0" def test_an_idle_tick_is_recorded_too_and_repeating_it_is_harmless(adapters): services.tick() services.tick() assert JobRun.objects.filter(job="mail.flush").count() == 2 assert adapters.mailer.sent == [] def test_a_restart_sends_nothing_twice(adapters): mailer.enqueue("test", "lena@example.test") services.tick() services.tick(timezone.now() + timedelta(minutes=5)) # "after a restart" assert len(adapters.mailer.sent) == 1 def test_a_failing_job_is_recorded_and_the_others_still_run(): ran = [] def broken(now): raise RuntimeError("secret detail that must not be stored") jobs = ( services.Job("first", lambda now: ran.append("first") or "fine"), services.Job("broken", broken), services.Job("last", lambda now: ran.append("last") or "also fine"), ) services.tick(jobs=jobs) assert ran == ["first", "last"] broken_run = JobRun.objects.get(job="broken") assert broken_run.status == JobRun.Status.ERROR and broken_run.detail == "RuntimeError" assert JobRun.objects.get(job="last").status == JobRun.Status.OK def test_old_run_records_are_removed(): now = timezone.now() JobRun.objects.create(job="x", started_at=now - timedelta(days=services.KEEP_RUNS_DAYS + 1)) JobRun.objects.create(job="x", started_at=now - timedelta(days=1)) services.tick(now, jobs=()) assert JobRun.objects.filter(job="x").count() == 1 # -- the thread ----------------------------------------- def test_the_thread_ticks_repeatedly_and_stops(): ticked = threading.Semaphore(0) runner = Runner(tick=ticked.release, interval=0.01) runner.start() assert ticked.acquire(timeout=2) and ticked.acquire(timeout=2) runner.stop() assert not runner._thread.is_alive() def test_a_failing_tick_does_not_stop_the_thread(): calls = threading.Semaphore(0) def tick(): calls.release() raise RuntimeError("boom") runner = Runner(tick=tick, interval=0.01) runner.start() assert calls.acquire(timeout=2) and calls.acquire(timeout=2) runner.stop() def test_starting_twice_runs_only_one_thread(): runner = Runner(tick=lambda: None, interval=0.05) runner.start() first = runner._thread runner.start() assert runner._thread is first runner.stop() assert sum(1 for t in threading.enumerate() if t.name == "scheduler") == 0 def test_the_default_interval_is_thirty_seconds(): from portal.scheduler.runner import TICK_SECONDS, runner assert TICK_SECONDS == 30 and runner._interval == 30