asfpy/justone.py (47 lines of code) (raw):

# -*- coding: utf-8 -*- # # justone.py -- utilities to ensure one process runs at a time # # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed # with this work for additional information regarding copyright # ownership. The ASF licenses this file to You under the Apache # License, Version 2.0 (the "License"); you may not use this file # except in compliance with the License. You may obtain a copy of the # License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. See the License for the specific language governing # permissions and limitations under the License. # # # GENERAL OUTLINE # # This module will construct a FIFO at given pathname. The process # designated as "running" will open the FIFO for reading. On exit, # the FIFO will be closed and unlinked. # # A tentative-run process will look for the FIFO. If it doesn't # exist, then it is clear to run (per above). If it *does* exist, # then the tentative will test whether a process is reading from # the FIFO. If yes, then clearly another is running, and the # tentative will just exit. If there does not appear to be a reader, # then the module tests if the FIFO is "stale" (based on time) and # removes it, or exits without running. # import os import errno import time import contextlib STALE = 3600 # seconds DID_NOT_RUN = object() def maybe_run(fifo_fname, func, stale=STALE): try: info = os.stat(fifo_fname) except OSError: # Some error (assume it doesn't exist), so run the func return _run_func(fifo_fname, func) # Opening a FIFO in non-blocking will throw ENXIO if no reader # is on the other end. try: fd = os.open(fifo_fname, os.O_WRONLY | os.O_NONBLOCK) # Successful open means a reader exists. Thus, we're done. os.close(fd) return DID_NOT_RUN except OSError as e: if e.errno == errno.ENOENT: # RACE: the FIFO just disappeared. Meaning another # process *just* completed. Go ahead and run the func. return _run_func(fifo_fname, func) # Note: ENXIO means there is no reader on the other end. # Bail on anything else, as we can't handle it. if e.errno != errno.ENXIO: raise # Check for a stale FIFO. # There is no reader process, but the FIFO exists. Could be # a race condition (FIFO is opening/closing in the reader), # or maybe a stale FIFO. if time.time() - info.st_mtime > stale: ### RACE: we might be removing another runner's FIFO os.unlink(fifo_fname) return _run_func(fifo_fname, func) # Not stale (yet). Don't do anything for now. return DID_NOT_RUN def _run_func(fifo_fname, func): with _temp_fifo(fifo_fname) as okay: if okay: return func() @contextlib.contextmanager def _temp_fifo(fifo_fname): # Create the FIFO try: os.mkfifo(fifo_fname) except OSError: # Likely RACE: a FIFO exists/appeared (and mkfifo failed). # Let the other process run with this. yield False # not okay return # stop iterating try: # Open it for reading, to signal "we got this". fd = os.open(fifo_fname, os.O_RDONLY | os.O_NONBLOCK) try: # Okay to run. yield True finally: os.close(fd) finally: # Note: there might be a RACE where the OPEN fails after # we created the FIFO (eg. another process removed it). # Not sure how this would happen. Just bail. try: os.unlink(fifo_fname) except OSError: # In case the FIFO disappeared somehow: we don't care. pass # stop iterating return