redis-smq
Version:
A high-performance, reliable, and scalable message queue for Node.js.
364 lines • 13.5 kB
JavaScript
"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