Également disponible en : 🇬🇧

Verrouiller de Buzhug

J’ai récemment décidé d’utiliser Buzhug pour un projet. À ce que je puisse en juger, il s’est avéré efficace, rapide, facile à utiliser et à maintenir. Cependant, j’ai rencontré quelques problèmes.

Les solutions simples sont souvent les meilleures

J’en suis venu à utiliser Buzhug pour les raisons suivantes :

  • J’avais besoin d’une seule table
  • Je ne voulais pas ajouter de dépendances supplémentaires au projet
  • La taille de la table serait en moyenne de 5K entrées (sans dépasser 10k entrées en pic)

Et une raison supplémentaire (personnelle) :

  • Je ne voulais pas me soucier de SQL. Vraiment pas. Pas question !

Cela ne me laissait qu’une option : une base de données embarquée en pur Python.

Après avoir considéré quelques bibliothèques, j’ai été séduit par la manière dont l’interface de Buzhug est proche de la manipulation d’objets Python. Et les benchmarks semblaient montrer qu’il est assez performant pour ce projet.

Après un rapide prototypage (1 jour), le choix était fait.

Puis vinrent quelques semaines de développement et les premiers tests de charge…

Et la réalité est revenue rapidement

Plusieurs fois par jour, l’application soutenue par cette base de données est intensément utilisée :

  • Elle peut être exécutée jusqu’à 50 fois simultanément dans des processus Python séparés
  • Chaque exécution effectue une opération de lecture et une opération d’écriture/suppression

Cela provoque une condition de course sur les fichiers utilisés pour stocker les données, et les écritures concurrentes corrompent la base de données.

L’utilisation de buzhug.TS_Base au lieu de buzhug.Base n’a rien résolu, car le problème n’est pas lié aux threads, mais aux processus. Ce dont j’ai besoin est un verrou inter-processus.

Voici la solution

La première étape a été de trouver comment implémenter un verrou inter-processus et système.

Comme cela doit seulement fonctionner sur Linux, la classe Lock donnée par Chris de Vmfarms convient parfaitement. Voici une version légèrement modifiée pour en faire un gestionnaire de contexte :

import fcntl

class PsLock:
    """
    Adapté de :
    http://blog.vmfarms.com/2011/03/cross-process-locking-and.html
    """
    def __init__(self, filename):
        self.filename = filename
        self.handle = open(filename, 'w')
    
    # Faites un OR bit à bit avec fcntl.LOCK_NB si vous avez besoin d'un verrou non bloquant
    def acquire(self):
        fcntl.flock(self.handle, fcntl.LOCK_EX)
        
    def release(self):
        fcntl.flock(self.handle, fcntl.LOCK_UN)
        
    def __del__(self):
        self.handle.close()
        
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is None:
            pass
        self.release()
        
    def __enter__(self):
        self.acquire()

La deuxième étape consiste à définir une nouvelle classe qui hérite de buzhug.Base et qui utilise PsLock (inspiré de TS_Base) :

import buzhug

_lock = PsLock("/tmp/buzhug.lck")

class PS_Base(buzhug.Base):
    def create(self, *args, **kw):
        with _lock:
            res = buzhug.Base.create(self, *args, **kw)
        return res

    def open(self, *args, **kw):
        with _lock:
            res = buzhug.Base.open(self, *args, **kw)
        return res

    def close(self, *args, **kw):
        with _lock:
            res = buzhug.Base.close(self, *args, **kw)
        return res
        
    def destroy(self, *args, **kw):
        with _lock:
            res = buzhug.Base.destroy(self, *args, **kw)
        return res
        
    def set_default(self, *args, **kw):
        with _lock:
            res = buzhug.Base.set_default(self, *args, **kw)
        return res
        
    def insert(self, *args, **kw):
        with _lock:
            res = buzhug.Base.insert(self, *args, **kw)
        return res
        
    def update(self, *args, **kw):
        with _lock:
            res = buzhug.Base.update(self, *args, **kw)
        return res
        
    def delete(self, *args, **kw):
        with _lock:
            res = buzhug.Base.delete(self, *args, **kw)
        return res
        
    def cleanup(self, *args, **kw):
        with _lock:
            res = buzhug.Base.cleanup(self, *args, **kw)
        return res
        
    def commit(self, *args, **kw):
        with _lock:
            res = buzhug.Base.commit(self, *args, **kw)
        return res
        
    def add_field(self, *args, **kw):
        with _lock:
            res = buzhug.Base.add_field(self, *args, **kw)
        return res
        
    def drop_field(self, *args, **kw):
        with _lock:
            res = buzhug.Base.drop_field(self, *args, **kw)
        return res

Maintenant, j’utilise simplement

    database = PS_Base( ... )

Et toutes les erreurs ont disparu.