185 lines
6.3 KiB
JavaScript
185 lines
6.3 KiB
JavaScript
/**
|
|
* Connection Pool for HTTP/2 and HTTP/3 Proxies
|
|
* Provides connection reuse to reduce latency by 20-30%
|
|
*/
|
|
import http2 from 'http2';
|
|
import { logger } from './logger.js';
|
|
export class ConnectionPool {
|
|
pools = new Map();
|
|
config;
|
|
cleanupInterval;
|
|
constructor(config = {}) {
|
|
this.config = {
|
|
maxSize: config.maxSize || 10,
|
|
maxIdleTime: config.maxIdleTime || 60000, // 60 seconds
|
|
acquireTimeout: config.acquireTimeout || 5000 // 5 seconds
|
|
};
|
|
// Cleanup expired connections every 30 seconds
|
|
this.cleanupInterval = setInterval(() => {
|
|
this.cleanup();
|
|
}, 30000);
|
|
}
|
|
async acquire(host) {
|
|
const pool = this.pools.get(host) || [];
|
|
const now = Date.now();
|
|
// Find idle, non-expired connection
|
|
const idle = pool.find(c => !c.busy &&
|
|
!this.isExpired(c, now) &&
|
|
!c.session.closed &&
|
|
!c.session.destroyed);
|
|
if (idle) {
|
|
idle.busy = true;
|
|
idle.lastUsed = now;
|
|
logger.debug('Reusing pooled connection', { host, poolSize: pool.length });
|
|
return idle.session;
|
|
}
|
|
// Create new if under limit
|
|
if (pool.length < this.config.maxSize) {
|
|
const session = await this.createConnection(host);
|
|
const conn = {
|
|
session,
|
|
host,
|
|
busy: true,
|
|
createdAt: now,
|
|
lastUsed: now
|
|
};
|
|
pool.push(conn);
|
|
this.pools.set(host, pool);
|
|
logger.debug('Created new pooled connection', {
|
|
host,
|
|
poolSize: pool.length,
|
|
maxSize: this.config.maxSize
|
|
});
|
|
return session;
|
|
}
|
|
// Wait for available connection
|
|
logger.debug('Pool full, waiting for connection', { host, poolSize: pool.length });
|
|
return this.waitForConnection(host);
|
|
}
|
|
async release(session, host) {
|
|
const pool = this.pools.get(host);
|
|
if (!pool)
|
|
return;
|
|
const conn = pool.find(c => c.session === session);
|
|
if (conn) {
|
|
conn.busy = false;
|
|
conn.lastUsed = Date.now();
|
|
logger.debug('Released pooled connection', { host });
|
|
}
|
|
}
|
|
async createConnection(host) {
|
|
return new Promise((resolve, reject) => {
|
|
const session = http2.connect(host, {
|
|
maxSessionMemory: 10 // 10MB per session
|
|
});
|
|
session.once('connect', () => {
|
|
logger.info('HTTP/2 session connected', { host });
|
|
resolve(session);
|
|
});
|
|
session.once('error', (error) => {
|
|
logger.error('HTTP/2 session error', { host, error: error.message });
|
|
reject(error);
|
|
});
|
|
// Cleanup on session close
|
|
session.once('close', () => {
|
|
this.removeConnection(session, host);
|
|
});
|
|
});
|
|
}
|
|
async waitForConnection(host) {
|
|
const startTime = Date.now();
|
|
return new Promise((resolve, reject) => {
|
|
const checkInterval = setInterval(() => {
|
|
const pool = this.pools.get(host);
|
|
if (!pool) {
|
|
clearInterval(checkInterval);
|
|
reject(new Error('Pool disappeared'));
|
|
return;
|
|
}
|
|
const now = Date.now();
|
|
const available = pool.find(c => !c.busy &&
|
|
!this.isExpired(c, now) &&
|
|
!c.session.closed);
|
|
if (available) {
|
|
clearInterval(checkInterval);
|
|
available.busy = true;
|
|
available.lastUsed = now;
|
|
resolve(available.session);
|
|
return;
|
|
}
|
|
if (now - startTime > this.config.acquireTimeout) {
|
|
clearInterval(checkInterval);
|
|
reject(new Error('Connection acquire timeout'));
|
|
}
|
|
}, 100); // Check every 100ms
|
|
});
|
|
}
|
|
isExpired(conn, now) {
|
|
return (now - conn.lastUsed) > this.config.maxIdleTime;
|
|
}
|
|
removeConnection(session, host) {
|
|
const pool = this.pools.get(host);
|
|
if (!pool)
|
|
return;
|
|
const index = pool.findIndex(c => c.session === session);
|
|
if (index !== -1) {
|
|
pool.splice(index, 1);
|
|
logger.debug('Removed closed connection from pool', { host, poolSize: pool.length });
|
|
}
|
|
if (pool.length === 0) {
|
|
this.pools.delete(host);
|
|
}
|
|
}
|
|
cleanup() {
|
|
const now = Date.now();
|
|
let removed = 0;
|
|
for (const [host, pool] of this.pools.entries()) {
|
|
const before = pool.length;
|
|
// Remove expired and closed connections
|
|
const active = pool.filter(c => {
|
|
if (this.isExpired(c, now) || c.session.closed || c.session.destroyed) {
|
|
if (!c.session.closed) {
|
|
c.session.close();
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
removed += before - active.length;
|
|
if (active.length === 0) {
|
|
this.pools.delete(host);
|
|
}
|
|
else {
|
|
this.pools.set(host, active);
|
|
}
|
|
}
|
|
if (removed > 0) {
|
|
logger.debug('Cleaned up expired connections', { removed });
|
|
}
|
|
}
|
|
destroy() {
|
|
clearInterval(this.cleanupInterval);
|
|
for (const [host, pool] of this.pools.entries()) {
|
|
for (const conn of pool) {
|
|
if (!conn.session.closed) {
|
|
conn.session.close();
|
|
}
|
|
}
|
|
}
|
|
this.pools.clear();
|
|
logger.info('Connection pool destroyed');
|
|
}
|
|
getStats() {
|
|
const stats = {};
|
|
for (const [host, pool] of this.pools.entries()) {
|
|
const busy = pool.filter(c => c.busy).length;
|
|
stats[host] = {
|
|
total: pool.length,
|
|
busy,
|
|
idle: pool.length - busy
|
|
};
|
|
}
|
|
return stats;
|
|
}
|
|
}
|
|
//# sourceMappingURL=connection-pool.js.map
|