Implement real-time stats tracking including connection counts, request metrics, bandwidth usage, and route-specific monitoring. Adds MetricsCollector with observable streams for reactive monitoring integration.
250 lines
7.5 KiB
TypeScript
250 lines
7.5 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('keepalive support - verify keepalive connections are properly handled', async (tools) => {
|
|
console.log('\n=== KeepAlive Support Test ===');
|
|
console.log('Purpose: Verify that keepalive connections are not prematurely cleaned up');
|
|
|
|
// Create a simple echo backend
|
|
const echoBackend = net.createServer((socket) => {
|
|
socket.on('data', (data) => {
|
|
// Echo back received data
|
|
try {
|
|
socket.write(data);
|
|
} catch (err) {
|
|
// Ignore write errors during shutdown
|
|
}
|
|
});
|
|
|
|
socket.on('error', (err) => {
|
|
// Ignore errors from backend sockets
|
|
console.log(`Backend socket error (expected during cleanup): ${err.code}`);
|
|
});
|
|
});
|
|
|
|
await new Promise<void>((resolve) => {
|
|
echoBackend.listen(9998, () => {
|
|
console.log('✓ Echo backend started on port 9998');
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
// Test 1: Standard keepalive treatment
|
|
console.log('\n--- Test 1: Standard KeepAlive Treatment ---');
|
|
|
|
const proxy1 = new SmartProxy({
|
|
routes: [{
|
|
name: 'keepalive-route',
|
|
match: { ports: 8590 },
|
|
action: {
|
|
type: 'forward',
|
|
target: { host: 'localhost', port: 9998 }
|
|
}
|
|
}],
|
|
keepAlive: true,
|
|
keepAliveTreatment: 'standard',
|
|
inactivityTimeout: 5000, // 5 seconds for faster testing
|
|
enableDetailedLogging: false,
|
|
});
|
|
|
|
await proxy1.start();
|
|
console.log('✓ Proxy with standard keepalive started on port 8590');
|
|
|
|
// Create a keepalive connection
|
|
const client1 = net.connect(8590, 'localhost');
|
|
|
|
// Add error handler to prevent unhandled errors
|
|
client1.on('error', (err) => {
|
|
console.log(`Client1 error (expected during cleanup): ${err.code}`);
|
|
});
|
|
|
|
await new Promise<void>((resolve) => {
|
|
client1.on('connect', () => {
|
|
console.log('Client connected');
|
|
client1.setKeepAlive(true, 1000);
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
// Send initial data
|
|
client1.write('Hello keepalive\n');
|
|
|
|
// Wait for echo
|
|
await new Promise<void>((resolve) => {
|
|
client1.once('data', (data) => {
|
|
console.log(`Received echo: ${data.toString().trim()}`);
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
// Check connection is marked as keepalive
|
|
const cm1 = (proxy1 as any).connectionManager;
|
|
const connections1 = cm1.getConnections();
|
|
let keepAliveCount = 0;
|
|
|
|
for (const [id, record] of connections1) {
|
|
if (record.hasKeepAlive) {
|
|
keepAliveCount++;
|
|
console.log(`KeepAlive connection ${id}: hasKeepAlive=${record.hasKeepAlive}`);
|
|
}
|
|
}
|
|
|
|
expect(keepAliveCount).toEqual(1);
|
|
|
|
// Wait to ensure it's not cleaned up prematurely
|
|
await plugins.smartdelay.delayFor(6000);
|
|
|
|
const afterWaitCount1 = cm1.getConnectionCount();
|
|
console.log(`Connections after 6s wait: ${afterWaitCount1}`);
|
|
expect(afterWaitCount1).toEqual(1); // Should still be connected
|
|
|
|
// Send more data to keep it alive
|
|
client1.write('Still alive\n');
|
|
|
|
// Clean up test 1
|
|
client1.destroy();
|
|
await proxy1.stop();
|
|
await plugins.smartdelay.delayFor(500); // Wait for port to be released
|
|
|
|
// Test 2: Extended keepalive treatment
|
|
console.log('\n--- Test 2: Extended KeepAlive Treatment ---');
|
|
|
|
const proxy2 = new SmartProxy({
|
|
routes: [{
|
|
name: 'keepalive-extended',
|
|
match: { ports: 8591 },
|
|
action: {
|
|
type: 'forward',
|
|
target: { host: 'localhost', port: 9998 }
|
|
}
|
|
}],
|
|
keepAlive: true,
|
|
keepAliveTreatment: 'extended',
|
|
keepAliveInactivityMultiplier: 6,
|
|
inactivityTimeout: 2000, // 2 seconds base, 12 seconds with multiplier
|
|
enableDetailedLogging: false,
|
|
});
|
|
|
|
await proxy2.start();
|
|
console.log('✓ Proxy with extended keepalive started on port 8591');
|
|
|
|
const client2 = net.connect(8591, 'localhost');
|
|
|
|
// Add error handler to prevent unhandled errors
|
|
client2.on('error', (err) => {
|
|
console.log(`Client2 error (expected during cleanup): ${err.code}`);
|
|
});
|
|
|
|
await new Promise<void>((resolve) => {
|
|
client2.on('connect', () => {
|
|
console.log('Client connected with extended timeout');
|
|
client2.setKeepAlive(true, 1000);
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
// Send initial data
|
|
client2.write('Extended keepalive\n');
|
|
|
|
// Check connection
|
|
const cm2 = (proxy2 as any).connectionManager;
|
|
await plugins.smartdelay.delayFor(1000);
|
|
|
|
const connections2 = cm2.getConnections();
|
|
for (const [id, record] of connections2) {
|
|
console.log(`Extended connection ${id}: hasKeepAlive=${record.hasKeepAlive}, treatment=extended`);
|
|
}
|
|
|
|
// Wait 3 seconds (would timeout with standard treatment)
|
|
await plugins.smartdelay.delayFor(3000);
|
|
|
|
const midWaitCount = cm2.getConnectionCount();
|
|
console.log(`Connections after 3s (base timeout exceeded): ${midWaitCount}`);
|
|
expect(midWaitCount).toEqual(1); // Should still be connected due to extended treatment
|
|
|
|
// Clean up test 2
|
|
client2.destroy();
|
|
await proxy2.stop();
|
|
await plugins.smartdelay.delayFor(500); // Wait for port to be released
|
|
|
|
// Test 3: Immortal keepalive treatment
|
|
console.log('\n--- Test 3: Immortal KeepAlive Treatment ---');
|
|
|
|
const proxy3 = new SmartProxy({
|
|
routes: [{
|
|
name: 'keepalive-immortal',
|
|
match: { ports: 8592 },
|
|
action: {
|
|
type: 'forward',
|
|
target: { host: 'localhost', port: 9998 }
|
|
}
|
|
}],
|
|
keepAlive: true,
|
|
keepAliveTreatment: 'immortal',
|
|
inactivityTimeout: 1000, // 1 second - should be ignored for immortal
|
|
enableDetailedLogging: false,
|
|
});
|
|
|
|
await proxy3.start();
|
|
console.log('✓ Proxy with immortal keepalive started on port 8592');
|
|
|
|
const client3 = net.connect(8592, 'localhost');
|
|
|
|
// Add error handler to prevent unhandled errors
|
|
client3.on('error', (err) => {
|
|
console.log(`Client3 error (expected during cleanup): ${err.code}`);
|
|
});
|
|
|
|
await new Promise<void>((resolve) => {
|
|
client3.on('connect', () => {
|
|
console.log('Client connected with immortal treatment');
|
|
client3.setKeepAlive(true, 1000);
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
// Send initial data
|
|
client3.write('Immortal connection\n');
|
|
|
|
// Wait well beyond normal timeout
|
|
await plugins.smartdelay.delayFor(5000);
|
|
|
|
const cm3 = (proxy3 as any).connectionManager;
|
|
const immortalCount = cm3.getConnectionCount();
|
|
console.log(`Immortal connections after 5s inactivity: ${immortalCount}`);
|
|
expect(immortalCount).toEqual(1); // Should never timeout
|
|
|
|
// Verify zombie detection doesn't affect immortal connections
|
|
console.log('\n--- Verifying zombie detection respects keepalive ---');
|
|
|
|
// Manually trigger inactivity check
|
|
cm3.performOptimizedInactivityCheck();
|
|
|
|
await plugins.smartdelay.delayFor(1000);
|
|
|
|
const afterCheckCount = cm3.getConnectionCount();
|
|
console.log(`Connections after manual inactivity check: ${afterCheckCount}`);
|
|
expect(afterCheckCount).toEqual(1); // Should still be alive
|
|
|
|
// Clean up
|
|
client3.destroy();
|
|
await proxy3.stop();
|
|
|
|
// Close backend and wait for it to fully close
|
|
await new Promise<void>((resolve) => {
|
|
echoBackend.close(() => {
|
|
console.log('Echo backend closed');
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
console.log('\n✓ All keepalive tests passed:');
|
|
console.log(' - Standard treatment works correctly');
|
|
console.log(' - Extended treatment applies multiplier');
|
|
console.log(' - Immortal treatment never times out');
|
|
console.log(' - Zombie detection respects keepalive settings');
|
|
});
|
|
|
|
tap.start(); |