stouputils.lock.base module#

_lock_fd(
fd: int,
blocking: bool,
timeout: float | None,
) None[source]#

Try to acquire an exclusive lock on an open file descriptor.

This helper attempts POSIX fcntl first, then Windows msvcrt. It raises BlockingIOError when the lock is busy, ImportError if neither backend is available, or OSError for unexpected errors.

_unlock_fd(fd: int | None) None[source]#

Unlock an open file descriptor using the available backend.

_remove_file_if_unlocked(path: str) None[source]#

Attempt to remove a file only if we can confirm nobody holds the lock.

Uses a non-blocking lock test via fcntl or msvcrt. This is best-effort and will not raise on failure.

_worker(lp: str, op: str, idx: int) None[source]#

Module-level helper used by doctests as a multiprocessing target.

_hold(path: str) None[source]#

Module-level helper used by doctests as a multiprocessing target.

This creates a small readiness marker file while holding the lock so doctests can reliably detect when the child process has acquired it (useful on Windows spawn semantics).

class LockFifo(
name: str,
timeout: float | None = None,
blocking: bool = True,
check_interval: float = 0.05,
fifo: bool = True,
fifo_stale_timeout: float | None = None,
)[source]#

Bases: AbstractContextManager[LockFifo]

A simple cross-platform inter-process lock backed by a file.

This implementation supports optional Fifo ordering via a small ticket queue stored alongside the lock file. Fifo is enabled by default to avoid starvation. Fifo behaviour is implemented with a small sequence file and per-ticket files in <lockpath>.queue/. On platforms without fcntl the implementation falls back to a timestamp-based ticket.

Parameters:
  • 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

>>> # Basic context-manager usage (Fifo enabled by default)
>>> with LockFifo("my.lock", timeout=1):
...     pass
>>> # Explicit acquire/release
>>> lock = LockFifo("my.lock", timeout=1)
>>> lock.acquire()
>>> lock.release()
>>> # Doctest: simple multi-process Fifo check (fast and deterministic)
>>> import tempfile, multiprocessing, time
>>> tmpdir = tempfile.mkdtemp()
>>> lockpath = tmpdir + "/t.lock"
>>> out = tmpdir + "/out.txt"
>>> # Worker function is module-level: `_worker`
>>> # (Defined at module scope so it can be pickled on Windows)
>>> procs = []
>>> for i in range(3):
...     p = multiprocessing.Process(target=_worker, args=(lockpath, out, i))
...     p.start(); procs.append(p); time.sleep(0.05)
>>> for p in procs: p.join(1)
>>> with open(out) as f: print([int(x) for x in f.read().splitlines()])
[0, 1, 2]
>>> # Doctest: cleanup of artifacts on close
>>> import tempfile, os
>>> tmp = tempfile.mkdtemp()
>>> p = tmp + "/tlock"
>>> l = LockFifo(p, timeout=1)
>>> l.acquire(); l.release(); l.close()
>>> import os
>>> # The lock file should not remain on any platform after close()
>>> assert not os.path.exists(p)
>>> assert not os.path.exists(p + ".queue")
>>> # Non-Fifo fast-path should not create a queue directory
>>> tmp2 = tempfile.mkdtemp()
>>> p2 = tmp2 + "/tlock2"
>>> l2 = LockFifo(p2, fifo=False, timeout=1)
>>> l2.acquire(); l2.release(); l2.close()
>>> os.path.exists(p2 + ".queue")
False
>>> # Attempting a non-blocking acquire while another process holds the lock raises LockTimeoutError
>>> import multiprocessing, time
>>> # Hold function is module-level: `_hold`
>>> # (Defined at module scope so it can be pickled on Windows)
>>> p = multiprocessing.Process(target=_hold, args=(p2,))
>>> p.start()
>>> import time, os
>>> deadline = time.time() + 1.0
>>> while not os.path.exists(p2 + ".held") and time.time() < deadline:
...     time.sleep(0.01)
>>> l3 = LockFifo(p2, timeout=1)
>>> try:
...     l3.acquire(blocking=False)
... except LockTimeoutError:
...     print("timeout")
... finally:
...     p.terminate(); p.join()
timeout
path: str#

The lock file path.

timeout: float | None#

Maximum time to wait for the lock, in seconds. None means wait indefinitely.

blocking: bool#

Whether to block until the lock is acquired (subject to timeout).

check_interval: float#

Interval between lock acquisition attempts, in seconds.

file: IO[bytes] | None#

The underlying file object.

fd: int | None#

The underlying file descriptor.

is_locked: bool#

Whether the lock is currently held.

fifo: bool#

Whether Fifo ordering is enabled (default True).

fifo_stale_timeout: float | None#

Seconds to consider a ticket stale and eligible for cleanup. If None, the lock’s timeout value will be used; if that is also None, no stale cleanup will be performed.

queue_dir: str#

Directory used to store queue metadata and ticket files.

_abc_impl = <_abc._abc_data object>#
_get_ticket() int[source]#

Obtain a monotonically increasing ticket number.

Uses a small sequence file protected by an exclusive lock (fcntl) when available. When fcntl is not available, falls back to a timestamp-based ticket (still monotonic enough on typical systems).

_cleanup_stale_tickets() None[source]#

Remove stale ticket files from the queue directory.

A ticket is considered stale when its mtime is older than the effective stale timeout. If self.fifo_stale_timeout is None, the lock’s timeout value is used; if that is also None, no cleanup is performed.

perform_lock(
blocking: bool,
timeout: float | None,
check_interval: float,
) None[source]#

Core platform-specific lock acquisition. This contains the original flock-based implementation and is used both by Fifo and non-Fifo paths.

acquire(
timeout: float | None = None,
blocking: bool | None = None,
check_interval: float | None = None,
) None[source]#

Acquire the lock, optionally using Fifo ordering.

When Fifo is enabled (default), a ticket file is created and the caller waits until its ticket becomes head of the queue before attempting the actual underlying lock. This avoids starvation by ensuring waiters are served in arrival order.

release() None[source]#

Release the lock.

close() None[source]#

Release and close underlying file descriptor.

Also attempts best-effort cleanup of queue artifacts and the lock file itself when it is safe to do so (no waiting clients and the lock is not held). This avoids leaving behind <lock>.queue/ and <lock> files when they are no longer in use.