API Reference#

API reference for flufl.lock:

class flufl.lock.Lock(lockfile: str, lifetime: timedelta | int | None = None, separator: str = '|', default_timeout: timedelta | int | None = None)#

Portable, NFS-safe file locking with timeouts for POSIX systems.

This class implements an NFS-safe file-based locking algorithm influenced by the GNU/Linux open(2) manpage, under the description of the O_EXCL option:

[…] O_EXCL is broken on NFS file systems, programs which rely on it for performing locking tasks will contain a race condition. The solution for performing atomic file locking using a lockfile is to create a unique file on the same fs (e.g., incorporating hostname and pid), use link(2) to make a link to the lockfile. If link() returns 0, the lock is successful. Otherwise, use stat(2) on the unique file to check if its link count has increased to 2, in which case the lock is also successful.

The assumption made here is that there will be no outside interference, e.g. no agent external to this code will ever link() to the specific lock files used.

The user specifies a lock file in the constructor of this class. This is whatever file system path the user wants to coordinate locks on. When a process attempts to acquire a lock, it first writes a claim file which contains unique details about the lock being acquired (e.g. the lock file name, the hostname, the pid of the process, and a random integer). Then it attempts to create a hard link from the claim file to the lock file. If no other process has the lock, this hard link succeeds and the process accquires the lock. If another process already has the lock, the hard link will fail and the lock will not be acquired. What happens in this and other error conditions are described in the more detailed documentation.

Lock objects support lock-breaking so that you can’t wedge a process forever. This is especially helpful in a web environment, but may not be appropriate for all applications.

Locks have a lifetime, which is the maximum length of time the process expects to retain the lock. It is important to pick a good number here because other processes will not break an existing lock until the expected lifetime has expired. Too long and other processes will hang; too short and you’ll end up trampling on existing process locks – and possibly corrupting data. However locks also support extending a lock’s lifetime. In a distributed (NFS) environment, you also need to make sure that your clocks are properly synchronized.

Each process laying claim to this resource lock will create their own temporary lock file based on the path specified. An optional lifetime is the length of time that the process expects to hold the lock.

Parameters:
  • lockfile (str) – The full path to the lock file.

  • lifetime (timedelta | int | None) – The expected maximum lifetime of the lock, as a timedelta or integer number of seconds, relative to now. Defaults to 15 seconds.

  • separator (str) – The separator character to use in the lock file’s file name. Defaults to the vertical bar (|) on POSIX systems and caret (^) on Windows.

  • default_timeout (timedelta | int | None) – Default timeout for approximately how long the lock acquisition attempt should be made. The value given in the .lock() call always overrides this.

Create the resource lock using the given file name and lifetime.

property claimfile: str#

Return the claim file name.

property details: Tuple[str, int, str]#

Details as read from the lock file.

Returns:

A 3-tuple of hostname, process id, lock file name.

Raises:

NotLockedError – if the lock is not acquired.

property expiration: datetime#

The lock’s expiration time, regardless of ownership.

property hostname: str#

The current machine’s host name.

Returns:

The current machine’s hostname, as used in the .details property.

property is_locked: bool#

True if we own the lock, False if we do not.

Checking the status of the lock resets the lock’s lifetime, which helps avoid race conditions during the lock status test.

property lifetime: timedelta#

The current lock life time.

lock(timeout: timedelta | int | None = None) None#

Acquire the lock.

This blocks until the lock is acquired unless optional timeout is not None, in which case a TimeOutError is raised when the timeout expires without lock acquisition.

Parameters:

timeout (timedelta | int | None) – Approximately how long the lock acquisition attempt should be made. None (the default) means keep trying forever.

Raises:
  • AlreadyLockedError – if the lock is already acquired.

  • TimeOutError – if timeout is not None and the indicated time interval expires without a lock acquisition.

Return type:

None

property lockfile: str#

Return the lock file name.

refresh(lifetime: timedelta | int | None = None, *, unconditionally: bool = False) None#

Refresh the lifetime of a locked file.

Use this if you realize that you need to keep a resource locked longer than you thought.

Parameters:
  • lifetime (timedelta | int | None) – If given, this sets the lock’s new lifetime. It represents the number of seconds into the future that the lock’s lifetime will expire, relative to now, or whenever it is refreshed, either explicitly or implicitly. If not given, the original lifetime interval is used.

  • unconditionally (bool) – When False (the default), a NotLockedError is raised if an unlocked lock is refreshed.

Raises:

NotLockedError – if the lock is not set, unless optional unconditionally flag is set to True.

Return type:

None

property retry_errnos: List[int]#

The set of errno values that cause a read retry.

property state: LockState#

Infer the state of the lock.

unlock(*, unconditionally: bool = False) None#

Release the lock.

Parameters:

unconditionally (bool) – When False (the default), a NotLockedError is raised if this is called on an unlocked lock.

Raises:

NotLockedError – if we don’t own the lock, either because of unbalanced unlock calls, or because the lock was stolen out from under us, unless optional unconditionally is True.

Return type:

None

Note that successfully unlocking the lock also unlinks the claim file, even if it is already unlocked and unconditionally is True.

class flufl.lock.LockState(value)#

Infer the state of the lock.

There are cases in which it is impossible to infer the state of the lock, due to the distributed nature of the locking mechanism and environment. However it is possible to provide some insights into the state of the lock. Note that the policy on what to do with this information is left entirely to the user of the library.

unlocked = 1#

There is no lock file so the lock is unlocked.

ours = 2#

We own the lock and it is fresh.

ours_expired = 3#

We own the lock but it has expired. Another process trying to acquire the lock will break it.

stale = 4#

We don’t own the lock; the hostname in the details matches our hostname and there is no pid running that matches pid. Therefore, the lock is stale.

theirs_expired = 5#

Some other process owns the lock (probably) but it has expired. Another process trying to acquire the lock will break it.

unknown = 6#

We don’t own the lock; either our hostname does not match the details, or there is a process (that’s not us) with a matching pid. The state of the lock is therefore unknown.

Exceptions#

exception flufl.lock.LockError#

Base class for all exceptions in this module.

exception flufl.lock.AlreadyLockedError#

An attempt is made to lock an already locked object.

exception flufl.lock.NotLockedError#

An attempt is made to unlock an object that isn’t locked.

exception flufl.lock.TimeOutError#

The timeout interval elapsed before the lock succeeded.