update
This commit is contained in:
@ -0,0 +1,616 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { startTestSmtpServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
||||
import * as net from 'net';
|
||||
|
||||
let testServer: any;
|
||||
|
||||
tap.test('setup test SMTP server', async () => {
|
||||
testServer = await startTestSmtpServer();
|
||||
expect(testServer).toBeTruthy();
|
||||
expect(testServer.port).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('CERR-09: Pool exhaustion', async () => {
|
||||
const pooledClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 3,
|
||||
maxMessages: 100,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log('Testing connection pool exhaustion...');
|
||||
console.log('Pool configuration: maxConnections=3');
|
||||
|
||||
// Track pool state
|
||||
const poolStats = {
|
||||
active: 0,
|
||||
idle: 0,
|
||||
pending: 0,
|
||||
created: 0,
|
||||
destroyed: 0
|
||||
};
|
||||
|
||||
pooledClient.on('pool-connection-create', () => {
|
||||
poolStats.created++;
|
||||
console.log(` Pool: Connection created (total: ${poolStats.created})`);
|
||||
});
|
||||
|
||||
pooledClient.on('pool-connection-close', () => {
|
||||
poolStats.destroyed++;
|
||||
console.log(` Pool: Connection closed (total: ${poolStats.destroyed})`);
|
||||
});
|
||||
|
||||
// Send more concurrent messages than pool size
|
||||
const messageCount = 10;
|
||||
const emails = Array.from({ length: messageCount }, (_, i) => new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i}@example.com`],
|
||||
subject: `Pool test ${i}`,
|
||||
text: 'Testing connection pool exhaustion'
|
||||
}));
|
||||
|
||||
console.log(`\nSending ${messageCount} concurrent messages...`);
|
||||
|
||||
const startTime = Date.now();
|
||||
const results = await Promise.allSettled(
|
||||
emails.map((email, i) => {
|
||||
return pooledClient.sendMail(email).then(() => {
|
||||
console.log(` Message ${i}: Sent`);
|
||||
return { index: i, status: 'sent' };
|
||||
}).catch(error => {
|
||||
console.log(` Message ${i}: Failed - ${error.message}`);
|
||||
return { index: i, status: 'failed', error: error.message };
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const elapsed = Date.now() - startTime;
|
||||
|
||||
const successful = results.filter(r => r.status === 'fulfilled').length;
|
||||
const failed = results.filter(r => r.status === 'rejected').length;
|
||||
|
||||
console.log(`\nResults after ${elapsed}ms:`);
|
||||
console.log(` Successful: ${successful}/${messageCount}`);
|
||||
console.log(` Failed: ${failed}/${messageCount}`);
|
||||
console.log(` Connections created: ${poolStats.created}`);
|
||||
console.log(` Connections destroyed: ${poolStats.destroyed}`);
|
||||
|
||||
// Pool should limit concurrent connections
|
||||
expect(poolStats.created).toBeLessThanOrEqual(3);
|
||||
|
||||
await pooledClient.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-09: Connection pool timeouts', async () => {
|
||||
// Create slow server
|
||||
const slowServer = net.createServer((socket) => {
|
||||
socket.write('220 Slow Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
// Add delays to simulate slow responses
|
||||
setTimeout(() => {
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else {
|
||||
// Slow response for other commands
|
||||
setTimeout(() => {
|
||||
socket.write('250 OK\r\n');
|
||||
}, 3000); // 3 second delay
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
slowServer.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
|
||||
const slowPort = (slowServer.address() as net.AddressInfo).port;
|
||||
|
||||
const pooledClient = createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: slowPort,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 2,
|
||||
poolTimeout: 2000, // 2 second timeout for getting connection from pool
|
||||
commandTimeout: 4000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log('\nTesting connection pool timeouts...');
|
||||
console.log('Pool timeout: 2 seconds');
|
||||
|
||||
// Send multiple messages to trigger pool timeout
|
||||
const emails = Array.from({ length: 5 }, (_, i) => new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i}@example.com`],
|
||||
subject: `Timeout test ${i}`,
|
||||
text: 'Testing pool timeout'
|
||||
}));
|
||||
|
||||
const timeoutErrors = [];
|
||||
|
||||
await Promise.allSettled(
|
||||
emails.map(async (email, i) => {
|
||||
try {
|
||||
console.log(` Message ${i}: Attempting to send...`);
|
||||
await pooledClient.sendMail(email);
|
||||
console.log(` Message ${i}: Sent successfully`);
|
||||
} catch (error) {
|
||||
console.log(` Message ${i}: ${error.message}`);
|
||||
if (error.message.includes('timeout')) {
|
||||
timeoutErrors.push(error);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
console.log(`\nTimeout errors: ${timeoutErrors.length}`);
|
||||
expect(timeoutErrors.length).toBeGreaterThan(0);
|
||||
|
||||
await pooledClient.close();
|
||||
slowServer.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-09: Dead connection detection', async () => {
|
||||
const pooledClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 3,
|
||||
poolIdleTimeout: 5000, // Connections idle for 5s are closed
|
||||
poolPingInterval: 2000, // Ping idle connections every 2s
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log('\nTesting dead connection detection...');
|
||||
|
||||
// Track connection health checks
|
||||
let pingCount = 0;
|
||||
let deadConnections = 0;
|
||||
|
||||
pooledClient.on('pool-connection-ping', (result) => {
|
||||
pingCount++;
|
||||
console.log(` Ping ${pingCount}: ${result.alive ? 'Connection alive' : 'Connection dead'}`);
|
||||
if (!result.alive) {
|
||||
deadConnections++;
|
||||
}
|
||||
});
|
||||
|
||||
// Send initial message to create connection
|
||||
await pooledClient.sendMail(new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Initial message',
|
||||
text: 'Creating connection'
|
||||
}));
|
||||
|
||||
console.log('Connection created, waiting for health checks...');
|
||||
|
||||
// Wait for health checks
|
||||
await new Promise(resolve => setTimeout(resolve, 6000));
|
||||
|
||||
console.log(`\nHealth check results:`);
|
||||
console.log(` Total pings: ${pingCount}`);
|
||||
console.log(` Dead connections detected: ${deadConnections}`);
|
||||
|
||||
// Send another message to test connection recovery
|
||||
try {
|
||||
await pooledClient.sendMail(new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'After idle',
|
||||
text: 'Testing after idle period'
|
||||
}));
|
||||
console.log('Message sent successfully after idle period');
|
||||
} catch (error) {
|
||||
console.log('Error after idle:', error.message);
|
||||
}
|
||||
|
||||
await pooledClient.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-09: Pool connection limit per host', async () => {
|
||||
// Create multiple servers
|
||||
const servers = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const server = net.createServer((socket) => {
|
||||
socket.write(`220 Server ${i + 1}\r\n`);
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write(`250 server${i + 1}.example.com\r\n`);
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
|
||||
servers.push({
|
||||
server,
|
||||
port: (server.address() as net.AddressInfo).port
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\nTesting per-host connection limits...');
|
||||
|
||||
// Create pooled client with per-host limits
|
||||
const pooledClient = createSmtpClient({
|
||||
pool: true,
|
||||
maxConnections: 10, // Total pool size
|
||||
maxConnectionsPerHost: 2, // Per-host limit
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Track connections per host
|
||||
const hostConnections: { [key: string]: number } = {};
|
||||
|
||||
pooledClient.on('pool-connection-create', (info) => {
|
||||
const host = info.host || 'unknown';
|
||||
hostConnections[host] = (hostConnections[host] || 0) + 1;
|
||||
console.log(` Created connection to ${host} (total: ${hostConnections[host]})`);
|
||||
});
|
||||
|
||||
// Send messages to different servers
|
||||
const messages = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
for (let j = 0; j < 4; j++) {
|
||||
messages.push({
|
||||
server: i,
|
||||
email: new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${j}@server${i}.com`],
|
||||
subject: `Test ${j} to server ${i}`,
|
||||
text: 'Testing per-host limits'
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Override host/port for each message
|
||||
await Promise.allSettled(
|
||||
messages.map(async ({ server, email }) => {
|
||||
const client = createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: servers[server].port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 10,
|
||||
maxConnectionsPerHost: 2,
|
||||
debug: false
|
||||
});
|
||||
|
||||
try {
|
||||
await client.sendMail(email);
|
||||
console.log(` Sent to server ${server + 1}`);
|
||||
} catch (error) {
|
||||
console.log(` Failed to server ${server + 1}: ${error.message}`);
|
||||
}
|
||||
|
||||
await client.close();
|
||||
})
|
||||
);
|
||||
|
||||
console.log('\nConnections per host:');
|
||||
Object.entries(hostConnections).forEach(([host, count]) => {
|
||||
console.log(` ${host}: ${count} connections`);
|
||||
expect(count).toBeLessThanOrEqual(2); // Should respect per-host limit
|
||||
});
|
||||
|
||||
// Clean up servers
|
||||
servers.forEach(s => s.server.close());
|
||||
});
|
||||
|
||||
tap.test('CERR-09: Connection pool recovery', async () => {
|
||||
// Create unstable server
|
||||
let shouldFail = true;
|
||||
let requestCount = 0;
|
||||
|
||||
const unstableServer = net.createServer((socket) => {
|
||||
requestCount++;
|
||||
|
||||
if (shouldFail && requestCount <= 3) {
|
||||
// Abruptly close connection for first 3 requests
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
socket.write('220 Unstable Server\r\n');
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
unstableServer.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
|
||||
const unstablePort = (unstableServer.address() as net.AddressInfo).port;
|
||||
|
||||
const pooledClient = createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: unstablePort,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 2,
|
||||
retryFailedConnections: true,
|
||||
connectionRetryDelay: 1000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log('\nTesting connection pool recovery...');
|
||||
console.log('Server will fail first 3 connection attempts');
|
||||
|
||||
// Track recovery attempts
|
||||
let recoveryAttempts = 0;
|
||||
pooledClient.on('pool-connection-retry', () => {
|
||||
recoveryAttempts++;
|
||||
console.log(` Recovery attempt ${recoveryAttempts}`);
|
||||
});
|
||||
|
||||
// Try to send messages
|
||||
const results = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i}@example.com`],
|
||||
subject: `Recovery test ${i}`,
|
||||
text: 'Testing connection recovery'
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(`\nMessage ${i}: Attempting...`);
|
||||
await pooledClient.sendMail(email);
|
||||
console.log(`Message ${i}: Success`);
|
||||
results.push('success');
|
||||
} catch (error) {
|
||||
console.log(`Message ${i}: Failed - ${error.message}`);
|
||||
results.push('failed');
|
||||
|
||||
// After some failures, allow connections
|
||||
if (i === 2) {
|
||||
shouldFail = false;
|
||||
console.log(' Server stabilized, connections should succeed now');
|
||||
}
|
||||
}
|
||||
|
||||
// Small delay between attempts
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
console.log('\nFinal results:', results);
|
||||
const successCount = results.filter(r => r === 'success').length;
|
||||
expect(successCount).toBeGreaterThan(0); // Should recover eventually
|
||||
|
||||
await pooledClient.close();
|
||||
unstableServer.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-09: Pool metrics and monitoring', async () => {
|
||||
const pooledClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 5,
|
||||
poolMetrics: true,
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log('\nTesting pool metrics collection...');
|
||||
|
||||
// Collect metrics
|
||||
const metrics = {
|
||||
connectionsCreated: 0,
|
||||
connectionsDestroyed: 0,
|
||||
messagesQueued: 0,
|
||||
messagesSent: 0,
|
||||
errors: 0,
|
||||
avgWaitTime: 0,
|
||||
waitTimes: [] as number[]
|
||||
};
|
||||
|
||||
pooledClient.on('pool-metrics', (data) => {
|
||||
Object.assign(metrics, data);
|
||||
});
|
||||
|
||||
pooledClient.on('message-queued', () => {
|
||||
metrics.messagesQueued++;
|
||||
});
|
||||
|
||||
pooledClient.on('message-sent', (info) => {
|
||||
metrics.messagesSent++;
|
||||
if (info.waitTime) {
|
||||
metrics.waitTimes.push(info.waitTime);
|
||||
}
|
||||
});
|
||||
|
||||
// Send batch of messages
|
||||
const messageCount = 20;
|
||||
const startTime = Date.now();
|
||||
|
||||
await Promise.allSettled(
|
||||
Array.from({ length: messageCount }, (_, i) =>
|
||||
pooledClient.sendMail(new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i}@example.com`],
|
||||
subject: `Metrics test ${i}`,
|
||||
text: 'Testing pool metrics'
|
||||
}))
|
||||
)
|
||||
);
|
||||
|
||||
const totalTime = Date.now() - startTime;
|
||||
|
||||
// Calculate average wait time
|
||||
if (metrics.waitTimes.length > 0) {
|
||||
metrics.avgWaitTime = metrics.waitTimes.reduce((a, b) => a + b, 0) / metrics.waitTimes.length;
|
||||
}
|
||||
|
||||
// Get final pool status
|
||||
const poolStatus = pooledClient.getPoolStatus();
|
||||
|
||||
console.log('\nPool Metrics:');
|
||||
console.log(` Messages queued: ${metrics.messagesQueued}`);
|
||||
console.log(` Messages sent: ${metrics.messagesSent}`);
|
||||
console.log(` Average wait time: ${metrics.avgWaitTime.toFixed(2)}ms`);
|
||||
console.log(` Total time: ${totalTime}ms`);
|
||||
console.log(` Throughput: ${(messageCount / totalTime * 1000).toFixed(2)} msg/sec`);
|
||||
console.log('\nPool Status:');
|
||||
console.log(` Active connections: ${poolStatus.active}`);
|
||||
console.log(` Idle connections: ${poolStatus.idle}`);
|
||||
console.log(` Total connections: ${poolStatus.total}`);
|
||||
|
||||
await pooledClient.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-09: Connection affinity', async () => {
|
||||
const pooledClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 3,
|
||||
connectionAffinity: 'sender', // Reuse same connection for same sender
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log('\nTesting connection affinity...');
|
||||
|
||||
// Track which connection handles which sender
|
||||
const senderConnections: { [sender: string]: string } = {};
|
||||
|
||||
pooledClient.on('connection-assigned', (info) => {
|
||||
senderConnections[info.sender] = info.connectionId;
|
||||
console.log(` Sender ${info.sender} assigned to connection ${info.connectionId}`);
|
||||
});
|
||||
|
||||
// Send messages from different senders
|
||||
const senders = ['alice@example.com', 'bob@example.com', 'alice@example.com', 'charlie@example.com', 'bob@example.com'];
|
||||
|
||||
for (const sender of senders) {
|
||||
const email = new Email({
|
||||
from: sender,
|
||||
to: ['recipient@example.com'],
|
||||
subject: `From ${sender}`,
|
||||
text: 'Testing connection affinity'
|
||||
});
|
||||
|
||||
await pooledClient.sendMail(email);
|
||||
|
||||
const connectionId = senderConnections[sender];
|
||||
console.log(` Message from ${sender} sent via connection ${connectionId}`);
|
||||
}
|
||||
|
||||
// Verify affinity
|
||||
console.log('\nConnection affinity results:');
|
||||
const uniqueSenders = [...new Set(senders)];
|
||||
uniqueSenders.forEach(sender => {
|
||||
const messages = senders.filter(s => s === sender).length;
|
||||
console.log(` ${sender}: ${messages} messages, connection ${senderConnections[sender]}`);
|
||||
});
|
||||
|
||||
await pooledClient.close();
|
||||
});
|
||||
|
||||
tap.test('CERR-09: Pool resource cleanup', async () => {
|
||||
const pooledClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 3,
|
||||
poolCleanupInterval: 1000, // Clean up every second
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log('\nTesting pool resource cleanup...');
|
||||
|
||||
// Track cleanup events
|
||||
const cleanupStats = {
|
||||
idleClosed: 0,
|
||||
staleClosed: 0,
|
||||
errorClosed: 0
|
||||
};
|
||||
|
||||
pooledClient.on('pool-connection-cleanup', (reason) => {
|
||||
switch (reason.type) {
|
||||
case 'idle':
|
||||
cleanupStats.idleClosed++;
|
||||
console.log(` Closed idle connection: ${reason.connectionId}`);
|
||||
break;
|
||||
case 'stale':
|
||||
cleanupStats.staleClosed++;
|
||||
console.log(` Closed stale connection: ${reason.connectionId}`);
|
||||
break;
|
||||
case 'error':
|
||||
cleanupStats.errorClosed++;
|
||||
console.log(` Closed errored connection: ${reason.connectionId}`);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Send some messages
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await pooledClient.sendMail(new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i}@example.com`],
|
||||
subject: `Cleanup test ${i}`,
|
||||
text: 'Testing cleanup'
|
||||
}));
|
||||
}
|
||||
|
||||
console.log('Messages sent, waiting for cleanup...');
|
||||
|
||||
// Wait for cleanup cycles
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
|
||||
console.log('\nCleanup statistics:');
|
||||
console.log(` Idle connections closed: ${cleanupStats.idleClosed}`);
|
||||
console.log(` Stale connections closed: ${cleanupStats.staleClosed}`);
|
||||
console.log(` Errored connections closed: ${cleanupStats.errorClosed}`);
|
||||
|
||||
const finalStatus = pooledClient.getPoolStatus();
|
||||
console.log(`\nFinal pool status:`);
|
||||
console.log(` Active: ${finalStatus.active}`);
|
||||
console.log(` Idle: ${finalStatus.idle}`);
|
||||
console.log(` Total: ${finalStatus.total}`);
|
||||
|
||||
await pooledClient.close();
|
||||
});
|
||||
|
||||
tap.test('cleanup test SMTP server', async () => {
|
||||
if (testServer) {
|
||||
await testServer.stop();
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
Reference in New Issue
Block a user