@yihuangdb/storage-object
Version:
A Node.js storage object layer library using Redis OM
309 lines • 11 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConnectionPool = void 0;
const redis_1 = require("redis");
class ConnectionPool {
static instance;
pool = new Map();
waitQueue = [];
options;
cleanupInterval = null;
constructor(options = {}) {
// Filter out undefined values before applying defaults
const cleanOptions = Object.entries(options).reduce((acc, [key, value]) => {
if (value !== undefined) {
acc[key] = value;
}
return acc;
}, {});
this.options = {
poolSize: 10,
poolTimeout: 5000,
idleTimeout: 60000,
minConnections: 2,
preWarm: false,
...cleanOptions,
};
// Pre-warm connections if requested
if (this.options.preWarm) {
this.warmConnections().catch(err => {
console.error('Failed to pre-warm connection pool:', err);
});
}
// Start cleanup interval for idle connections
this.startCleanup();
}
async warmConnections() {
const minConns = Math.min(this.options.minConnections || 2, this.options.poolSize || 10);
const promises = [];
for (let i = 0; i < minConns; i++) {
promises.push(this.createConnection().then(client => {
const id = `pool-${Date.now()}-${Math.random().toString(36).substring(7)}`;
this.pool.set(id, {
client,
inUse: false,
lastUsed: Date.now(),
id
});
}).catch(err => {
console.error(`Failed to pre-warm connection ${i}:`, err);
}));
}
await Promise.all(promises);
}
static getInstance(options) {
if (!ConnectionPool.instance) {
ConnectionPool.instance = new ConnectionPool(options);
}
return ConnectionPool.instance;
}
static reset() {
if (ConnectionPool.instance) {
ConnectionPool.instance.shutdown().catch(err => {
console.error('Error during pool reset:', err);
});
ConnectionPool.instance = null;
}
}
async createConnection() {
const config = {};
if (this.options.url) {
config.url = this.options.url;
}
else {
config.socket = {
host: this.options.host || 'localhost',
port: this.options.port || 6379,
};
}
if (this.options.password) {
config.password = this.options.password;
}
if (this.options.database !== undefined) {
config.database = this.options.database;
}
// Add connection name for debugging
config.name = this.options.connectionName || 'storage-object-pool';
// Retry strategy with exponential backoff
config.socket = config.socket || {};
config.socket.reconnectStrategy = (retries) => {
const maxRetries = this.options.maxRetries || 10;
const delay = this.options.retryDelay || 50;
if (retries > maxRetries) {
return new Error('Max connection retries reached');
}
// Exponential backoff with max 3s delay
return Math.min(retries * delay, 3000);
};
// Enable offline queue by default
config.commandsQueueMaxLength = this.options.enableOfflineQueue !== false ? 1000 : 0;
const client = (0, redis_1.createClient)(config);
client.on('error', (err) => {
console.error('Redis Pool Client Error:', err);
});
await client.connect();
return client;
}
async acquire() {
// First, try to find an idle connection
for (const [id, conn] of this.pool.entries()) {
if (!conn.inUse && conn.client.isOpen) {
conn.inUse = true;
conn.lastUsed = Date.now();
return conn.client;
}
}
// If pool is not full, create a new connection
if (this.pool.size < this.options.poolSize) {
const client = await this.createConnection();
const id = `conn-${Date.now()}-${Math.random().toString(36).substring(7)}`;
const conn = {
client,
inUse: true,
lastUsed: Date.now(),
id,
};
this.pool.set(id, conn);
return client;
}
// Pool is full, wait for a connection to become available
return new Promise((resolve, reject) => {
let timeoutHandle;
const waitingRequest = {
resolve: (client) => {
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
resolve(client);
},
reject: (error) => {
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
reject(error);
},
};
timeoutHandle = setTimeout(() => {
const index = this.waitQueue.indexOf(waitingRequest);
if (index > -1) {
this.waitQueue.splice(index, 1);
}
reject(new Error(`Connection pool timeout after ${this.options.poolTimeout}ms`));
}, this.options.poolTimeout);
this.waitQueue.push(waitingRequest);
});
}
release(client) {
// Find the connection in the pool
for (const [id, conn] of this.pool.entries()) {
if (conn.client === client) {
conn.inUse = false;
conn.lastUsed = Date.now();
// If there are waiting requests, give them this connection
if (this.waitQueue.length > 0) {
const waitingRequest = this.waitQueue.shift();
if (waitingRequest) {
conn.inUse = true;
waitingRequest.resolve(client);
}
}
return;
}
}
}
async warmUp(connections = 5) {
// Pre-create connections to warm up the pool
const warmupPromises = [];
for (let i = 0; i < Math.min(connections, this.options.poolSize); i++) {
warmupPromises.push(this.acquire().then(client => {
// Immediately release the connection back to pool
this.release(client);
}).catch(err => {
console.warn(`Failed to warm up connection ${i}:`, err);
}));
}
await Promise.all(warmupPromises);
}
async destroy(client) {
// Find and remove the connection from the pool
for (const [id, conn] of this.pool.entries()) {
if (conn.client === client) {
this.pool.delete(id);
try {
if (client.isOpen) {
await client.quit();
}
}
catch (error) {
console.error('Error closing connection:', error);
}
return;
}
}
}
startCleanup() {
if (this.cleanupInterval) {
return;
}
// Run cleanup every 30 seconds
this.cleanupInterval = setInterval(() => {
const now = Date.now();
const idleTimeout = this.options.idleTimeout;
for (const [id, conn] of this.pool.entries()) {
// Remove idle connections that haven't been used recently
if (!conn.inUse && (now - conn.lastUsed) > idleTimeout) {
this.pool.delete(id);
if (conn.client.isOpen) {
conn.client.quit().catch(err => {
console.error('Error closing idle connection:', err);
});
}
}
}
}, 30000);
}
/**
* Get pool statistics for monitoring
* @returns Pool statistics including active connections, idle connections, etc.
*/
getStats() {
let activeConnections = 0;
let idleConnections = 0;
for (const conn of this.pool.values()) {
if (conn.inUse) {
activeConnections++;
}
else {
idleConnections++;
}
}
return {
totalConnections: this.pool.size,
activeConnections,
idleConnections,
waitingRequests: this.waitQueue.length,
poolSize: this.options.poolSize,
};
}
/**
* Check if the pool is healthy
* @returns true if pool is operating normally
*/
isHealthy() {
const stats = this.getStats();
return stats.totalConnections > 0 && stats.waitingRequests < stats.poolSize;
}
/**
* Update pool options dynamically
* @param options - New pool options to apply
*/
updateOptions(options) {
this.options = { ...this.options, ...options };
}
/**
* Force cleanup of idle connections
*/
forceCleanup() {
const now = Date.now();
const idleTimeout = 0; // Force immediate cleanup
for (const [id, conn] of this.pool.entries()) {
if (!conn.inUse) {
this.pool.delete(id);
if (conn.client.isOpen) {
conn.client.quit().catch(err => {
console.error('Error closing idle connection during forced cleanup:', err);
});
}
}
}
}
async shutdown() {
// Stop cleanup interval
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}
// Clear wait queue and reject all waiting requests
const waitingRequests = [...this.waitQueue];
this.waitQueue = [];
// Reject all waiting promises
waitingRequests.forEach(request => {
request.reject(new Error('Connection pool shutting down'));
});
// Close all connections
const closePromises = [];
for (const [id, conn] of this.pool.entries()) {
if (conn.client.isOpen) {
closePromises.push(conn.client.quit().then(() => {
// Successfully closed
}).catch(err => {
console.error('Error closing connection during shutdown:', err);
}));
}
}
await Promise.all(closePromises);
this.pool.clear();
}
}
exports.ConnectionPool = ConnectionPool;
//# sourceMappingURL=connection-pool.js.map
;