pg-pool-cloudsql-reconnect

Par divinevideo · divine-mobile

Corriger le problème de reconnexion de pg-pool Node.js après une réinitialisation de connexion CloudSQL (ECONNRESET). À utiliser quand : (1) les tentatives de réessai échouent avec la même erreur ECONNRESET malgré la logique de retry, (2) les erreurs « Connection terminated » ou « connection already closed » persistent entre les tentatives, (3) des erreurs « remaining connection slots are reserved » apparaissent dans CloudSQL, (4) application Node.js à haute concurrence utilisant pg-pool et CloudSQL/PostgreSQL managé. Le pool met en cache des connexions mortes — vous devez recréer le pool, pas seulement réessayer.

npx skills add https://github.com/divinevideo/divine-mobile --skill pg-pool-cloudsql-reconnect

pg-pool CloudSQL Reconnexion

Problème

Les applications Node.js utilisant pg-pool avec CloudSQL (ou autre PostgreSQL géré) ne parviennent pas à se rétablir après une réinitialisation de connexion. La logique de retry semble fonctionner mais continue d'échouer avec la même erreur ECONNRESET car le pool met en cache les connexions mortes.

Contexte / Conditions déclencheurs

  • Erreurs ECONNRESET qui persistent malgré la logique de retry
  • Messages d'erreur comme :
    • read ECONNRESET
    • Connection terminated
    • connection already closed
    • remaining connection slots are reserved for non-replication superuser connections
  • Applications haute concurrence (traitement batch parallèle)
  • CloudSQL avec petits niveaux d'instance (db-f1-micro, db-g1-small)
  • Scripts longue durée avec périodes d'inactivité suivies de pics

Cause racine

pg-pool maintient un pool de connexions à la base de données. Quand CloudSQL réinitialise les connexions (à cause d'un timeout, maintenance ou limites de connexions), le pool garde des références à ces connexions mortes. Simplement réessayer l'opération utilise la même connexion morte du pool.

Solution

Au lieu de simplement réessayer, recréez l'intégralité du pool en cas d'erreur de réinitialisation de connexion :

export class PostgresDatabase {
  private pool: Pool;
  private config: PostgresConfig;

  constructor(config: PostgresConfig) {
    this.config = config;
    this.pool = this.createPool();
  }

  private createPool(): Pool {
    const pool = new Pool({
      ...this.config,
      max: 25,                          // Adequat pour la concurrence
      idleTimeoutMillis: 30000,         // Ferme les connexions inactives
      connectionTimeoutMillis: 10000,   // Timeout pour nouvelles connexions
      keepAlive: true,                  // TCP keepalive
    });

    pool.on('error', (err) => {
      console.error('Pool error:', err.message);
    });

    return pool;
  }

  private async reconnect(): Promise<void> {
    console.log("Reconnecting to database...");
    try {
      await this.pool.end();
    } catch {
      // Ignore cleanup errors
    }
    this.pool = this.createPool();
    await this.pool.query("SELECT 1"); // Verify connection
    console.log("Database reconnected");
  }

  private async withRetry<T>(
    operation: () => Promise<T>,
    maxRetries = 3
  ): Promise<T> {
    for (let attempt = 0; attempt < maxRetries; attempt++) {
      try {
        return await operation();
      } catch (error) {
        const msg = error instanceof Error ? error.message : String(error);
        const needsReconnect =
          msg.includes("ECONNRESET") ||
          msg.includes("Connection terminated") ||
          msg.includes("connection already closed") ||
          msg.includes("remaining connection slots");

        if (!needsReconnect || attempt === maxRetries - 1) {
          throw error;
        }

        const delay = 1000 * Math.pow(2, attempt);
        console.log(`Connection error, reconnecting in ${delay}ms...`);
        await new Promise(r => setTimeout(r, delay));

        // KEY: Recreate the pool, don't just retry
        await this.reconnect();
      }
    }
    throw new Error("Max retries exceeded");
  }
}

Aperçu clé

Réessayer avec le même pool utilise des connexions mortes en cache. Le correctif consiste à :

  1. Détecter les erreurs de réinitialisation de connexion
  2. Appeler pool.end() pour fermer toutes les connexions
  3. Créer une nouvelle instance de pool
  4. Vérifier que la nouvelle connexion fonctionne
  5. Puis réessayer l'opération

Considérations spécifiques à CloudSQL

  1. Le niveau d'instance importe : db-f1-micro ne supporte ~25 connexions. Utilisez db-g1-small ou supérieur pour les charges concurrentes.

  2. Vérifiez max_connections :

    gcloud sql instances describe INSTANCE --format='value(settings.tier)'
  3. Cloud SQL Proxy : N'aide pas avec le pooling de connexions - cela fait juste un tunnel TCP. Considérez PgBouncer pour un vrai pooling de connexions.

Vérification

Après implémentation :

  1. Exécutez une charge haute concurrence
  2. Déclenchez intentionnellement une réinitialisation de connexion (redémarrez proxy, attendez timeout d'inactivité)
  3. Vérifiez que les opérations reprennent avec le log "Reconnecting to database..."
  4. Plus de défaillances en cascade

Notes

  • Ce pattern s'applique à tout PostgreSQL géré, pas seulement CloudSQL
  • Considérez PgBouncer pour les charges production avec très haute concurrence
  • Le gestionnaire pool.on('error') prévient les exceptions non attrapées de faire crasher l'app
  • Réglez la taille max du pool pour correspondre à votre niveau de concurrence plus marge

Skills similaires