Deadlock PostgreSQL lors de l'Initialisation Concurrente du Schéma
Problème
Plusieurs processus worker (jobs Cloud Run, pods K8s, fonctions serverless) qui démarrent simultanément tentent tous d'exécuter du code d'initialisation du schéma, causant des deadlocks PostgreSQL même en utilisant des clauses « IF NOT EXISTS ».
Contexte / Conditions de Déclenchement
- Erreur :
psycopg2.errors.DeadlockDetected: deadlock detected - Log affiche :
Process X waits for RowExclusiveLock on relation... blocked by process Y - Plusieurs workers/jobs démarrant à peu près au même moment
- Chaque worker appelle
init_schema()ou exécute des migrations au démarrage - Utilisation de
CREATE TABLE IF NOT EXISTSouCREATE INDEX IF NOT EXISTS
Pourquoi Cela Se Produit
IF NOT EXISTS de PostgreSQL n'est pas concurrent-safe :
CREATE INDEX IF NOT EXISTSacquiert quand même des verrous avant de vérifier l'existence- Plusieurs processus acquérant des verrous sur des objets différents peuvent causer un deadlock
- Même du DDL « sûr » peut entrer en conflit lors d'une exécution concurrente
Solution
Option 1 : Ignorer l'Init en Production (Recommandée)
Le schéma existe déjà - n'exécutez pas init_schema() dans les workers :
with Database() as db:
# Le schéma existe déjà en production - ignorer pour éviter les deadlocks
# db.init_schema()
# ... code du worker
Option 2 : Utiliser des Verrous Consultatifs
Sérialisez l'initialisation du schéma avec les verrous consultatifs PostgreSQL :
def init_schema_safe(self):
cursor = self._cursor()
# Acquérir un verrou consultatif (bloque les autres processus)
cursor.execute("SELECT pg_advisory_lock(12345)")
try:
self.init_schema()
finally:
cursor.execute("SELECT pg_advisory_unlock(12345)")
self.conn.commit()
Option 3 : Étape de Migration Séparée
Exécutez les migrations comme un job séparé avant de démarrer les workers :
# Dans le pipeline de déploiement
python -m src.migrate # Un seul processus, exécuté en premier
# Puis démarrer les workers
gcloud run jobs execute worker-job
Option 4 : Timeout de Verrou + Retry
Définissez un timeout de verrou et réessayez en cas de deadlock :
def init_schema_with_retry(self, max_retries=3):
for attempt in range(max_retries):
try:
cursor = self._cursor()
cursor.execute("SET lock_timeout = '5s'")
self.init_schema()
return
except psycopg2.errors.DeadlockDetected:
self.conn.rollback()
if attempt == max_retries - 1:
raise
time.sleep(random.uniform(1, 3))
Vérification
Après application du correctif :
- Démarrer plusieurs workers simultanément
- Vérifier dans les logs l'absence d'erreurs de deadlock
- Vérifier que tous les workers démarrent avec succès
Exemple
Avant (deadlocks avec 6 jobs Cloud Run concurrents) :
# src/download.py
with VineDatabase() as db:
db.init_schema() # DEADLOCK quand plusieurs jobs démarrent !
# ... logique de téléchargement
Après (pas de deadlocks) :
# src/download.py
with VineDatabase() as db:
# Le schéma existe déjà en production - ignorer pour éviter les deadlocks
# db.init_schema()
# ... logique de téléchargement
Notes
- Cela s'applique à tout pattern de worker concurrent : Cloud Run, Celery, Kubernetes, Lambda
- Le deadlock peut être intermittent - dépend du timing exact des démarrages de workers
CREATE TABLE IF NOT EXISTSest généralement plus sûr queCREATE INDEX IF NOT EXISTS- Les jobs Cloud Run démarrent souvent simultanément quand déclenchés, rendant cela courant
- Considérez l'utilisation d'outils de migration de base de données (Alembic, Flyway) avec un verrouillage approprié