UNPKG

redis-smq

Version:

A high-performance, reliable, and scalable message queue for Node.js.

364 lines 13.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RedisConnectionPool = void 0; const redis_smq_common_1 = require("redis-smq-common"); const redis_client_js_1 = require("../redis-client/redis-client.js"); const connection_pool_js_1 = require("./types/connection-pool.js"); class RedisConnectionPool extends redis_smq_common_1.EventEmitter { constructor(redisConfig, poolConfig = {}) { var _a, _b, _c, _d, _e; super(); this.connections = new Map(); this.waitingQueue = []; this.initialized = false; this.shuttingDown = false; this.reapTimer = null; this.connectionIdCounter = 0; this.redisConfig = redisConfig; this.poolConfig = { min: (_a = poolConfig.min) !== null && _a !== void 0 ? _a : 2, max: (_b = poolConfig.max) !== null && _b !== void 0 ? _b : Infinity, acquireTimeoutMillis: (_c = poolConfig.acquireTimeoutMillis) !== null && _c !== void 0 ? _c : 5000, idleTimeoutMillis: (_d = poolConfig.idleTimeoutMillis) !== null && _d !== void 0 ? _d : 30000, reapIntervalMillis: (_e = poolConfig.reapIntervalMillis) !== null && _e !== void 0 ? _e : 10000, }; if (this.poolConfig.min < 0) { throw new Error('Pool min size cannot be negative'); } if (this.poolConfig.max < this.poolConfig.min) { throw new Error('Pool max size cannot be less than min size'); } } static initialize(redisConfig, poolConfig = {}, cb) { if (RedisConnectionPool.instance) { throw new Error('RedisConnectionPool already initialized'); } const instance = new RedisConnectionPool(redisConfig, poolConfig); instance.createMinimumConnections((err) => { if (err) return cb(err); instance.initialized = true; instance.startReaper(); RedisConnectionPool.instance = instance; cb(null, instance); }); } static getInstance() { if (!RedisConnectionPool.instance) { throw new Error('RedisConnectionPool is not initialized. Use initialize() before calling getInstance()'); } return RedisConnectionPool.instance; } static shutdown(cb) { if (RedisConnectionPool.instance) { return RedisConnectionPool.instance.shutdown(() => { RedisConnectionPool.instance = null; cb(); }); } cb(); } releaseExclusiveConnection(connection) { connection.inUse = false; connection.exclusivelyAcquired = false; connection.acquisitionMode = connection_pool_js_1.ERedisConnectionAcquisitionMode.EXCLUSIVE; connection.lastUsed = Date.now(); this.emit('connectionReleased', connection); this.processWaitingQueue(); } releaseSharedConnection(connection) { if (connection.sharedUsers > 0) { connection.sharedUsers--; connection.lastUsed = Date.now(); if (connection.sharedUsers === 0) { connection.inUse = false; this.emit('connectionReleased', connection); this.processWaitingQueue(); } } } createMinimumConnections(cb) { if (this.poolConfig.min === 0) { return cb(); } let created = 0; let hasError = false; const createNext = () => { if (hasError || created >= this.poolConfig.min) { return cb(hasError ? new Error('Failed to create minimum connections') : undefined); } this.createConnection((err) => { if (err) { hasError = true; return cb(err); } created++; createNext(); }); }; createNext(); } createConnection(cb) { const redisClient = new redis_client_js_1.RedisClient(this.redisConfig); redisClient.init((err) => { if (err) return cb(err); const clientInstance = redisClient.getInstance(); const connectionId = this.generateConnectionId(); const connection = { client: clientInstance, createdAt: Date.now(), lastUsed: Date.now(), inUse: false, sharedUsers: 0, exclusivelyAcquired: false, acquisitionMode: connection_pool_js_1.ERedisConnectionAcquisitionMode.EXCLUSIVE, }; clientInstance.__poolConnectionId = connectionId; clientInstance.on('error', (err) => { this.handleConnectionError(connectionId, err); }); this.connections.set(connectionId, connection); this.emit('connectionCreated', connection); cb(null, connection); }); } findAvailableConnection(mode) { for (const connection of this.connections.values()) { if (mode === connection_pool_js_1.ERedisConnectionAcquisitionMode.EXCLUSIVE) { if (!connection.inUse) { return connection; } } else if (mode === connection_pool_js_1.ERedisConnectionAcquisitionMode.SHARED) { if (!connection.exclusivelyAcquired) { return connection; } } } return null; } prepareConnection(connection, mode, cb) { connection.lastUsed = Date.now(); connection.acquisitionMode = mode; if (mode === connection_pool_js_1.ERedisConnectionAcquisitionMode.EXCLUSIVE) { connection.inUse = true; connection.exclusivelyAcquired = true; connection.sharedUsers = 0; } else { connection.inUse = true; connection.exclusivelyAcquired = false; connection.sharedUsers++; } this.emit('connectionAcquired', connection); cb(null, connection.client); } processWaitingQueue() { if (this.waitingQueue.length === 0) return; for (let i = 0; i < this.waitingQueue.length; i++) { const waitingItem = this.waitingQueue[i]; const availableConnection = this.findAvailableConnection(waitingItem.mode); if (availableConnection) { this.waitingQueue.splice(i, 1); this.prepareConnection(availableConnection, waitingItem.mode, waitingItem.callback); break; } } } startReaper() { if (this.reapTimer) return; this.reapTimer = setInterval(() => { this.reapIdleConnections(); }, this.poolConfig.reapIntervalMillis); } stopReaper() { if (this.reapTimer) { clearInterval(this.reapTimer); this.reapTimer = null; } } reapIdleConnections() { if (this.shuttingDown) return; const now = Date.now(); const connectionsToDestroy = []; for (const [connectionId, connection] of this.connections) { const isIdle = !connection.inUse && !connection.exclusivelyAcquired && connection.sharedUsers === 0; if (isIdle && this.connections.size > this.poolConfig.min && now - connection.lastUsed > this.poolConfig.idleTimeoutMillis) { connectionsToDestroy.push(connectionId); } } connectionsToDestroy.forEach((connectionId) => { this.destroyConnection(connectionId); }); } destroyConnection(connectionId) { const connection = this.connections.get(connectionId); if (!connection) return; this.connections.delete(connectionId); connection.client.halt(() => { this.emit('connectionDestroyed', connection); }); } handleConnectionError(connectionId, error) { this.emit('connectionError', error); this.destroyConnection(connectionId); if (this.connections.size < this.poolConfig.min && !this.shuttingDown) { this.createConnection((err) => { if (err) { this.emit('connectionError', new Error(`Failed to replace failed connection: ${err.message}`)); } }); } } getConnection(client) { const connectionId = this.getConnectionId(client); if (!connectionId) { this.emit('connectionError', new Error('Attempted to retrieve unknown connection')); return; } const connection = this.connections.get(connectionId); if (!connection) { this.emit('connectionError', new Error('Connection not found in pool')); } return connection; } getConnectionId(client) { return client.__poolConnectionId || null; } generateConnectionId() { return `conn_${++this.connectionIdCounter}_${Date.now()}`; } acquire(mode, cb) { if (!this.initialized) { return cb(new Error('Connection pool not initialized')); } if (this.shuttingDown) { return cb(new Error('Connection pool is shutting down')); } const availableConnection = this.findAvailableConnection(mode); if (availableConnection) { return this.prepareConnection(availableConnection, mode, cb); } if (this.connections.size < this.poolConfig.max) { return this.createConnection((err, connection) => { if (err) return cb(err); if (!connection) return cb(new Error('Failed to create connection')); this.prepareConnection(connection, mode, cb); }); } this.waitingQueue.push({ callback: cb, timestamp: Date.now(), mode, }); setTimeout(() => { const index = this.waitingQueue.findIndex((item) => item.callback === cb); if (index !== -1) { this.waitingQueue.splice(index, 1); cb(new Error('Connection acquire timeout')); } }, this.poolConfig.acquireTimeoutMillis); } release(client) { const connection = this.getConnection(client); if (!connection) return; if (connection.exclusivelyAcquired) { return this.releaseExclusiveConnection(connection); } this.releaseSharedConnection(connection); } destroy(client, cb) { const connectionId = this.getConnectionId(client); if (!connectionId) return cb(new Error('Attempted to destroy unknown connection')); this.destroyConnection(connectionId); const currentPoolSize = this.connections.size; const minConnections = this.poolConfig.min; if (currentPoolSize < minConnections) { const connectionsToCreate = minConnections - currentPoolSize; for (let i = 0; i < connectionsToCreate; i++) { this.createConnection((err) => { if (err) { this.emit('connectionError', err); } }); } } cb(); } getStats() { let exclusively = 0; let shared = 0; let sharedUsers = 0; let inUse = 0; for (const connection of this.connections.values()) { if (connection.exclusivelyAcquired) { exclusively++; inUse++; } else if (connection.sharedUsers > 0) { shared++; sharedUsers += connection.sharedUsers; inUse++; } } return { total: this.connections.size, available: this.connections.size - inUse, inUse, exclusively, shared, sharedUsers, waiting: this.waitingQueue.length, }; } shutdown(cb) { if (this.shuttingDown) { return cb(new Error('Connection pool already shutting down')); } this.shuttingDown = true; this.stopReaper(); while (this.waitingQueue.length > 0) { const item = this.waitingQueue.shift(); if (item) { item.callback(new Error('Connection pool shutting down')); } } const connectionPromises = []; for (const [, connection] of this.connections) { connectionPromises.push(new Promise((resolve) => { connection.client.halt(() => { this.emit('connectionDestroyed', connection); resolve(); }); })); } Promise.all(connectionPromises) .then(() => { this.connections.clear(); this.initialized = false; this.shuttingDown = false; cb(); }) .catch((err) => { cb(err); }); } } exports.RedisConnectionPool = RedisConnectionPool; RedisConnectionPool.instance = null; //# sourceMappingURL=redis-connection-pool.js.map