class Lock: """A inter-process and inter-thread lock This internally uses :class:`fasteners.InterProcessLock` but provides non-blocking acquire. It also guarantees thread-safety when using the :meth:`singleton` class method to create / retrieve a lock instance. :param path: Path of the lock file to use / create. """ _instances: Dict[str, "Lock"] = {} _singleton_lock = threading.Lock() @classmethod def singleton(cls, path: str) -> "Lock": """ Retrieve an existing lock object with a given 'name' or create a new one. Use this method for thread-safe locks. :param path: Path of the lock file to use / create. """ with cls._singleton_lock: try: instance = cls._instances[path] except KeyError: instance = cls(path) cls._instances[path] = instance return instance def __init__(self, path: str) -> None: self.path = path self._internal_lock = threading.Semaphore() self._external_lock = InterProcessLock(self.path) self._lock = threading.RLock() def acquire(self) -> bool: """ Attempts to acquire the given lock. :returns: Whether or not the acquisition succeeded. """ with self._lock: locked_internal = self._internal_lock.acquire(blocking=False) if not locked_internal: return False try: locked_external = self._external_lock.acquire(blocking=False) except Exception: self._internal_lock.release() raise else: if locked_external: return True else: self._internal_lock.release() return False def release(self) -> None: """Release the previously acquired lock.""" with self._lock: self._external_lock.release() self._internal_lock.release() def locked(self) -> bool: """ Checks if the lock is currently held by any thread or process. :returns: Whether the lock is acquired. """ with self._lock: gotten = self.acquire() if gotten: self.release() return not gotten def locking_pid(self) -> Optional[int]: """ Returns the PID of the process which currently holds the lock or ``None``. This should work on macOS, OpenBSD and Linux but may fail on some platforms. Always use :meth:`locked` to check if the lock is held by any process. :returns: The PID of the process which currently holds the lock or ``None``. """ with self._lock: if self._external_lock.acquired: return os.getpid() try: # Don't close again in case we are the locking process. self._external_lock._do_open() lockdata, fmt, pid_index = _get_lockdata() lockdata = fcntl.fcntl(self._external_lock.lockfile, fcntl.F_GETLK, lockdata) lockdata_list = struct.unpack(fmt, lockdata) pid = lockdata_list[pid_index] if pid > 0: return pid except OSError: pass return None
class Lock: """ A inter-process and inter-thread lock. This reuses uses code from oslo.concurrency but provides non-blocking acquire. Use the :meth:`singleton` class method to retrieve an existing instance for thread-safe usage. """ _instances: Dict[str, "Lock"] = dict() _singleton_lock = threading.Lock() @classmethod def singleton(cls, name: str, lock_path: Optional[str] = None) -> "Lock": """ Retrieve an existing lock object with a given 'name' or create a new one. Use this method for thread-safe locks. :param name: Name of lock file. :param lock_path: Directory for lock files. Defaults to the temporary directory returned by :func:`tempfile.gettempdir()` if not given. """ with cls._singleton_lock: try: instance = cls._instances[name] except KeyError: instance = cls(name, lock_path) cls._instances[name] = instance return instance def __init__(self, name: str, lock_path: Optional[str] = None) -> None: self.name = name dirname = lock_path or tempfile.gettempdir() lock_path = os.path.join(dirname, name) self._internal_lock = threading.Semaphore() self._external_lock = InterProcessLock(lock_path) self._lock = threading.RLock() def acquire(self) -> bool: """ Attempts to acquire the given lock. :returns: Whether or not the acquisition succeeded. """ with self._lock: locked_internal = self._internal_lock.acquire(blocking=False) if not locked_internal: return False try: locked_external = self._external_lock.acquire(blocking=False) except Exception: self._internal_lock.release() raise else: if locked_external: return True else: self._internal_lock.release() return False def release(self) -> None: """Release the previously acquired lock.""" with self._lock: self._external_lock.release() self._internal_lock.release() def locked(self) -> bool: """Checks if the lock is currently held by any thread or process.""" with self._lock: gotten = self.acquire() if gotten: self.release() return not gotten def locking_pid(self) -> Optional[int]: """ Returns the PID of the process which currently holds the lock or ``None``. This should work on macOS, OpenBSD and Linux but may fail on some platforms. Always use :meth:`locked` to check if the lock is held by any process. :returns: The PID of the process which currently holds the lock or ``None``. """ with self._lock: if self._external_lock.acquired: return os.getpid() try: # don't close again in case we are the locking process self._external_lock._do_open() lockdata, fmt, pid_index = _get_lockdata() lockdata = fcntl.fcntl(self._external_lock.lockfile, fcntl.F_GETLK, lockdata) lockdata_list = struct.unpack(fmt, lockdata) pid = lockdata_list[pid_index] if pid > 0: return pid except OSError: pass return None