]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
cephadm: move FileLock and friends to locking.py
authorJohn Mulligan <jmulligan@redhat.com>
Thu, 17 Aug 2023 18:39:56 +0000 (14:39 -0400)
committerJohn Mulligan <jmulligan@redhat.com>
Wed, 30 Aug 2023 18:01:08 +0000 (14:01 -0400)
Signed-off-by: John Mulligan <jmulligan@redhat.com>
Pair-programmed-with: Adam King <adking@redhat.com>
Co-authored-by: Adam King <adking@redhat.com>
src/cephadm/cephadm.py
src/cephadm/cephadmlib/locking.py [new file with mode: 0644]

index 3a66041ac245ab59c7519760e76398d42aa0e25a..c58ae76502b63e5347e3c2e88953773446a9e9f0 100755 (executable)
@@ -2,7 +2,6 @@
 
 import argparse
 import datetime
-import fcntl
 import ipaddress
 import io
 import json
@@ -76,7 +75,6 @@ from cephadmlib.constants import (
     DEFAULT_RETRY,
     DEFAULT_TIMEOUT,
     LATEST_STABLE_RELEASE,
-    LOCK_DIR,
     LOGROTATE_DIR,
     LOG_DIR,
     LOG_DIR_MODE,
@@ -138,6 +136,7 @@ from cephadmlib.net_utils import (
     unwrap_ipv6,
     wrap_ipv6,
 )
+from cephadmlib.locking import FileLock
 
 FuncT = TypeVar('FuncT', bound=Callable)
 
@@ -1383,196 +1382,6 @@ def get_supported_daemons():
 ##################################
 
 
-# this is an abbreviated version of
-# https://github.com/benediktschmitt/py-filelock/blob/master/filelock.py
-# that drops all of the compatibility (this is Unix/Linux only).
-
-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
-        return None
-
-    def __str__(self) -> str:
-        temp = "The file lock '{}' could not be acquired."\
-               .format(self.lock_file)
-        return temp
-
-
-class _Acquire_ReturnProxy(object):
-    def __init__(self, lock: 'FileLock') -> None:
-        self.lock = lock
-        return None
-
-    def __enter__(self) -> 'FileLock':
-        return self.lock
-
-    def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
-        self.lock.release()
-        return None
-
-
-class FileLock(object):
-    def __init__(self, ctx: CephadmContext, name: str, timeout: int = -1) -> None:
-        if not os.path.exists(LOCK_DIR):
-            os.mkdir(LOCK_DIR, 0o700)
-        self._lock_file = os.path.join(LOCK_DIR, name + '.lock')
-        self.ctx = ctx
-
-        # The file descriptor for the *_lock_file* as it is returned by the
-        # os.open() function.
-        # This file lock is only NOT None, if the object currently holds the
-        # lock.
-        self._lock_file_fd: Optional[int] = None
-        self.timeout = timeout
-        # 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.
-        self._lock_counter = 0
-        return None
-
-    @property
-    def is_locked(self) -> bool:
-        return self._lock_file_fd is not None
-
-    def acquire(self, timeout: Optional[int] = None, poll_intervall: float = 0.05) -> _Acquire_ReturnProxy:
-        """
-        Acquires the file lock or fails with a :exc:`Timeout` error.
-        .. code-block:: python
-            # You can use this method in the context manager (recommended)
-            with lock.acquire():
-                pass
-            # Or use an equivalent try-finally construct:
-            lock.acquire()
-            try:
-                pass
-            finally:
-                lock.release()
-        :arg float timeout:
-            The maximum time waited for the file lock.
-            If ``timeout < 0``, there is no timeout and this method will
-            block until the lock could be acquired.
-            If ``timeout`` is None, the default :attr:`~timeout` is used.
-        :arg float poll_intervall:
-            We check once in *poll_intervall* seconds if we can acquire the
-            file lock.
-        :raises Timeout:
-            if the lock could not be acquired in *timeout* seconds.
-        .. versionchanged:: 2.0.0
-            This method returns now a *proxy* object instead of *self*,
-            so that it can be used in a with statement without side effects.
-        """
-
-        # Use the default timeout, if no timeout is provided.
-        if timeout is None:
-            timeout = self.timeout
-
-        # Increment the number right at the beginning.
-        # We can still undo it, if something fails.
-        self._lock_counter += 1
-
-        lock_id = id(self)
-        lock_filename = self._lock_file
-        start_time = time.time()
-        try:
-            while True:
-                if not self.is_locked:
-                    logger.log(QUIET_LOG_LEVEL, 'Acquiring lock %s on %s', lock_id,
-                               lock_filename)
-                    self._acquire()
-
-                if self.is_locked:
-                    logger.log(QUIET_LOG_LEVEL, 'Lock %s acquired on %s', lock_id,
-                               lock_filename)
-                    break
-                elif timeout >= 0 and time.time() - start_time > timeout:
-                    logger.warning('Timeout acquiring lock %s on %s', lock_id,
-                                   lock_filename)
-                    raise Timeout(self._lock_file)
-                else:
-                    logger.log(
-                        QUIET_LOG_LEVEL,
-                        'Lock %s not acquired on %s, waiting %s seconds ...',
-                        lock_id, lock_filename, poll_intervall
-                    )
-                    time.sleep(poll_intervall)
-        except Exception:
-            # Something did go wrong, so decrement the counter.
-            self._lock_counter = max(0, self._lock_counter - 1)
-
-            raise
-        return _Acquire_ReturnProxy(lock=self)
-
-    def release(self, force: bool = False) -> None:
-        """
-        Releases the file lock.
-        Please note, that the lock is only completely released, if the lock
-        counter is 0.
-        Also note, that the lock file itself is not automatically deleted.
-        :arg bool force:
-            If true, the lock counter is ignored and the lock is released in
-            every case.
-        """
-        if self.is_locked:
-            self._lock_counter -= 1
-
-            if self._lock_counter == 0 or force:
-                # lock_id = id(self)
-                # lock_filename = self._lock_file
-
-                # Can't log in shutdown:
-                #  File "/usr/lib64/python3.9/logging/__init__.py", line 1175, in _open
-                #    NameError: name 'open' is not defined
-                # logger.debug('Releasing 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)
-
-        return None
-
-    def __enter__(self) -> 'FileLock':
-        self.acquire()
-        return self
-
-    def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
-        self.release()
-        return None
-
-    def __del__(self) -> None:
-        self.release(force=True)
-        return None
-
-    def _acquire(self) -> None:
-        open_mode = os.O_RDWR | os.O_CREAT | os.O_TRUNC
-        fd = os.open(self._lock_file, open_mode)
-
-        try:
-            fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
-        except (IOError, OSError):
-            os.close(fd)
-        else:
-            self._lock_file_fd = fd
-        return None
-
-    def _release(self) -> None:
-        # Do not remove the lockfile:
-        #
-        #   https://github.com/benediktschmitt/py-filelock/issues/31
-        #   https://stackoverflow.com/questions/17708885/flock-removing-locked-file-without-race-condition
-        fd = self._lock_file_fd
-        self._lock_file_fd = None
-        fcntl.flock(fd, fcntl.LOCK_UN)  # type: ignore
-        os.close(fd)  # type: ignore
-        return None
-
-
 ##################################
 
 
diff --git a/src/cephadm/cephadmlib/locking.py b/src/cephadm/cephadmlib/locking.py
new file mode 100644 (file)
index 0000000..1e24548
--- /dev/null
@@ -0,0 +1,204 @@
+# locking.py - cephadm mutual exclusion via file locking
+
+import fcntl
+import logging
+import os
+import time
+
+from typing import Any, Optional
+
+from .context import CephadmContext
+from .constants import LOCK_DIR, QUIET_LOG_LEVEL
+
+logger = logging.getLogger()
+
+
+# this is an abbreviated version of
+# https://github.com/benediktschmitt/py-filelock/blob/master/filelock.py
+# that drops all of the compatibility (this is Unix/Linux only).
+
+
+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
+        return None
+
+    def __str__(self) -> str:
+        temp = "The file lock '{}' could not be acquired."\
+               .format(self.lock_file)
+        return temp
+
+
+class _Acquire_ReturnProxy(object):
+    def __init__(self, lock: 'FileLock') -> None:
+        self.lock = lock
+        return None
+
+    def __enter__(self) -> 'FileLock':
+        return self.lock
+
+    def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
+        self.lock.release()
+        return None
+
+
+class FileLock(object):
+    def __init__(self, ctx: CephadmContext, name: str, timeout: int = -1) -> None:
+        if not os.path.exists(LOCK_DIR):
+            os.mkdir(LOCK_DIR, 0o700)
+        self._lock_file = os.path.join(LOCK_DIR, name + '.lock')
+        self.ctx = ctx
+
+        # The file descriptor for the *_lock_file* as it is returned by the
+        # os.open() function.
+        # This file lock is only NOT None, if the object currently holds the
+        # lock.
+        self._lock_file_fd: Optional[int] = None
+        self.timeout = timeout
+        # 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.
+        self._lock_counter = 0
+        return None
+
+    @property
+    def is_locked(self) -> bool:
+        return self._lock_file_fd is not None
+
+    def acquire(self, timeout: Optional[int] = None, poll_intervall: float = 0.05) -> _Acquire_ReturnProxy:
+        """
+        Acquires the file lock or fails with a :exc:`Timeout` error.
+        .. code-block:: python
+            # You can use this method in the context manager (recommended)
+            with lock.acquire():
+                pass
+            # Or use an equivalent try-finally construct:
+            lock.acquire()
+            try:
+                pass
+            finally:
+                lock.release()
+        :arg float timeout:
+            The maximum time waited for the file lock.
+            If ``timeout < 0``, there is no timeout and this method will
+            block until the lock could be acquired.
+            If ``timeout`` is None, the default :attr:`~timeout` is used.
+        :arg float poll_intervall:
+            We check once in *poll_intervall* seconds if we can acquire the
+            file lock.
+        :raises Timeout:
+            if the lock could not be acquired in *timeout* seconds.
+        .. versionchanged:: 2.0.0
+            This method returns now a *proxy* object instead of *self*,
+            so that it can be used in a with statement without side effects.
+        """
+
+        # Use the default timeout, if no timeout is provided.
+        if timeout is None:
+            timeout = self.timeout
+
+        # Increment the number right at the beginning.
+        # We can still undo it, if something fails.
+        self._lock_counter += 1
+
+        lock_id = id(self)
+        lock_filename = self._lock_file
+        start_time = time.time()
+        try:
+            while True:
+                if not self.is_locked:
+                    logger.log(QUIET_LOG_LEVEL, 'Acquiring lock %s on %s', lock_id,
+                               lock_filename)
+                    self._acquire()
+
+                if self.is_locked:
+                    logger.log(QUIET_LOG_LEVEL, 'Lock %s acquired on %s', lock_id,
+                               lock_filename)
+                    break
+                elif timeout >= 0 and time.time() - start_time > timeout:
+                    logger.warning('Timeout acquiring lock %s on %s', lock_id,
+                                   lock_filename)
+                    raise Timeout(self._lock_file)
+                else:
+                    logger.log(
+                        QUIET_LOG_LEVEL,
+                        'Lock %s not acquired on %s, waiting %s seconds ...',
+                        lock_id, lock_filename, poll_intervall
+                    )
+                    time.sleep(poll_intervall)
+        except Exception:
+            # Something did go wrong, so decrement the counter.
+            self._lock_counter = max(0, self._lock_counter - 1)
+
+            raise
+        return _Acquire_ReturnProxy(lock=self)
+
+    def release(self, force: bool = False) -> None:
+        """
+        Releases the file lock.
+        Please note, that the lock is only completely released, if the lock
+        counter is 0.
+        Also note, that the lock file itself is not automatically deleted.
+        :arg bool force:
+            If true, the lock counter is ignored and the lock is released in
+            every case.
+        """
+        if self.is_locked:
+            self._lock_counter -= 1
+
+            if self._lock_counter == 0 or force:
+                # lock_id = id(self)
+                # lock_filename = self._lock_file
+
+                # Can't log in shutdown:
+                #  File "/usr/lib64/python3.9/logging/__init__.py", line 1175, in _open
+                #    NameError: name 'open' is not defined
+                # logger.debug('Releasing 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)
+
+        return None
+
+    def __enter__(self) -> 'FileLock':
+        self.acquire()
+        return self
+
+    def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
+        self.release()
+        return None
+
+    def __del__(self) -> None:
+        self.release(force=True)
+        return None
+
+    def _acquire(self) -> None:
+        open_mode = os.O_RDWR | os.O_CREAT | os.O_TRUNC
+        fd = os.open(self._lock_file, open_mode)
+
+        try:
+            fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
+        except (IOError, OSError):
+            os.close(fd)
+        else:
+            self._lock_file_fd = fd
+        return None
+
+    def _release(self) -> None:
+        # Do not remove the lockfile:
+        #
+        #   https://github.com/benediktschmitt/py-filelock/issues/31
+        #   https://stackoverflow.com/questions/17708885/flock-removing-locked-file-without-race-condition
+        fd = self._lock_file_fd
+        self._lock_file_fd = None
+        fcntl.flock(fd, fcntl.LOCK_UN)  # type: ignore
+        os.close(fd)  # type: ignore
+        return None