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((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 => { 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((resolve) => targetServer.close(resolve)); console.log('Memory leak test completed successfully'); }); // Run with: node --expose-gc test.memory-leak-check.node.ts tap.start();