Source code for pacbio_data_processing.sentinel

#######################################################################
#
# Copyright (C) 2022 David Palao
#
# This file is part of PacBioDataProcessing.
#
#  PacBioDataProcessing is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  PacBio data processing is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with PacBioDataProcessing. If not, see <http://www.gnu.org/licenses/>.
#
#######################################################################

from pathlib import Path
import time
from concurrent.futures import ThreadPoolExecutor
import logging


SLEEP_SECONDS = 20
TOO_OLD_AGE_SECONDS = 10*SLEEP_SECONDS
_ABANDONED_FILE_TEMPLATE_STR = "Abandoned sentinel '{}' detected; overwritten."


[docs]class SentinelFileFound(Exception): """Exception expected when the sentinel file is there before its creation. """
[docs]class SentinelFileNotFound(Exception): """Exception expected if the sentinel file is missing before the ``Sentinel`` removes it. """
[docs]class Sentinel: """This class creates objects that are expected to be used as context managers. At ``__enter__`` a sentinel file is created. At ``__exit__`` the sentinel file is removed. If the file is there before entering the context, or is not there when the context is exited, an exception is raised. """
[docs] def __init__(self, checkpoint: Path): path_name = "."+checkpoint.name+".wip" self.path = checkpoint.with_name(path_name) self._executor = ThreadPoolExecutor(max_workers=1)
def __enter__(self): try: self.path.touch(exist_ok=False) except FileExistsError: if self.is_file_too_old: self.path.touch(exist_ok=True) logging.info(_ABANDONED_FILE_TEMPLATE_STR.format(self.path)) else: raise SentinelFileFound() self._executor.submit(self._anti_aging)
[docs] def _anti_aging(self): """Method that updates the modification time of the sentinel file every :py:data:`SLEEP_SECONDS` seconds. This is part of the mechanism to ensure that the sentinel does not get fooled by an abandoned leftover sentinel file. :meta public: """ while self.path.exists(): self.path.touch() time.sleep(SLEEP_SECONDS)
def __exit__(self, exc_type, exc_val, exc_tb): try: self.path.unlink() except FileNotFoundError: raise SentinelFileNotFound() @property def is_file_too_old(self): """Property that answers the question: is the sentinel file too old to be taken as an active sentinel file, or not? """ result = False if self.path.exists(): now = time.time() age = now-self.path.stat().st_mtime if age > TOO_OLD_AGE_SECONDS: result = True return result
############################################################### # # Note to developers: # ------------------- # # Something like the following implementation is *simpler* # but I found it *harder to test*. (DPalao, 21March2022): # # @contextlib.contextmanager # def sentinel(checkpoint): # path_name = "."+checkpoint.name+".wip" # path = checkpoint.with_name(path_name) # try: # path.touch(exist_ok=False) # except FileExistsError: # raise SentinelFileExists() # yield path # try: # path.unlink() # except FileNotFoundError: # raise SentinelFileNotFound() # # [Obviously it lacks the (anti-)aging mechanism as well # (DPalao, 25March2022)]