(feature) Add cancellable backup runs and clearer dry-run logs
Add a cancel action for queued and running backup runs. Queued runs are cancelled immediately, while running runs are marked for cancellation and the worker terminates the active rsync process group. Make dry-run log paths run-specific and add a defensive default dry-run timeout so stuck dry-runs do not remain running indefinitely. Remove rsync exit codes from run overview tables while keeping detailed diagnostics available on the run detail payload.
This commit is contained in:
@@ -48,22 +48,87 @@ class BackupWorkerTests(TestCase):
|
||||
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},
|
||||
}
|
||||
def fake_run_scheduled(**kwargs):
|
||||
run.refresh_from_db()
|
||||
self.assertIn("execution", run.result)
|
||||
return {
|
||||
"ok": True,
|
||||
"dry_run": False,
|
||||
"host": host.host,
|
||||
"snapshot": str(snapshot_dir),
|
||||
"base": None,
|
||||
"rsync": {"exit_code": 0},
|
||||
}
|
||||
|
||||
run_scheduled.side_effect = fake_run_scheduled
|
||||
count = Command()._run_once(prefix=Path(tmp) / "home")
|
||||
run_scheduled.assert_called_once()
|
||||
|
||||
self.assertEqual(count, 1)
|
||||
self.assertEqual(run_scheduled.call_args.kwargs["run_id"], run.id)
|
||||
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_records_dry_run_log_path_while_running(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")
|
||||
run = queue_backup_run(host=host, dry_run=True)
|
||||
|
||||
with patch("pobsync_backend.backup_runner.run_scheduled") as run_scheduled:
|
||||
def fake_run_scheduled(**kwargs):
|
||||
run.refresh_from_db()
|
||||
self.assertEqual(
|
||||
run.result["execution"]["log"],
|
||||
f"/tmp/pobsync-dryrun/{host.host}/run-{run.id}/rsync.log",
|
||||
)
|
||||
return {
|
||||
"ok": True,
|
||||
"dry_run": True,
|
||||
"host": host.host,
|
||||
"base": None,
|
||||
"log": run.result["execution"]["log"],
|
||||
"rsync": {"exit_code": 0},
|
||||
}
|
||||
|
||||
run_scheduled.side_effect = fake_run_scheduled
|
||||
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(run.result["log"], f"/tmp/pobsync-dryrun/{host.host}/run-{run.id}/rsync.log")
|
||||
|
||||
def test_worker_preserves_cancelled_status_from_running_run(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")
|
||||
run = queue_backup_run(host=host, dry_run=True)
|
||||
|
||||
with patch("pobsync_backend.backup_runner.run_scheduled") as run_scheduled:
|
||||
def fake_run_scheduled(**kwargs):
|
||||
BackupRun.objects.filter(id=run.id).update(status=BackupRun.Status.CANCELLED)
|
||||
self.assertTrue(kwargs["cancel_check"]())
|
||||
return {
|
||||
"ok": False,
|
||||
"dry_run": True,
|
||||
"cancelled": True,
|
||||
"host": host.host,
|
||||
"base": None,
|
||||
"log": f"/tmp/pobsync-dryrun/{host.host}/run-{run.id}/rsync.log",
|
||||
"rsync": {"exit_code": 130},
|
||||
}
|
||||
|
||||
run_scheduled.side_effect = fake_run_scheduled
|
||||
count = Command()._run_once(prefix=Path(tmp) / "home")
|
||||
|
||||
self.assertEqual(count, 1)
|
||||
run.refresh_from_db()
|
||||
self.assertEqual(run.status, BackupRun.Status.CANCELLED)
|
||||
self.assertEqual(run.rsync_exit_code, 130)
|
||||
|
||||
def test_worker_returns_zero_without_queued_runs(self) -> None:
|
||||
count = Command()._run_once(prefix=Path("/opt/pobsync"))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user