UNPKG

pg-cache

Version:

PostgreSQL connection pool LRU cache manager

151 lines (150 loc) 4.62 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.teardownPgPools = exports.close = exports.pgCache = exports.PgPoolCacheManager = void 0; const logger_1 = require("@launchql/logger"); const lru_cache_1 = require("lru-cache"); const log = new logger_1.Logger('pg-cache'); const ONE_HOUR_IN_MS = 1000 * 60 * 60; const ONE_DAY = ONE_HOUR_IN_MS * 24; const ONE_YEAR = ONE_DAY * 366; // Kubernetes sends only SIGTERM on pod shutdown const SYS_EVENTS = ['SIGTERM']; class ManagedPgPool { pool; key; isDisposed = false; disposePromise = null; constructor(pool, key) { this.pool = pool; this.key = key; } async dispose() { if (this.isDisposed) return this.disposePromise; this.isDisposed = true; this.disposePromise = (async () => { try { if (!this.pool.ended) { await this.pool.end(); log.success(`pg.Pool ${this.key} ended.`); } else { log.info(`pg.Pool ${this.key} already ended.`); } } catch (err) { log.error(`Error ending pg.Pool ${this.key}: ${err.message}`); throw err; } })(); return this.disposePromise; } } class PgPoolCacheManager { cleanupTasks = []; closed = false; cleanupCallbacks = new Set(); pgCache = new lru_cache_1.LRUCache({ max: 10, ttl: ONE_YEAR, updateAgeOnGet: true, dispose: (managedPool, key, reason) => { log.debug(`Disposing pg pool [${key}] (${reason})`); this.notifyCleanup(key); this.disposePool(managedPool); } }); // Register a cleanup callback to be called when pools are disposed registerCleanupCallback(callback) { this.cleanupCallbacks.add(callback); // Return unregister function return () => { this.cleanupCallbacks.delete(callback); }; } get(key) { return this.pgCache.get(key)?.pool; } has(key) { return this.pgCache.has(key); } set(key, pool) { if (this.closed) throw new Error(`Cannot add to cache after it has been closed (key: ${key})`); this.pgCache.set(key, new ManagedPgPool(pool, key)); } delete(key) { const managedPool = this.pgCache.get(key); const existed = this.pgCache.delete(key); if (!existed && managedPool) { this.notifyCleanup(key); this.disposePool(managedPool); } } clear() { const entries = [...this.pgCache.entries()]; this.pgCache.clear(); for (const [key, managedPool] of entries) { this.notifyCleanup(key); this.disposePool(managedPool); } } async close() { if (this.closed) return; this.closed = true; this.clear(); await this.waitForDisposals(); } async waitForDisposals() { if (this.cleanupTasks.length === 0) return; const tasks = [...this.cleanupTasks]; this.cleanupTasks = []; await Promise.allSettled(tasks); } notifyCleanup(pgPoolKey) { this.cleanupCallbacks.forEach(callback => { try { callback(pgPoolKey); } catch (err) { log.error(`Error in cleanup callback for pool ${pgPoolKey}: ${err.message}`); } }); } disposePool(managedPool) { if (managedPool.isDisposed) return; const task = managedPool.dispose(); this.cleanupTasks.push(task); } } exports.PgPoolCacheManager = PgPoolCacheManager; // Create the singleton instance exports.pgCache = new PgPoolCacheManager(); // --- Graceful Shutdown --- const closePromise = { promise: null }; const close = async (verbose = false) => { if (closePromise.promise) return closePromise.promise; closePromise.promise = (async () => { if (verbose) log.info('Closing pg cache...'); await exports.pgCache.close(); if (verbose) log.success('PG cache disposed.'); })(); return closePromise.promise; }; exports.close = close; SYS_EVENTS.forEach(event => { process.on(event, () => { log.info(`Received ${event}`); (0, exports.close)(); }); }); const teardownPgPools = async (verbose = false) => { return (0, exports.close)(verbose); }; exports.teardownPgPools = teardownPgPools;