dcrouter/test/suite/smtpclient_reliability/test.crel-07.resource-cleanup.ts
2025-05-25 19:02:18 +00:00

586 lines
22 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
import { Email } from '../../../ts/mail/core/classes.email.js';
import * as fs from 'fs';
import * as path from 'path';
tap.test('CREL-07: Resource Cleanup Reliability Tests', async () => {
console.log('\n🧹 Testing SMTP Client Resource Cleanup Reliability');
console.log('=' .repeat(60));
const tempDir = path.join(process.cwd(), '.nogit', 'test-resource-cleanup');
// Ensure test directory exists
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
// Helper function to count active resources
const getResourceCounts = () => {
const usage = process.memoryUsage();
return {
memory: Math.round(usage.heapUsed / 1024 / 1024 * 100) / 100, // MB
handles: process._getActiveHandles ? process._getActiveHandles().length : 0,
requests: process._getActiveRequests ? process._getActiveRequests().length : 0
};
};
// Scenario 1: Connection Pool Cleanup
await test.test('Scenario 1: Connection Pool Cleanup', async () => {
console.log('\n🏊 Testing connection pool resource cleanup...');
let openConnections = 0;
let closedConnections = 0;
const connectionIds: string[] = [];
const testServer = await createTestServer({
responseDelay: 20,
onConnect: (socket: any) => {
openConnections++;
const connId = `CONN-${openConnections}`;
connectionIds.push(connId);
console.log(` [Server] ${connId} opened (total open: ${openConnections})`);
socket.on('close', () => {
closedConnections++;
console.log(` [Server] Connection closed (total closed: ${closedConnections})`);
});
}
});
try {
const initialResources = getResourceCounts();
console.log(` Initial resources: ${initialResources.memory}MB memory, ${initialResources.handles} handles`);
console.log(' Phase 1: Creating and using connection pools...');
const clients = [];
for (let poolIndex = 0; poolIndex < 4; poolIndex++) {
console.log(` Creating connection pool ${poolIndex + 1}...`);
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 3,
maxMessages: 20,
resourceCleanup: true,
autoCleanupInterval: 1000
});
clients.push(smtpClient);
// Send emails through this pool
const emails = [];
for (let i = 0; i < 5; i++) {
emails.push(new Email({
from: `sender${poolIndex}@cleanup.test`,
to: [`recipient${i}@cleanup.test`],
subject: `Pool Cleanup Test ${poolIndex + 1}-${i + 1}`,
text: `Testing connection pool cleanup ${poolIndex + 1}-${i + 1}`,
messageId: `pool-cleanup-${poolIndex}-${i}@cleanup.test`
}));
}
const promises = emails.map((email, index) => {
return smtpClient.sendMail(email).then(result => {
console.log(` ✓ Pool ${poolIndex + 1} Email ${index + 1} sent`);
return { success: true };
}).catch(error => {
console.log(` ✗ Pool ${poolIndex + 1} Email ${index + 1} failed`);
return { success: false, error };
});
});
const results = await Promise.all(promises);
const successful = results.filter(r => r.success).length;
console.log(` Pool ${poolIndex + 1}: ${successful}/${emails.length} emails sent`);
}
const afterCreation = getResourceCounts();
console.log(` After pool creation: ${afterCreation.memory}MB memory, ${afterCreation.handles} handles`);
console.log(' Phase 2: Closing all pools and testing cleanup...');
for (let i = 0; i < clients.length; i++) {
console.log(` Closing pool ${i + 1}...`);
clients[i].close();
// Wait for cleanup to occur
await new Promise(resolve => setTimeout(resolve, 200));
const currentResources = getResourceCounts();
console.log(` Resources after closing pool ${i + 1}: ${currentResources.memory}MB, ${currentResources.handles} handles`);
}
// Wait for all cleanup to complete
await new Promise(resolve => setTimeout(resolve, 1000));
const finalResources = getResourceCounts();
console.log(`\n Resource cleanup assessment:`);
console.log(` Initial: ${initialResources.memory}MB memory, ${initialResources.handles} handles`);
console.log(` Final: ${finalResources.memory}MB memory, ${finalResources.handles} handles`);
console.log(` Memory cleanup: ${finalResources.memory - initialResources.memory < 2 ? 'Good' : 'Memory retained'}`);
console.log(` Handle cleanup: ${finalResources.handles <= initialResources.handles + 1 ? 'Good' : 'Handles remaining'}`);
console.log(` Connection cleanup: ${closedConnections >= openConnections - 1 ? 'Complete' : 'Incomplete'}`);
} finally {
testServer.close();
}
});
// Scenario 2: File Handle and Stream Cleanup
await test.test('Scenario 2: File Handle and Stream Cleanup', async () => {
console.log('\n📁 Testing file handle and stream cleanup...');
const testServer = await createTestServer({
responseDelay: 30,
onData: (data: string) => {
if (data.includes('Attachment Test')) {
console.log(' [Server] Processing attachment email');
}
}
});
try {
const initialResources = getResourceCounts();
console.log(` Initial resources: ${initialResources.memory}MB memory, ${initialResources.handles} handles`);
console.log(' Creating temporary files for attachment testing...');
const tempFiles: string[] = [];
for (let i = 0; i < 8; i++) {
const fileName = path.join(tempDir, `attachment-${i}.txt`);
const content = `Attachment content ${i + 1}\n${'X'.repeat(1000)}`; // 1KB files
fs.writeFileSync(fileName, content);
tempFiles.push(fileName);
console.log(` Created temp file: ${fileName}`);
}
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 2,
streamCleanup: true,
fileHandleManagement: true
});
console.log(' Sending emails with file attachments...');
const emailPromises = tempFiles.map((filePath, index) => {
const email = new Email({
from: 'sender@filehandle.test',
to: [`recipient${index}@filehandle.test`],
subject: `File Handle Cleanup Test ${index + 1}`,
text: `Testing file handle cleanup with attachment ${index + 1}`,
attachments: [{
filename: `attachment-${index}.txt`,
path: filePath
}],
messageId: `filehandle-${index}@filehandle.test`
});
return smtpClient.sendMail(email).then(result => {
console.log(` ✓ Email ${index + 1} with attachment sent`);
return { success: true, index };
}).catch(error => {
console.log(` ✗ Email ${index + 1} failed: ${error.message}`);
return { success: false, index, error };
});
});
const results = await Promise.all(emailPromises);
const successful = results.filter(r => r.success).length;
console.log(` Email sending completed: ${successful}/${tempFiles.length} successful`);
const afterSending = getResourceCounts();
console.log(` Resources after sending: ${afterSending.memory}MB memory, ${afterSending.handles} handles`);
console.log(' Closing client and testing file handle cleanup...');
smtpClient.close();
// Wait for cleanup
await new Promise(resolve => setTimeout(resolve, 500));
// Clean up temp files
tempFiles.forEach(filePath => {
try {
fs.unlinkSync(filePath);
console.log(` Cleaned up: ${filePath}`);
} catch (error) {
console.log(` Failed to clean up ${filePath}: ${error.message}`);
}
});
const finalResources = getResourceCounts();
console.log(`\n File handle cleanup assessment:`);
console.log(` Initial handles: ${initialResources.handles}`);
console.log(` After sending: ${afterSending.handles}`);
console.log(` Final handles: ${finalResources.handles}`);
console.log(` Handle management: ${finalResources.handles <= initialResources.handles + 2 ? 'Effective' : 'Handles leaked'}`);
console.log(` Memory cleanup: ${finalResources.memory - initialResources.memory < 3 ? 'Good' : 'Memory retained'}`);
} finally {
testServer.close();
}
});
// Scenario 3: Timer and Interval Cleanup
await test.test('Scenario 3: Timer and Interval Cleanup', async () => {
console.log('\n⏰ Testing timer and interval cleanup...');
const testServer = await createTestServer({
responseDelay: 40
});
try {
const initialResources = getResourceCounts();
console.log(` Initial resources: ${initialResources.memory}MB memory, ${initialResources.handles} handles`);
console.log(' Creating clients with various timer configurations...');
const clients = [];
for (let i = 0; i < 3; i++) {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 2,
// Timer configurations
connectionTimeout: 2000,
keepAlive: true,
keepAliveInterval: 1000,
retryDelay: 500,
healthCheckInterval: 800,
timerCleanup: true
});
clients.push(smtpClient);
// Send an email to activate timers
const email = new Email({
from: `sender${i}@timer.test`,
to: [`recipient${i}@timer.test`],
subject: `Timer Cleanup Test ${i + 1}`,
text: `Testing timer cleanup ${i + 1}`,
messageId: `timer-${i}@timer.test`
});
try {
await smtpClient.sendMail(email);
console.log(` ✓ Client ${i + 1} email sent (timers active)`);
} catch (error) {
console.log(` ✗ Client ${i + 1} failed: ${error.message}`);
}
}
// Let timers run for a while
console.log(' Allowing timers to run...');
await new Promise(resolve => setTimeout(resolve, 2000));
const withTimers = getResourceCounts();
console.log(` Resources with active timers: ${withTimers.memory}MB memory, ${withTimers.handles} handles`);
console.log(' Closing clients and testing timer cleanup...');
for (let i = 0; i < clients.length; i++) {
console.log(` Closing client ${i + 1}...`);
clients[i].close();
// Wait for timer cleanup
await new Promise(resolve => setTimeout(resolve, 300));
const currentResources = getResourceCounts();
console.log(` Resources after closing client ${i + 1}: ${currentResources.handles} handles`);
}
// Wait for all timer cleanup to complete
await new Promise(resolve => setTimeout(resolve, 1500));
const finalResources = getResourceCounts();
console.log(`\n Timer cleanup assessment:`);
console.log(` Initial handles: ${initialResources.handles}`);
console.log(` With timers: ${withTimers.handles}`);
console.log(` Final handles: ${finalResources.handles}`);
console.log(` Timer cleanup: ${finalResources.handles <= initialResources.handles + 1 ? 'Complete' : 'Timers remaining'}`);
console.log(` Resource management: ${finalResources.handles < withTimers.handles ? 'Effective' : 'Incomplete'}`);
} finally {
testServer.close();
}
});
// Scenario 4: Event Listener and Callback Cleanup
await test.test('Scenario 4: Event Listener and Callback Cleanup', async () => {
console.log('\n🎧 Testing event listener and callback cleanup...');
const testServer = await createTestServer({
responseDelay: 25,
onConnect: () => {
console.log(' [Server] Connection for event cleanup test');
}
});
try {
const initialResources = getResourceCounts();
console.log(` Initial resources: ${initialResources.memory}MB memory`);
console.log(' Creating clients with extensive event listeners...');
const clients = [];
const eventHandlers: any[] = [];
for (let clientIndex = 0; clientIndex < 5; clientIndex++) {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 1,
eventCleanup: true
});
clients.push(smtpClient);
// Add multiple event listeners
const handlers = [];
for (let eventIndex = 0; eventIndex < 6; eventIndex++) {
const handler = (data: any) => {
// Event handler with closure
console.log(` Event ${eventIndex} from client ${clientIndex}: ${typeof data}`);
};
handlers.push(handler);
// Add event listeners (simulated)
if (smtpClient.on) {
smtpClient.on('connect', handler);
smtpClient.on('error', handler);
smtpClient.on('close', handler);
smtpClient.on('data', handler);
}
}
eventHandlers.push(handlers);
// Send test email to trigger events
const email = new Email({
from: `sender${clientIndex}@eventcleanup.test`,
to: [`recipient${clientIndex}@eventcleanup.test`],
subject: `Event Cleanup Test ${clientIndex + 1}`,
text: `Testing event listener cleanup ${clientIndex + 1}`,
messageId: `event-cleanup-${clientIndex}@eventcleanup.test`
});
try {
await smtpClient.sendMail(email);
console.log(` ✓ Client ${clientIndex + 1} email sent (events active)`);
} catch (error) {
console.log(` ✗ Client ${clientIndex + 1} failed: ${error.message}`);
}
}
const withEvents = getResourceCounts();
console.log(` Resources with event listeners: ${withEvents.memory}MB memory`);
console.log(' Closing clients and testing event listener cleanup...');
for (let i = 0; i < clients.length; i++) {
console.log(` Closing client ${i + 1} and removing ${eventHandlers[i].length} event listeners...`);
// Remove event listeners manually first
if (clients[i].removeAllListeners) {
clients[i].removeAllListeners();
}
// Close client
clients[i].close();
// Clear handler references
eventHandlers[i].length = 0;
await new Promise(resolve => setTimeout(resolve, 100));
}
// Force garbage collection if available
if (global.gc) {
global.gc();
global.gc();
}
await new Promise(resolve => setTimeout(resolve, 500));
const finalResources = getResourceCounts();
console.log(`\n Event listener cleanup assessment:`);
console.log(` Initial memory: ${initialResources.memory}MB`);
console.log(` With events: ${withEvents.memory}MB`);
console.log(` Final memory: ${finalResources.memory}MB`);
console.log(` Memory cleanup: ${finalResources.memory - initialResources.memory < 2 ? 'Effective' : 'Memory retained'}`);
console.log(` Event cleanup: ${finalResources.memory < withEvents.memory ? 'Successful' : 'Partial'}`);
} finally {
testServer.close();
}
});
// Scenario 5: Error State Cleanup
await test.test('Scenario 5: Error State Cleanup', async () => {
console.log('\n💥 Testing error state cleanup...');
let connectionCount = 0;
let errorInjectionActive = false;
const testServer = await createTestServer({
responseDelay: 30,
onConnect: (socket: any) => {
connectionCount++;
console.log(` [Server] Connection ${connectionCount}`);
if (errorInjectionActive && connectionCount > 2) {
console.log(` [Server] Injecting connection error ${connectionCount}`);
setTimeout(() => socket.destroy(), 50);
}
},
onData: (data: string, socket: any) => {
if (errorInjectionActive && data.includes('MAIL FROM')) {
socket.write('500 Internal server error\r\n');
return false;
}
return true;
}
});
try {
const initialResources = getResourceCounts();
console.log(` Initial resources: ${initialResources.memory}MB memory, ${initialResources.handles} handles`);
console.log(' Creating clients for error state testing...');
const clients = [];
for (let i = 0; i < 4; i++) {
clients.push(createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 2,
retryDelay: 200,
retries: 2,
errorStateCleanup: true,
gracefulErrorHandling: true
}));
}
console.log(' Phase 1: Normal operation...');
const email1 = new Email({
from: 'sender@errorstate.test',
to: ['recipient1@errorstate.test'],
subject: 'Error State Test - Normal',
text: 'Normal operation before errors',
messageId: 'error-normal@errorstate.test'
});
try {
await clients[0].sendMail(email1);
console.log(' ✓ Normal operation successful');
} catch (error) {
console.log(' ✗ Normal operation failed');
}
const afterNormal = getResourceCounts();
console.log(` Resources after normal operation: ${afterNormal.handles} handles`);
console.log(' Phase 2: Error injection phase...');
errorInjectionActive = true;
const errorEmails = [];
for (let i = 0; i < 6; i++) {
errorEmails.push(new Email({
from: 'sender@errorstate.test',
to: [`recipient${i}@errorstate.test`],
subject: `Error State Test ${i + 1}`,
text: `Testing error state cleanup ${i + 1}`,
messageId: `error-state-${i}@errorstate.test`
}));
}
const errorPromises = errorEmails.map((email, index) => {
const client = clients[index % clients.length];
return client.sendMail(email).then(result => {
console.log(` ✓ Error email ${index + 1} unexpectedly succeeded`);
return { success: true, index };
}).catch(error => {
console.log(` ✗ Error email ${index + 1} failed as expected`);
return { success: false, index, error: error.message };
});
});
const errorResults = await Promise.all(errorPromises);
const afterErrors = getResourceCounts();
console.log(` Resources after error phase: ${afterErrors.handles} handles`);
console.log(' Phase 3: Recovery and cleanup...');
errorInjectionActive = false;
// Test recovery
const recoveryEmail = new Email({
from: 'sender@errorstate.test',
to: ['recovery@errorstate.test'],
subject: 'Error State Test - Recovery',
text: 'Testing recovery after errors',
messageId: 'error-recovery@errorstate.test'
});
try {
await clients[0].sendMail(recoveryEmail);
console.log(' ✓ Recovery successful');
} catch (error) {
console.log(' ✗ Recovery failed');
}
console.log(' Closing all clients...');
clients.forEach((client, index) => {
console.log(` Closing client ${index + 1}...`);
client.close();
});
await new Promise(resolve => setTimeout(resolve, 1000));
const finalResources = getResourceCounts();
const errorSuccessful = errorResults.filter(r => r.success).length;
const errorFailed = errorResults.filter(r => !r.success).length;
console.log(`\n Error state cleanup assessment:`);
console.log(` Error phase results: ${errorSuccessful} succeeded, ${errorFailed} failed`);
console.log(` Initial handles: ${initialResources.handles}`);
console.log(` After errors: ${afterErrors.handles}`);
console.log(` Final handles: ${finalResources.handles}`);
console.log(` Error state cleanup: ${finalResources.handles <= initialResources.handles + 1 ? 'Complete' : 'Incomplete'}`);
console.log(` Recovery capability: ${errorFailed > 0 ? 'Error handling active' : 'No errors detected'}`);
console.log(` Resource management: ${finalResources.handles < afterErrors.handles ? 'Effective' : 'Needs improvement'}`);
} finally {
testServer.close();
}
});
// Cleanup test directory
try {
if (fs.existsSync(tempDir)) {
const files = fs.readdirSync(tempDir);
for (const file of files) {
const filePath = path.join(tempDir, file);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
}
fs.rmdirSync(tempDir);
console.log('\n🧹 Test directory cleaned up successfully');
}
} catch (error) {
console.log(`\n⚠ Warning: Could not clean up test directory: ${error.message}`);
}
console.log('\n✅ CREL-07: Resource Cleanup Reliability Tests completed');
console.log('🧹 All resource cleanup scenarios tested successfully');
});