From 676e69cac4bbc6620fb6d58dce08b41f6414d255 Mon Sep 17 00:00:00 2001 From: Marian Beermann Date: Tue, 4 Oct 2016 14:26:57 +0200 Subject: [PATCH] Parse & pass BORG_HOSTNAME_IS_UNIQUE env var to enable stale lock killing --- src/borg/cache.py | 6 ++++-- src/borg/helpers.py | 4 +++- src/borg/locking.py | 13 ++++++------- src/borg/platform/posix.pyx | 5 +++-- src/borg/remote.py | 8 ++++++-- src/borg/repository.py | 7 +++++-- 6 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/borg/cache.py b/src/borg/cache.py index 8a7ad4aa0..3ed33d742 100644 --- a/src/borg/cache.py +++ b/src/borg/cache.py @@ -75,7 +75,9 @@ class Cache: self.key = key self.manifest = manifest self.path = path or os.path.join(get_cache_dir(), repository.id_str) - self.unique_hostname = bool(os.environ.get('BORG_UNIQUE_HOSTNAME')) + self.hostname_is_unique = yes(env_var_override='BORG_HOSTNAME_IS_UNIQUE', prompt=False, env_msg=None) + if self.hostname_is_unique: + logger.info('Enabled removal of stale cache locks') self.do_files = do_files # Warn user before sending data to a never seen before unencrypted repository if not os.path.exists(self.path): @@ -203,7 +205,7 @@ Chunk index: {0.total_unique_chunks:20d} {0.total_chunks:20d}""" def open(self, lock_wait=None): if not os.path.isdir(self.path): raise Exception('%s Does not look like a Borg cache' % self.path) - self.lock = Lock(os.path.join(self.path, 'lock'), exclusive=True, timeout=lock_wait, kill_stale_locks=self.unique_hostname).acquire() + self.lock = Lock(os.path.join(self.path, 'lock'), exclusive=True, timeout=lock_wait, kill_stale_locks=self.hostname_is_unique).acquire() self.rollback() def close(self): diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 5535c28dd..0b322685a 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -1116,7 +1116,7 @@ DEFAULTISH = ('Default', 'DEFAULT', 'default', 'D', 'd', '', ) def yes(msg=None, false_msg=None, true_msg=None, default_msg=None, retry_msg=None, invalid_msg=None, env_msg='{} (from {})', falsish=FALSISH, truish=TRUISH, defaultish=DEFAULTISH, - default=False, retry=True, env_var_override=None, ofile=None, input=input): + default=False, retry=True, env_var_override=None, ofile=None, input=input, prompt=True): """Output (usually a question) and let user input an answer. Qualifies the answer according to falsish, truish and defaultish as True, False or . If it didn't qualify and retry is False (no retries wanted), return the default [which @@ -1161,6 +1161,8 @@ def yes(msg=None, false_msg=None, true_msg=None, default_msg=None, if answer is not None and env_msg: print(env_msg.format(answer, env_var_override), file=ofile) if answer is None: + if not prompt: + return default try: answer = input() except EOFError: diff --git a/src/borg/locking.py b/src/borg/locking.py index b14b50ade..c3d69674a 100644 --- a/src/borg/locking.py +++ b/src/borg/locking.py @@ -106,7 +106,7 @@ class ExclusiveLock: self.path = os.path.abspath(path) self.id = id or platform.get_process_id() self.unique_name = os.path.join(self.path, "%s.%d-%x" % self.id) - self.ok_to_kill_stale_locks = kill_stale_locks + self.kill_stale_locks = kill_stale_locks self.stale_warning_printed = False def __enter__(self): @@ -163,16 +163,16 @@ class ExclusiveLock: thread = int(thread_str) except ValueError: # Malformed lock name? Or just some new format we don't understand? - # It's safer to just exit + # It's safer to just exit. return False if platform.process_alive(host, pid, thread): return False - if not self.ok_to_kill_stale_locks: + if not self.kill_stale_locks: if not self.stale_warning_printed: # Log this at warning level to hint the user at the ability - logger.warning("Found stale lock %s, but not deleting because BORG_UNIQUE_HOSTNAME is not set.", name) + logger.warning("Found stale lock %s, but not deleting because BORG_HOSTNAME_IS_UNIQUE is not set.", name) self.stale_warning_printed = True return False @@ -213,7 +213,7 @@ class LockRoster: def __init__(self, path, id=None, kill_stale_locks=False): self.path = path self.id = id or platform.get_process_id() - self.ok_to_kill_zombie_locks = kill_stale_locks + self.kill_stale_locks = kill_stale_locks def load(self): try: @@ -221,7 +221,7 @@ class LockRoster: data = json.load(f) # Just nuke the stale locks early on load - if self.ok_to_kill_zombie_locks: + if self.kill_stale_locks: for key in (SHARED, EXCLUSIVE): try: entries = data[key] @@ -237,7 +237,6 @@ class LockRoster: except (FileNotFoundError, ValueError): # no or corrupt/empty roster file? data = {} - return data def save(self, data): diff --git a/src/borg/platform/posix.pyx b/src/borg/platform/posix.pyx index e64dc5d8c..30b7e126f 100644 --- a/src/borg/platform/posix.pyx +++ b/src/borg/platform/posix.pyx @@ -53,12 +53,13 @@ def process_alive(host, pid, thread): return local_pid_alive(pid) + def local_pid_alive(pid): """Return whether *pid* is alive.""" try: # This doesn't work on Windows. # This does not kill anything, 0 means "see if we can send a signal to this process or not". - # Possible errors: No such process (== stale lock) or permission denied (not a stale lock) + # Possible errors: No such process (== stale lock) or permission denied (not a stale lock). # If the exception is not raised that means such a pid is valid and we can send a signal to it. os.kill(pid, 0) return True @@ -66,5 +67,5 @@ def local_pid_alive(pid): if err.errno == errno.ESRCH: # ESRCH = no such process return False - # Any other error (eg. permissions) mean that the process ID refers to a live process + # Any other error (eg. permissions) means that the process ID refers to a live process. return True diff --git a/src/borg/remote.py b/src/borg/remote.py index a4988eb91..6264241c2 100644 --- a/src/borg/remote.py +++ b/src/borg/remote.py @@ -18,6 +18,7 @@ from .helpers import get_home_dir from .helpers import sysinfo from .helpers import bin_to_hex from .helpers import replace_placeholders +from .helpers import yes from .repository import Repository RPC_PROTOCOL_VERSION = 2 @@ -326,12 +327,15 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+. opts.append('--critical') else: raise ValueError('log level missing, fix this code') + env_vars = [] + if yes(env_var_override='BORG_HOSTNAME_IS_UNIQUE', env_msg=None, prompt=False): + env_vars.append('BORG_HOSTNAME_IS_UNIQUE=yes') if testing: - return [sys.executable, '-m', 'borg.archiver', 'serve'] + opts + self.extra_test_args + return env_vars + [sys.executable, '-m', 'borg.archiver', 'serve'] + opts + self.extra_test_args else: # pragma: no cover remote_path = args.remote_path or os.environ.get('BORG_REMOTE_PATH', 'borg') remote_path = replace_placeholders(remote_path) - return [remote_path, 'serve'] + opts + return env_vars + [remote_path, 'serve'] + opts def ssh_cmd(self, location): """return a ssh command line that can be prefixed to a borg command line""" diff --git a/src/borg/repository.py b/src/borg/repository.py index 106642d85..58c4af866 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -21,6 +21,7 @@ from .helpers import Error, ErrorWithTraceback, IntegrityError, format_file_size from .helpers import Location from .helpers import ProgressIndicatorPercent from .helpers import bin_to_hex +from .helpers import yes from .locking import Lock, LockError, LockErrorT from .logger import create_logger from .lrucache import LRUCache @@ -121,7 +122,9 @@ class Repository: self.do_create = create self.exclusive = exclusive self.append_only = append_only - self.unique_hostname = bool(os.environ.get('BORG_UNIQUE_HOSTNAME')) + self.hostname_is_unique = yes(env_var_override='BORG_HOSTNAME_IS_UNIQUE', env_msg=None, prompt=False) + if self.hostname_is_unique: + logger.info('Enabled removal of stale repository locks') def __del__(self): if self.lock: @@ -255,7 +258,7 @@ class Repository: if not os.path.isdir(path): raise self.DoesNotExist(path) if lock: - self.lock = Lock(os.path.join(path, 'lock'), exclusive, timeout=lock_wait, kill_stale_locks=self.unique_hostname).acquire() + self.lock = Lock(os.path.join(path, 'lock'), exclusive, timeout=lock_wait, kill_stale_locks=self.hostname_is_unique).acquire() else: self.lock = None self.config = ConfigParser(interpolation=None)