persistent connections

This commit is contained in:
Para Dox
2025-06-02 01:37:09 +07:00
parent 6f830e77cd
commit eb50b33aa3

View File

@@ -4,6 +4,10 @@ const logger = require('./logger');
const crypto = require('crypto'); const crypto = require('crypto');
const http = require('http'); const http = require('http');
const https = require('https'); const https = require('https');
const dns = require('dns');
const { promisify } = require('util');
const dnsLookup = promisify(dns.lookup);
// Create HTTP agents with DNS caching disabled and connection pooling // Create HTTP agents with DNS caching disabled and connection pooling
const httpAgent = new http.Agent({ const httpAgent = new http.Agent({
@@ -51,60 +55,181 @@ class RPCProxy {
this.compareEndpoint = this.primaryEndpoint; this.compareEndpoint = this.primaryEndpoint;
} }
// Create persistent axios clients
this.clients = new Map();
this.createPersistentClient(this.primaryEndpoint);
this.createPersistentClient(this.secondaryEndpoint);
// Track DNS resolution for each endpoint
this.dnsCache = new Map();
// Initialize socket creation time tagging
this.tagSocketCreationTime();
// Start DNS refresh timer // Start DNS refresh timer
this.startDnsRefreshTimer(); this.startDnsRefreshTimer();
} }
// Create a fresh axios instance for each request to ensure DNS resolution // Create a persistent axios client for an endpoint
createClient(baseURL) { createPersistentClient(baseURL) {
const isHttps = baseURL.startsWith('https://'); const isHttps = baseURL.startsWith('https://');
return axios.create({ const client = axios.create({
baseURL, baseURL,
timeout: config.requestTimeout, timeout: config.requestTimeout,
maxContentLength: Infinity, maxContentLength: Infinity,
maxBodyLength: Infinity, maxBodyLength: Infinity,
httpAgent: isHttps ? undefined : httpAgent, httpAgent: isHttps ? undefined : httpAgent,
httpsAgent: isHttps ? httpsAgent : undefined, httpsAgent: isHttps ? httpsAgent : undefined,
// Disable axios's built-in DNS caching
transformRequest: [
(data, headers) => {
// Add timestamp to force fresh connections periodically
headers['X-Request-Time'] = Date.now().toString();
return data;
},
...axios.defaults.transformRequest
]
}); });
this.clients.set(baseURL, client);
logger.info({ baseURL }, 'Created persistent axios client');
return client;
}
// Get or create a client for an endpoint
getClient(endpoint) {
let client = this.clients.get(endpoint);
if (!client) {
client = this.createPersistentClient(endpoint);
}
return client;
}
// Extract hostname from URL
getHostnameFromUrl(url) {
try {
const urlObj = new URL(url);
return urlObj.hostname;
} catch (e) {
logger.error({ url, error: e.message }, 'Failed to parse URL');
return null;
}
}
// Check if DNS has changed for an endpoint
async checkDnsChange(endpoint) {
const hostname = this.getHostnameFromUrl(endpoint);
if (!hostname) return false;
try {
const { address } = await dnsLookup(hostname);
const cachedAddress = this.dnsCache.get(hostname);
if (!cachedAddress) {
// First time checking this hostname
this.dnsCache.set(hostname, address);
logger.info({ hostname, address }, 'Initial DNS resolution cached');
return false;
}
if (cachedAddress !== address) {
// DNS has changed
logger.info({
hostname,
oldAddress: cachedAddress,
newAddress: address
}, 'DNS change detected');
this.dnsCache.set(hostname, address);
return true;
}
return false;
} catch (error) {
logger.error({ hostname, error: error.message }, 'DNS lookup failed');
return false;
}
}
// Recreate client for an endpoint if DNS changed
async refreshClientIfDnsChanged(endpoint) {
const dnsChanged = await this.checkDnsChange(endpoint);
if (dnsChanged) {
const hostname = this.getHostnameFromUrl(endpoint);
const isHttps = endpoint.startsWith('https://');
const agent = isHttps ? httpsAgent : httpAgent;
// Only destroy sockets for this specific hostname
if (agent.sockets) {
Object.keys(agent.sockets).forEach(name => {
if (name.includes(hostname)) {
agent.sockets[name].forEach(socket => socket.destroy());
delete agent.sockets[name];
}
});
}
if (agent.freeSockets) {
Object.keys(agent.freeSockets).forEach(name => {
if (name.includes(hostname)) {
agent.freeSockets[name].forEach(socket => socket.destroy());
delete agent.freeSockets[name];
}
});
}
// Recreate the client for this endpoint
this.createPersistentClient(endpoint);
logger.info({ endpoint }, 'Recreated client due to DNS change');
}
} }
startDnsRefreshTimer() { startDnsRefreshTimer() {
// Only clear IDLE sockets, not active ones setInterval(async () => {
setInterval(() => { logger.debug('Checking for DNS changes');
logger.debug('Refreshing DNS cache - clearing idle sockets only');
// Only destroy free (idle) sockets, not active ones // Check DNS for all known endpoints
if (httpAgent.freeSockets) { const endpoints = [this.primaryEndpoint, this.secondaryEndpoint];
Object.keys(httpAgent.freeSockets).forEach(name => { const uniqueEndpoints = [...new Set(endpoints)];
if (httpAgent.freeSockets[name]) {
httpAgent.freeSockets[name].forEach(socket => { for (const endpoint of uniqueEndpoints) {
socket.destroy(); await this.refreshClientIfDnsChanged(endpoint);
});
}
});
} }
if (httpsAgent.freeSockets) { // Clean up very old idle sockets (older than 5 minutes)
Object.keys(httpsAgent.freeSockets).forEach(name => { const maxIdleTime = 5 * 60 * 1000;
if (httpsAgent.freeSockets[name]) { const now = Date.now();
httpsAgent.freeSockets[name].forEach(socket => {
[httpAgent, httpsAgent].forEach(agent => {
if (agent.freeSockets) {
Object.keys(agent.freeSockets).forEach(name => {
if (agent.freeSockets[name]) {
agent.freeSockets[name] = agent.freeSockets[name].filter(socket => {
const socketAge = now - (socket._createdTime || now);
if (socketAge > maxIdleTime) {
socket.destroy(); socket.destroy();
logger.debug({ name, socketAge }, 'Destroyed old idle socket');
return false;
}
return true;
}); });
if (agent.freeSockets[name].length === 0) {
delete agent.freeSockets[name];
}
} }
}); });
} }
});
}, config.dnsRefreshInterval); }, config.dnsRefreshInterval);
} }
// Tag sockets with creation time for cleanup
tagSocketCreationTime() {
[httpAgent, httpsAgent].forEach(agent => {
const originalCreateConnection = agent.createConnection.bind(agent);
agent.createConnection = function(options, callback) {
return originalCreateConnection(options, (err, socket) => {
if (!err && socket) {
socket._createdTime = Date.now();
}
callback(err, socket);
});
};
});
}
generateRequestId() { generateRequestId() {
return crypto.randomBytes(16).toString('hex'); return crypto.randomBytes(16).toString('hex');
} }
@@ -357,8 +482,8 @@ class RPCProxy {
const streamMethodStartTime = process.hrtime.bigint(); const streamMethodStartTime = process.hrtime.bigint();
try { try {
// Create fresh client for this request // Get persistent client for this endpoint
const client = this.createClient(this.streamEndpoint); const client = this.getClient(this.streamEndpoint);
// Get the original Accept-Encoding from the client request // Get the original Accept-Encoding from the client request
const acceptEncoding = res.req.headers['accept-encoding'] || 'identity'; const acceptEncoding = res.req.headers['accept-encoding'] || 'identity';
@@ -701,8 +826,8 @@ class RPCProxy {
try { try {
const compareStart = Date.now(); const compareStart = Date.now();
const compareStartHR = process.hrtime.bigint(); // Add high-resolution timing const compareStartHR = process.hrtime.bigint(); // Add high-resolution timing
// Create fresh client for this request // Get persistent client for this endpoint
const client = this.createClient(this.compareEndpoint); const client = this.getClient(this.compareEndpoint);
const response = await client.post('/', requestBody, { const response = await client.post('/', requestBody, {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',