(feature) Improve retention UX and prune safety #14
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user