NFS-safe File Locking
The flufl.lock library provides an NFS-safe, file-based locking algorithm for POSIX and Windows systems, heavily influenced by the GNU/Linux open(2) manpage's O_EXCL option. It aims to prevent race conditions on NFS file systems by using atomic file operations with robust timeouts and lock-breaking capabilities. Currently at version 9.0.0, the library is under active development with regular updates and maintenance releases.
Warnings
- gotcha Choosing an appropriate 'lifetime' for a lock is crucial. If too long, stale locks (from crashed processes) will block others excessively. If too short, other processes might prematurely break an active lock, leading to data corruption.
- gotcha For distributed (NFS) environments, proper clock synchronization between participating machines is essential. Inaccurate clocks can lead to incorrect lock expiration calculations, causing stale locks to persist or active locks to be prematurely broken.
- breaking Version 9.0.0 removed support for Python 3.9. Previous major versions also dropped support for older Python versions (e.g., 8.0 dropped 3.7).
- gotcha Some applications (e.g., DVC) have reported `pkg_resources.DistributionNotFound` errors when using `flufl.lock` version 8.0 or newer due to potential changes in how its metadata is packaged or resolved by downstream tools.
Install
-
pip install flufl.lock
Imports
- Lock
from flufl.lock import Lock
- AlreadyLockedError
from flufl.lock import AlreadyLockedError
- NotLockedError
from flufl.lock import NotLockedError
Quickstart
import tempfile
import os
from flufl.lock import Lock, AlreadyLockedError
# Create a temporary file to use as the lock file
# In a real application, use a persistent path accessible to all processes
with tempfile.NamedTemporaryFile(delete=False) as tmp_lock_file:
lock_file_path = tmp_lock_file.name
try:
# Acquire the lock using a context manager for automatic release
# default lifetime is 15 seconds
with Lock(lock_file_path) as lock:
print(f"Lock acquired on {lock_file_path}")
print(f"Is locked: {lock.is_locked}")
print("Performing some locked operation...")
# Simulate another process trying to acquire the same lock
try:
with Lock(lock_file_path, default_timeout=1) as other_lock:
print("This should not be reached if the lock is held.")
except AlreadyLockedError:
print("Another process correctly detected the lock is held.")
# The lock is automatically released when exiting the 'with' block
print("Lock released.")
# Acquire and release manually
lock = Lock(lock_file_path)
lock.lock()
print("Lock acquired manually.")
lock.refresh()
print("Lock refreshed (lifetime extended).")
lock.unlock()
print("Lock released manually.")
finally:
# Clean up the temporary lock file
if os.path.exists(lock_file_path):
os.remove(lock_file_path)
# The library might create a claim file; ensure it's also cleaned.
# For this simple example, we assume lock_file_path is sufficient.