flufl_lock-8.2.0/conftest.py 0000644 0000000 0000000 00000005621 13615410400 012764 0 ustar 00 import os
import time
import multiprocessing
from sybil import Sybil
from psutil import pid_exists
from doctest import ELLIPSIS, NORMALIZE_WHITESPACE, REPORT_NDIFF
from datetime import timedelta
from tempfile import TemporaryDirectory
from contextlib import ExitStack
from flufl.lock import SEP, Lock
from sybil.parsers.doctest import DocTestParser
from sybil.parsers.codeblock import PythonCodeBlockParser
DOCTEST_FLAGS = ELLIPSIS | NORMALIZE_WHITESPACE | REPORT_NDIFF
def child_locker(filename, lifetime, queue, extra_sleep):
# First, acquire the file lock.
with Lock(filename, lifetime):
# Now inform the parent that we've acquired the lock.
queue.put(True)
# Keep the file lock for a while.
time.sleep(lifetime.seconds + extra_sleep)
def acquire(filename, *, lifetime=None, extra_sleep=-1):
"""Acquire the named lock file in a subprocess."""
queue = multiprocessing.Queue()
proc = multiprocessing.Process(
target=child_locker,
args=(filename, timedelta(seconds=lifetime), queue, extra_sleep))
proc.start()
while not queue.get():
time.sleep(0.1)
def child_waitfor(filename, lifetime, queue):
t0 = time.time()
# Try to acquire the lock.
with Lock(filename, lifetime):
# Tell the parent how long it took to acquire the lock.
queue.put(time.time() - t0)
def waitfor(filename, lifetime):
"""Fire off a child that waits for a lock."""
queue = multiprocessing.Queue()
proc = multiprocessing.Process(
target=child_waitfor,
args=(filename, lifetime, queue))
proc.start()
return queue.get()
def simulate_process_crash(lockfile):
# We simulate an unclean crash of the process holding the lock by
# deliberately fiddling with the pid in the lock file, ensuring that no
# process with the new pid exists.
with open(lockfile) as fp:
filename = fp.read().strip()
lockfile, hostname, pid_str, random = filename.split(SEP)
pid = int(pid_str)
while pid_exists(pid):
pid += 1
with open(lockfile, 'w') as fp:
fp.write(SEP.join((lockfile, hostname, str(pid), random)))
class DoctestNamespace:
def __init__(self):
self._resources = ExitStack()
def setup(self, namespace):
namespace['acquire'] = acquire
namespace['temporary_lockfile'] = self.temporary_lockfile
namespace['waitfor'] = waitfor
namespace['simulate_process_crash'] = simulate_process_crash
def teardown(self, namespace):
self._resources.close()
def temporary_lockfile(self):
lock_dir = self._resources.enter_context(TemporaryDirectory())
return os.path.join(lock_dir, 'test.lck')
namespace = DoctestNamespace()
pytest_collect_file = Sybil(
parsers=[
DocTestParser(optionflags=DOCTEST_FLAGS),
PythonCodeBlockParser(),
],
pattern='*.rst',
setup=namespace.setup,
).pytest()
flufl_lock-8.2.0/docs/NEWS.rst 0000644 0000000 0000000 00000017405 13615410400 013026 0 ustar 00 =====================
flufl.lock change log
=====================
8.2 (2025-05-08)
================
* Add support for Python 3.13 and 3.14; remove support for Python 3.8.
* Modernize type hints.
8.1 (2024-03-30)
================
* Add support for Python 3.12. (GL#35)
* Switch to ``hatch``, replacing ``pdm`` and ``tox``. (GL#36)
* Switch to ``ruff`` from ``blue`` and ``isort``. (GL#37)
8.0.2 (2023-07-21)
==================
* Update dependencies.
* Other minor improvements and cleanups.
8.0.1 (2023-06-22)
==================
* Minor documentation fix.
8.0 (2023-06-21)
================
* Drop Python 3.7 support (GL#34)
* Added a ``claimfile`` property to ``Lock`` objects (GL#30)
* Switch to ``pdm-backend`` (GL#33)
* Use ``ruff`` for linting, since its much faster. (GL#17)
* Bump dependencies.
* More GitLab CI integration improvements.
* Make ``tox.ini`` consistent with flufl defaults.
7.1.1 (2022-09-03)
==================
* Improvements to the GitLab CI integration.
7.1 (2022-08-27)
================
* Add support for Python 3.11.
* Update to pdm 1.3.
* Update all dependencies eagerly.
7.0 (2022-01-11)
================
* Fix spurious log messages when *not* breaking the lock. (GL#29)
* Use modern package management by adopting `pdm
`_ and ``pyproject.toml``, and dropping ``setup.py``
and ``setup.cfg``.
* Build the docs with Python 3.8.
* Update to version 3.0 of `Sybil `_.
* Adopt the `Furo `_ documentation theme.
* Add a favicon and logos to the published documentation.
* Use `importlib.metadata.version()
`_
as a better way to get the package version number for the documentation.
* Drop Python 3.6 support.
* Update Windows GitLab runner to include Python 3.10.
* Update copyright years.
6.0 (2021-08-18)
================
* Added a ``default_timeout`` argument to the ``Lock`` constructor, which can
be used in the context manager syntax as well. (GL#24)
* When a ``Lock`` uses a lock file that already exists and does not appear to
be a lock file (i.e. because its contents are ill-formatted), do a better
job of not clobbering that file. (GL#25)
* Improve some QA by re-adding diff-cover, Gitlab SAST during CI, and testing
on Python 3.10 beta (except for Windows)
* The ``master`` branch is renamed to ``main``. (GL#28)
5.1 (2021-05-28)
================
* Added a ``py.typed`` file to satisfy type checkers. (GL#27)
5.0.5 (2021-02-12)
==================
* I `blue `_ it!
5.0.4 (2021-01-01)
==================
* Update copyright years.
* Include ``test/__init__.py`` and ``docs/__init__.py``.
5.0.3 (2020-10-22)
==================
* Rename top-level tests/ directory to test/ (GL#26)
5.0.2 (2020-10-21)
==================
* Minor housekeeping and cleanups.
* Add some missing licensing text.
* Don't install the ``tests`` and ``docs`` directories at the top of
``site-packages`` (GL#22)
* Fix the Windows CI tests.
* Add an index to the documentation.
5.0.1 (2020-08-21)
==================
* Reorganized docs and tests out of the code directory.
* Fix Read The Docs presentation.
5.0 (2020-08-20)
================
* **Breaking change** - The following methods have been removed:
``Lock.transfer_to()``, ``Lock.take_possession()``, ``Lock.disown()``.
These were crufty, undocumented APIs used in older versions of Mailman and
were not sustainable. (GL#21)
* Added official support for Python 3.9.
* Improvements to the documentation, including a better API reference and a
"theory of operation" page that gives more implementation technical
details. (GL#20) (GL#17)
* Boosted test coverage to 100%. (GL#18)
4.0 (2020-06-30)
================
API
---
* **Breaking change** - In ``Lock.refresh()`` and ``Lock.unlock()`` the
``unconditionally`` flag is now a keyword-only argument. (GL#13)
* **Breaking change** - Removed ``Lock.__del__()`` and ``Lock.finalize()``.
It's impossible to make ``__del__()`` work properly, and this is obsoleted
by context manager protocol support anyway. Since ``finalize()`` only
existed to help with ``__del__()`` and its functionality is identical to
``.unlock(unconditionally=True)``, this method is also removed. (GL#7)
* Added a ``Lock.expiration`` property. (GL#15)
* Added a ``Lock.lockfile`` property. (GL#16)
* Added a ``Lock.state`` property and the ``LockState`` enum. (GL#12)
* In all APIs, the ``lifetime`` parameter can now also be an integer number of
seconds, in addition to the previously allowed ``datetime.timedelta``. The
``lifetime`` property always gives you a ``datetime.timedelta``.
* The API is now properly type annotated.
* Some library-defined exceptions support exception chaining.
Behavior
--------
* Getting the ``repr()`` of a lock no longer refreshes it (GL#11)
Other
-----
* Add support for Python 3.7 and 3.8; drop support for Python 3.4 and 3.5.
* We now run the test suite on both Linux and Windows.
* The LICENSE file is now included in the sdist tarball.
* API documentation is now built automatically.
* Numerous quality improvements and modernizations.
3.2 (2017-09-03)
================
* Expose the host name used in the ``.details`` property, as a property.
(Closes #4).
3.1 (2017-07-15)
================
* Expose the ``SEP`` as a public attribute. (Closes #3)
3.0 (2017-05-31)
================
* Drop Python 2.7, add Python 3.6. (Closes #2)
* Added Windows support.
* Switch to the Apache License Version 2.0.
* Use flufl.testing for nose2 and flake8 plugins.
* Allow the claim file separator to be configurable, to support file systems
where the vertical bar is problematic. Defaults to ``^`` on Windows and
``|`` everywhere else (unchanged). (Closes #1)
2.4.1 (2015-10-29)
==================
* Fix the MANIFEST.in so that tox.ini is included in the sdist.
2.4 (2015-10-10)
================
* Drop Python 2.6 compatibility.
* Add Python 3.5 compatibility.
2.3.1 (2014-09-26)
==================
* Include MANIFEST.in in the sdist tarball, otherwise the Debian package
won't built correctly.
2.3 (2014-09-25)
================
* Fix documentation bug. (LP: #1026403)
* Catch ESTALE along with ENOENT, as NFS servers are supposed to (but don't
always) throw ESTALE instead of ENOENT. (LP: #977999)
* Purge all references to ``distribute``. (LP: #1263794)
2.2.1 (2012-04-19)
==================
* Add classifiers to setup.py and make the long description more compatible
with the Cheeseshop.
* Other changes to make the Cheeseshop page look nicer. (LP: #680136)
* setup_helper.py version 2.1.
2.2 (2012-01-19)
================
* Support Python 3 without the use of 2to3.
* Make the documentation clear that the ``flufl.test.subproc`` functions are
not part of the public API. (LP: #838338)
* Fix claim file format in ``take_possession()``. (LP: #872096)
* Provide a new API for dealing with possible additional unexpected errnos
while trying to read the lock file. These can happen in some NFS
environments. If you want to retry the read, set the lock file's
``retry_errnos`` property to a sequence of errnos. If one of those errnos
occurs, the read is unconditionally (and infinitely) retried.
``retry_errnos`` is a property which must be set to a sequence; it has a
getter and a deleter too. (LP: #882261)
2.1.1 (2011-08-20)
==================
* Fixed TypeError in .lock() method due to race condition in _releasetime
property. Found by Stephen A. Goss. (LP: #827052)
2.1 (2010-12-22)
================
* Added lock.details.
2.0.2 (2010-12-19)
==================
* Small adjustment to doctest.
2.0.1 (2010-11-27)
==================
* Add missing exception to __all__.
2.0 (2010-11-26)
================
* Package renamed to flufl.lock.
Earlier
=======
Try ``bzr log lp:flufl.lock`` for details.
flufl_lock-8.2.0/docs/__init__.py 0000644 0000000 0000000 00000000000 13615410400 013610 0 ustar 00 flufl_lock-8.2.0/docs/apiref.rst 0000644 0000000 0000000 00000000625 13615410400 013514 0 ustar 00 =============
API Reference
=============
API reference for ``flufl.lock``:
.. autoclass:: flufl.lock.Lock
:members:
.. autoclass:: flufl.lock.LockState
:members:
:member-order: bysource
Exceptions
==========
.. autoexception:: flufl.lock.LockError
.. autoexception:: flufl.lock.AlreadyLockedError
.. autoexception:: flufl.lock.NotLockedError
.. autoexception:: flufl.lock.TimeOutError
flufl_lock-8.2.0/docs/conf.py 0000644 0000000 0000000 00000015266 13615410400 013022 0 ustar 00 # -*- coding: utf-8 -*-
#
# flufl.lock documentation build configuration file, created by
# sphinx-quickstart on Thu Jan 7 18:41:30 2010.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
from datetime import date
import importlib.metadata
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.append(os.path.abspath('.'))
# -- General configuration -----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
]
intersphinx_mapping = {
'python': ('https://docs.python.org/3/', None),
}
autodoc_typehints = 'both'
autoclass_content = 'both'
# Add any paths that contain templates here, relative to this directory.
templates_path = ['../../_templates']
# The suffix of source filenames.
source_suffix = {'.rst': 'restructuredtext'}
# The encoding of source files.
#source_encoding = 'utf-8'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'flufl.lock'
author = 'Barry Warsaw'
copyright = f'2004-{date.today().year}, {author}'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = importlib.metadata.version(project)
# The full version, including alpha/beta/rc tags.
release = version
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of documents that shouldn't be included in the build.
#unused_docs = []
# List of directories, relative to source directory, that shouldn't be searched
# for source files.
exclude_trees = ['_build', 'build', 'flufl.lock.egg-info', 'distribute-0.6.10']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
html_theme = 'furo'
html_favicon = '_static/lock-light.svg'
html_theme_options = {
'light_logo': 'logo-light.png',
'dark_logo': 'logo-dark.png',
}
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# " v documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_use_modindex = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = ''
# Output file base name for HTML help builder.
htmlhelp_basename = 'flufllockdoc'
# -- Options for LaTeX output --------------------------------------------------
# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt').
#latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('README.rst', 'flufllock.tex', 'flufl.lock Documentation',
author, 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# Additional stuff for the LaTeX preamble.
#latex_preamble = ''
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_use_modindex = True
flufl_lock-8.2.0/docs/index.rst 0000644 0000000 0000000 00000003452 13615410400 013356 0 ustar 00 ==================================
flufl.lock - An NFS-safe file lock
==================================
This package is called ``flufl.lock``. It is an NFS-safe file-based lock with
timeouts for POSIX systems.
Requirements
============
``flufl.lock`` requires Python 3.9 or newer.
Documentation
=============
A `simple guide`_ to using the library is available, along with a detailed
`API reference`_.
Project details
===============
* Project home: https://gitlab.com/warsaw/flufl.lock
* Report bugs at: https://gitlab.com/warsaw/flufl.lock/issues
* Code hosting: https://gitlab.com/warsaw/flufl.lock.git
* Documentation: https://flufllock.readthedocs.io/
You can install it with ``pip``::
% pip install flufl.lock
You can grab the latest development copy of the code using git. The master
repository is hosted on GitLab. If you have git installed, you can grab
your own branch of the code like this::
$ git clone https://gitlab.com/warsaw/flufl.lock.git
You can contact the author via barry@python.org.
Copyright
=========
Copyright (C) 2004-2025 Barry A. Warsaw
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Table of Contents and Index
===========================
* :ref:`genindex`
.. toctree::
:glob:
using
theory
apiref
NEWS
.. _`simple guide`: using.html
.. _`API reference`: apiref.html
flufl_lock-8.2.0/docs/theory.rst 0000644 0000000 0000000 00000022206 13615410400 013557 0 ustar 00 ===================
Technical details
===================
.. currentmodule:: flufl.lock
The :doc:`flufl.lock ` package provides NFS-safe file locking. We say
"NFS-safe" because the behavior of the locks are dictated by POSIX standards,
and are guaranteed to work even when the processes involved are running on
different machines which only share a common file system over `NFS`_.
The basic implementation of our :class:`Lock` objects is described 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 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.
Lock files and claim files
==========================
When a :class:`Lock` object is instantiated, the user names a file system
path in the constructor. This is the file that all processes will synchronize
on when attempting to acquire the lock. We call this the *lock file*. This
file should not already exist.
Locks have a *lifetime* which is the period of time that the process expects
to keep the lock, once it has been acquired. This lifetime is used to
calculate whether a lock is stale, i.e. the process that acquired the lock
exited without cleanly unlocking it. Stale locks can be broken by another
process.
It's good to use a reasonable lifetime, not too long and not too short. If
the lifetime is too long, then stale locks may not be broken for quite some
time. Too short and another process will break a lock that isn't stale. Keep
in mind though that lock lifetimes can be explicitly extended, and in some
cases will be implicitly extended. So if you're using the lock to protect a
resource for a long time, try to periodically refresh the lock after you've
acquired it, and set the lifetime to just longer than your expected refresh
period.
Internally, every :class:`Lock` object also has a *claim file*. This is a
file system path unique to the object which will be hard linked to the lock
file. The claim file isn't actually created until the process attempts to
acquire the lock. The name of the claim file is made up of several bits of
data intended to make this claim file name unique across all :class:`Lock`
object. The claim file name consists of:
* The name of the lock file
* The current hostname as returned by :func:`socket.getfqdn()`
* The current process id
* A random integer from 0 to :data:`sys.maxsize`
Once defined, the claim file name never changes. The claim file only ever
actually exists while the process is acquiring or has acquired the lock,
although it may hang around in an unlinked state if the lock has been broken.
Acquiring the lock
==================
These are the steps for acquiring a lock:
* The user defines a *timeout* period, which is the length of time in seconds
that the lock acquisition will be attempted. If the lock cannot be acquired
within this time period, a :class:`TimeOutError` is raised.
* The claim file is created. The name of the claim file is written to the
contents of the claim file. The reason for this will be evident later.
* The claim file is *touched*. Touching the file means that the file's
``atime`` and ``mtime`` are modified to point to a timestamp some time in the
future. This future time is the current time plus the lock's lifetime, and
it indicates the the time in the future after which the lock will be
considered stale.
* A `hard link`_ is created with the claim file as the source and the lock file
as the destination. This makes sense: the claim file has already been
created (see above), and if no other process has acquired the lock, then
this step creates the lock file. On success, the lock file and claim file
will point to the same physical file and the lock will be considered
acquired.
* If the hard link is successful, the lock's lifetime is immediately
refreshed to ensure it represents the lifetime plus the timestamp when the
lock was acquired. This is because if a process needed to wait some amount
of time for another process to release the lock, you want the current lock's
lifetime to be relative to the actual lock acquisition time.
There are several reasons why the hard link attempt could fail. If any of
these conditions occur, the :meth:`Lock.lock()` method will sleep for a short
amount of time and try again, assuming the timeout period hasn't expired, and
that the lock isn't considered stale.
* The *link count* of the lock file is not 2. The only way this can happen is
if some outside agent is messing with us. That weird state is logged, but
the lock acquisition attempt continues after a sleep.
* Some weird, but innocuous exception can occur, such as an ``ENOENT`` or an
``ESTALE``. NFS servers can return these errnos depending on the platform.
As above, when this happens, the lock acquisition just sleeps for a short
time and tries again.
* Any other :class:`OSError` will get logged, the claim file will be unlinked
and the exception will get re-raised immediately.
* If the contents of lock file is equal to the claim file name, then this
indicates that the current process *already* has the lock acquired, and a
:class:`AlreadyLockedError` is raised. This is why we write the claim file
name into the file.
Breaking the lock
=================
Let's say the linking operation fails because :meth:`Lock.lock()` determines
that another process has acquired the lock. The next thing that happens is a
check for lock staleness. Remember that a lock is defined to be stale if the
timestamp recorded in its ``atime`` and ``mtime`` have been exceeded. In this
case, the lock will be broken. This is also known as *stealing the lock*.
When breaking a lock, the lock file is first removed. This is technically the
act that breaks the lock. However, before removing the lock file, its
contents are read to find the other process's claim file path. This claim
file, if it exists, is also removed. This should leave the file system in a
fairly clean state, even if process that used to own the lock has exited
uncleanly.
There are a few caveats to keep in mind.
First, it's not technically possible to determine whether another active,
running process actually owns the lock. This is especially true if the locks
are acquired on different machines coordinating through a shared (e.g. NFS)
file system. However you can gain some insight into the state of a lock by
using the :data:`Lock.state` attribute and checking the :class:`LockState`
enum that's returned. This state is *not* consulted when deciding whether to
break a lock though; in that case only the lock's lifetime is considered.
There is a small race condition window present when breaking the lock. It's
possible that two processes are blocked on acquiring a stale lock that was
previously acquired by a now-defunct third process. To reduce this (it cannot
be totally eliminated), a process breaking the lock starts by *touching* the
lock, thus simulating an extension of the lock's lifetime. If the second
process looks at the expiration time, it may in fact see that the lock has
*not* yet expired, closing the race window. Ultimately, even if this fails,
the lock is still guaranteed to be acquired by only one process, because the
link creation operation is guaranteed to be atomic by POSIX.
Lifetime extension
==================
As mentioned, processes can explicitly extend the lifetime of a lock it owns,
which can be used to ensure continuous ownership of resources in long running
tasks. There are some cases where the :class:`Lock` implementation implicitly
extends the lifetime of a lock:
* When first attempting to acquire a lock
* After a lock is acquired
* When asking whether a lock is acquired through the :data:`Lock.is_locked`
property. This prevents a lock breaking race condition between the time the
property is accessed and the answer is returned.
* When a lock is about to be broken
Generally, you don't need to worry about implicit lifetime extension since the
only user-facing case is when accessing :data:`Lock.is_locked`.
Locks always have a default lifetime, but this can be set in the :class:`Lock`
constructor. It can also be optionally changed in the :meth:`Lock.refresh()`
method. In any case, the lifetime always represents the number of seconds
into the future at which the lock will expire, relative to "now". "Now" is
always calculated when the lock is refreshed, either explicitly or implicitly.
.. _`open(2)`: http://manpages.ubuntu.com/manpages/dapper/en/man2/open.2.html
.. _`link()`: https://manpages.ubuntu.com/manpages/focal/en/man2/link.2.html
.. _`NFS`: https://en.wikipedia.org/wiki/Network_File_System
.. _`hard link`: https://en.wikipedia.org/wiki/Hard_link
flufl_lock-8.2.0/docs/using.rst 0000644 0000000 0000000 00000022326 13615410400 013375 0 ustar 00 ============================
Using the flufl.lock library
============================
.. currentmodule:: flufl.lock
The :doc:`flufl.lock ` package provides safe file locking with
timeouts for POSIX and Windows systems. You can read more about how the
library works in the :doc:`technical details `.
Lock objects support lock-breaking so that you can't wedge a process forever.
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, lock lifetimes can be explicitly extended, and are implicitly
extended in some cases.
In a distributed (NFS) environment, you also need to make sure that your
clocks are properly synchronized.
Creating a lock
===============
To create a lock, you must first instantiate a :class:`Lock` object,
specifying the path to a file that will be used to synchronize the lock. This
file should not already exist.
::
# This function comes from the test infrastructure.
>>> filename = temporary_lockfile()
>>> from flufl.lock import Lock
>>> lock = Lock(filename)
>>> lock
Locks have a default lifetime...
>>> lock.lifetime.seconds
15
...which you can change.
>>> from datetime import timedelta
>>> lock.lifetime = timedelta(seconds=30)
>>> lock.lifetime.seconds
30
>>> lock.lifetime = timedelta(seconds=15)
You can ask whether the lock is acquired or not.
>>> lock.is_locked
False
Acquiring the lock is easy if no other process has already acquired it.
>>> lock.lock()
>>> lock.is_locked
True
Once you have the lock, it's easy to release it.
>>> lock.unlock()
>>> lock.is_locked
False
It is an error to attempt to acquire the lock more than once in the same
process.
::
>>> from flufl.lock import AlreadyLockedError
>>> lock.lock()
>>> try:
... lock.lock()
... except AlreadyLockedError as error:
... print(error)
We already had the lock
>>> lock.unlock()
Lock objects also support the context manager protocol.
>>> lock.is_locked
False
>>> with lock:
... lock.is_locked
True
>>> lock.is_locked
False
Lock acquisition can block
==========================
When trying to lock the file when the lock is unavailable (because another
process has already acquired it), the lock call will block.
::
>>> import time
>>> t0 = time.time()
# This function comes from the test infrastructure.
>>> acquire(filename, lifetime=5)
>>> lock.lock()
>>> t1 = time.time()
>>> lock.unlock()
>>> t1 - t0 > 4
True
Refreshing a lock
=================
A process can *refresh* a lock if it realizes that it needs to hold the lock
for a little longer. You cannot refresh an unlocked lock.
>>> from flufl.lock import NotLockedError
>>> try:
... lock.refresh()
... except NotLockedError as error:
... print(error)
>> from datetime import datetime
>>> lock.lifetime = 2 # seconds
>>> lock.lock()
>>> lock.is_locked
True
After the current lifetime expires, the lock is stolen from the parent process
even if the parent never unlocks it.
::
# This function comes from the test infrastructure.
>>> t_broken = waitfor(filename, lock.lifetime)
>>> lock.is_locked
False
However, if the process holding the lock refreshes it, it will hold it can
hold it for as long as it needs.
>>> lock.lock()
>>> lock.refresh(5) # seconds
>>> t_broken = waitfor(filename, lock.lifetime)
>>> lock.is_locked
False
Time outs
=========
When attempting to acquire a lock, you can specify a timeout interval as
either an integer number of seconds, or as a :class:`datetime.timedelta`.
If the lock is not acquired within this interval, a :class:`TimeOutError` is
raised.
You can specify a default timeout interval in the :class:`Lock` constructor.
>>> from flufl.lock import TimeOutError
>>> acquire(filename, lifetime=5)
>>> try:
... with Lock(filename, default_timeout=1) as my_lock:
... pass
... except TimeOutError:
... print('Timed out, as expected')
Timed out, as expected
You can also specify a timeout interval in the :func:`Lock.lock` call. This
overrides the constructor argument.
>>> acquire(filename, lifetime=5)
>>> my_lock = Lock(filename, default_timeout=1)
>>> try:
... my_lock.lock(timeout=10)
... my_lock.is_locked
... finally:
... my_lock.unlock()
True
Lock details
============
Lock files are written with unique contents that can be queried for
information about the host name the lock was acquired on, the id of the
process that acquired the lock, and the path to the lock file.
>>> import os
>>> lock.lock()
>>> hostname, pid, lockfile = lock.details
>>> hostname == lock.hostname
True
>>> pid == os.getpid()
True
>>> lockfile == filename
True
>>> lock.unlock()
Even if another process has acquired the lock, the details can be queried.
>>> acquire(filename, lifetime=3)
>>> lock.is_locked
False
>>> hostname, pid, lockfile = lock.details
>>> hostname == lock.hostname
True
>>> pid == os.getpid()
False
>>> lockfile == filename
True
However, if no process has acquired the lock, the details are unavailable.
>>> lock.lock()
>>> lock.unlock()
>>> try:
... lock.details
... except NotLockedError as error:
... print(error)
Details are unavailable
You can also get the time at which the lock will expire.
>>> now = datetime.now()
>>> import time
>>> time.sleep(1)
>>> with lock:
... lock.refresh()
... lock.expiration > now + lock.lifetime
True
Lock state
==========
You might want to try to infer the state of the lock. This is not always
possible, but this library does try to provide some insights into the lock's
state. However, it is up to the user of the library to enforce policy based
on the lock state.
The lock state is embodied in an enumeration.
>>> from flufl.lock import LockState
The lock can be in the unlocked state.
>>> lock.state
We could own the lock, as long as it is still fresh (i.e. it hasn't expired
its lifetime yet), the state will tell us.
>>> with lock:
... lock.state
It's possible that we own the lock, but that its lifetime has expired. In
this case, another process trying to acquire the lock will break the original
lock.
>>> lock.lifetime = 1
>>> with lock:
... time.sleep(1.5)
... lock.state
It's also possible that another process once owned the lock but it exited
uncleanly. If the lock file still exists, but there is no process running
that matches the recorded pid, then the lock's state is stale.
>>> acquire(lock.lockfile, lifetime=10)
>>> simulate_process_crash(lock.lockfile)
>>> lock.state
If some other process owns the lock, we can't really infer much about it.
while we can see that there is a running process matching the pid in the lock
file, we don't know whether that process is really the one claiming the lock,
or what its intent with the lock is.
::
# This function comes from the test infrastructure.
>>> acquire(lock.lockfile, lifetime=2, extra_sleep=3)
>>> lock.state
However, once the lock has expired, we can at least report that.
>>> time.sleep(2)
>>> lock.state
Lock file separator
===================
Lock claim file names contain useful bits of information concatenated by a
*separator character*. This character is the caret (``^``) by default on
Windows and the vertical bar (``|``) by default everywhere else. You can
change this character. There are some restrictions:
* It cannot be an alphanumeric;
* It cannot appear in the host machine's fully qualified domain name
(e.g. the value of :data:`Lock.hostname`);
* It cannot appear in the lock's file name (the argument passed to the
:class:`Lock` constructor)
It may also be helpful to avoid `any reserved characters
`_ on
the file systems where you intend to run the code.
You can also get both the lock file and claim file names from the lock object.
>>> lock = Lock(filename, separator='+')
>>> lock.lock()
>>> hostname, pid, lockfile = lock.details
>>> hostname == lock.hostname
True
>>> pid == os.getpid()
True
>>> lockfile == filename
True
>>> lock.lockfile == lockfile
True
The claim file name is also stored in the contents of any acquired lock file.
>>> with open(filename) as fp:
... claimfile = fp.read().strip()
>>> lock.claimfile == claimfile
True
>>> '+' in claimfile
True
>>> lock.unlock()
flufl_lock-8.2.0/docs/_static/lock-dark.svg 0000644 0000000 0000000 00000000421 13615410400 015524 0 ustar 00