12 votes

fcntl.flock - comment implémenter un délai d'attente ?

J'utilise python 2.7

Je veux créer une fonction enveloppante autour de fcntl.flock() qui se termine après un intervalle défini :

wrapper_function(timeout):

J'ai essayé d'appeler sur un autre thread et d'utiliser thread.join(timeout) mais il semble que fcntl.flock() continue de bloquer :

def GetLock(self, timeout):
    """Returns true if lock is aquired, false if lock is already in use"""
    self.__lock_file = open('proc_lock', 'w')

    def GetLockOrTimeOut():
        print 'ProcessLock: Acquiring Lock'            
        fcntl.flock(self.__lock_file.fileno(), fcntl.LOCK_EX)
        print 'ProcessLock: Lock Acquired'

    thread = threading.Thread(target=GetLockOrTimeOut)
    thread.start()
    thread.join(timeout)

    if thread.isAlive():
        print 'GetLock timed out'
        return False
    else:
        return True

J'ai cherché des solutions pour mettre fin aux threads, la solution la plus populaire semble être de sous-classer threading.thread et d'ajouter une fonction pour lever une exception dans le thread. Cependant, je suis tombé sur une lien qui dit que cette méthode ne fonctionnera pas avec les appels natifs, alors que je suis presque sûr que fcntl.flock() appelle une fonction native. Des suggestions ?

Contexte : J'utilise un verrou de fichier pour créer une application à instance unique, mais je ne veux pas qu'une deuxième instance de l'application reste en suspens jusqu'à ce que la première instance se termine.

25voto

Glenn Maynard Points 24451

Les délais d'attente pour les appels système sont effectués par des signaux. La plupart des appels système bloquants retournent avec EINTR lorsqu'un signal se produit, vous pouvez donc utiliser la fonction alarm pour mettre en œuvre des délais d'attente.

Voici un gestionnaire de contexte qui fonctionne avec la plupart des appels système, provoquant la levée d'une IOError à partir d'un appel système bloquant s'il prend trop de temps.

import signal, errno
from contextlib import contextmanager
import fcntl

@contextmanager
def timeout(seconds):
    def timeout_handler(signum, frame):
        pass

    original_handler = signal.signal(signal.SIGALRM, timeout_handler)

    try:
        signal.alarm(seconds)
        yield
    finally:
        signal.alarm(0)
        signal.signal(signal.SIGALRM, original_handler)

with timeout(1):
    f = open("test.lck", "w")
    try:
        fcntl.flock(f.fileno(), fcntl.LOCK_EX)
    except IOError, e:
        if e.errno != errno.EINTR:
            raise e
        print "Lock timed out"

10voto

jcomeau_ictx Points 15736

Je suis sûr qu'il existe plusieurs moyens, mais pourquoi ne pas utiliser un verrou non bloquant ? Après quelques n tentatives, abandonner et sortir ?

Pour utiliser le verrouillage non bloquant, incluez l'option fcntl.LOCK_NB comme dans :

fcntl.flock(self.__lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)

3voto

Richard Maw Points 41

Je suis un partisan du flocage ici, car essayer de faire un verrouillage bloquant avec un timeout nécessite des changements de l'état global, ce qui rend plus difficile le raisonnement sur votre programme, surtout si le threading est impliqué.

Vous pourriez créer un sous-processus et implémenter l'alarme comme ci-dessus, ou vous pourriez simplement exécuter http://man7.org/linux/man-pages/man1/flock.1.html

import subprocess
def flock_with_timeout(fd, timeout, shared=True):
    rc = subprocess.call(['flock', '--shared' if shared else '--exclusive', '--timeout', str(timeout), str(fd)])
    if rc != 0:
        raise Exception('Failed to take lock')

Si vous avez une version suffisamment récente de flock, vous pouvez utiliser -E pour spécifier un code de sortie différent pour la commande qui a autrement réussi, mais qui n'a pas réussi à prendre le verrou après un délai d'attente, afin que vous puissiez savoir si la commande a échoué pour une autre raison à la place.

3voto

remram Points 2249

Pour Python 3.5+, La solution de Glenn Maynard ne fonctionne plus à cause de PEP-475 . Il s'agit d'une version modifiée :

import signal, errno
from contextlib import contextmanager
import fcntl

@contextmanager
def timeout(seconds):
    def timeout_handler(signum, frame):
        # Now that flock retries automatically when interrupted, we need
        # an exception to stop it
        # This exception will propagate on the main thread, make sure you're calling flock there
        raise InterruptedError

    original_handler = signal.signal(signal.SIGALRM, timeout_handler)

    try:
        signal.alarm(seconds)
        yield
    finally:
        signal.alarm(0)
        signal.signal(signal.SIGALRM, original_handler)

with timeout(1):
    f = open("test.lck", "w")
    try:
        fcntl.flock(f.fileno(), fcntl.LOCK_EX)
    except InterruptedError:
        # Catch the exception raised by the handler
        # If we weren't raising an exception, flock would automatically retry on signals
        print("Lock timed out")

1voto

HTE Points 21

En complément de la réponse de @Richard Maw ci-dessus https://stackoverflow.com/a/32580233/17454091 (Je n'ai pas assez de réputation pour poster un commentaire).

Dans Python 3.2 et plus récent, pour que les fds soient disponibles dans les sous-processus, il faut également fournir pass_fds argument.

La solution complète se résume à :

import subprocess
def flock_with_timeout(fd, timeout, shared=True):
    rc = subprocess.call(['flock',
                          '--shared' if shared else '--exclusive',
                          '--timeout', str(timeout),
                          str(fd)],
                         pass_fds=[fd])
    if rc != 0:
        raise Exception('Failed to take lock')

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X