X-Git-Url: https://wannabe.guru.org/gitweb/?a=blobdiff_plain;f=lockfile.py;h=6993cb84d5e88f8dd6fc1a9d28a849f0cfd28713;hb=e8fbbb7306430478dec55d2c963eed116d8330cc;hp=d275f407ff237c38ceaec83c94a1abf92174b04a;hpb=d1fdbad2a4b96a374fdd23e8c8550484a9e6523a;p=python_utils.git diff --git a/lockfile.py b/lockfile.py index d275f40..6993cb8 100644 --- a/lockfile.py +++ b/lockfile.py @@ -1,41 +1,45 @@ #!/usr/bin/env python3 -from dataclasses import dataclass +"""File-based locking helper.""" + import datetime import json import logging import os import signal import sys +import warnings +from dataclasses import dataclass from typing import Optional import config import datetime_utils import decorator_utils - -cfg = config.add_commandline_args( - f'Lockfile ({__file__})', - 'Args related to lockfiles') +cfg = config.add_commandline_args(f'Lockfile ({__file__})', 'Args related to lockfiles') cfg.add_argument( '--lockfile_held_duration_warning_threshold_sec', type=float, - default=10.0, + default=60.0, metavar='SECONDS', - help='If a lock is held for longer than this threshold we log a warning' + help='If a lock is held for longer than this threshold we log a warning', ) logger = logging.getLogger(__name__) class LockFileException(Exception): + """An exception related to lock files.""" + pass @dataclass class LockFileContents: + """The contents we'll write to each lock file.""" + pid: int commandline: str - expiration_timestamp: float + expiration_timestamp: Optional[float] class LockFile(object): @@ -49,17 +53,19 @@ class LockFile(object): # some logic for detecting stale locks. """ + def __init__( - self, - lockfile_path: str, - *, - do_signal_cleanup: bool = True, - expiration_timestamp: Optional[float] = None, - override_command: Optional[str] = None, + self, + lockfile_path: str, + *, + do_signal_cleanup: bool = True, + expiration_timestamp: Optional[float] = None, + override_command: Optional[str] = None, ) -> None: - self.is_locked = False - self.lockfile = lockfile_path - self.override_command = override_command + self.is_locked: bool = False + self.lockfile: str = lockfile_path + self.locktime: Optional[int] = None + self.override_command: Optional[str] = override_command if do_signal_cleanup: signal.signal(signal.SIGINT, self._signal) signal.signal(signal.SIGTERM, self._signal) @@ -72,7 +78,7 @@ class LockFile(object): return not os.path.exists(self.lockfile) def try_acquire_lock_once(self) -> bool: - logger.debug(f"Trying to acquire {self.lockfile}.") + logger.debug("Trying to acquire %s.", self.lockfile) try: # Attempt to create the lockfile. These flags cause # os.open to raise an OSError if the file already @@ -82,25 +88,24 @@ class LockFile(object): contents = self._get_lockfile_contents() logger.debug(contents) f.write(contents) - logger.debug(f'Success; I own {self.lockfile}.') + logger.debug('Success; I own %s.', self.lockfile) self.is_locked = True return True except OSError: pass - logger.warning(f'Could not acquire {self.lockfile}.') + logger.warning('Couldn\'t acquire %s.', self.lockfile) return False def acquire_with_retries( - self, - *, - initial_delay: float = 1.0, - backoff_factor: float = 2.0, - max_attempts = 5 + self, + *, + initial_delay: float = 1.0, + backoff_factor: float = 2.0, + max_attempts=5, ) -> bool: - - @decorator_utils.retry_if_false(tries = max_attempts, - delay_sec = initial_delay, - backoff = backoff_factor) + @decorator_utils.retry_if_false( + tries=max_attempts, delay_sec=initial_delay, backoff=backoff_factor + ) def _try_acquire_lock_with_retries() -> bool: success = self.try_acquire_lock_once() if not success and os.path.exists(self.lockfile): @@ -126,13 +131,15 @@ class LockFile(object): logger.warning(msg) raise LockFileException(msg) - def __exit__(self, type, value, traceback): + def __exit__(self, _, value, traceback): if self.locktime: ts = datetime.datetime.now().timestamp() duration = ts - self.locktime if duration >= config.config['lockfile_held_duration_warning_threshold_sec']: str_duration = datetime_utils.describe_duration_briefly(duration) - logger.warning(f'Held {self.lockfile} for {str_duration}') + msg = f'Held {self.lockfile} for {str_duration}' + logger.warning(msg) + warnings.warn(msg, stacklevel=2) self.release() def __del__(self): @@ -148,11 +155,10 @@ class LockFile(object): cmd = self.override_command else: cmd = ' '.join(sys.argv) - print(cmd) contents = LockFileContents( - pid = os.getpid(), - commandline = cmd, - expiration_timestamp = self.expiration_timestamp, + pid=os.getpid(), + commandline=cmd, + expiration_timestamp=self.expiration_timestamp, ) return json.dumps(contents.__dict__) @@ -164,22 +170,22 @@ class LockFile(object): line = lines[0] line_dict = json.loads(line) contents = LockFileContents(**line_dict) - logger.debug(f'Blocking lock contents="{contents}"') + logger.debug('Blocking lock contents="%s"', contents) # Does the PID exist still? try: os.kill(contents.pid, 0) except OSError: - logger.warning(f'Lockfile {self.lockfile}\'s pid ({contents.pid}) is stale; ' + - 'force acquiring') + msg = f'Lockfile {self.lockfile}\'s pid ({contents.pid}) is stale; force acquiring' + logger.warning(msg) self.release() # Has the lock expiration expired? if contents.expiration_timestamp is not None: now = datetime.datetime.now().timestamp() - if now > contents.expiration_datetime: - logger.warning(f'Lockfile {self.lockfile} expiration time has passed; ' + - 'force acquiring') + if now > contents.expiration_timestamp: + msg = f'Lockfile {self.lockfile} expiration time has passed; force acquiring' + logger.warning(msg) self.release() except Exception: pass