import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as net from 'net'; import { SmartProxy } from '../ts/index.js'; import * as plugins from '../ts/plugins.js'; tap.test('stuck connection cleanup - verify connections to hanging backends are cleaned up', async (tools) => { console.log('\n=== Stuck Connection Cleanup Test ==='); console.log('Purpose: Verify that connections to backends that accept but never respond are cleaned up'); // Create a hanging backend that accepts connections but never responds let backendConnections = 0; const hangingBackend = net.createServer((socket) => { backendConnections++; console.log(`Hanging backend: Connection ${backendConnections} received`); // Accept the connection but never send any data back // This simulates a hung backend service }); await new Promise((resolve) => { hangingBackend.listen(9997, () => { console.log('✓ Hanging backend started on port 9997'); resolve(); }); }); // Create proxy that forwards to hanging backend const proxy = new SmartProxy({ routes: [{ name: 'to-hanging-backend', match: { ports: 8589 }, action: { type: 'forward', target: { host: 'localhost', port: 9997 } } }], keepAlive: true, enableDetailedLogging: false, inactivityTimeout: 5000, // 5 second inactivity check interval for faster testing }); await proxy.start(); console.log('✓ Proxy started on port 8589'); // Create connections that will get stuck console.log('\n--- Creating connections to hanging backend ---'); const clients: net.Socket[] = []; for (let i = 0; i < 5; i++) { const client = net.connect(8589, 'localhost'); clients.push(client); await new Promise((resolve) => { client.on('connect', () => { console.log(`Client ${i} connected`); // Send data that will never get a response client.write(`GET / HTTP/1.1\r\nHost: localhost\r\n\r\n`); resolve(); }); client.on('error', (err) => { console.log(`Client ${i} error: ${err.message}`); resolve(); }); }); } // Wait a moment for connections to establish await plugins.smartdelay.delayFor(1000); // Check initial connection count const initialCount = (proxy as any).connectionManager.getConnectionCount(); console.log(`\nInitial connection count: ${initialCount}`); expect(initialCount).toEqual(5); // Get connection details const connections = (proxy as any).connectionManager.getConnections(); let stuckCount = 0; for (const [id, record] of connections) { if (record.bytesReceived > 0 && record.bytesSent === 0) { stuckCount++; console.log(`Stuck connection ${id}: received=${record.bytesReceived}, sent=${record.bytesSent}`); } } console.log(`Stuck connections found: ${stuckCount}`); expect(stuckCount).toEqual(5); // Wait for inactivity check to run (it checks every 30s by default, but we set it to 5s) console.log('\n--- Waiting for stuck connection detection (65 seconds) ---'); console.log('Note: Stuck connections are cleaned up after 60 seconds with no response'); // Speed up time by manually triggering inactivity check after simulating time passage // First, age the connections by updating their timestamps const now = Date.now(); for (const [id, record] of connections) { // Simulate that these connections are 61 seconds old record.incomingStartTime = now - 61000; record.lastActivity = now - 61000; } // Manually trigger inactivity check console.log('Manually triggering inactivity check...'); (proxy as any).connectionManager.performOptimizedInactivityCheck(); // Wait for cleanup to complete await plugins.smartdelay.delayFor(1000); // Check connection count after cleanup const afterCleanupCount = (proxy as any).connectionManager.getConnectionCount(); console.log(`\nConnection count after cleanup: ${afterCleanupCount}`); // Verify termination stats const stats = (proxy as any).connectionManager.getTerminationStats(); console.log('\nTermination stats:', stats); // All connections should be cleaned up as "stuck_no_response" expect(afterCleanupCount).toEqual(0); // The termination reason might be under incoming or general stats const stuckCleanups = (stats.incoming.stuck_no_response || 0) + (stats.outgoing?.stuck_no_response || 0); console.log(`Stuck cleanups detected: ${stuckCleanups}`); expect(stuckCleanups).toBeGreaterThan(0); // Verify clients were disconnected let closedClients = 0; for (const client of clients) { if (client.destroyed) { closedClients++; } } console.log(`Closed clients: ${closedClients}/5`); expect(closedClients).toEqual(5); // Cleanup console.log('\n--- Cleanup ---'); await proxy.stop(); hangingBackend.close(); console.log('✓ Test complete: Stuck connections are properly detected and cleaned up'); }); tap.start();