From 994f7f66c4f8e1ae43fc950b9cc114a00e6ea99e Mon Sep 17 00:00:00 2001 From: Peter van Arkel Date: Thu, 21 May 2026 01:22:06 +0200 Subject: [PATCH] (bugfix) Preserve scheduled backup warning status Update the scheduler to reflect the actual scheduled BackupRun status after a run completes, so prune warnings are shown as schedule warnings instead of being reported as successful schedule executions. --- .../commands/run_pobsync_scheduler.py | 19 ++++++++++-- src/pobsync_backend/tests/test_scheduler.py | 29 ++++++++++++++++++- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/pobsync_backend/management/commands/run_pobsync_scheduler.py b/src/pobsync_backend/management/commands/run_pobsync_scheduler.py index 7443ad7..f4e7638 100644 --- a/src/pobsync_backend/management/commands/run_pobsync_scheduler.py +++ b/src/pobsync_backend/management/commands/run_pobsync_scheduler.py @@ -10,7 +10,7 @@ from django.core.management.base import BaseCommand from django.db import transaction from django.utils import timezone -from pobsync_backend.models import ScheduleConfig +from pobsync_backend.models import BackupRun, ScheduleConfig from pobsync_backend.scheduler import due_key, is_due @@ -52,12 +52,13 @@ class Command(BaseCommand): if not is_due(schedule.cron_expr, now): continue + schedule_started_at = timezone.now() with transaction.atomic(): locked = ScheduleConfig.objects.select_for_update().get(pk=schedule.pk) if locked.last_due_key == current_due_key: continue locked.last_due_key = current_due_key - locked.last_started_at = timezone.now() + locked.last_started_at = schedule_started_at locked.last_status = "running" locked.save(update_fields=["last_due_key", "last_started_at", "last_status", "updated_at"]) @@ -72,6 +73,7 @@ class Command(BaseCommand): prune_max_delete=schedule.prune_max_delete, prune_protect_bases=schedule.prune_protect_bases, ) + status = _latest_scheduled_run_status(host_id=schedule.host_id, started_at=schedule_started_at) or status except Exception as exc: status = "failed" self.stderr.write(f"{schedule.host.host}: {type(exc).__name__}: {exc}") @@ -83,3 +85,16 @@ class Command(BaseCommand): ran += 1 return ran + + +def _latest_scheduled_run_status(*, host_id: int, started_at) -> str | None: + run = ( + BackupRun.objects.filter( + host_id=host_id, + run_type=BackupRun.RunType.SCHEDULED, + created_at__gte=started_at, + ) + .order_by("-created_at", "-id") + .first() + ) + return run.status if run is not None else None diff --git a/src/pobsync_backend/tests/test_scheduler.py b/src/pobsync_backend/tests/test_scheduler.py index 8cfd105..a19e989 100644 --- a/src/pobsync_backend/tests/test_scheduler.py +++ b/src/pobsync_backend/tests/test_scheduler.py @@ -8,7 +8,7 @@ from zoneinfo import ZoneInfo from django.test import SimpleTestCase, TestCase from pobsync_backend.management.commands.run_pobsync_scheduler import Command -from pobsync_backend.models import HostConfig, ScheduleConfig +from pobsync_backend.models import BackupRun, HostConfig, ScheduleConfig from pobsync_backend.scheduler import due_key, is_due, next_due_after @@ -64,3 +64,30 @@ class SchedulerCommandTests(TestCase): self.assertEqual(call.call_count, 1) schedule = ScheduleConfig.objects.get(host=host) self.assertEqual(schedule.last_status, "success") + + def test_run_due_records_warning_status_from_scheduled_backup_run(self) -> None: + host = HostConfig.objects.create(host="web-01", address="web-01.example.test") + ScheduleConfig.objects.create(host=host, cron_expr="* * * * *", prune=True, prune_max_delete=1) + + def create_warning_run(*args, **kwargs) -> None: + BackupRun.objects.create( + host=host, + run_type=BackupRun.RunType.SCHEDULED, + status=BackupRun.Status.WARNING, + result={ + "ok": True, + "prune": { + "ok": False, + "type": "ConfigError", + "error": "Refusing to delete 2 snapshots (exceeds --max-delete=1)", + }, + }, + ) + + command = Command() + with patch("pobsync_backend.management.commands.run_pobsync_scheduler.call_command", side_effect=create_warning_run): + count = command._run_due(prefix=Path("/opt/pobsync"), dry_run=False) + + self.assertEqual(count, 1) + schedule = ScheduleConfig.objects.get(host=host) + self.assertEqual(schedule.last_status, "warning")