mirror of
https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git
synced 2024-11-18 08:29:14 +00:00
fix(backups): do not infinitely retry automatic backup if it errors out
This commit is contained in:
parent
bc98e41be8
commit
8caf7e1b24
|
@ -1,7 +1,8 @@
|
||||||
"""
|
"""
|
||||||
This module contains the controller class for backups.
|
This module contains the controller class for backups.
|
||||||
"""
|
"""
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta, timezone
|
||||||
|
import time
|
||||||
import os
|
import os
|
||||||
from os import statvfs
|
from os import statvfs
|
||||||
from typing import Callable, List, Optional
|
from typing import Callable, List, Optional
|
||||||
|
@ -37,6 +38,7 @@ from selfprivacy_api.backup.providers import get_provider
|
||||||
from selfprivacy_api.backup.storage import Storage
|
from selfprivacy_api.backup.storage import Storage
|
||||||
from selfprivacy_api.backup.jobs import (
|
from selfprivacy_api.backup.jobs import (
|
||||||
get_backup_job,
|
get_backup_job,
|
||||||
|
get_backup_fail,
|
||||||
add_backup_job,
|
add_backup_job,
|
||||||
get_restore_job,
|
get_restore_job,
|
||||||
add_restore_job,
|
add_restore_job,
|
||||||
|
@ -292,9 +294,9 @@ class Backups:
|
||||||
def back_up(
|
def back_up(
|
||||||
service: Service, reason: BackupReason = BackupReason.EXPLICIT
|
service: Service, reason: BackupReason = BackupReason.EXPLICIT
|
||||||
) -> Snapshot:
|
) -> Snapshot:
|
||||||
"""The top-level function to back up a service"""
|
"""The top-level function to back up a service
|
||||||
folders = service.get_folders()
|
If it fails for any reason at all, it should both mark job as
|
||||||
service_name = service.get_id()
|
errored and re-raise an error"""
|
||||||
|
|
||||||
job = get_backup_job(service)
|
job = get_backup_job(service)
|
||||||
if job is None:
|
if job is None:
|
||||||
|
@ -302,6 +304,10 @@ class Backups:
|
||||||
Jobs.update(job, status=JobStatus.RUNNING)
|
Jobs.update(job, status=JobStatus.RUNNING)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if service.can_be_backed_up() is False:
|
||||||
|
raise ValueError("cannot backup a non-backuppable service")
|
||||||
|
folders = service.get_folders()
|
||||||
|
service_name = service.get_id()
|
||||||
service.pre_backup()
|
service.pre_backup()
|
||||||
snapshot = Backups.provider().backupper.start_backup(
|
snapshot = Backups.provider().backupper.start_backup(
|
||||||
folders,
|
folders,
|
||||||
|
@ -692,23 +698,43 @@ class Backups:
|
||||||
"""Get a timezone-aware time of the last backup of a service"""
|
"""Get a timezone-aware time of the last backup of a service"""
|
||||||
return Storage.get_last_backup_time(service.get_id())
|
return Storage.get_last_backup_time(service.get_id())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_last_backup_error_time(service: Service) -> Optional[datetime]:
|
||||||
|
"""Get a timezone-aware time of the last backup of a service"""
|
||||||
|
job = get_backup_fail(service)
|
||||||
|
if job is not None:
|
||||||
|
datetime_created = job.created_at
|
||||||
|
if datetime_created.tzinfo is None:
|
||||||
|
# assume it is in localtime
|
||||||
|
offset = timedelta(seconds=time.localtime().tm_gmtoff)
|
||||||
|
datetime_created = datetime_created - offset
|
||||||
|
return datetime.combine(datetime_created.date(), datetime_created.time(),timezone.utc)
|
||||||
|
return datetime_created
|
||||||
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_time_to_backup_service(service: Service, time: datetime):
|
def is_time_to_backup_service(service: Service, time: datetime):
|
||||||
"""Returns True if it is time to back up a service"""
|
"""Returns True if it is time to back up a service"""
|
||||||
period = Backups.autobackup_period_minutes()
|
period = Backups.autobackup_period_minutes()
|
||||||
service_id = service.get_id()
|
|
||||||
if not service.can_be_backed_up():
|
if not service.can_be_backed_up():
|
||||||
return False
|
return False
|
||||||
if period is None:
|
if period is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
last_backup = Storage.get_last_backup_time(service_id)
|
last_error = Backups.get_last_backup_error_time(service)
|
||||||
|
|
||||||
|
if last_error is not None:
|
||||||
|
if time < last_error + timedelta(seconds=AUTOBACKUP_JOB_EXPIRATION_SECONDS):
|
||||||
|
return False
|
||||||
|
|
||||||
|
last_backup = Backups.get_last_backed_up(service)
|
||||||
if last_backup is None:
|
if last_backup is None:
|
||||||
# queue a backup immediately if there are no previous backups
|
# queue a backup immediately if there are no previous backups
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if time > last_backup + timedelta(minutes=period):
|
if time > last_backup + timedelta(minutes=period):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Helpers
|
# Helpers
|
||||||
|
|
|
@ -80,9 +80,19 @@ def get_job_by_type(type_id: str) -> Optional[Job]:
|
||||||
return job
|
return job
|
||||||
|
|
||||||
|
|
||||||
|
def get_failed_job_by_type(type_id: str) -> Optional[Job]:
|
||||||
|
for job in Jobs.get_jobs():
|
||||||
|
if job.type_id == type_id and job.status == JobStatus.ERROR:
|
||||||
|
return job
|
||||||
|
|
||||||
|
|
||||||
def get_backup_job(service: Service) -> Optional[Job]:
|
def get_backup_job(service: Service) -> Optional[Job]:
|
||||||
return get_job_by_type(backup_job_type(service))
|
return get_job_by_type(backup_job_type(service))
|
||||||
|
|
||||||
|
|
||||||
|
def get_backup_fail(service: Service) -> Optional[Job]:
|
||||||
|
return get_failed_job_by_type(backup_job_type(service))
|
||||||
|
|
||||||
|
|
||||||
def get_restore_job(service: Service) -> Optional[Job]:
|
def get_restore_job(service: Service) -> Optional[Job]:
|
||||||
return get_job_by_type(restore_job_type(service))
|
return get_job_by_type(restore_job_type(service))
|
||||||
|
|
|
@ -14,6 +14,8 @@ import secrets
|
||||||
|
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
from selfprivacy_api.utils.huey import huey
|
||||||
|
|
||||||
import selfprivacy_api.services as services
|
import selfprivacy_api.services as services
|
||||||
from selfprivacy_api.services import Service, get_all_services
|
from selfprivacy_api.services import Service, get_all_services
|
||||||
from selfprivacy_api.services.service import ServiceStatus
|
from selfprivacy_api.services.service import ServiceStatus
|
||||||
|
@ -119,6 +121,10 @@ def dummy_service(tmpdir, backups, raw_dummy_service) -> Service:
|
||||||
# register our service
|
# register our service
|
||||||
services.services.append(service)
|
services.services.append(service)
|
||||||
|
|
||||||
|
# make sure we are in immediate mode because this thing is non pickleable to store on queue.
|
||||||
|
huey.immediate = True
|
||||||
|
assert huey.immediate is True
|
||||||
|
|
||||||
assert get_service_by_id(service.get_id()) is not None
|
assert get_service_by_id(service.get_id()) is not None
|
||||||
yield service
|
yield service
|
||||||
|
|
||||||
|
@ -996,6 +1002,32 @@ def test_autobackup_timing(backups, dummy_service):
|
||||||
assert Backups.is_time_to_backup_service(dummy_service, future)
|
assert Backups.is_time_to_backup_service(dummy_service, future)
|
||||||
|
|
||||||
|
|
||||||
|
def test_backup_unbackuppable(backups, dummy_service):
|
||||||
|
dummy_service.set_backuppable(False)
|
||||||
|
assert dummy_service.can_be_backed_up() is False
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
Backups.back_up(dummy_service)
|
||||||
|
|
||||||
|
|
||||||
|
def test_failed_autoback_prevents_more_autobackup(backups, dummy_service):
|
||||||
|
backup_period = 13 # minutes
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
Backups.set_autobackup_period_minutes(backup_period)
|
||||||
|
assert Backups.is_time_to_backup_service(dummy_service, now)
|
||||||
|
|
||||||
|
# artificially making an errored out backup job
|
||||||
|
dummy_service.set_backuppable(False)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
Backups.back_up(dummy_service)
|
||||||
|
dummy_service.set_backuppable(True)
|
||||||
|
|
||||||
|
assert Backups.get_last_backed_up(dummy_service) is None
|
||||||
|
assert Backups.get_last_backup_error_time(dummy_service) is not None
|
||||||
|
|
||||||
|
assert Backups.is_time_to_backup_service(dummy_service, now) is False
|
||||||
|
|
||||||
|
|
||||||
# Storage
|
# Storage
|
||||||
def test_snapshots_caching(backups, dummy_service):
|
def test_snapshots_caching(backups, dummy_service):
|
||||||
Backups.back_up(dummy_service)
|
Backups.back_up(dummy_service)
|
||||||
|
|
Loading…
Reference in a new issue