mirror of
https://github.com/kevinveenbirkenbach/docker-volume-backup.git
synced 2026-08-20 13:12:48 +00:00
fix(backup): claim the generation dir exclusively
A run starting in the same wall-clock second as its predecessor reused that predecessor's generation directory: mkdir carried exist_ok=True, so rsync --delete overwrote a finished generation before create_stamp_file refused the already-stamped directory and exited 2. The guard fired after the damage. Claim the directory exclusively instead. create_version_directory is the first filesystem action of a run, so the abort now happens with zero writes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ import pathlib
|
|||||||
|
|
||||||
from dirval import create_stamp_file
|
from dirval import create_stamp_file
|
||||||
|
|
||||||
from .shell import execute_shell_command
|
from .shell import BackupException, execute_shell_command
|
||||||
|
|
||||||
|
|
||||||
def get_machine_id() -> str:
|
def get_machine_id() -> str:
|
||||||
@@ -23,7 +23,14 @@ def stamp_directory(version_dir: str) -> None:
|
|||||||
|
|
||||||
def create_version_directory(versions_dir: str, backup_time: str) -> str:
|
def create_version_directory(versions_dir: str, backup_time: str) -> str:
|
||||||
version_dir = os.path.join(versions_dir, backup_time)
|
version_dir = os.path.join(versions_dir, backup_time)
|
||||||
pathlib.Path(version_dir).mkdir(parents=True, exist_ok=True)
|
try:
|
||||||
|
pathlib.Path(version_dir).mkdir(parents=True)
|
||||||
|
except FileExistsError:
|
||||||
|
raise BackupException(
|
||||||
|
f"generation {backup_time} already exists at {version_dir}; "
|
||||||
|
"another run claimed this second - refusing to write into it, "
|
||||||
|
"since rsync --delete would overwrite that generation"
|
||||||
|
) from None
|
||||||
return version_dir
|
return version_dir
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from pathlib import Path
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from baudolo.backup import layout as mod
|
from baudolo.backup import layout as mod
|
||||||
|
from baudolo.backup.shell import BackupException
|
||||||
|
|
||||||
|
|
||||||
class TestVersionDirectory(unittest.TestCase):
|
class TestVersionDirectory(unittest.TestCase):
|
||||||
@@ -17,11 +18,12 @@ class TestVersionDirectory(unittest.TestCase):
|
|||||||
self.assertTrue(Path(created).is_dir())
|
self.assertTrue(Path(created).is_dir())
|
||||||
self.assertEqual(Path(created).name, "20260731020304")
|
self.assertEqual(Path(created).name, "20260731020304")
|
||||||
|
|
||||||
def test_it_is_idempotent(self) -> None:
|
def test_it_refuses_a_generation_another_run_already_claimed(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
first = mod.create_version_directory(tmp, "20260731")
|
mod.create_version_directory(tmp, "20260731")
|
||||||
second = mod.create_version_directory(tmp, "20260731")
|
with self.assertRaises(BackupException) as caught:
|
||||||
self.assertEqual(first, second)
|
mod.create_version_directory(tmp, "20260731")
|
||||||
|
self.assertIn("20260731", str(caught.exception))
|
||||||
|
|
||||||
def test_it_creates_missing_parents(self) -> None:
|
def test_it_creates_missing_parents(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
|||||||
Reference in New Issue
Block a user