Przeglądaj źródła

deal with the SaveFile/SyncFile race, docs

Thomas Waldmann 3 lat temu
rodzic
commit
708a5853e7
2 zmienionych plików z 16 dodań i 3 usunięć
  1. 8 2
      src/borg/helpers/fs.py
  2. 8 1
      src/borg/platform/base.py

+ 8 - 2
src/borg/helpers/fs.py

@@ -100,8 +100,14 @@ def get_cache_dir():
         #       http://www.bford.info/cachedir/spec.html
         #       http://www.bford.info/cachedir/spec.html
         """).encode('ascii')
         """).encode('ascii')
         from ..platform import SaveFile
         from ..platform import SaveFile
-        with SaveFile(cache_tag_fn, binary=True) as fd:
-            fd.write(cache_tag_contents)
+        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'