Browse Source

Merge pull request #6056 from ThomasWaldmann/fix-cache-tag-race-master

atomically create the CACHE_TAG file, see #6028
TW 3 years ago
parent
commit
bf392367f1
2 changed files with 24 additions and 10 deletions
  1. 16 9
      src/borg/helpers/fs.py
  2. 8 1
      src/borg/platform/base.py

+ 16 - 9
src/borg/helpers/fs.py

@@ -92,15 +92,22 @@ def get_cache_dir():
     cache_dir = os.environ.get('BORG_CACHE_DIR', os.path.join(cache_home, 'borg'))
     cache_dir = os.environ.get('BORG_CACHE_DIR', os.path.join(cache_home, 'borg'))
     # Create path if it doesn't exist yet
     # Create path if it doesn't exist yet
     ensure_dir(cache_dir)
     ensure_dir(cache_dir)
-    cache_fn = os.path.join(cache_dir, CACHE_TAG_NAME)
-    if not os.path.exists(cache_fn):
-        with open(cache_fn, 'wb') as fd:
-            fd.write(CACHE_TAG_CONTENTS)
-            fd.write(textwrap.dedent("""
-            # This file is a cache directory tag created by Borg.
-            # For information about cache directory tags, see:
-            #       http://www.bford.info/cachedir/spec.html
-            """).encode('ascii'))
+    cache_tag_fn = os.path.join(cache_dir, CACHE_TAG_NAME)
+    if not os.path.exists(cache_tag_fn):
+        cache_tag_contents = CACHE_TAG_CONTENTS + textwrap.dedent("""
+        # This file is a cache directory tag created by Borg.
+        # For information about cache directory tags, see:
+        #       http://www.bford.info/cachedir/spec.html
+        """).encode('ascii')
+        from ..platform import SaveFile
+        try:
+            with SaveFile(cache_tag_fn, binary=True) as fd:
+                fd.write(cache_tag_contents)
+        except FileExistsError:
+            # if we have multiple SaveFile calls running in parallel for same cache_tag_fn,
+            # it is fine if just one (usually first/quicker one) of them run gets through
+            # and all others raise FileExistsError.
+            pass
     return cache_dir
     return cache_dir
 
 
 
 

+ 8 - 1
src/borg/platform/base.py

@@ -141,12 +141,14 @@ class SyncFile:
     Note that POSIX doesn't specify *anything* about power failures (or similar failures). A system that
     Note that POSIX doesn't specify *anything* about power failures (or similar failures). A system that
     routinely loses files or corrupts file on power loss is POSIX compliant.
     routinely loses files or corrupts file on power loss is POSIX compliant.
 
 
+    Calling SyncFile(path) for an existing path will raise FileExistsError, see comment in __init__.
+
     TODO: Use F_FULLSYNC on OSX.
     TODO: Use F_FULLSYNC on OSX.
     TODO: A Windows implementation should use CreateFile with FILE_FLAG_WRITE_THROUGH.
     TODO: A Windows implementation should use CreateFile with FILE_FLAG_WRITE_THROUGH.
     """
     """
 
 
     def __init__(self, path, binary=False):
     def __init__(self, path, binary=False):
-        mode = 'xb' if binary else 'x'
+        mode = 'xb' if binary else 'x'  # x -> raise FileExists exception in open() if file exists already
         self.fd = open(path, mode)
         self.fd = open(path, mode)
         self.fileno = self.fd.fileno()
         self.fileno = self.fd.fileno()
 
 
@@ -193,6 +195,11 @@ class SaveFile:
     On a journaling file system the file contents are always updated
     On a journaling file system the file contents are always updated
     atomically and won't become corrupted, even on power failures or
     atomically and won't become corrupted, even on power failures or
     crashes (for caveats see SyncFile).
     crashes (for caveats see SyncFile).
+
+    Calling SaveFile(path) in parallel for same path is safe (even when using the same SUFFIX), but the
+    caller needs to catch potential FileExistsError exceptions that may happen in this racy situation.
+    The caller executing SaveFile->SyncFile->open() first will win.
+    All other callers will raise a FileExistsError in open(), at least until the os.replace is executed.
     """
     """
 
 
     SUFFIX = '.tmp'
     SUFFIX = '.tmp'