filelock

repository·main·Indexed 21 days ago

https://github.com/tox-dev/filelock

A platform-independent Python library for file-based locking to coordinate access to shared resources across multiple processes. It provides various implementations including OS-level locks (FileLock, UnixFileLock, WindowsFileLock), cooperative soft locks (SoftFileLock, StrictSoftFileLock), read-write locks (ReadWriteLock, SoftReadWriteLock), and expiring leases (SoftFileLease), with corresponding Async variants.

Tokens
24.7K
Snippets
69
Records
100
Agent score
74%

What's inside filelock

  1. Compare filelock lock types

    main

    Choose a lock type based on your filesystem capabilities and synchronization requirements:

    Lock TypeKey FeaturesBest Use Case
    FileLockNative backend support, cancellable acquire, self-deadlock detection.Recommended default for most applications.
    SoftFileLockCooperative file-existence marker; no native API required.Shared filesystems where you can verify semantics.
    StrictSoftFileLockFail-closed; removes claims by unique name; manual recovery.Filesystems with coherent directory reads and atomic hard links.
    SoftFileLeaseExpiring claim with a heartbeat; on_compromise callback.When progress matters more than strict exclusion (overlap acceptable).
    ReadWriteLockSQLite-backed; concurrent readers + one writer; reentrant.Scenarios requiring multiple readers or async support via AsyncReadWriteLock.
    SoftReadWriteLockReader/writer marker lease; heartbeat-based expiry; writer preference.Tested shared filesystems requiring reader/writer semantics.
    AsyncFileLockAsync/await support for all lock types via thread pools.Integration into asynchronous event loops.
  2. Detect stale locks with SoftFileLock

    main

    SoftFileLock automatically detects and cleans up stale locks on the same host. It stores the PID and hostname of the lock holder. If a process holding the lock dies, another process on the same host can automatically remove the marker by proving the PID no longer exists.

    Note: Cross-host records (on different machines) are not automatically cleaned up because the PID cannot be interpreted locally. On some platforms, PID recycling might also prevent automatic cleanup.

    from filelock import SoftFileLock
    
    lock = SoftFileLock("work.lock")
    
    with lock:
            # If the process holding the lock dies,
            # another process will automatically clean up the stale lock
            pass
  3. Handle task cancellation in async locks

    main

    When using async wrappers (e.g., AsyncFileLock, AsyncSoftFileLease), canceling an awaiting task while it is attempting to acquire a lock will roll the attempt back atomically. A canceled acquire will not leave a half-held lock.

    Note: If a task already holds a lock and is then canceled, the task still owns the responsibility of releasing that lock. Cancellation does not automatically release held locks.

  4. Choose between StrictSoftFileLock and SoftFileLease

    main

    When using soft locks, you must choose between mutual exclusion and progress based on your system's failure model:

    1. StrictSoftFileLock (Fail-Closed):

      • Provides mutual exclusion without a native lock.
      • Behavior: It never breaks a claim based on age. If a process crashes while holding the lock, the lock remains held until an operator manually clears it using force_break(claim.name).
      • Use case: When you prefer the system to hang rather than risk two processes overlapping (e.g., Terraform state locks).
    2. SoftFileLease (Fail-Open):

      • Behavior: The claim expires after a lease_duration. A peer can take over if the holder wedges. You can provide an on_compromise callback to stop work when the lease is lost.
      • Use case: When you prioritize progress and can handle a process resuming after its lease has expired (e.g., by fencing the resource).

    Clearing a Strict lock:

    stale = StrictSoftFileLock("work.lock")
    for claim in stale.claims:
        stale.force_break(claim.name)
    from filelock import StrictSoftFileLock
    
    lock = StrictSoftFileLock("work.lock")
    with lock:
        do_exclusive_work()
    
    # Manual recovery after a crash:
    stale = StrictSoftFileLock("work.lock")
    for claim in stale.claims:
        stale.force_break(claim.name)
  5. Use reentrant locks to avoid deadlocks

    main

    The FileLock implementation is reentrant. This means if a thread/process already holds a lock, it can acquire the same lock again without deadlocking. The lock maintains an internal counter and only truly releases the lock on disk when the acquisition count reaches zero.

    This is useful for helper functions that need to acquire a lock defensively, even if the caller might already hold it.

    from filelock import FileLock
    
    lock = FileLock("reentrant.lock")
    
    def read_cached_info():
        with lock:  # This is safe even if the caller holds the lock
            return load_info()
    
    with lock:
        refresh_cache()
        info = read_cached_info()  # Re-acquires the same lock; no deadlock
  6. Use StrictSoftFileLock for mutual exclusion without native locks

    main

    If you are on a filesystem without native locking but require strict mutual exclusion, use StrictSoftFileLock.

    Unlike standard SoftFileLock which uses a single shared marker, StrictSoftFileLock uses immutable owner claims via hard links. This prevents multiple contenders from both reading themselves as the lowest claim.

    Important considerations:

    • Crash Recovery: It does not automatically infer that an owner has died. An orphaned or crashed claim will block entry until manually cleared using force_break().
    • Compatibility: It uses a <path>.filelock/claims directory. It is compatible with legacy SoftFileLock holders, but mixing them (e.g., by deleting lock files or using break_lock()) can void mutual exclusion.
    • Requirements: Requires filesystem support for coherent directory reads and atomic no-replace hard links (e.g., Linux open(2) pattern or NTFS hard links).
    # To clear a stuck lock after a crash:
    lock.force_break()
  7. Why use file locks for multi-process coordination

    main

    In multi-process applications, processes often need to coordinate access to shared resources (like configuration files) to prevent race conditions. A race condition occurs when multiple processes attempt to read and write to the same file simultaneously, leading to data corruption where one process's changes overwrite another's.

    File locks solve this by ensuring that only one process can access a specific resource at a time. A process must successfully acquire a lock before performing operations on the shared resource and must release the lock once the operation is complete. This forces other processes attempting to acquire the same lock to wait, ensuring sequential and safe access.

  8. Use platform-agnostic FileLock and AsyncFileLock

    main

    Instead of importing platform-specific implementations like UnixFileLock or WindowsFileLock, you should use the platform aliases FileLock and AsyncFileLock.

    At import time, these aliases automatically resolve to the appropriate backend for your operating system: UnixFileLock or WindowsFileLock. If fcntl is unavailable on a Unix-like system, they will resolve to a soft backend. Both classes provide a consistent interface for file locking using acquire, release, and timeout methods, inheriting from BaseFileLock and BaseAsyncFileLock respectively.

    from filelock import FileLock
    
    lock = FileLock("my_lock_file.lock")
    with lock:
        # Critical section protected by the lock
        pass
  9. Keep correctness independent of the lock

    main

    Since filelock provides advisory locking, it only protects processes that explicitly use it. To ensure data integrity even if a lock is bypassed (e.g., by a different tool or a network mount error), design your system so that the lock is an optimization rather than the sole source of correctness.

    Use atomic filesystem operations like os.replace to ensure readers always see a complete file. In this model, losing the lock results in duplicated work (e.g., two processes downloading the same file) rather than data corruption.

    import os
        import tempfile
        from pathlib import Path
    
    from filelock import FileLock, Timeout
    
    def populate(target: Path, produce) -> None:
            fd, tmp = tempfile.mkstemp(dir=target.parent)
            try:
                with os.fdopen(fd, "wb") as handle:
                    handle.write(produce())
                os.chmod(tmp, 0o644)  # mkstemp creates 0600; widen before publishing
                os.replace(tmp, target)  # atomic: readers see the old file or the new one, never a partial one
            except BaseException:
                os.unlink(tmp)
                raise
    
    def get(target: Path, produce) -> bytes:
            lock = FileLock(f"{target}.lock")
            try:
                with lock.acquire(timeout=30):
                    if not target.exists():
                        populate(target, produce)
            except Timeout:
                if not target.exists():  # the lock only saved duplicate work; do the work anyway
                    populate(target, produce)
            return target.read_bytes()
  10. How stale lock detection works (PID and Start Token)

    main

    To prevent accidental eviction of a live process, filelock uses a combination of PID and a process start token. A contender only reclaims a marker if the owner is provably dead.

    An owner is considered 'gone' if:

    • The PID no longer exists.
    • The PID exists, but the process start token differs (indicating the PID was recycled).

    A lock is considered held (and not stale) if:

    • The PID is live and the start token matches.
    • The start token cannot be read.
    • The marker was written by a different host.

    Start Token Implementation:

    • Linux: kill(pid, 0) combined with starttime from /proc/<pid>/stat and the boot ID.
    • macOS: kill(pid, 0) combined with sysctl process start time.
    • Windows: kill(pid, 0) combined with GetProcessTimes creation time.
  11. Choose a soft-lock contract: StrictSoftFileLock vs SoftFileLease

    main

    When using soft locks (locks that rely on marker files rather than kernel primitives), you must choose between two distinct contracts. Do not mix these contracts on the same file path, as they use different record formats and can interfere with each other.

    StrictSoftFileLock

    StrictSoftFileLock prioritizes safety and mutual exclusion. It never automatically reclaims a marker. If a holder crashes, the marker remains, and subsequent contenders will wait indefinitely rather than risking an overlap. To recover from a crashed holder, you must manually call force_break().

    SoftFileLease

    SoftFileLease prioritizes progress over strict exclusion. It uses a lease mechanism where a holder periodically refreshes its claim. If a holder becomes stale (exceeds lease_duration), a peer can take the marker. This means a holder and its successor might overlap briefly. Use the on_compromise callback to handle cases where your lease is lost.

    Comparison Table

    Contenders on one pathHolds?
    StrictSoftFileLock with StrictSoftFileLockYes
    SoftFileLease with SoftFileLease (same lease_duration)Until claim expires; then overlap occurs
    SoftFileLock with StrictSoftFileLockYes (since v3.30.0)
    SoftFileLock with SoftFileLeaseNo (Legacy contender may evict live lease)
    StrictSoftFileLock with SoftFileLeaseLease waits for strict holder; strict holder never reclaims expired lease
    from filelock import SoftFileLease, StrictSoftFileLock
    
    # Strict mode: no peer enters while this holder lives
    with StrictSoftFileLock("work.lock", timeout=30):
            pass
    
    def stop_working(compromise):
            print("lost the claim:", compromise.reason)
    
    # Lease mode: a peer may enter 60s after the last refresh
    with SoftFileLease("work.lock", lease_duration=60, on_compromise=stop_working):
            pass
  12. Compare read-write lock types

    main

    When you need multiple readers but only one writer, use the read-write lock variants:

    • ReadWriteLock: Uses a local SQLite database for state. Best for local filesystems. Medium overhead.
    • SoftReadWriteLock: Uses a marker tree with a heartbeat. Works on network filesystems (including multi-node clusters). Medium overhead.

    Both types support Async variants and are singletons by default.