import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as net from 'net'; import * as plugins from '../ts/plugins.js'; // Import SmartProxy and configurations import { SmartProxy } from '../ts/index.js'; tap.test('should handle rapid connection retries without leaking connections', async () => { console.log('\n=== Testing Rapid Connection Retry Cleanup ==='); // Create a SmartProxy instance const proxy = new SmartProxy({ ports: [8550], enableDetailedLogging: false, maxConnectionLifetime: 10000, socketTimeout: 5000, routes: [{ name: 'test-route', match: { ports: 8550 }, action: { type: 'forward', target: { host: 'localhost', port: 9999 // Non-existent port to force connection failures } } }] }); // Start the proxy await proxy.start(); console.log('✓ Proxy started on port 8550'); // Helper to get active connection count const getActiveConnections = () => { const connectionManager = (proxy as any).connectionManager; return connectionManager ? connectionManager.getConnectionCount() : 0; }; // Track connection counts const connectionCounts: number[] = []; const initialCount = getActiveConnections(); console.log(`Initial connection count: ${initialCount}`); // Simulate rapid retries const retryCount = 20; const retryDelay = 50; // 50ms between retries let successfulConnections = 0; let failedConnections = 0; console.log(`\nSimulating ${retryCount} rapid connection attempts...`); for (let i = 0; i < retryCount; i++) { await new Promise((resolve) => { const client = new net.Socket(); client.on('error', () => { failedConnections++; client.destroy(); resolve(); }); client.on('close', () => { resolve(); }); client.connect(8550, 'localhost', () => { // Send some data to trigger routing client.write('GET / HTTP/1.1\r\nHost: test.com\r\n\r\n'); successfulConnections++; }); // Force close after a short time setTimeout(() => { if (!client.destroyed) { client.destroy(); } }, 100); }); // Small delay between retries await new Promise(resolve => setTimeout(resolve, retryDelay)); // Check connection count after each attempt const currentCount = getActiveConnections(); connectionCounts.push(currentCount); if ((i + 1) % 5 === 0) { console.log(`After ${i + 1} attempts: ${currentCount} active connections`); } } console.log(`\nConnection attempts complete:`); console.log(`- Successful: ${successfulConnections}`); console.log(`- Failed: ${failedConnections}`); // Wait a bit for any pending cleanups console.log('\nWaiting for cleanup...'); await new Promise(resolve => setTimeout(resolve, 1000)); // Check final connection count const finalCount = getActiveConnections(); console.log(`\nFinal connection count: ${finalCount}`); // Analyze connection count trend const maxCount = Math.max(...connectionCounts); const avgCount = connectionCounts.reduce((a, b) => a + b, 0) / connectionCounts.length; console.log(`\nConnection count statistics:`); console.log(`- Maximum: ${maxCount}`); console.log(`- Average: ${avgCount.toFixed(2)}`); console.log(`- Initial: ${initialCount}`); console.log(`- Final: ${finalCount}`); // Stop the proxy await proxy.stop(); console.log('\n✓ Proxy stopped'); // Verify results expect(finalCount).toEqual(initialCount); expect(maxCount).toBeLessThan(10); // Should not accumulate many connections console.log('\n✅ PASS: Connection cleanup working correctly under rapid retries!'); }); tap.test('should handle routing failures without leaking connections', async () => { console.log('\n=== Testing Routing Failure Cleanup ==='); // Create a SmartProxy instance with no routes const proxy = new SmartProxy({ ports: [8551], enableDetailedLogging: false, maxConnectionLifetime: 10000, socketTimeout: 5000, routes: [] // No routes - all connections will fail routing }); // Start the proxy await proxy.start(); console.log('✓ Proxy started on port 8551 with no routes'); // Helper to get active connection count const getActiveConnections = () => { const connectionManager = (proxy as any).connectionManager; return connectionManager ? connectionManager.getConnectionCount() : 0; }; const initialCount = getActiveConnections(); console.log(`Initial connection count: ${initialCount}`); // Create multiple connections that will fail routing const connectionPromises = []; for (let i = 0; i < 10; i++) { connectionPromises.push(new Promise((resolve) => { const client = new net.Socket(); client.on('error', () => { client.destroy(); resolve(); }); client.on('close', () => { resolve(); }); client.connect(8551, 'localhost', () => { // Send data to trigger routing (which will fail) client.write('GET / HTTP/1.1\r\nHost: test.com\r\n\r\n'); }); // Force close after a short time setTimeout(() => { if (!client.destroyed) { client.destroy(); } resolve(); }, 500); })); } // Wait for all connections to complete await Promise.all(connectionPromises); console.log('✓ All connection attempts completed'); // Wait for cleanup await new Promise(resolve => setTimeout(resolve, 500)); const finalCount = getActiveConnections(); console.log(`Final connection count: ${finalCount}`); // Stop the proxy await proxy.stop(); console.log('✓ Proxy stopped'); // Verify no connections leaked expect(finalCount).toEqual(initialCount); console.log('\n✅ PASS: Routing failures cleaned up correctly!'); }); tap.start();