Source code for stouputils.lock.re_entrant


# Imports
import os
from typing import ClassVar

from .base import LockFifo


[docs] class RLockFifo(LockFifo): """ A re-entrant cross-process lock backed by a file. This lock is re-entrant for the same owner, where owner identity is the tuple ``(path, pid, thread_id)``. Repeated calls to :meth:`acquire` by the same owner increment an internal counter; only the final :meth:`release` will release the underlying file lock managed by :class:`LockFifo`. Key behaviour: - Owner identity: (path, pid, thread_id) - Reentrancy applies only within the same thread of the same process. Other threads or processes will block (or raise ``LockTimeoutError``) according to the lock's timeout and blocking parameters. - Implemented on top of :class:`LockFifo` and shares its constructor parameters and error semantics. Args: name (str): Lock filename or path. If a simple name is given, it is created in the system temporary directory. timeout (float | None): Seconds to wait for the lock. ``None`` means block indefinitely. blocking (bool): Whether to block until acquired (subject to ``timeout``). check_interval (float): Interval between lock attempts, in seconds. fifo (bool): Whether to enforce Fifo ordering (default: True). fifo_stale_timeout (float | None): Seconds after which a ticket is considered stale; if ``None`` the lock's ``timeout`` value will be used. Raises: LockTimeoutError: If the lock could not be acquired within the timeout (LockError & TimeoutError subclass) LockError: On unexpected locking errors. (RunTimeError subclass) Examples: >>> with RLockFifo("my.lock", timeout=5): ... # critical section ... pass >>> lock = RLockFifo("my.lock") >>> lock.acquire() >>> lock.acquire() # re-entrant acquire by same thread/process >>> lock.release() >>> lock.release() # underlying lock released here >>> # Reentrancy with Fifo enabled should not create multiple tickets >>> lock = RLockFifo("my_r.lock", fifo=True, timeout=1) >>> lock.acquire() >>> lock.acquire() >>> lock.release() >>> lock.release() >>> # Cleanup behaviour: after closing a re-entrant lock the queue should be removed when empty >>> import tempfile, os >>> tmp = tempfile.mkdtemp() >>> p = tmp + "/rlock" >>> r = RLockFifo(p, fifo=True, timeout=1) >>> r.acquire(); r.acquire(); r.release(); r.release(); r.close() >>> os.path.exists(p + ".queue") False """ owners: ClassVar[dict[tuple[str, int, int], int]] = {} """ Mapping of owner keys to re-entrant acquisition counts. """ def __init__( self, name: str, timeout: float | None = None, blocking: bool = True, check_interval: float = 0.05, fifo: bool = True, fifo_stale_timeout: float | None = None ) -> None: """ Initialize the re-entrant lock and compute owner key. The attribute ``self.key`` is a tuple ``(path, pid, thread_id)`` used to track ownership and re-entrant acquisition counts in the :class:`owners` mapping. """ super().__init__(name, timeout=timeout, blocking=blocking, check_interval=check_interval, fifo=fifo, fifo_stale_timeout=fifo_stale_timeout) self.key: tuple[str, int, int] = (self.path, os.getpid(), __import__("threading").get_ident())
[docs] def acquire(self, timeout: float | None = None, blocking: bool | None = None, check_interval: float | None = None) -> None: """ Acquire the lock with re-entrancy for the same owner. If the current owner (same ``self.key``) already holds the lock, the internal counter is incremented and the underlying file lock is not re-acquired. Otherwise this delegates to :meth:`LockFifo.acquire`. """ cnt: int = self.owners.get(self.key, 0) if cnt > 0: self.owners[self.key] = cnt + 1 return super().acquire(timeout=timeout, blocking=blocking, check_interval=check_interval) self.owners[self.key] = 1
[docs] def release(self) -> None: """ Release the lock for this owner. Decrements the re-entrant counter for the current owner and only when the counter reaches zero the underlying :class:`LockFifo` is released. """ cnt: int = self.owners.get(self.key, 0) if cnt <= 1: # last release: release underlying lock try: super().release() finally: self.owners.pop(self.key, None) else: self.owners[self.key] = cnt - 1