395 lines
12 KiB
TypeScript
395 lines
12 KiB
TypeScript
|
import * as plugins from '@push.rocks/tapbundle';
|
||
|
import { expect, tap } from '@push.rocks/tapbundle';
|
||
|
import * as net from 'net';
|
||
|
import { startTestServer, stopTestServer } from '../server.loader.js';
|
||
|
|
||
|
const TEST_PORT = 2525;
|
||
|
|
||
|
interface ResourceMetrics {
|
||
|
timestamp: number;
|
||
|
memoryUsage: {
|
||
|
rss: number;
|
||
|
heapTotal: number;
|
||
|
heapUsed: number;
|
||
|
external: number;
|
||
|
};
|
||
|
processInfo: {
|
||
|
pid: number;
|
||
|
uptime: number;
|
||
|
cpuUsage: NodeJS.CpuUsage;
|
||
|
};
|
||
|
}
|
||
|
|
||
|
interface LeakAnalysis {
|
||
|
memoryGrowthMB: number;
|
||
|
memoryTrend: number;
|
||
|
stabilityScore: number;
|
||
|
memoryLeakDetected: boolean;
|
||
|
resourcesStable: boolean;
|
||
|
samplesAnalyzed: number;
|
||
|
initialMemoryMB: number;
|
||
|
finalMemoryMB: number;
|
||
|
}
|
||
|
|
||
|
const captureResourceMetrics = async (): Promise<ResourceMetrics> => {
|
||
|
// Force GC if available before measurement
|
||
|
if (global.gc) {
|
||
|
global.gc();
|
||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||
|
}
|
||
|
|
||
|
const memUsage = process.memoryUsage();
|
||
|
|
||
|
return {
|
||
|
timestamp: Date.now(),
|
||
|
memoryUsage: {
|
||
|
rss: Math.round(memUsage.rss / 1024 / 1024 * 100) / 100,
|
||
|
heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024 * 100) / 100,
|
||
|
heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024 * 100) / 100,
|
||
|
external: Math.round(memUsage.external / 1024 / 1024 * 100) / 100
|
||
|
},
|
||
|
processInfo: {
|
||
|
pid: process.pid,
|
||
|
uptime: process.uptime(),
|
||
|
cpuUsage: process.cpuUsage()
|
||
|
}
|
||
|
};
|
||
|
};
|
||
|
|
||
|
const analyzeResourceLeaks = (initial: ResourceMetrics, samples: Array<{ operation: number; metrics: ResourceMetrics }>, final: ResourceMetrics): LeakAnalysis => {
|
||
|
const memoryGrowthMB = final.memoryUsage.heapUsed - initial.memoryUsage.heapUsed;
|
||
|
|
||
|
// Analyze memory trend over samples
|
||
|
let memoryTrend = 0;
|
||
|
if (samples.length > 1) {
|
||
|
const firstSample = samples[0].metrics.memoryUsage.heapUsed;
|
||
|
const lastSample = samples[samples.length - 1].metrics.memoryUsage.heapUsed;
|
||
|
memoryTrend = lastSample - firstSample;
|
||
|
}
|
||
|
|
||
|
// Calculate stability score based on memory variance
|
||
|
let stabilityScore = 1.0;
|
||
|
if (samples.length > 2) {
|
||
|
const memoryValues = samples.map(s => s.metrics.memoryUsage.heapUsed);
|
||
|
const average = memoryValues.reduce((a, b) => a + b, 0) / memoryValues.length;
|
||
|
const variance = memoryValues.reduce((acc, val) => acc + Math.pow(val - average, 2), 0) / memoryValues.length;
|
||
|
const stdDev = Math.sqrt(variance);
|
||
|
stabilityScore = Math.max(0, 1 - (stdDev / average));
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
memoryGrowthMB: Math.round(memoryGrowthMB * 100) / 100,
|
||
|
memoryTrend: Math.round(memoryTrend * 100) / 100,
|
||
|
stabilityScore: Math.round(stabilityScore * 100) / 100,
|
||
|
memoryLeakDetected: memoryGrowthMB > 50,
|
||
|
resourcesStable: stabilityScore > 0.8 && memoryGrowthMB < 25,
|
||
|
samplesAnalyzed: samples.length,
|
||
|
initialMemoryMB: initial.memoryUsage.heapUsed,
|
||
|
finalMemoryMB: final.memoryUsage.heapUsed
|
||
|
};
|
||
|
};
|
||
|
|
||
|
tap.test('prepare server', async () => {
|
||
|
await startTestServer();
|
||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||
|
});
|
||
|
|
||
|
tap.test('REL-03: Resource leak detection - Memory leak analysis', async (tools) => {
|
||
|
const done = tools.defer();
|
||
|
const operationCount = 20;
|
||
|
const connections: net.Socket[] = [];
|
||
|
const samples: Array<{ operation: number; metrics: ResourceMetrics }> = [];
|
||
|
|
||
|
try {
|
||
|
const initialMetrics = await captureResourceMetrics();
|
||
|
console.log(`📊 Initial memory: ${initialMetrics.memoryUsage.heapUsed}MB`);
|
||
|
|
||
|
for (let i = 0; i < operationCount; i++) {
|
||
|
console.log(`🔄 Operation ${i + 1}/${operationCount}...`);
|
||
|
|
||
|
const socket = net.createConnection({
|
||
|
host: 'localhost',
|
||
|
port: TEST_PORT,
|
||
|
timeout: 30000
|
||
|
});
|
||
|
|
||
|
connections.push(socket);
|
||
|
|
||
|
await new Promise<void>((resolve, reject) => {
|
||
|
socket.once('connect', resolve);
|
||
|
socket.once('error', reject);
|
||
|
});
|
||
|
|
||
|
// Read greeting
|
||
|
await new Promise<void>((resolve) => {
|
||
|
socket.once('data', () => resolve());
|
||
|
});
|
||
|
|
||
|
// Send EHLO
|
||
|
socket.write(`EHLO leaktest-${i}\r\n`);
|
||
|
|
||
|
await new Promise<void>((resolve) => {
|
||
|
let data = '';
|
||
|
const handleData = (chunk: Buffer) => {
|
||
|
data += chunk.toString();
|
||
|
if (data.includes('250 ') && !data.includes('250-')) {
|
||
|
socket.removeListener('data', handleData);
|
||
|
resolve();
|
||
|
}
|
||
|
};
|
||
|
socket.on('data', handleData);
|
||
|
});
|
||
|
|
||
|
// Complete email transaction
|
||
|
socket.write(`MAIL FROM:<sender${i}@example.com>\r\n`);
|
||
|
|
||
|
await new Promise<void>((resolve) => {
|
||
|
socket.once('data', (chunk) => {
|
||
|
const response = chunk.toString();
|
||
|
expect(response).toInclude('250');
|
||
|
resolve();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
socket.write(`RCPT TO:<recipient${i}@example.com>\r\n`);
|
||
|
|
||
|
await new Promise<void>((resolve) => {
|
||
|
socket.once('data', (chunk) => {
|
||
|
const response = chunk.toString();
|
||
|
expect(response).toInclude('250');
|
||
|
resolve();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
socket.write('DATA\r\n');
|
||
|
|
||
|
await new Promise<void>((resolve) => {
|
||
|
socket.once('data', (chunk) => {
|
||
|
const response = chunk.toString();
|
||
|
expect(response).toInclude('354');
|
||
|
resolve();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
const emailContent = [
|
||
|
`From: sender${i}@example.com`,
|
||
|
`To: recipient${i}@example.com`,
|
||
|
`Subject: Resource Leak Test ${i + 1}`,
|
||
|
`Message-ID: <leak-test-${i}-${Date.now()}@example.com>`,
|
||
|
'',
|
||
|
`This is resource leak test iteration ${i + 1}.`,
|
||
|
'.',
|
||
|
''
|
||
|
].join('\r\n');
|
||
|
|
||
|
socket.write(emailContent);
|
||
|
|
||
|
await new Promise<void>((resolve) => {
|
||
|
socket.once('data', (chunk) => {
|
||
|
const response = chunk.toString();
|
||
|
expect(response).toInclude('250');
|
||
|
resolve();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
socket.write('QUIT\r\n');
|
||
|
await new Promise<void>((resolve) => {
|
||
|
socket.once('data', () => {
|
||
|
socket.end();
|
||
|
resolve();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
// Capture metrics every 5 operations
|
||
|
if ((i + 1) % 5 === 0) {
|
||
|
const metrics = await captureResourceMetrics();
|
||
|
samples.push({
|
||
|
operation: i + 1,
|
||
|
metrics
|
||
|
});
|
||
|
console.log(`📈 Sample ${samples.length}: Memory ${metrics.memoryUsage.heapUsed}MB`);
|
||
|
}
|
||
|
|
||
|
// Small delay between operations
|
||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||
|
}
|
||
|
|
||
|
// Clean up all connections
|
||
|
connections.forEach(conn => {
|
||
|
if (!conn.destroyed) {
|
||
|
conn.destroy();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Wait for cleanup
|
||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||
|
|
||
|
const finalMetrics = await captureResourceMetrics();
|
||
|
const leakAnalysis = analyzeResourceLeaks(initialMetrics, samples, finalMetrics);
|
||
|
|
||
|
console.log('\n📊 Resource Leak Analysis:');
|
||
|
console.log(`Initial memory: ${leakAnalysis.initialMemoryMB}MB`);
|
||
|
console.log(`Final memory: ${leakAnalysis.finalMemoryMB}MB`);
|
||
|
console.log(`Memory growth: ${leakAnalysis.memoryGrowthMB}MB`);
|
||
|
console.log(`Memory trend: ${leakAnalysis.memoryTrend}MB`);
|
||
|
console.log(`Stability score: ${leakAnalysis.stabilityScore}`);
|
||
|
console.log(`Memory leak detected: ${leakAnalysis.memoryLeakDetected}`);
|
||
|
console.log(`Resources stable: ${leakAnalysis.resourcesStable}`);
|
||
|
|
||
|
expect(leakAnalysis.memoryLeakDetected).toBeFalse();
|
||
|
expect(leakAnalysis.resourcesStable).toBeTrue();
|
||
|
done.resolve();
|
||
|
} catch (error) {
|
||
|
connections.forEach(conn => conn.destroy());
|
||
|
done.reject(error);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
tap.test('REL-03: Resource leak detection - Connection leak test', async (tools) => {
|
||
|
const done = tools.defer();
|
||
|
const abandonedConnections: net.Socket[] = [];
|
||
|
|
||
|
try {
|
||
|
console.log('\nTesting for connection resource leaks...');
|
||
|
|
||
|
// Create connections that are abandoned without proper cleanup
|
||
|
for (let i = 0; i < 10; i++) {
|
||
|
const socket = net.createConnection({
|
||
|
host: 'localhost',
|
||
|
port: TEST_PORT,
|
||
|
timeout: 30000
|
||
|
});
|
||
|
|
||
|
abandonedConnections.push(socket);
|
||
|
|
||
|
await new Promise<void>((resolve, reject) => {
|
||
|
socket.once('connect', resolve);
|
||
|
socket.once('error', reject);
|
||
|
});
|
||
|
|
||
|
// Read greeting but don't complete transaction
|
||
|
await new Promise<void>((resolve) => {
|
||
|
socket.once('data', () => resolve());
|
||
|
});
|
||
|
|
||
|
// Start but don't complete EHLO
|
||
|
socket.write(`EHLO abandoned-${i}\r\n`);
|
||
|
|
||
|
// Don't wait for response, just move to next
|
||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||
|
}
|
||
|
|
||
|
console.log('Created 10 abandoned connections');
|
||
|
|
||
|
// Wait a bit
|
||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||
|
|
||
|
// Try to create new connections - should still work
|
||
|
let newConnectionsSuccessful = 0;
|
||
|
for (let i = 0; i < 5; i++) {
|
||
|
try {
|
||
|
const socket = net.createConnection({
|
||
|
host: 'localhost',
|
||
|
port: TEST_PORT,
|
||
|
timeout: 5000
|
||
|
});
|
||
|
|
||
|
await new Promise<void>((resolve, reject) => {
|
||
|
const timeout = setTimeout(() => {
|
||
|
socket.destroy();
|
||
|
reject(new Error('Connection timeout'));
|
||
|
}, 5000);
|
||
|
|
||
|
socket.once('connect', () => {
|
||
|
clearTimeout(timeout);
|
||
|
resolve();
|
||
|
});
|
||
|
socket.once('error', (err) => {
|
||
|
clearTimeout(timeout);
|
||
|
reject(err);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
// Verify connection works
|
||
|
const greeting = await new Promise<string>((resolve) => {
|
||
|
socket.once('data', (chunk) => {
|
||
|
resolve(chunk.toString());
|
||
|
});
|
||
|
});
|
||
|
|
||
|
if (greeting.includes('220')) {
|
||
|
newConnectionsSuccessful++;
|
||
|
socket.write('QUIT\r\n');
|
||
|
socket.end();
|
||
|
}
|
||
|
} catch (error) {
|
||
|
console.log(`New connection ${i + 1} failed:`, error.message);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Clean up abandoned connections
|
||
|
abandonedConnections.forEach(conn => conn.destroy());
|
||
|
|
||
|
console.log(`New connections successful: ${newConnectionsSuccessful}/5`);
|
||
|
|
||
|
// Server should still accept new connections despite abandoned ones
|
||
|
expect(newConnectionsSuccessful).toBeGreaterThanOrEqual(4);
|
||
|
done.resolve();
|
||
|
} catch (error) {
|
||
|
abandonedConnections.forEach(conn => conn.destroy());
|
||
|
done.reject(error);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
tap.test('REL-03: Resource leak detection - Rapid create/destroy cycles', async (tools) => {
|
||
|
const done = tools.defer();
|
||
|
const cycles = 30;
|
||
|
const initialMetrics = await captureResourceMetrics();
|
||
|
|
||
|
try {
|
||
|
console.log('\nTesting rapid connection create/destroy cycles...');
|
||
|
|
||
|
for (let i = 0; i < cycles; i++) {
|
||
|
const socket = net.createConnection({
|
||
|
host: 'localhost',
|
||
|
port: TEST_PORT,
|
||
|
timeout: 5000
|
||
|
});
|
||
|
|
||
|
await new Promise<void>((resolve, reject) => {
|
||
|
socket.once('connect', resolve);
|
||
|
socket.once('error', reject);
|
||
|
});
|
||
|
|
||
|
// Immediately destroy after connect
|
||
|
socket.destroy();
|
||
|
|
||
|
// Very short delay
|
||
|
await new Promise(resolve => setTimeout(resolve, 20));
|
||
|
|
||
|
if ((i + 1) % 10 === 0) {
|
||
|
console.log(`Completed ${i + 1} cycles`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Wait for resources to be released
|
||
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||
|
|
||
|
const finalMetrics = await captureResourceMetrics();
|
||
|
const memoryGrowth = finalMetrics.memoryUsage.heapUsed - initialMetrics.memoryUsage.heapUsed;
|
||
|
|
||
|
console.log(`Memory growth after ${cycles} cycles: ${memoryGrowth.toFixed(2)}MB`);
|
||
|
|
||
|
// Memory growth should be minimal
|
||
|
expect(memoryGrowth).toBeLessThan(10);
|
||
|
done.resolve();
|
||
|
} catch (error) {
|
||
|
done.reject(error);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
tap.test('cleanup server', async () => {
|
||
|
await stopTestServer();
|
||
|
});
|
||
|
|
||
|
tap.start();
|