lock_manager fournit un système de verrous distribués (mutex) qui empêche deux processus ou workers Celery d'exécuter simultanément la même opération critique.
Problème typique sans verrou : deux workers reçoivent la même tâche en même temps (OCR d'un document, expiration des checkouts) et produisent des résultats corrompus ou des doublons. Le verrou garantit qu'un seul gagne l'accès, l'autre échoue immédiatement (LockError).
LockingBackendTous les backends exposent la même interface :
# Acquérir un verrou (non-bloquant — lève LockError si déjà pris)
lock = LockingBackend.get_backend().acquire_lock(name='mon_verrou', timeout=30)
# ... faire le travail protégé ...
# Relâcher
lock.release()
name : identifiant unique du verrou (chaîne arbitraire)timeout : durée en secondes après laquelle le verrou expire automatiquement (défaut : 30 s)LockError est levée immédiatement, sans attenteFileLock (défaut)Stocke les verrous dans un fichier JSON unique dans le répertoire temporaire. Le nom du fichier est un hash SHA-256 de SECRET_KEY — ce qui l'isole par installation.
/tmp/<sha256_de_secret_key>
→ contenu : {"nom_verrou": {"expiration": 1713260400.0, "uuid": "abc123..."}}
Mécanisme : verrou OS (fcntl/LockEx) sur le fichier pendant chaque lecture/écriture + threading.Lock() pour la sécurité intra-processus.
Limites : fonctionne uniquement si tous les workers partagent le même système de fichiers. Ne convient pas à une architecture multi-nœuds.
ModelLockStocke les verrous dans la table lock_manager_lock en base de données, via SELECT FOR UPDATE. Fonctionne dans toute architecture multi-processus partageant la même base.
Table lock_manager_lock :
┌────┬──────────────────────┬─────────────────────┬─────────┐
│ id │ name │ creation_datetime │ timeout │
├────┼──────────────────────┼─────────────────────┼─────────┤
│ 1 │ task_expired_... │ 2026-04-16 10:00:00 │ 30 │
└────┴──────────────────────┴─────────────────────┴─────────┘
La libération vérifie (name, creation_datetime) — si le verrou a expiré et a été réassigné à quelqu'un d'autre, la libération est silencieusement ignorée.
RedisLockUtilise le mécanisme natif de verrou Redis (SET NX PX). Le plus performant pour les architectures distribuées avec Redis déjà en place (ce qui est le cas de Mayan en production).
# Configuration dans config.yml ou variables d'environnement
MAYAN_LOCK_MANAGER_BACKEND: mayan.apps.lock_manager.backends.redis_lock.RedisLock
MAYAN_LOCK_MANAGER_BACKEND_ARGUMENTS: '{"redis_url": "redis://redis:6379/4"}'
Utilise un pool de connexions et préfixe les clés Redis (mayan_lock_) pour éviter les collisions.
| Backend | Usage recommandé |
|---|---|
FileLock |
Développement local, installation mono-serveur |
ModelLock |
Multi-processus, base de données partagée, pas de Redis |
RedisLock |
Production avec Redis (le plus performant et fiable) |
from mayan.apps.lock_manager.backends.base import LockingBackend
from mayan.apps.lock_manager.exceptions import LockError
try:
lock = LockingBackend.get_backend().acquire_lock(
name='task_check_expired_checkouts', timeout=60
)
except LockError:
# Un autre worker traite déjà cette tâche
return
try:
do_critical_work()
finally:
lock.release()
@locked_class_methodAcquiert et libère le verrou automatiquement autour d'une méthode. Le nom du verrou est fourni par self._lock_manager_get_lock_name() :
from mayan.apps.lock_manager.decorators import locked_class_method
class DocumentProcessor:
def _lock_manager_get_lock_name(self, *args, **kwargs):
return 'document_process_{}'.format(self.document.pk)
@locked_class_method
def process(self):
# Protégé par le verrou — un seul appel concurrent autorisé
...
@acquire_lock_class_method / @release_lock_class_methodPour les cas où l'acquisition et la libération sont dans des méthodes séparées (ex. open() / close()) :
@acquire_lock_class_method
def open(self):
# self._lock est positionné automatiquement
...
@release_lock_class_method
def close(self):
# self._lock.release() est appelé automatiquement
...
Chaque verrou a un timeout (défaut 30 s). Si le processus qui détient le verrou meurt sans le libérer, le verrou expire et peut être réacquis. Utile pour éviter les verrous zombies après un crash.
En cas de maintenance ou de verrous bloquants, purger manuellement :
./manage.py lock_manager_purge_locks --settings=mayan.settings.development
| Fichier | Rôle |
|---|---|
backends/base.py |
LockingBackend — interface commune + sélection du backend actif |
backends/file_lock.py |
FileLock — verrous dans un fichier JSON partagé |
backends/model_lock.py |
ModelLock — verrous en base de données |
backends/redis_lock.py |
RedisLock — verrous Redis natifs |
decorators.py |
@locked_class_method, @acquire_lock_class_method, @release_lock_class_method |
models.py |
Lock — modèle pour le backend ModelLock |
literals.py |
Backend par défaut (FileLock), timeout par défaut (30 s) |