Skip to content

Commit

Permalink
Refactored locking implementation.
Browse files Browse the repository at this point in the history
We have refactored the locking implementation in the worker
as a self-contained package "packages/openlock". The package is
also available on github

https://github.com/vdbergh/openlock

and on PyPi

https://pypi.org/search/?q=openlock

This PR cleans up the worker code considerably.
  • Loading branch information
vdbergh authored and ppigazzini committed Jul 5, 2024
1 parent 657ba4f commit ca9b8b3
Show file tree
Hide file tree
Showing 10 changed files with 633 additions and 127 deletions.
3 changes: 3 additions & 0 deletions worker/packages/openlock/AUTHORS
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Pasquale Pigazzini (ppigazzini)
Michel Van den Bergh (vdbergh)
Joost VandeVondele (vondele)
21 changes: 21 additions & 0 deletions worker/packages/openlock/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 The openlock authors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
44 changes: 44 additions & 0 deletions worker/packages/openlock/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# openlock

A locking library not depending on inter-process locking primitives in the OS.

## API

- `FileLock(lock_file="openlock.lock", timeout=None)`. Constructor. The optional `timeout` argument is the default for the corresponding argument of `acquire()` (see below). A `FileLock` object supports the context manager protocol.
- `FileLock.acquire(timeout=None)`. Attempts to acquire the lock. The optional `timeout` argument specifies the maximum waiting time in seconds before a `Timeout` exception is raised.
- `FileLock.release()`. Releases the lock. May raise an `InvalidRelease` exception.
- `FileLock.locked()`. Indicates if the lock is held by a process.
- `FileLock.getpid()`. The PID of the process that holds the lock, if any. Otherwise returns `None`.
- `FileLock.lock_file`. The name of the lock file.
- `FileLock.timeout`. The value of the timeout parameter.
- `openlock.set_defaults(**kw)`. Sets default values for the internal parameters. Currently `tries`, `retry_period`, `race_delay` with values of `2`, `0.3s` and `0.2s` respectively.
- `openlock.get_defaults()`. Returns a dictionary with the default values for the internal parameters.

## How does it work

A valid lock file has two lines of text containing respectively:

- `pid`: the PID of the process holding the lock;
- `name`: the content of `argv[0]` of the process holding the lock.

A lock file is considered stale if the pair `(pid, name)` does not belong to a Python process in the process table.

A process that seeks to acquire a lock first atomically tries to create a new lock file. If this succeeds then it has acquired the lock. If it fails then this means that a lock file exists. If it is valid, i.e. not stale and syntactically valid, then this implies that the lock has already been acquired and the process will periodically retry to acquire it - subject to the `timeout` parameter. If the lock file is invalid, then the process atomically overwrites it with its own data. It sleeps `race_delay` seconds and then checks if the lock file has again been overwritten (necessarily by a different process). If not then it has acquired the lock.

Once the lock is acquired the process installs an exit handler to remove the lock file on exit.

To release the lock, the process deletes the lock file and uninstall the exit handler.

In follows from this description that the algorithm is latency free in the common use case where there are no invalid lock files.

## Issues

There are no known issues in the common use case where there are no invalid lock files. In general the following is true:

- The algorithm for dealing with invalid lock files fails if a process needs more time than indicated by the `race_delay` parameter to create a new lock file after detecting the absence of a valid one. The library will issue a warning if it thinks the system is too slow for the algorithm to work correctly and it will recommend to increase the value of the `race_delay` parameter.

- Since PIDs are only unique over the lifetime of a process, it may be, although it is very unlikely, that the data `(pid, name)` matches a Python process different from the one that created the lock file. In that case the algorithm fails to recognize the lock file as stale.

## History

This is a refactored version of the locking algorithm used by the worker for the Fishtest web application <https://tests.stockfishchess.org/tests>.
12 changes: 12 additions & 0 deletions worker/packages/openlock/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from .openlock import ( # noqa: F401
FileLock,
InvalidLockFile,
InvalidOption,
InvalidRelease,
OpenLockException,
Timeout,
__version__,
get_defaults,
logger,
set_defaults,
)
29 changes: 29 additions & 0 deletions worker/packages/openlock/_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import sys
import time

from openlock import FileLock, Timeout


def other_process1(lock_file):
r = FileLock(lock_file)
try:
r.acquire(timeout=0)
except Timeout:
return 1
return 0


def other_process2(lock_file):
r = FileLock(lock_file)
r.acquire(timeout=0)
time.sleep(2)
return 2


if __name__ == "__main__":
lock_file = sys.argv[1]
cmd = sys.argv[2]
if cmd == "1":
print(other_process1(lock_file))
else:
print(other_process2(lock_file))
Loading

0 comments on commit ca9b8b3

Please sign in to comment.