Browse Source

Merge pull request #441 from ThomasWaldmann/break-lock

Break lock
TW 9 years ago
parent
commit
3972269d6f
5 changed files with 54 additions and 11 deletions
  1. 27 3
      borg/archiver.py
  2. 5 0
      borg/cache.py
  3. 8 4
      borg/remote.py
  4. 10 4
      borg/repository.py
  5. 4 0
      borg/testsuite/archiver.py

+ 27 - 3
borg/archiver.py

@@ -42,11 +42,11 @@ class Archiver:
         self.verbose = verbose
         self.lock_wait = lock_wait
 
-    def open_repository(self, location, create=False, exclusive=False):
+    def open_repository(self, location, create=False, exclusive=False, lock=True):
         if location.proto == 'ssh':
-            repository = RemoteRepository(location, create=create, lock_wait=self.lock_wait)
+            repository = RemoteRepository(location, create=create, lock_wait=self.lock_wait, lock=lock)
         else:
-            repository = Repository(location.path, create=create, exclusive=exclusive, lock_wait=self.lock_wait)
+            repository = Repository(location.path, create=create, exclusive=exclusive, lock_wait=self.lock_wait, lock=lock)
         repository._location = location
         return repository
 
@@ -573,6 +573,16 @@ class Archiver:
         print('Done.')
         return EXIT_SUCCESS
 
+    def do_break_lock(self, args):
+        """Break the repository lock (e.g. in case it was left by a dead borg."""
+        repository = self.open_repository(args.repository, lock=False)
+        try:
+            repository.break_lock()
+            Cache.break_lock(repository)
+        finally:
+            repository.close()
+        return self.exit_code
+
     helptext = {}
     helptext['patterns'] = '''
         Exclude patterns use a variant of shell pattern syntax, with '*' matching any
@@ -972,6 +982,20 @@ class Archiver:
                                type=location_validator(archive=True),
                                help='archive to display information about')
 
+        break_lock_epilog = textwrap.dedent("""
+        This command breaks the repository and cache locks.
+        Please use carefully and only while no borg process (on any machine) is
+        trying to access the Cache or the Repository.
+        """)
+        subparser = subparsers.add_parser('break-lock', parents=[common_parser],
+                                          description=self.do_break_lock.__doc__,
+                                          epilog=break_lock_epilog,
+                                          formatter_class=argparse.RawDescriptionHelpFormatter)
+        subparser.set_defaults(func=self.do_break_lock)
+        subparser.add_argument('repository', metavar='REPOSITORY',
+                               type=location_validator(archive=False),
+                               help='repository for which to break the locks')
+
         prune_epilog = textwrap.dedent("""
         The prune command prunes a repository by deleting archives not matching
         any of the specified retention options. This command is normally used by

+ 5 - 0
borg/cache.py

@@ -32,6 +32,11 @@ class Cache:
     class EncryptionMethodMismatch(Error):
         """Repository encryption method changed since last access, refusing to continue"""
 
+    @staticmethod
+    def break_lock(repository, path=None):
+        path = path or os.path.join(get_cache_dir(), hexlify(repository.id).decode('ascii'))
+        UpgradableLock(os.path.join(path, 'lock'), exclusive=True).break_lock()
+
     def __init__(self, repository, key, manifest, path=None, sync=True, do_files=False, warn_if_unencrypted=True,
                  lock_wait=None):
         self.lock = None

+ 8 - 4
borg/remote.py

@@ -50,6 +50,7 @@ class RepositoryServer:  # pragma: no cover
         'rollback',
         'save_key',
         'load_key',
+        'break_lock',
     )
 
     def __init__(self, restrict_to_paths):
@@ -97,7 +98,7 @@ class RepositoryServer:  # pragma: no cover
     def negotiate(self, versions):
         return 1
 
-    def open(self, path, create=False, lock_wait=None):
+    def open(self, path, create=False, lock_wait=None, lock=True):
         path = os.fsdecode(path)
         if path.startswith('/~'):
             path = path[1:]
@@ -108,7 +109,7 @@ class RepositoryServer:  # pragma: no cover
                     break
             else:
                 raise PathNotAllowed(path)
-        self.repository = Repository(path, create, lock_wait=lock_wait)
+        self.repository = Repository(path, create, lock_wait=lock_wait, lock=lock)
         return self.repository.id
 
 
@@ -122,7 +123,7 @@ class RemoteRepository:
         def __init__(self, name):
             self.name = name
 
-    def __init__(self, location, create=False, lock_wait=None):
+    def __init__(self, location, create=False, lock_wait=None, lock=True):
         self.location = location
         self.preload_ids = []
         self.msgid = 0
@@ -154,7 +155,7 @@ class RemoteRepository:
             raise ConnectionClosedWithHint('Is borg working on the server?')
         if version != 1:
             raise Exception('Server insisted on using unsupported protocol version %d' % version)
-        self.id = self.call('open', location.path, create, lock_wait)
+        self.id = self.call('open', location.path, create, lock_wait, lock)
 
     def __del__(self):
         self.close()
@@ -308,6 +309,9 @@ class RemoteRepository:
     def load_key(self):
         return self.call('load_key')
 
+    def break_lock(self):
+        return self.call('break_lock')
+
     def close(self):
         if self.p:
             self.p.stdin.close()

+ 10 - 4
borg/repository.py

@@ -51,7 +51,7 @@ class Repository:
     class ObjectNotFound(ErrorWithTraceback):
         """Object with key {} not found in repository {}."""
 
-    def __init__(self, path, create=False, exclusive=False, lock_wait=None):
+    def __init__(self, path, create=False, exclusive=False, lock_wait=None, lock=True):
         self.path = os.path.abspath(path)
         self.io = None
         self.lock = None
@@ -59,7 +59,7 @@ class Repository:
         self._active_txn = False
         if create:
             self.create(self.path)
-        self.open(self.path, exclusive, lock_wait=lock_wait)
+        self.open(self.path, exclusive, lock_wait=lock_wait, lock=lock)
 
     def __del__(self):
         self.close()
@@ -129,11 +129,17 @@ class Repository:
             self.replay_segments(replay_from, segments_transaction_id)
         return self.get_index_transaction_id()
 
-    def open(self, path, exclusive, lock_wait=None):
+    def break_lock(self):
+        UpgradableLock(os.path.join(self.path, 'lock')).break_lock()
+
+    def open(self, path, exclusive, lock_wait=None, lock=True):
         self.path = path
         if not os.path.isdir(path):
             raise self.DoesNotExist(path)
-        self.lock = UpgradableLock(os.path.join(path, 'lock'), exclusive, timeout=lock_wait).acquire()
+        if lock:
+            self.lock = UpgradableLock(os.path.join(path, 'lock'), exclusive, timeout=lock_wait).acquire()
+        else:
+            self.lock = None
         self.config = ConfigParser(interpolation=None)
         self.config.read(os.path.join(self.path, 'config'))
         if 'repository' not in self.config.sections() or self.config.getint('repository', 'version') != 1:

+ 4 - 0
borg/testsuite/archiver.py

@@ -724,6 +724,10 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         self.assert_in('test-2', output)
         self.assert_not_in('something-else', output)
 
+    def test_break_lock(self):
+        self.cmd('init', self.repository_location)
+        self.cmd('break-lock', self.repository_location)
+
     def test_usage(self):
         if self.FORK_DEFAULT:
             self.cmd(exit_code=0)