|
@@ -0,0 +1,286 @@
|
|
|
|
+import errno
|
|
|
|
+import json
|
|
|
|
+import os
|
|
|
|
+import socket
|
|
|
|
+import threading
|
|
|
|
+import time
|
|
|
|
+
|
|
|
|
+from borg.helpers import Error
|
|
|
|
+
|
|
|
|
+ADD, REMOVE = 'add', 'remove'
|
|
|
|
+SHARED, EXCLUSIVE = 'shared', 'exclusive'
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def get_id():
|
|
|
|
+ """Get identification tuple for 'us'"""
|
|
|
|
+ hostname = socket.gethostname()
|
|
|
|
+ pid = os.getpid()
|
|
|
|
+ tid = threading.current_thread().ident & 0xffffffff
|
|
|
|
+ return hostname, pid, tid
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+class TimeoutTimer:
|
|
|
|
+ """
|
|
|
|
+ A timer for timeout checks (can also deal with no timeout, give timeout=None [default]).
|
|
|
|
+ It can also compute and optionally execute a reasonable sleep time (e.g. to avoid
|
|
|
|
+ polling too often or to support thread/process rescheduling).
|
|
|
|
+ """
|
|
|
|
+ def __init__(self, timeout=None, sleep=None):
|
|
|
|
+ """
|
|
|
|
+ Initialize a timer.
|
|
|
|
+
|
|
|
|
+ :param timeout: time out interval [s] or None (no timeout)
|
|
|
|
+ :param sleep: sleep interval [s] (>= 0: do sleep call, <0: don't call sleep)
|
|
|
|
+ or None (autocompute: use 10% of timeout, or 1s for no timeout)
|
|
|
|
+ """
|
|
|
|
+ if timeout is not None and timeout < 0:
|
|
|
|
+ raise ValueError("timeout must be >= 0")
|
|
|
|
+ self.timeout_interval = timeout
|
|
|
|
+ if sleep is None:
|
|
|
|
+ if timeout is None:
|
|
|
|
+ sleep = 1.0
|
|
|
|
+ else:
|
|
|
|
+ sleep = timeout / 10.0
|
|
|
|
+ self.sleep_interval = sleep
|
|
|
|
+ self.start_time = None
|
|
|
|
+ self.end_time = None
|
|
|
|
+
|
|
|
|
+ def __repr__(self):
|
|
|
|
+ return "<%s: start=%r end=%r timeout=%r sleep=%r>" % (
|
|
|
|
+ self.__class__.__name__, self.start_time, self.end_time,
|
|
|
|
+ self.timeout_interval, self.sleep_interval)
|
|
|
|
+
|
|
|
|
+ def start(self):
|
|
|
|
+ self.start_time = time.time()
|
|
|
|
+ if self.timeout_interval is not None:
|
|
|
|
+ self.end_time = self.start_time + self.timeout_interval
|
|
|
|
+ return self
|
|
|
|
+
|
|
|
|
+ def sleep(self):
|
|
|
|
+ if self.sleep_interval >= 0:
|
|
|
|
+ time.sleep(self.sleep_interval)
|
|
|
|
+
|
|
|
|
+ def timed_out(self):
|
|
|
|
+ return self.end_time is not None and time.time() >= self.end_time
|
|
|
|
+
|
|
|
|
+ def timed_out_or_sleep(self):
|
|
|
|
+ if self.timed_out():
|
|
|
|
+ return True
|
|
|
|
+ else:
|
|
|
|
+ self.sleep()
|
|
|
|
+ return False
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+class ExclusiveLock:
|
|
|
|
+ """An exclusive Lock based on mkdir fs operation being atomic"""
|
|
|
|
+ class LockError(Error):
|
|
|
|
+ """Failed to acquire the lock {}."""
|
|
|
|
+
|
|
|
|
+ class LockTimeout(LockError):
|
|
|
|
+ """Failed to create/acquire the lock {} (timeout)."""
|
|
|
|
+
|
|
|
|
+ class LockFailed(LockError):
|
|
|
|
+ """Failed to create/acquire the lock {} ({})."""
|
|
|
|
+
|
|
|
|
+ class UnlockError(Error):
|
|
|
|
+ """Failed to release the lock {}."""
|
|
|
|
+
|
|
|
|
+ class NotLocked(UnlockError):
|
|
|
|
+ """Failed to release the lock {} (was not locked)."""
|
|
|
|
+
|
|
|
|
+ class NotMyLock(UnlockError):
|
|
|
|
+ """Failed to release the lock {} (was/is locked, but not by me)."""
|
|
|
|
+
|
|
|
|
+ def __init__(self, path, timeout=None, sleep=None, id=None):
|
|
|
|
+ self.timeout = timeout
|
|
|
|
+ self.sleep = sleep
|
|
|
|
+ self.path = os.path.abspath(path)
|
|
|
|
+ self.id = id or get_id()
|
|
|
|
+ self.unique_name = os.path.join(self.path, "%s.%d-%x" % self.id)
|
|
|
|
+
|
|
|
|
+ def __enter__(self):
|
|
|
|
+ return self.acquire()
|
|
|
|
+
|
|
|
|
+ def __exit__(self, *exc):
|
|
|
|
+ self.release()
|
|
|
|
+
|
|
|
|
+ def __repr__(self):
|
|
|
|
+ return "<%s: %r>" % (self.__class__.__name__, self.unique_name)
|
|
|
|
+
|
|
|
|
+ def acquire(self, timeout=None, sleep=None):
|
|
|
|
+ if timeout is None:
|
|
|
|
+ timeout = self.timeout
|
|
|
|
+ if sleep is None:
|
|
|
|
+ sleep = self.sleep
|
|
|
|
+ timer = TimeoutTimer(timeout, sleep).start()
|
|
|
|
+ while True:
|
|
|
|
+ try:
|
|
|
|
+ os.mkdir(self.path)
|
|
|
|
+ except OSError as err:
|
|
|
|
+ if err.errno == errno.EEXIST: # already locked
|
|
|
|
+ if self.by_me():
|
|
|
|
+ return self
|
|
|
|
+ if timer.timed_out_or_sleep():
|
|
|
|
+ raise self.LockTimeout(self.path)
|
|
|
|
+ else:
|
|
|
|
+ raise self.LockFailed(self.path, str(err))
|
|
|
|
+ else:
|
|
|
|
+ with open(self.unique_name, "wb"):
|
|
|
|
+ pass
|
|
|
|
+ return self
|
|
|
|
+
|
|
|
|
+ def release(self):
|
|
|
|
+ if not self.is_locked():
|
|
|
|
+ raise self.NotLocked(self.path)
|
|
|
|
+ if not self.by_me():
|
|
|
|
+ raise self.NotMyLock(self.path)
|
|
|
|
+ os.unlink(self.unique_name)
|
|
|
|
+ os.rmdir(self.path)
|
|
|
|
+
|
|
|
|
+ def is_locked(self):
|
|
|
|
+ return os.path.exists(self.path)
|
|
|
|
+
|
|
|
|
+ def by_me(self):
|
|
|
|
+ return os.path.exists(self.unique_name)
|
|
|
|
+
|
|
|
|
+ def break_lock(self):
|
|
|
|
+ if self.is_locked():
|
|
|
|
+ for name in os.listdir(self.path):
|
|
|
|
+ os.unlink(os.path.join(self.path, name))
|
|
|
|
+ os.rmdir(self.path)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+class LockRoster:
|
|
|
|
+ """
|
|
|
|
+ A Lock Roster to track shared/exclusive lockers.
|
|
|
|
+
|
|
|
|
+ Note: you usually should call the methods with an exclusive lock held,
|
|
|
|
+ to avoid conflicting access by multiple threads/processes/machines.
|
|
|
|
+ """
|
|
|
|
+ def __init__(self, path, id=None):
|
|
|
|
+ self.path = path
|
|
|
|
+ self.id = id or get_id()
|
|
|
|
+
|
|
|
|
+ def load(self):
|
|
|
|
+ try:
|
|
|
|
+ with open(self.path) as f:
|
|
|
|
+ data = json.load(f)
|
|
|
|
+ except IOError as err:
|
|
|
|
+ if err.errno != errno.ENOENT:
|
|
|
|
+ raise
|
|
|
|
+ data = {}
|
|
|
|
+ return data
|
|
|
|
+
|
|
|
|
+ def save(self, data):
|
|
|
|
+ with open(self.path, "w") as f:
|
|
|
|
+ json.dump(data, f)
|
|
|
|
+
|
|
|
|
+ def remove(self):
|
|
|
|
+ os.unlink(self.path)
|
|
|
|
+
|
|
|
|
+ def get(self, key):
|
|
|
|
+ roster = self.load()
|
|
|
|
+ return set(tuple(e) for e in roster.get(key, []))
|
|
|
|
+
|
|
|
|
+ def modify(self, key, op):
|
|
|
|
+ roster = self.load()
|
|
|
|
+ try:
|
|
|
|
+ elements = set(tuple(e) for e in roster[key])
|
|
|
|
+ except KeyError:
|
|
|
|
+ elements = set()
|
|
|
|
+ if op == ADD:
|
|
|
|
+ elements.add(self.id)
|
|
|
|
+ elif op == REMOVE:
|
|
|
|
+ elements.remove(self.id)
|
|
|
|
+ else:
|
|
|
|
+ raise ValueError('Unknown LockRoster op %r' % op)
|
|
|
|
+ roster[key] = list(list(e) for e in elements)
|
|
|
|
+ self.save(roster)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+class UpgradableLock:
|
|
|
|
+ """
|
|
|
|
+ A Lock for a resource that can be accessed in a shared or exclusive way.
|
|
|
|
+ Typically, write access to a resource needs an exclusive lock (1 writer,
|
|
|
|
+ noone is allowed reading) and read access to a resource needs a shared
|
|
|
|
+ lock (multiple readers are allowed).
|
|
|
|
+ """
|
|
|
|
+ class SharedLockFailed(Error):
|
|
|
|
+ """Failed to acquire shared lock [{}]"""
|
|
|
|
+
|
|
|
|
+ class ExclusiveLockFailed(Error):
|
|
|
|
+ """Failed to acquire write lock [{}]"""
|
|
|
|
+
|
|
|
|
+ def __init__(self, path, exclusive=False, sleep=None, id=None):
|
|
|
|
+ self.path = path
|
|
|
|
+ self.is_exclusive = exclusive
|
|
|
|
+ self.sleep = sleep
|
|
|
|
+ self.id = id or get_id()
|
|
|
|
+ # globally keeping track of shared and exclusive lockers:
|
|
|
|
+ self._roster = LockRoster(path + '.roster', id=id)
|
|
|
|
+ # an exclusive lock, used for:
|
|
|
|
+ # - holding while doing roster queries / updates
|
|
|
|
+ # - holding while the UpgradableLock itself is exclusive
|
|
|
|
+ self._lock = ExclusiveLock(path + '.lock', id=id)
|
|
|
|
+
|
|
|
|
+ def __enter__(self):
|
|
|
|
+ return self.acquire()
|
|
|
|
+
|
|
|
|
+ def __exit__(self, *exc):
|
|
|
|
+ self.release()
|
|
|
|
+
|
|
|
|
+ def __repr__(self):
|
|
|
|
+ return "<%s: %r>" % (self.__class__.__name__, self.id)
|
|
|
|
+
|
|
|
|
+ def acquire(self, exclusive=None, remove=None, sleep=None):
|
|
|
|
+ if exclusive is None:
|
|
|
|
+ exclusive = self.is_exclusive
|
|
|
|
+ sleep = sleep or self.sleep or 0.2
|
|
|
|
+ try:
|
|
|
|
+ if exclusive:
|
|
|
|
+ self._wait_for_readers_finishing(remove, sleep)
|
|
|
|
+ self._roster.modify(EXCLUSIVE, ADD)
|
|
|
|
+ else:
|
|
|
|
+ with self._lock:
|
|
|
|
+ if remove is not None:
|
|
|
|
+ self._roster.modify(remove, REMOVE)
|
|
|
|
+ self._roster.modify(SHARED, ADD)
|
|
|
|
+ self.is_exclusive = exclusive
|
|
|
|
+ return self
|
|
|
|
+ except ExclusiveLock.LockError as err:
|
|
|
|
+ msg = str(err)
|
|
|
|
+ if exclusive:
|
|
|
|
+ raise self.ExclusiveLockFailed(msg)
|
|
|
|
+ else:
|
|
|
|
+ raise self.SharedLockFailed(msg)
|
|
|
|
+
|
|
|
|
+ def _wait_for_readers_finishing(self, remove, sleep):
|
|
|
|
+ while True:
|
|
|
|
+ self._lock.acquire()
|
|
|
|
+ if remove is not None:
|
|
|
|
+ self._roster.modify(remove, REMOVE)
|
|
|
|
+ remove = None
|
|
|
|
+ if len(self._roster.get(SHARED)) == 0:
|
|
|
|
+ return # we are the only one and we keep the lock!
|
|
|
|
+ self._lock.release()
|
|
|
|
+ time.sleep(sleep)
|
|
|
|
+
|
|
|
|
+ def release(self):
|
|
|
|
+ if self.is_exclusive:
|
|
|
|
+ self._roster.modify(EXCLUSIVE, REMOVE)
|
|
|
|
+ self._lock.release()
|
|
|
|
+ else:
|
|
|
|
+ with self._lock:
|
|
|
|
+ self._roster.modify(SHARED, REMOVE)
|
|
|
|
+
|
|
|
|
+ def upgrade(self):
|
|
|
|
+ if not self.is_exclusive:
|
|
|
|
+ self.acquire(exclusive=True, remove=SHARED)
|
|
|
|
+
|
|
|
|
+ def downgrade(self):
|
|
|
|
+ if self.is_exclusive:
|
|
|
|
+ self.acquire(exclusive=False, remove=EXCLUSIVE)
|
|
|
|
+
|
|
|
|
+ def break_lock(self):
|
|
|
|
+ self._roster.remove()
|
|
|
|
+ self._lock.break_lock()
|