144 lines
5.0 KiB
TypeScript
144 lines
5.0 KiB
TypeScript
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<void>((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<void>((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(); |