(feature) add queued backup worker foundation
Move backup execution out of the management command into a reusable backup runner service that can execute an existing BackupRun record. Add queue primitives and a run_pobsync_worker command so manual backup requests can be recorded as queued SQL state and processed outside the web request path. Add a worker Docker service and pobsync worker CLI alias, with tests for queued run creation, worker execution, manual run typing, and command mapping.
This commit is contained in:
70
src/pobsync_backend/tests/test_backup_worker.py
Normal file
70
src/pobsync_backend/tests/test_backup_worker.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from pobsync.util import write_yaml_atomic
|
||||
from pobsync_backend.backup_runner import queue_backup_run
|
||||
from pobsync_backend.management.commands.run_pobsync_worker import Command
|
||||
from pobsync_backend.models import BackupRun, GlobalConfig, HostConfig, SnapshotRecord
|
||||
|
||||
|
||||
class BackupWorkerTests(TestCase):
|
||||
def test_queue_backup_run_records_requested_options(self) -> None:
|
||||
host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
|
||||
|
||||
run = queue_backup_run(
|
||||
host=host,
|
||||
dry_run=True,
|
||||
prune=True,
|
||||
prune_max_delete=3,
|
||||
prune_protect_bases=True,
|
||||
)
|
||||
|
||||
self.assertEqual(run.status, BackupRun.Status.QUEUED)
|
||||
self.assertEqual(run.run_type, BackupRun.RunType.MANUAL)
|
||||
self.assertEqual(
|
||||
run.result["requested"],
|
||||
{
|
||||
"dry_run": True,
|
||||
"prune": True,
|
||||
"prune_max_delete": 3,
|
||||
"prune_protect_bases": True,
|
||||
},
|
||||
)
|
||||
|
||||
def test_worker_executes_next_queued_run(self) -> None:
|
||||
with TemporaryDirectory() as tmp:
|
||||
backup_root = Path(tmp) / "backups"
|
||||
GlobalConfig.objects.create(name="default", backup_root=str(backup_root))
|
||||
host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
|
||||
snapshot_dir = backup_root / host.host / "scheduled" / "20260519-021500Z__ABCDEFGH"
|
||||
meta_dir = snapshot_dir / "meta"
|
||||
meta_dir.mkdir(parents=True)
|
||||
write_yaml_atomic(meta_dir / "meta.yaml", {"status": "success", "started_at": "2026-05-19T02:15:00Z"})
|
||||
run = queue_backup_run(host=host)
|
||||
|
||||
with patch("pobsync_backend.backup_runner.run_scheduled") as run_scheduled:
|
||||
run_scheduled.return_value = {
|
||||
"ok": True,
|
||||
"dry_run": False,
|
||||
"host": host.host,
|
||||
"snapshot": str(snapshot_dir),
|
||||
"base": None,
|
||||
"rsync": {"exit_code": 0},
|
||||
}
|
||||
count = Command()._run_once(prefix=Path(tmp) / "home")
|
||||
|
||||
self.assertEqual(count, 1)
|
||||
run.refresh_from_db()
|
||||
self.assertEqual(run.status, BackupRun.Status.SUCCESS)
|
||||
self.assertEqual(SnapshotRecord.objects.count(), 1)
|
||||
self.assertEqual(run.snapshot, SnapshotRecord.objects.get())
|
||||
|
||||
def test_worker_returns_zero_without_queued_runs(self) -> None:
|
||||
count = Command()._run_once(prefix=Path("/opt/pobsync"))
|
||||
|
||||
self.assertEqual(count, 0)
|
||||
@@ -46,3 +46,10 @@ class ConsoleEntrypointTests(SimpleTestCase):
|
||||
|
||||
self.assertEqual(exit_code, 0)
|
||||
execute.assert_called_once_with(["pobsync", "discover_pobsync_snapshots", "--host", "web-01"])
|
||||
|
||||
def test_maps_worker_alias_to_django_command(self) -> None:
|
||||
with patch("pobsync.cli.execute_from_command_line") as execute:
|
||||
exit_code = main(["worker", "--once"])
|
||||
|
||||
self.assertEqual(exit_code, 0)
|
||||
execute.assert_called_once_with(["pobsync", "run_pobsync_worker", "--once"])
|
||||
|
||||
@@ -32,7 +32,7 @@ class RunBackupRecordsSnapshotTests(TestCase):
|
||||
},
|
||||
)
|
||||
|
||||
with patch("pobsync_backend.management.commands.run_pobsync_backup.run_scheduled") as run_scheduled:
|
||||
with patch("pobsync_backend.backup_runner.run_scheduled") as run_scheduled:
|
||||
run_scheduled.return_value = {
|
||||
"ok": True,
|
||||
"dry_run": False,
|
||||
@@ -63,9 +63,9 @@ class RunBackupRecordsSnapshotTests(TestCase):
|
||||
write_yaml_atomic(meta_dir / "meta.yaml", {"status": "success", "started_at": "2026-05-19T02:15:00Z"})
|
||||
|
||||
with (
|
||||
patch("pobsync_backend.management.commands.run_pobsync_backup.run_scheduled") as run_scheduled,
|
||||
patch("pobsync_backend.backup_runner.run_scheduled") as run_scheduled,
|
||||
patch(
|
||||
"pobsync_backend.management.commands.run_pobsync_backup.run_sql_retention_apply"
|
||||
"pobsync_backend.backup_runner.run_sql_retention_apply"
|
||||
) as retention_apply,
|
||||
):
|
||||
run_scheduled.return_value = {
|
||||
@@ -113,9 +113,9 @@ class RunBackupRecordsSnapshotTests(TestCase):
|
||||
write_yaml_atomic(meta_dir / "meta.yaml", {"status": "success", "started_at": "2026-05-19T02:15:00Z"})
|
||||
|
||||
with (
|
||||
patch("pobsync_backend.management.commands.run_pobsync_backup.run_scheduled") as run_scheduled,
|
||||
patch("pobsync_backend.backup_runner.run_scheduled") as run_scheduled,
|
||||
patch(
|
||||
"pobsync_backend.management.commands.run_pobsync_backup.run_sql_retention_apply"
|
||||
"pobsync_backend.backup_runner.run_sql_retention_apply"
|
||||
) as retention_apply,
|
||||
):
|
||||
run_scheduled.return_value = {
|
||||
@@ -155,7 +155,7 @@ class RunBackupRecordsSnapshotTests(TestCase):
|
||||
meta_dir.mkdir(parents=True)
|
||||
write_yaml_atomic(meta_dir / "meta.yaml", {"status": "failed", "started_at": "2026-05-19T02:15:00Z"})
|
||||
|
||||
with patch("pobsync_backend.management.commands.run_pobsync_backup.run_scheduled") as run_scheduled:
|
||||
with patch("pobsync_backend.backup_runner.run_scheduled") as run_scheduled:
|
||||
run_scheduled.return_value = {
|
||||
"ok": False,
|
||||
"dry_run": False,
|
||||
@@ -179,7 +179,7 @@ class RunBackupRecordsSnapshotTests(TestCase):
|
||||
GlobalConfig.objects.create(name="default", backup_root=str(Path(tmp) / "backups"))
|
||||
host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
|
||||
|
||||
with patch("pobsync_backend.management.commands.run_pobsync_backup.run_scheduled") as run_scheduled:
|
||||
with patch("pobsync_backend.backup_runner.run_scheduled") as run_scheduled:
|
||||
run_scheduled.return_value = {
|
||||
"ok": True,
|
||||
"dry_run": True,
|
||||
@@ -198,3 +198,28 @@ class RunBackupRecordsSnapshotTests(TestCase):
|
||||
self.assertEqual(BackupRun.objects.count(), 1)
|
||||
self.assertIsNone(BackupRun.objects.get().snapshot)
|
||||
self.assertEqual(SnapshotRecord.objects.count(), 0)
|
||||
|
||||
def test_manual_flag_records_manual_run_type(self) -> None:
|
||||
with TemporaryDirectory() as tmp:
|
||||
GlobalConfig.objects.create(name="default", backup_root=str(Path(tmp) / "backups"))
|
||||
host = HostConfig.objects.create(host="web-01", address="web-01.example.test")
|
||||
|
||||
with patch("pobsync_backend.backup_runner.run_scheduled") as run_scheduled:
|
||||
run_scheduled.return_value = {
|
||||
"ok": True,
|
||||
"dry_run": True,
|
||||
"host": host.host,
|
||||
"base": None,
|
||||
"rsync": {"exit_code": 0},
|
||||
}
|
||||
call_command(
|
||||
"run_pobsync_backup",
|
||||
host.host,
|
||||
prefix=str(Path(tmp) / "home"),
|
||||
dry_run=True,
|
||||
manual=True,
|
||||
stdout=StringIO(),
|
||||
)
|
||||
|
||||
run = BackupRun.objects.get()
|
||||
self.assertEqual(run.run_type, BackupRun.RunType.MANUAL)
|
||||
|
||||
Reference in New Issue
Block a user