Cross-process and cross-host reader/writer lock built on :class:`SoftFileLock` primitives. Use this class instead of :class:`~filelock.ReadWriteLock` when the lock file lives on a network filesystem (NFS, Lustre with ``-o flock``, HPC cluster shared storage). ``ReadWriteLock`` is backe
| 113 | |
| 114 | |
| 115 | class SoftReadWriteLock(metaclass=_SoftRWMeta): |
| 116 | """ |
| 117 | Cross-process and cross-host reader/writer lock built on :class:`SoftFileLock` primitives. |
| 118 | |
| 119 | Use this class instead of :class:`~filelock.ReadWriteLock` when the lock file lives on a network |
| 120 | filesystem (NFS, Lustre with ``-o flock``, HPC cluster shared storage). ``ReadWriteLock`` is backed |
| 121 | by SQLite and cannot run on NFS because SQLite's ``fcntl`` locking is unreliable there. |
| 122 | |
| 123 | Layout on disk for a lock at ``foo.lock``: |
| 124 | |
| 125 | - ``foo.lock.state`` — a :class:`SoftFileLock` taken only during state transitions (microseconds). |
| 126 | - ``foo.lock.write`` — writer marker; its presence means a writer is claiming or holding the lock. |
| 127 | - ``foo.lock.readers/<host>.<pid>.<uuid>`` — one file per reader. |
| 128 | |
| 129 | Each marker stores a random token (``secrets.token_hex(16)``), the holder's pid, and the holder's |
| 130 | hostname. A daemon heartbeat thread refreshes ``mtime`` on every held marker. A marker whose mtime |
| 131 | has not advanced in ``stale_threshold`` seconds may be evicted by any process on any host, giving |
| 132 | correct behavior when a compute node crashes with a lock held. |
| 133 | |
| 134 | Writer acquire is two-phase and writer-preferring: phase 1 claims ``.write`` (blocking any new |
| 135 | reader), phase 2 waits for existing readers to drain. Writer starvation is impossible. |
| 136 | |
| 137 | Reentrancy, upgrade/downgrade rules, thread pinning, and singleton caching by resolved path match |
| 138 | :class:`~filelock.ReadWriteLock`. |
| 139 | |
| 140 | Forking invalidates the inherited instance in the child so the child cannot double-own the lock with its parent; |
| 141 | ``release()`` on that instance is a no-op, and the child must construct a new instance if it needs a lock. |
| 142 | |
| 143 | Trust boundary: protects against same-UID non-cooperating processes (one host or cross-host) and |
| 144 | same-host different-UID users via ``0o600`` / ``0o700`` permissions. Does not protect against root |
| 145 | compromise, NTP tampering on same-UID cross-host nodes, or multi-tenant mounts where hostile |
| 146 | co-tenants share the UID. |
| 147 | |
| 148 | :param lock_file: path to the lock file; sidecar state/write/readers live next to it |
| 149 | :param timeout: maximum wait time in seconds; ``-1`` means block indefinitely |
| 150 | :param blocking: if ``False``, raise :class:`~filelock.Timeout` immediately on contention |
| 151 | :param is_singleton: if ``True``, reuse existing instances for the same resolved path |
| 152 | :param heartbeat_interval: seconds between heartbeat refreshes; default 30 s |
| 153 | :param stale_threshold: seconds of ``mtime`` inactivity before a marker is stale; defaults to |
| 154 | ``3 * heartbeat_interval``, matching etcd's ``LeaseKeepAlive`` convention |
| 155 | :param poll_interval: seconds between acquire retries under contention; default 0.25 s |
| 156 | |
| 157 | .. versionadded:: 3.27.0 |
| 158 | |
| 159 | """ |
| 160 | |
| 161 | _instances: WeakValueDictionary[Path, SoftReadWriteLock] = WeakValueDictionary() |
| 162 | _instances_lock = threading.RLock() |
| 163 | |
| 164 | def __init__( # ruff:ignore[too-many-arguments] # public constructor: one parameter per documented lock option |
| 165 | self, |
| 166 | lock_file: str | os.PathLike[str], |
| 167 | timeout: float = -1, |
| 168 | *, |
| 169 | blocking: bool = True, |
| 170 | is_singleton: bool = True, # ruff:ignore[unused-method-argument] # consumed by _SoftRWMeta.__call__ |
| 171 | heartbeat_interval: float = 30.0, |
| 172 | stale_threshold: float | None = None, |
no outgoing calls