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 ECONNRESETConnection terminatedconnection already closedremaining 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 à :
- Détecter les erreurs de réinitialisation de connexion
- Appeler
pool.end()pour fermer toutes les connexions - Créer une nouvelle instance de pool
- Vérifier que la nouvelle connexion fonctionne
- Puis réessayer l'opération
Considérations spécifiques à CloudSQL
-
Le niveau d'instance importe : db-f1-micro ne supporte ~25 connexions. Utilisez db-g1-small ou supérieur pour les charges concurrentes.
-
Vérifiez max_connections :
gcloud sql instances describe INSTANCE --format='value(settings.tier)' -
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 :
- Exécutez une charge haute concurrence
- Déclenchez intentionnellement une réinitialisation de connexion (redémarrez proxy, attendez timeout d'inactivité)
- Vérifiez que les opérations reprennent avec le log "Reconnecting to database..."
- 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
maxdu pool pour correspondre à votre niveau de concurrence plus marge