diff --git a/lib/services/email-service.ts b/lib/services/email-service.ts index 9bec9fb3..1f12baa8 100644 --- a/lib/services/email-service.ts +++ b/lib/services/email-service.ts @@ -46,9 +46,12 @@ let totalReuseConnections = 0; let totalConnectionErrors = 0; let lastMetricsReset = Date.now(); -const CONNECTION_TIMEOUT = 15 * 60 * 1000; // Increased to 15 minutes for long-lived connections +// CRITICAL PERFORMANCE FIX: Increase idle timeout from 15 minutes to 30 minutes +// This will keep connections alive longer and reduce reconnection delays +const CONNECTION_TIMEOUT = 30 * 60 * 1000; // Increased to 30 minutes (was 15 minutes) const MAX_POOL_SIZE = 20; // Maximum number of connections to keep in the pool const CONNECTION_CHECK_INTERVAL = 60 * 1000; // Check every minute +const MIN_POOL_SIZE = 2; // Keep at least this many active connections per user // Clean up idle connections periodically setInterval(() => { @@ -65,56 +68,63 @@ setInterval(() => { lastMetricsReset = now; } - // If we're over the pool size limit, sort by last used and remove oldest - if (connectionKeys.length > MAX_POOL_SIZE) { - const sortedConnections = connectionKeys + // PERFORMANCE FIX: Group connections by user for better management + const connectionsByUser: Record = {}; + + connectionKeys.forEach(key => { + const userId = key.split(':')[0]; + if (!connectionsByUser[userId]) { + connectionsByUser[userId] = []; + } + connectionsByUser[userId].push(key); + }); + + // PERFORMANCE FIX: Manage pool size per user + Object.entries(connectionsByUser).forEach(([userId, userConnections]) => { + // Sort connections by last used (oldest first) + const sortedConnections = userConnections .map(key => ({ key, lastUsed: connectionPool[key].lastUsed })) .sort((a, b) => a.lastUsed - b.lastUsed); - // Keep the most recently used connections up to the max pool size - const connectionsToRemove = sortedConnections.slice(0, sortedConnections.length - MAX_POOL_SIZE); + // Keep the most recently used connections up to the min pool size + const connectionsToKeep = sortedConnections.slice(-MIN_POOL_SIZE); + const keepKeys = new Set(connectionsToKeep.map(conn => conn.key)); - connectionsToRemove.forEach(({ key }) => { - const connection = connectionPool[key]; - try { - if (connection.client.usable) { - connection.client.logout().catch(err => { - console.error(`Error closing excess connection for ${key}:`, err); - }); + // Check the rest for idle timeout + sortedConnections.forEach(({ key, lastUsed }) => { + // Skip connections to keep and those that are in the process of connecting + if (keepKeys.has(key) || connectionPool[key].isConnecting) { + return; + } + + // Only close connections idle for too long + if (now - lastUsed > CONNECTION_TIMEOUT) { + console.log(`Closing idle IMAP connection for ${key} (idle for ${Math.round((now - lastUsed)/1000)}s)`); + try { + if (connectionPool[key].client.usable) { + connectionPool[key].client.logout().catch(err => { + console.error(`Error closing idle connection for ${key}:`, err); + }); + } + } catch (error) { + console.error(`Error checking connection status for ${key}:`, error); + } finally { + delete connectionPool[key]; + console.log(`Removed idle connection for ${key} from pool (pool size: ${Object.keys(connectionPool).length})`); } - } catch (error) { - console.error(`Error checking excess connection status for ${key}:`, error); - } finally { - delete connectionPool[key]; - console.log(`Removed excess connection for ${key} from pool (pool size: ${Object.keys(connectionPool).length})`); } }); - } - - // Close idle connections - Object.entries(connectionPool).forEach(([key, connection]) => { - // Skip connections that are currently being established - if (connection.isConnecting) return; - - if (now - connection.lastUsed > CONNECTION_TIMEOUT) { - console.log(`Closing idle IMAP connection for ${key} (idle for ${Math.round((now - connection.lastUsed)/1000)}s)`); - try { - if (connection.client.usable) { - connection.client.logout().catch(err => { - console.error(`Error closing idle connection for ${key}:`, err); - }); - } - } catch (error) { - console.error(`Error checking connection status for ${key}:`, error); - } finally { - delete connectionPool[key]; - console.log(`Removed idle connection for ${key} from pool (pool size: ${Object.keys(connectionPool).length})`); - } - } }); - - // Log connection pool status - console.log(`[IMAP POOL] Current size: ${connectionKeys.length}, Max: ${MAX_POOL_SIZE}`); + + // Log connection pool status with more details + const activeCount = connectionKeys.filter(key => { + const conn = connectionPool[key]; + return !conn.isConnecting && (conn.client?.usable || false); + }).length; + + const connectingCount = connectionKeys.filter(key => connectionPool[key].isConnecting).length; + + console.log(`[IMAP POOL] Size: ${connectionKeys.length}, Active: ${activeCount}, Connecting: ${connectingCount}, Max: ${MAX_POOL_SIZE}`); }, CONNECTION_CHECK_INTERVAL); /** @@ -196,7 +206,8 @@ export async function getImapConnection( // Try to use existing connection if it's usable try { - if (connection.client.usable) { + // PERFORMANCE FIX: More robust connection status checking + if (connection.client && connection.client.usable) { // Touch the connection to mark it as recently used connection.lastUsed = Date.now(); console.log(`Reusing existing IMAP connection for ${connectionKey}`); @@ -246,6 +257,15 @@ export async function getImapConnection( connectionAttempts: (connectionPool[connectionKey]?.connectionAttempts || 0) + 1 }; + // PERFORMANCE FIX: Add connection timeout to prevent hanging connections + let connectionTimeout: NodeJS.Timeout | null = setTimeout(() => { + console.error(`[IMAP] Connection for ${connectionKey} timed out after 60 seconds`); + if (connectionPool[connectionKey]?.isConnecting) { + delete connectionPool[connectionKey]; + totalConnectionErrors++; + } + }, 60 * 1000); // 60 seconds timeout + // Create connection promise const connectionPromise = createImapConnection(credentials, connectionKey) .then(client => { @@ -254,6 +274,12 @@ export async function getImapConnection( connectionPool[connectionKey].isConnecting = false; connectionPool[connectionKey].lastUsed = Date.now(); + // Clear timeout since connection was successful + if (connectionTimeout) { + clearTimeout(connectionTimeout); + connectionTimeout = null; + } + // Update session data updateSessionData(userId, accountId).catch(err => { console.error(`Failed to update session data: ${err.message}`); @@ -264,6 +290,12 @@ export async function getImapConnection( return client; }) .catch(error => { + // Clear timeout to prevent double errors + if (connectionTimeout) { + clearTimeout(connectionTimeout); + connectionTimeout = null; + } + // Handle connection error console.error(`Failed to create IMAP connection for ${connectionKey}:`, error); delete connectionPool[connectionKey];