150 lines
5.1 KiB
TypeScript
150 lines
5.1 KiB
TypeScript
![]() |
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||
|
import { SmartProxy, createHttpRoute } from '../ts/index.js';
|
||
|
import * as http from 'http';
|
||
|
|
||
|
tap.test('should not have memory leaks in long-running operations', async (tools) => {
|
||
|
// Get initial memory usage
|
||
|
const getMemoryUsage = () => {
|
||
|
if (global.gc) {
|
||
|
global.gc();
|
||
|
}
|
||
|
const usage = process.memoryUsage();
|
||
|
return {
|
||
|
heapUsed: Math.round(usage.heapUsed / 1024 / 1024), // MB
|
||
|
external: Math.round(usage.external / 1024 / 1024), // MB
|
||
|
rss: Math.round(usage.rss / 1024 / 1024) // MB
|
||
|
};
|
||
|
};
|
||
|
|
||
|
// Create a target server
|
||
|
const targetServer = http.createServer((req, res) => {
|
||
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||
|
res.end('OK');
|
||
|
});
|
||
|
await new Promise<void>((resolve) => targetServer.listen(3100, resolve));
|
||
|
|
||
|
// Create the proxy - use non-privileged port
|
||
|
const routes = [
|
||
|
createHttpRoute(['test1.local', 'test2.local', 'test3.local'], { host: 'localhost', port: 3100 }),
|
||
|
];
|
||
|
// Update route to use port 8080
|
||
|
routes[0].match.ports = 8080;
|
||
|
|
||
|
const proxy = new SmartProxy({
|
||
|
ports: [8080], // Use non-privileged port
|
||
|
routes: routes
|
||
|
});
|
||
|
await proxy.start();
|
||
|
|
||
|
console.log('Starting memory leak test...');
|
||
|
const initialMemory = getMemoryUsage();
|
||
|
console.log('Initial memory:', initialMemory);
|
||
|
|
||
|
// Function to make requests
|
||
|
const makeRequest = (domain: string): Promise<void> => {
|
||
|
return new Promise((resolve, reject) => {
|
||
|
const req = http.request({
|
||
|
hostname: 'localhost',
|
||
|
port: 8080,
|
||
|
path: '/',
|
||
|
method: 'GET',
|
||
|
headers: {
|
||
|
'Host': domain
|
||
|
}
|
||
|
}, (res) => {
|
||
|
res.on('data', () => {});
|
||
|
res.on('end', resolve);
|
||
|
});
|
||
|
req.on('error', reject);
|
||
|
req.end();
|
||
|
});
|
||
|
};
|
||
|
|
||
|
// Test 1: Many requests to the same routes
|
||
|
console.log('Test 1: Making 1000 requests to same routes...');
|
||
|
for (let i = 0; i < 1000; i++) {
|
||
|
await makeRequest(`test${(i % 3) + 1}.local`);
|
||
|
if (i % 100 === 0) {
|
||
|
console.log(` Progress: ${i}/1000`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const afterSameRoutesMemory = getMemoryUsage();
|
||
|
console.log('Memory after same routes:', afterSameRoutesMemory);
|
||
|
|
||
|
// Test 2: Many requests to different routes (tests routeContextCache)
|
||
|
console.log('Test 2: Making 1000 requests to different routes...');
|
||
|
for (let i = 0; i < 1000; i++) {
|
||
|
// Create unique domain to test cache growth
|
||
|
await makeRequest(`test${i}.local`);
|
||
|
if (i % 100 === 0) {
|
||
|
console.log(` Progress: ${i}/1000`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const afterDifferentRoutesMemory = getMemoryUsage();
|
||
|
console.log('Memory after different routes:', afterDifferentRoutesMemory);
|
||
|
|
||
|
// Test 3: Check metrics collector memory
|
||
|
console.log('Test 3: Checking metrics collector...');
|
||
|
const stats = proxy.getStats();
|
||
|
console.log(`Active connections: ${stats.getActiveConnections()}`);
|
||
|
console.log(`Total connections: ${stats.getTotalConnections()}`);
|
||
|
console.log(`RPS: ${stats.getRequestsPerSecond()}`);
|
||
|
|
||
|
// Test 4: Many rapid connections (tests requestTimestamps array)
|
||
|
console.log('Test 4: Making 10000 rapid requests...');
|
||
|
const rapidRequests = [];
|
||
|
for (let i = 0; i < 10000; i++) {
|
||
|
rapidRequests.push(makeRequest('test1.local'));
|
||
|
if (i % 1000 === 0) {
|
||
|
// Wait a bit to let some complete
|
||
|
await Promise.all(rapidRequests);
|
||
|
rapidRequests.length = 0;
|
||
|
console.log(` Progress: ${i}/10000`);
|
||
|
}
|
||
|
}
|
||
|
await Promise.all(rapidRequests);
|
||
|
|
||
|
const afterRapidMemory = getMemoryUsage();
|
||
|
console.log('Memory after rapid requests:', afterRapidMemory);
|
||
|
|
||
|
// Force garbage collection and check final memory
|
||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||
|
const finalMemory = getMemoryUsage();
|
||
|
console.log('Final memory:', finalMemory);
|
||
|
|
||
|
// Memory leak checks
|
||
|
const memoryGrowth = finalMemory.heapUsed - initialMemory.heapUsed;
|
||
|
console.log(`Total memory growth: ${memoryGrowth} MB`);
|
||
|
|
||
|
// Check for excessive memory growth
|
||
|
// Allow some growth but not excessive (e.g., more than 50MB for this test)
|
||
|
expect(memoryGrowth).toBeLessThan(50);
|
||
|
|
||
|
// Check specific potential leaks
|
||
|
// 1. Route context cache should not grow unbounded
|
||
|
const routeHandler = proxy.routeConnectionHandler as any;
|
||
|
if (routeHandler.routeContextCache) {
|
||
|
console.log(`Route context cache size: ${routeHandler.routeContextCache.size}`);
|
||
|
// Should not have 1000 entries from different routes test
|
||
|
expect(routeHandler.routeContextCache.size).toBeLessThan(100);
|
||
|
}
|
||
|
|
||
|
// 2. Metrics collector should clean up old timestamps
|
||
|
const metricsCollector = (proxy.getStats() as any);
|
||
|
if (metricsCollector.requestTimestamps) {
|
||
|
console.log(`Request timestamps array length: ${metricsCollector.requestTimestamps.length}`);
|
||
|
// Should not exceed 10000 (the cleanup threshold)
|
||
|
expect(metricsCollector.requestTimestamps.length).toBeLessThanOrEqual(10000);
|
||
|
}
|
||
|
|
||
|
// Cleanup
|
||
|
await proxy.stop();
|
||
|
await new Promise<void>((resolve) => targetServer.close(resolve));
|
||
|
|
||
|
console.log('Memory leak test completed successfully');
|
||
|
});
|
||
|
|
||
|
// Run with: node --expose-gc test.memory-leak-check.node.ts
|
||
|
tap.start();
|