(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.
This commit is contained in:
2026-05-21 01:22:06 +02:00
parent f76b6cad14
commit 994f7f66c4
2 changed files with 45 additions and 3 deletions

View File

@@ -10,7 +10,7 @@ from django.core.management.base import BaseCommand
from django.db import transaction from django.db import transaction
from django.utils import timezone 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 from pobsync_backend.scheduler import due_key, is_due
@@ -52,12 +52,13 @@ class Command(BaseCommand):
if not is_due(schedule.cron_expr, now): if not is_due(schedule.cron_expr, now):
continue continue
schedule_started_at = timezone.now()
with transaction.atomic(): with transaction.atomic():
locked = ScheduleConfig.objects.select_for_update().get(pk=schedule.pk) locked = ScheduleConfig.objects.select_for_update().get(pk=schedule.pk)
if locked.last_due_key == current_due_key: if locked.last_due_key == current_due_key:
continue continue
locked.last_due_key = current_due_key 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.last_status = "running"
locked.save(update_fields=["last_due_key", "last_started_at", "last_status", "updated_at"]) 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_max_delete=schedule.prune_max_delete,
prune_protect_bases=schedule.prune_protect_bases, 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: except Exception as exc:
status = "failed" status = "failed"
self.stderr.write(f"{schedule.host.host}: {type(exc).__name__}: {exc}") self.stderr.write(f"{schedule.host.host}: {type(exc).__name__}: {exc}")
@@ -83,3 +85,16 @@ class Command(BaseCommand):
ran += 1 ran += 1
return ran 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

View File

@@ -8,7 +8,7 @@ from zoneinfo import ZoneInfo
from django.test import SimpleTestCase, TestCase from django.test import SimpleTestCase, TestCase
from pobsync_backend.management.commands.run_pobsync_scheduler import Command 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 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) self.assertEqual(call.call_count, 1)
schedule = ScheduleConfig.objects.get(host=host) schedule = ScheduleConfig.objects.get(host=host)
self.assertEqual(schedule.last_status, "success") 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")