postgres-concurrent-schema-init-deadlock

Par divinevideo · divine-mobile

Corrige les erreurs de deadlock PostgreSQL causées par l'initialisation concurrente du schéma dans les processus workers. À utiliser quand : (1) `psycopg2.errors.DeadlockDetected` lors d'un `CREATE INDEX/TABLE IF NOT EXISTS`, (2) plusieurs jobs Cloud Run, pods Kubernetes ou processus workers démarrent simultanément, (3) l'erreur indique « Process X waits for RowExclusiveLock... blocked by process Y », (4) `init_schema()` ou du code de migration s'exécute au démarrage des workers. Le point clé : `IF NOT EXISTS` n'est PAS vraiment concurrent-safe — PostgreSQL acquiert quand même des locks susceptibles de provoquer des deadlocks.

npx skills add https://github.com/divinevideo/divine-mobile --skill postgres-concurrent-schema-init-deadlock

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 EXISTS ou CREATE INDEX IF NOT EXISTS

Pourquoi Cela Se Produit

IF NOT EXISTS de PostgreSQL n'est pas concurrent-safe :

  1. CREATE INDEX IF NOT EXISTS acquiert quand même des verrous avant de vérifier l'existence
  2. Plusieurs processus acquérant des verrous sur des objets différents peuvent causer un deadlock
  3. 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 :

  1. Démarrer plusieurs workers simultanément
  2. Vérifier dans les logs l'absence d'erreurs de deadlock
  3. 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 EXISTS est généralement plus sûr que CREATE 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é

Références

Skills similaires