fix(backups): allow retrying when deleting service files

This commit is contained in:
Houkime 2024-08-09 15:14:12 +00:00 committed by Inex Code
parent 1bfe7cf8dc
commit 3c3b0f6be0
3 changed files with 84 additions and 5 deletions

View file

@ -11,6 +11,7 @@ from json.decoder import JSONDecodeError
from os.path import exists, join from os.path import exists, join
from os import mkdir from os import mkdir
from shutil import rmtree from shutil import rmtree
from selfprivacy_api.utils.waitloop import wait_until_success
from selfprivacy_api.graphql.common_types.backup import BackupReason from selfprivacy_api.graphql.common_types.backup import BackupReason
from selfprivacy_api.backup.util import output_yielder, sync from selfprivacy_api.backup.util import output_yielder, sync
@ -23,6 +24,7 @@ from selfprivacy_api.jobs import Jobs, JobStatus, Job
from selfprivacy_api.backup.local_secret import LocalBackupSecret from selfprivacy_api.backup.local_secret import LocalBackupSecret
SHORT_ID_LEN = 8 SHORT_ID_LEN = 8
FILESYSTEM_TIMEOUT_SEC = 60
T = TypeVar("T", bound=Callable) T = TypeVar("T", bound=Callable)
@ -391,7 +393,9 @@ class ResticBackupper(AbstractBackupper):
else: # attempting inplace restore else: # attempting inplace restore
for folder in folders: for folder in folders:
rmtree(folder) wait_until_success(
lambda: rmtree(folder), timeout_sec=FILESYSTEM_TIMEOUT_SEC
)
mkdir(folder) mkdir(folder)
self._raw_verified_restore(snapshot_id, target="/") self._raw_verified_restore(snapshot_id, target="/")
return return

View file

@ -1,20 +1,60 @@
from time import sleep from time import sleep
from typing import Callable from typing import Callable
from typing import Any
from typing import Optional from typing import Optional
MAX_TIMEOUT = 10e16
DEFAULT_INTERVAL_SEC = 0.1
def wait_until_true( def wait_until_true(
readiness_checker: Callable[[], bool], readiness_checker: Callable[[], bool],
*, *,
interval: float = 0.1, interval: float = DEFAULT_INTERVAL_SEC,
timeout_sec: Optional[float] = None timeout_sec: float = MAX_TIMEOUT
): ):
elapsed = 0.0 elapsed = 0.0
if timeout_sec is None:
timeout_sec = 10e16
while (not readiness_checker()) and elapsed < timeout_sec: while (not readiness_checker()) and elapsed < timeout_sec:
sleep(interval) sleep(interval)
elapsed += interval elapsed += interval
if elapsed > timeout_sec: if elapsed > timeout_sec:
raise TimeoutError() raise TimeoutError()
def is_fail(cal: Callable[[], Any]) -> Optional[Exception]:
try:
cal()
except Exception as e:
# We want it to be a logical True
assert e
return e
return None
def wait_until_success(
operation: Callable[[], Any],
*,
interval: float = DEFAULT_INTERVAL_SEC,
timeout_sec: float = MAX_TIMEOUT
):
elapsed = 0.0
error = is_fail(operation)
while error and elapsed < timeout_sec:
sleep(interval)
elapsed += interval
error = is_fail(operation)
if elapsed >= timeout_sec:
if isinstance(error, Exception):
raise TimeoutError(
"timed out on",
operation,
" with error: " + error.__class__.__name__ + ":" + str(error),
)
else:
raise TimeoutError(
"timed out waiting for an operation to stop failing", operation
)

35
tests/test_utils.py Normal file
View file

@ -0,0 +1,35 @@
import pytest
from selfprivacy_api.utils.waitloop import wait_until_success
class Counter:
def __init__(self):
self.count = 0
def tick(self):
self.count += 1
def reset(self):
self.count = 0
def failing_operation(c: Counter) -> str:
if c.count < 10:
c.tick()
raise ValueError("nooooope")
return "yeees"
def test_wait_until_success():
counter = Counter()
with pytest.raises(TimeoutError):
wait_until_success(
lambda: failing_operation(counter), interval=0.1, timeout_sec=0.5
)
counter.reset()
wait_until_success(
lambda: failing_operation(counter), interval=0.1, timeout_sec=1.1
)