"""Prevent multiple processes from doing the same operation on a host, using goodold Linux fnctl locking.fcntl's ``flock`` is a great choice to prevent deadlocks if/when your processcrashes because the system will release any locks of a process when it dies.``man fcntl`` for details.Anyway, we provide a Lock class that acts as a context manager for bothblocking and non-blocking fcntl locks.It's especially useful to orchestrate Ansible Action plugins.Blocking locks--------------Consider this little program:.. code-block:: python import cli2 import os os.environ['LOG_LEVEL'] = 'DEBUG' with cli2.Lock('/tmp/mylock') as lock: input('say hello')- Run it in a first terminal: it will log "Acquired" and show the "say hello" input.- Run it in another terminal: it will log "Waiting"- Type enter in the first terminal: it will release the lock and exit- Then the second terminal will log "Acquired" and display the say hello inputYou got this: two programs cannot enter the same blocking lock at the sametime.Non blocking locks------------------You're starting a bunch of processes that potentially want to do the same thingat the same time, ie. download a file to cache locally prior to sending it.Only one process must do the caching download, the first one that gets thelock, all others will sit there and wait.This is possible with a non-blocking lock that we later convert into a blockinglock... code-block:: python with cli2.Lock('/tmp/mylock', blocking=False) as lock: if lock.acquired: # we got the lock, proceed to downloading safely do_download() else: # couldn't acquired the lock because another process got it # let's just wait for that other process to finish by converting # the non-blocking lock into a blocking one lock.block() # all processes can safely process to uploading do_upload()"""importfcntlimportfunctoolsimportosfrompathlibimportPath
[docs]classLock:""" fcntl flock context manager, blocking and non blocking modes In doubt? set :envvar:`LOG_LEVEL` to ``DEBUG`` and you will see exactly what's happening. .. py:attribute:: lock_path Path to the file that we're going to use with flock. .. py:attribute:: blocking If True, the locker automatically blocks. Otherwise, you need to check :py:attr:`acquired` and call :py:meth:`block` yourself. .. py:attribute:: prefix Arbitrary string that will be added to logs. """def__init__(self,lock_path,blocking=True,prefix=None):self.lock_path=Path(lock_path)self.blocking=blockingself.acquired=Trueself.prefix=prefixor''@functools.cached_propertydeflog(self):from.logimportlogreturnlog.bind(blocking=self.blocking,prefix=self.prefix,lock_path=self.lock_path,)
[docs]defblock(self):""" Basically converts a non-blocking lock into a blocking one """self.blocking=Trueself.acquire()
def__enter__(self):self.log.debug('Waiting')self.lock_path.parent.mkdir(parents=True,exist_ok=True)self.fp=self.lock_path.open('w+')ifself.blocking:self.acquire()returnselftry:fcntl.flock(self.fp.fileno(),fcntl.LOCK_EX|fcntl.LOCK_NB,)exceptBlockingIOError:self.log.info('Not acquired but non blocking: proceeding')self.acquired=Falseelse:self.log.info('Acquired')returnselfdef__exit__(self,_type,value,tb):self.log.debug('Releasing')self.release()defacquire(self):fcntl.flock(self.fp.fileno(),fcntl.LOCK_EX)self.log.info('Acquired')defrelease(self):ifself.acquiredornotself.blocking:fcntl.flock(self.fp.fileno(),fcntl.LOCK_UN)self.fp.close()self.log.info('Released')ifself.acquired:try:os.unlink(self.lock_path)exceptFileNotFoundError:pass# already deleted, or locked by another processelse:self.log.info('Deleted')