UNPKG

pg-cache

Version:
145 lines (144 loc) 4.29 kB
import { Logger } from '@pgpmjs/logger'; import { LRUCache } from 'lru-cache'; const log = new 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; } } export class PgPoolCacheManager { cleanupTasks = []; closed = false; cleanupCallbacks = new Set(); pgCache = new 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); } } // Create the singleton instance export const pgCache = new PgPoolCacheManager(); // --- Graceful Shutdown --- const closePromise = { promise: null }; export const close = async (verbose = false) => { if (closePromise.promise) return closePromise.promise; closePromise.promise = (async () => { if (verbose) log.info('Closing pg cache...'); await pgCache.close(); if (verbose) log.success('PG cache disposed.'); })(); return closePromise.promise; }; SYS_EVENTS.forEach(event => { process.on(event, () => { log.info(`Received ${event}`); close(); }); }); export const teardownPgPools = async (verbose = false) => { return close(verbose); };