Merge branch 'feature/UpdateFilelock' into dev

This commit is contained in:
JackDandy 2023-04-27 11:31:57 +01:00
commit fce30c97ef
8 changed files with 67 additions and 48 deletions

View file

@ -1,5 +1,6 @@
### 3.29.0 (2023-xx-xx xx:xx:00 UTC)
* Update filelock 3.9.0 (ce3e891) to 3.11.0 (d3241b9)
* Update Msgpack 1.0.4 (b5acfd5) to 1.0.5 (0516c2c)
* Update SimpleJSON 3.18.1 (c891b95) to 3.19.1 (aeb63ee)
* Update Tornado Web Server 6.3.0 (7186b86) to 6.3.1 (419838b)

View file

@ -30,7 +30,7 @@ else: # pragma: win32 no cover
else:
_FileLock = SoftFileLock
if warnings is not None:
warnings.warn("only soft file lock is available")
warnings.warn("only soft file lock is available", stacklevel=2)
#: Alias for the lock, which should be used for the current platform. On Windows, this is an alias for
# :class:`WindowsFileLock`, on Unix for :class:`UnixFileLock` and otherwise for :class:`SoftFileLock`.

View file

@ -6,7 +6,7 @@ import os
import time
import warnings
from abc import ABC, abstractmethod
from threading import Lock
from threading import local
from types import TracebackType
from typing import Any
@ -36,10 +36,15 @@ class AcquireReturnProxy:
self.lock.release()
class BaseFileLock(ABC, contextlib.ContextDecorator):
class BaseFileLock(ABC, contextlib.ContextDecorator, local):
"""Abstract base class for a file lock object."""
def __init__(self, lock_file: str | os.PathLike[Any], timeout: float = -1) -> None:
def __init__(
self,
lock_file: str | os.PathLike[Any],
timeout: float = -1,
mode: int = 0o644,
) -> None:
"""
Create a new lock object.
@ -47,6 +52,7 @@ class BaseFileLock(ABC, contextlib.ContextDecorator):
:param timeout: default timeout when acquiring the lock, in seconds. It will be used as fallback value in
the acquire method, if no timeout value (``None``) is given. If you want to disable the timeout, set it
to a negative value. A timeout of 0 means, that there is exactly one attempt to acquire the file lock.
: param mode: file permissions for the lockfile.
"""
# The path to the lock file.
self._lock_file: str = os.fspath(lock_file)
@ -58,8 +64,8 @@ class BaseFileLock(ABC, contextlib.ContextDecorator):
# The default timeout value.
self._timeout: float = timeout
# We use this lock primarily for the lock counter.
self._thread_lock: Lock = Lock()
# The mode for the lock files
self._mode: int = mode
# The lock counter is used for implementing the nested locking mechanism. Whenever the lock is acquired, the
# counter is increased and the lock is only released, when this value is 0 again.
@ -159,26 +165,23 @@ class BaseFileLock(ABC, contextlib.ContextDecorator):
poll_interval = poll_intervall
# Increment the number right at the beginning. We can still undo it, if something fails.
with self._thread_lock:
self._lock_counter += 1
self._lock_counter += 1
lock_id = id(self)
lock_filename = self._lock_file
start_time = time.monotonic()
start_time = time.perf_counter()
try:
while True:
with self._thread_lock:
if not self.is_locked:
_LOGGER.debug("Attempting to acquire lock %s on %s", lock_id, lock_filename)
self._acquire()
if not self.is_locked:
_LOGGER.debug("Attempting to acquire lock %s on %s", lock_id, lock_filename)
self._acquire()
if self.is_locked:
_LOGGER.debug("Lock %s acquired on %s", lock_id, lock_filename)
break
elif blocking is False:
_LOGGER.debug("Failed to immediately acquire lock %s on %s", lock_id, lock_filename)
raise Timeout(self._lock_file)
elif 0 <= timeout < time.monotonic() - start_time:
elif 0 <= timeout < time.perf_counter() - start_time:
_LOGGER.debug("Timeout on acquiring lock %s on %s", lock_id, lock_filename)
raise Timeout(self._lock_file)
else:
@ -186,8 +189,7 @@ class BaseFileLock(ABC, contextlib.ContextDecorator):
_LOGGER.debug(msg, lock_id, lock_filename, poll_interval)
time.sleep(poll_interval)
except BaseException: # Something did go wrong, so decrement the counter.
with self._thread_lock:
self._lock_counter = max(0, self._lock_counter - 1)
self._lock_counter = max(0, self._lock_counter - 1)
raise
return AcquireReturnProxy(lock=self)
@ -198,18 +200,16 @@ class BaseFileLock(ABC, contextlib.ContextDecorator):
:param force: If true, the lock counter is ignored and the lock is released in every case/
"""
with self._thread_lock:
if self.is_locked:
self._lock_counter -= 1
if self.is_locked:
self._lock_counter -= 1
if self._lock_counter == 0 or force:
lock_id, lock_filename = id(self), self._lock_file
if self._lock_counter == 0 or force:
lock_id, lock_filename = id(self), self._lock_file
_LOGGER.debug("Attempting to release lock %s on %s", lock_id, lock_filename)
self._release()
self._lock_counter = 0
_LOGGER.debug("Lock %s released on %s", lock_id, lock_filename)
_LOGGER.debug("Attempting to release lock %s on %s", lock_id, lock_filename)
self._release()
self._lock_counter = 0
_LOGGER.debug("Lock %s released on %s", lock_id, lock_filename)
def __enter__(self) -> BaseFileLock:
"""

View file

@ -1,15 +1,28 @@
from __future__ import annotations
from typing import Any
class Timeout(TimeoutError):
"""Raised when the lock could not be acquired in *timeout* seconds."""
def __init__(self, lock_file: str) -> None:
#: The path of the file lock.
self.lock_file = lock_file
super().__init__()
self._lock_file = lock_file
def __reduce__(self) -> str | tuple[Any, ...]:
return self.__class__, (self._lock_file,) # Properly pickle the exception
def __str__(self) -> str:
return f"The file lock '{self.lock_file}' could not be acquired."
return f"The file lock '{self._lock_file}' could not be acquired."
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.lock_file!r})"
@property
def lock_file(self) -> str:
""":return: The path of the file lock."""
return self._lock_file
__all__ = [

View file

@ -2,7 +2,7 @@ from __future__ import annotations
import os
import sys
from errno import EACCES, EEXIST, ENOENT
from errno import EACCES, EEXIST
from ._api import BaseFileLock
from ._util import raise_on_exist_ro_file
@ -14,24 +14,22 @@ class SoftFileLock(BaseFileLock):
def _acquire(self) -> None:
raise_on_exist_ro_file(self._lock_file)
# first check for exists and read-only mode as the open will mask this case as EEXIST
mode = (
flags = (
os.O_WRONLY # open for writing only
| os.O_CREAT
| os.O_EXCL # together with above raise EEXIST if the file specified by filename exists
| os.O_TRUNC # truncate the file to zero byte
)
try:
fd = os.open(self._lock_file, mode)
except OSError as exception:
if exception.errno == EEXIST: # expected if cannot lock
pass
elif exception.errno == ENOENT: # No such file or directory - parent directory is missing
file_handler = os.open(self._lock_file, flags, self._mode)
except OSError as exception: # re-raise unless expected exception
if not (
exception.errno == EEXIST # lock already exist
or (exception.errno == EACCES and sys.platform == "win32") # has no access to this lock
): # pragma: win32 no cover
raise
elif exception.errno == EACCES and sys.platform != "win32": # pragma: win32 no cover
# Permission denied - parent dir is R/O
raise # note windows does not allow you to make a folder r/o only files
else:
self._lock_file_fd = fd
self._lock_file_fd = file_handler
def _release(self) -> None:
os.close(self._lock_file_fd) # type: ignore # the lock file is definitely not None

View file

@ -2,6 +2,7 @@ from __future__ import annotations
import os
import sys
from errno import ENOSYS
from typing import cast
from ._api import BaseFileLock
@ -31,12 +32,18 @@ else: # pragma: win32 no cover
"""Uses the :func:`fcntl.flock` to hard lock the lock file on unix systems."""
def _acquire(self) -> None:
open_mode = os.O_RDWR | os.O_CREAT | os.O_TRUNC
fd = os.open(self._lock_file, open_mode)
open_flags = os.O_RDWR | os.O_CREAT | os.O_TRUNC
fd = os.open(self._lock_file, open_flags, self._mode)
try:
os.fchmod(fd, self._mode)
except PermissionError:
pass # This locked is not owned by this UID
try:
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError:
except OSError as exception:
os.close(fd)
if exception.errno == ENOSYS: # NotImplemented error
raise NotImplementedError("FileSystem does not appear to support flock; user SoftFileLock instead")
else:
self._lock_file_fd = fd

View file

@ -16,13 +16,13 @@ if sys.platform == "win32": # pragma: win32 cover
def _acquire(self) -> None:
raise_on_exist_ro_file(self._lock_file)
mode = (
flags = (
os.O_RDWR # open for read and write
| os.O_CREAT # create file if not exists
| os.O_TRUNC # truncate file if not empty
)
try:
fd = os.open(self._lock_file, mode)
fd = os.open(self._lock_file, flags, self._mode)
except OSError as exception:
if exception.errno == ENOENT: # No such file or directory
raise

View file

@ -1,4 +1,4 @@
# file generated by setuptools_scm
# don't change, don't track in version control
__version__ = version = '3.9.0'
__version_tuple__ = version_tuple = (3, 9, 0)
__version__ = version = '3.11.0'
__version_tuple__ = version_tuple = (3, 11, 0)