瀏覽代碼

Avoid deadlock problems

Explicitly use write locks, instead of read locks (that are later
upgraded) unless we know we will never modify the repository
Jonas Borgström 11 年之前
父節點
當前提交
64cd6632a1
共有 5 個文件被更改,包括 30 次插入20 次删除
  1. 7 7
      attic/archiver.py
  2. 16 6
      attic/helpers.py
  3. 4 4
      attic/repository.py
  4. 1 1
      attic/testsuite/helpers.py
  5. 2 2
      attic/testsuite/repository.py

+ 7 - 7
attic/archiver.py

@@ -27,11 +27,11 @@ class Archiver:
     def __init__(self):
     def __init__(self):
         self.exit_code = 0
         self.exit_code = 0
 
 
-    def open_repository(self, location, create=False):
+    def open_repository(self, location, create=False, exclusive=False):
         if location.proto == 'ssh':
         if location.proto == 'ssh':
             repository = RemoteRepository(location, create=create)
             repository = RemoteRepository(location, create=create)
         else:
         else:
-            repository = Repository(location.path, create=create)
+            repository = Repository(location.path, create=create, exclusive=exclusive)
         repository._location = location
         repository._location = location
         return repository
         return repository
 
 
@@ -56,7 +56,7 @@ class Archiver:
     def do_init(self, args):
     def do_init(self, args):
         """Initialize an empty repository"""
         """Initialize an empty repository"""
         print('Initializing repository at "%s"' % args.repository.orig)
         print('Initializing repository at "%s"' % args.repository.orig)
-        repository = self.open_repository(args.repository, create=True)
+        repository = self.open_repository(args.repository, create=True, exclusive=True)
         key = key_creator(repository, args)
         key = key_creator(repository, args)
         manifest = Manifest(key, repository)
         manifest = Manifest(key, repository)
         manifest.key = key
         manifest.key = key
@@ -66,7 +66,7 @@ class Archiver:
 
 
     def do_check(self, args):
     def do_check(self, args):
         """Check repository consistency"""
         """Check repository consistency"""
-        repository = self.open_repository(args.repository)
+        repository = self.open_repository(args.repository, exclusive=args.repair)
         if args.repair:
         if args.repair:
             while not os.environ.get('ATTIC_CHECK_I_KNOW_WHAT_I_AM_DOING'):
             while not os.environ.get('ATTIC_CHECK_I_KNOW_WHAT_I_AM_DOING'):
                 self.print_error("""Warning: 'check --repair' is an experimental feature that might result
                 self.print_error("""Warning: 'check --repair' is an experimental feature that might result
@@ -95,7 +95,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
     def do_create(self, args):
     def do_create(self, args):
         """Create new archive"""
         """Create new archive"""
         t0 = datetime.now()
         t0 = datetime.now()
-        repository = self.open_repository(args.archive)
+        repository = self.open_repository(args.archive, exclusive=True)
         manifest, key = Manifest.load(repository)
         manifest, key = Manifest.load(repository)
         cache = Cache(repository, key, manifest)
         cache = Cache(repository, key, manifest)
         archive = Archive(repository, key, manifest, args.archive.archive, cache=cache,
         archive = Archive(repository, key, manifest, args.archive.archive, cache=cache,
@@ -216,7 +216,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
 
 
     def do_delete(self, args):
     def do_delete(self, args):
         """Delete an existing archive"""
         """Delete an existing archive"""
-        repository = self.open_repository(args.archive)
+        repository = self.open_repository(args.archive, exclusive=True)
         manifest, key = Manifest.load(repository)
         manifest, key = Manifest.load(repository)
         cache = Cache(repository, key, manifest)
         cache = Cache(repository, key, manifest)
         archive = Archive(repository, key, manifest, args.archive.archive, cache=cache)
         archive = Archive(repository, key, manifest, args.archive.archive, cache=cache)
@@ -308,7 +308,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
 
 
     def do_prune(self, args):
     def do_prune(self, args):
         """Prune repository archives according to specified rules"""
         """Prune repository archives according to specified rules"""
-        repository = self.open_repository(args.repository)
+        repository = self.open_repository(args.repository, exclusive=True)
         manifest, key = Manifest.load(repository)
         manifest, key = Manifest.load(repository)
         cache = Cache(repository, key, manifest)
         cache = Cache(repository, key, manifest)
         archives = list(sorted(Archive.list_archives(repository, key, manifest, cache),
         archives = list(sorted(Archive.list_archives(repository, key, manifest, cache),

+ 16 - 6
attic/helpers.py

@@ -33,7 +33,10 @@ class ExtensionModuleError(Error):
 
 
 class UpgradableLock:
 class UpgradableLock:
 
 
-    class LockUpgradeFailed(Error):
+    class ReadLockFailed(Error):
+        """Failed to acquire read lock on {}"""
+
+    class WriteLockFailed(Error):
         """Failed to acquire write lock on {}"""
         """Failed to acquire write lock on {}"""
 
 
     def __init__(self, path, exclusive=False):
     def __init__(self, path, exclusive=False):
@@ -42,10 +45,17 @@ class UpgradableLock:
             self.fd = open(path, 'r+')
             self.fd = open(path, 'r+')
         except IOError:
         except IOError:
             self.fd = open(path, 'r')
             self.fd = open(path, 'r')
-        if exclusive:
-            fcntl.lockf(self.fd, fcntl.LOCK_EX)
-        else:
-            fcntl.lockf(self.fd, fcntl.LOCK_SH)
+        try:
+            if exclusive:
+                fcntl.lockf(self.fd, fcntl.LOCK_EX)
+            else:
+                fcntl.lockf(self.fd, fcntl.LOCK_SH)
+        # Python 3.2 raises IOError, Python3.3+ raises OSError
+        except (IOError, OSError):
+            if exclusive:
+                raise self.WriteLockFailed(self.path)
+            else:
+                raise self.ReadLockFailed(self.path)
         self.is_exclusive = exclusive
         self.is_exclusive = exclusive
 
 
     def upgrade(self):
     def upgrade(self):
@@ -53,7 +63,7 @@ class UpgradableLock:
             fcntl.lockf(self.fd, fcntl.LOCK_EX)
             fcntl.lockf(self.fd, fcntl.LOCK_EX)
         # Python 3.2 raises IOError, Python3.3+ raises OSError
         # Python 3.2 raises IOError, Python3.3+ raises OSError
         except (IOError, OSError):
         except (IOError, OSError):
-            raise self.LockUpgradeFailed(self.path)
+            raise self.WriteLockFailed(self.path)
         self.is_exclusive = True
         self.is_exclusive = True
 
 
     def release(self):
     def release(self):

+ 4 - 4
attic/repository.py

@@ -44,7 +44,7 @@ class Repository(object):
     class CheckNeeded(Error):
     class CheckNeeded(Error):
         '''Inconsistency detected. Please run "attic check {}"'''
         '''Inconsistency detected. Please run "attic check {}"'''
 
 
-    def __init__(self, path, create=False):
+    def __init__(self, path, create=False, exclusive=False):
         self.path = path
         self.path = path
         self.io = None
         self.io = None
         self.lock = None
         self.lock = None
@@ -52,7 +52,7 @@ class Repository(object):
         self._active_txn = False
         self._active_txn = False
         if create:
         if create:
             self.create(path)
             self.create(path)
-        self.open(path)
+        self.open(path, exclusive)
 
 
     def __del__(self):
     def __del__(self):
         self.close()
         self.close()
@@ -98,7 +98,7 @@ class Repository(object):
             self.replay_segments(replay_from, segments_transaction_id)
             self.replay_segments(replay_from, segments_transaction_id)
         return self.get_index_transaction_id()
         return self.get_index_transaction_id()
 
 
-    def open(self, path):
+    def open(self, path, exclusive):
         self.path = path
         self.path = path
         if not os.path.isdir(path):
         if not os.path.isdir(path):
             raise self.DoesNotExist(path)
             raise self.DoesNotExist(path)
@@ -106,7 +106,7 @@ class Repository(object):
         self.config.read(os.path.join(self.path, 'config'))
         self.config.read(os.path.join(self.path, 'config'))
         if not 'repository' in self.config.sections() or self.config.getint('repository', 'version') != 1:
         if not 'repository' in self.config.sections() or self.config.getint('repository', 'version') != 1:
             raise self.InvalidRepository(path)
             raise self.InvalidRepository(path)
-        self.lock = UpgradableLock(os.path.join(path, 'config'))
+        self.lock = UpgradableLock(os.path.join(path, 'config'), exclusive)
         self.max_segment_size = self.config.getint('repository', 'max_segment_size')
         self.max_segment_size = self.config.getint('repository', 'max_segment_size')
         self.segments_per_dir = self.config.getint('repository', 'segments_per_dir')
         self.segments_per_dir = self.config.getint('repository', 'segments_per_dir')
         self.id = unhexlify(self.config.get('repository', 'id').strip())
         self.id = unhexlify(self.config.get('repository', 'id').strip())

+ 1 - 1
attic/testsuite/helpers.py

@@ -120,7 +120,7 @@ class UpgradableLockTestCase(AtticTestCase):
         file = tempfile.NamedTemporaryFile()
         file = tempfile.NamedTemporaryFile()
         os.chmod(file.name, 0o444)
         os.chmod(file.name, 0o444)
         lock = UpgradableLock(file.name)
         lock = UpgradableLock(file.name)
-        self.assert_raises(UpgradableLock.LockUpgradeFailed, lock.upgrade)
+        self.assert_raises(UpgradableLock.WriteLockFailed, lock.upgrade)
         lock.release()
         lock.release()
 
 
 
 

+ 2 - 2
attic/testsuite/repository.py

@@ -154,9 +154,9 @@ class RepositoryCommitTestCase(RepositoryTestCaseBase):
         for name in os.listdir(self.repository.path):
         for name in os.listdir(self.repository.path):
             if name.startswith('index.'):
             if name.startswith('index.'):
                 os.unlink(os.path.join(self.repository.path, name))
                 os.unlink(os.path.join(self.repository.path, name))
-        with patch.object(UpgradableLock, 'upgrade', side_effect=UpgradableLock.LockUpgradeFailed) as upgrade:
+        with patch.object(UpgradableLock, 'upgrade', side_effect=UpgradableLock.WriteLockFailed) as upgrade:
             self.reopen()
             self.reopen()
-            self.assert_raises(UpgradableLock.LockUpgradeFailed, lambda: len(self.repository))
+            self.assert_raises(UpgradableLock.WriteLockFailed, lambda: len(self.repository))
             upgrade.assert_called_once()
             upgrade.assert_called_once()