Enhance socket cleanup and management for improved connection handling
- Refactor cleanupSocket function to support options for immediate destruction, allowing drain, and grace periods. - Introduce createIndependentSocketHandlers for better management of half-open connections between client and server sockets. - Update various handlers (HTTP, HTTPS passthrough, HTTPS terminate) to utilize new cleanup and socket management functions. - Implement custom timeout handling in socket setup to prevent immediate closure during keep-alive connections. - Add tests for long-lived connections and half-open connection scenarios to ensure stability and reliability. - Adjust connection manager to handle socket cleanup based on activity status, improving resource management.
This commit is contained in:
@ -1,27 +1,62 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
|
||||
export interface CleanupOptions {
|
||||
immediate?: boolean; // Force immediate destruction
|
||||
allowDrain?: boolean; // Allow write buffer to drain
|
||||
gracePeriod?: number; // Ms to wait before force close
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely cleanup a socket by removing all listeners and destroying it
|
||||
* @param socket The socket to cleanup
|
||||
* @param socketName Optional name for logging
|
||||
* @param options Cleanup options
|
||||
*/
|
||||
export function cleanupSocket(socket: plugins.net.Socket | plugins.tls.TLSSocket | null, socketName?: string): void {
|
||||
if (!socket) return;
|
||||
export function cleanupSocket(
|
||||
socket: plugins.net.Socket | plugins.tls.TLSSocket | null,
|
||||
socketName?: string,
|
||||
options: CleanupOptions = {}
|
||||
): Promise<void> {
|
||||
if (!socket || socket.destroyed) return Promise.resolve();
|
||||
|
||||
try {
|
||||
// Remove all event listeners
|
||||
socket.removeAllListeners();
|
||||
return new Promise<void>((resolve) => {
|
||||
const cleanup = () => {
|
||||
try {
|
||||
// Remove all event listeners
|
||||
socket.removeAllListeners();
|
||||
|
||||
// Destroy if not already destroyed
|
||||
if (!socket.destroyed) {
|
||||
socket.destroy();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error cleaning up socket${socketName ? ` (${socketName})` : ''}: ${err}`);
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
|
||||
// Unpipe any streams
|
||||
socket.unpipe();
|
||||
|
||||
// Destroy if not already destroyed
|
||||
if (!socket.destroyed) {
|
||||
socket.destroy();
|
||||
if (options.immediate) {
|
||||
// Immediate cleanup (old behavior)
|
||||
socket.unpipe();
|
||||
cleanup();
|
||||
} else if (options.allowDrain && socket.writable) {
|
||||
// Allow pending writes to complete
|
||||
socket.end(() => cleanup());
|
||||
|
||||
// Force cleanup after grace period
|
||||
if (options.gracePeriod) {
|
||||
setTimeout(() => {
|
||||
if (!socket.destroyed) {
|
||||
cleanup();
|
||||
}
|
||||
}, options.gracePeriod);
|
||||
}
|
||||
} else {
|
||||
// Default: immediate cleanup
|
||||
socket.unpipe();
|
||||
cleanup();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error cleaning up socket${socketName ? ` (${socketName})` : ''}: ${err}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -30,6 +65,7 @@ export function cleanupSocket(socket: plugins.net.Socket | plugins.tls.TLSSocket
|
||||
* @param serverSocket The server socket (optional)
|
||||
* @param onCleanup Optional callback when cleanup is done
|
||||
* @returns A cleanup function that can be called multiple times safely
|
||||
* @deprecated Use createIndependentSocketHandlers for better half-open support
|
||||
*/
|
||||
export function createSocketCleanupHandler(
|
||||
clientSocket: plugins.net.Socket | plugins.tls.TLSSocket,
|
||||
@ -42,10 +78,10 @@ export function createSocketCleanupHandler(
|
||||
if (cleanedUp) return;
|
||||
cleanedUp = true;
|
||||
|
||||
// Cleanup both sockets
|
||||
cleanupSocket(clientSocket, 'client');
|
||||
// Cleanup both sockets (old behavior - too aggressive)
|
||||
cleanupSocket(clientSocket, 'client', { immediate: true });
|
||||
if (serverSocket) {
|
||||
cleanupSocket(serverSocket, 'server');
|
||||
cleanupSocket(serverSocket, 'server', { immediate: true });
|
||||
}
|
||||
|
||||
// Call cleanup callback if provided
|
||||
@ -55,15 +91,79 @@ export function createSocketCleanupHandler(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create independent cleanup handlers for paired sockets that support half-open connections
|
||||
* @param clientSocket The client socket
|
||||
* @param serverSocket The server socket
|
||||
* @param onBothClosed Callback when both sockets are closed
|
||||
* @returns Independent cleanup functions for each socket
|
||||
*/
|
||||
export function createIndependentSocketHandlers(
|
||||
clientSocket: plugins.net.Socket | plugins.tls.TLSSocket,
|
||||
serverSocket: plugins.net.Socket | plugins.tls.TLSSocket,
|
||||
onBothClosed: (reason: string) => void
|
||||
): { cleanupClient: (reason: string) => Promise<void>, cleanupServer: (reason: string) => Promise<void> } {
|
||||
let clientClosed = false;
|
||||
let serverClosed = false;
|
||||
let clientReason = '';
|
||||
let serverReason = '';
|
||||
|
||||
const checkBothClosed = () => {
|
||||
if (clientClosed && serverClosed) {
|
||||
onBothClosed(`client: ${clientReason}, server: ${serverReason}`);
|
||||
}
|
||||
};
|
||||
|
||||
const cleanupClient = async (reason: string) => {
|
||||
if (clientClosed) return;
|
||||
clientClosed = true;
|
||||
clientReason = reason;
|
||||
|
||||
// Allow server to continue if still active
|
||||
if (!serverClosed && serverSocket.writable) {
|
||||
// Half-close: stop reading from client, let server finish
|
||||
clientSocket.pause();
|
||||
clientSocket.unpipe(serverSocket);
|
||||
await cleanupSocket(clientSocket, 'client', { allowDrain: true, gracePeriod: 5000 });
|
||||
} else {
|
||||
await cleanupSocket(clientSocket, 'client', { immediate: true });
|
||||
}
|
||||
|
||||
checkBothClosed();
|
||||
};
|
||||
|
||||
const cleanupServer = async (reason: string) => {
|
||||
if (serverClosed) return;
|
||||
serverClosed = true;
|
||||
serverReason = reason;
|
||||
|
||||
// Allow client to continue if still active
|
||||
if (!clientClosed && clientSocket.writable) {
|
||||
// Half-close: stop reading from server, let client finish
|
||||
serverSocket.pause();
|
||||
serverSocket.unpipe(clientSocket);
|
||||
await cleanupSocket(serverSocket, 'server', { allowDrain: true, gracePeriod: 5000 });
|
||||
} else {
|
||||
await cleanupSocket(serverSocket, 'server', { immediate: true });
|
||||
}
|
||||
|
||||
checkBothClosed();
|
||||
};
|
||||
|
||||
return { cleanupClient, cleanupServer };
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup socket error and close handlers with proper cleanup
|
||||
* @param socket The socket to setup handlers for
|
||||
* @param handleClose The cleanup function to call
|
||||
* @param handleTimeout Optional custom timeout handler
|
||||
* @param errorPrefix Optional prefix for error messages
|
||||
*/
|
||||
export function setupSocketHandlers(
|
||||
socket: plugins.net.Socket | plugins.tls.TLSSocket,
|
||||
handleClose: (reason: string) => void,
|
||||
handleTimeout?: (socket: plugins.net.Socket | plugins.tls.TLSSocket) => void,
|
||||
errorPrefix?: string
|
||||
): void {
|
||||
socket.on('error', (error) => {
|
||||
@ -77,8 +177,12 @@ export function setupSocketHandlers(
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
const prefix = errorPrefix || 'socket';
|
||||
handleClose(`${prefix}_timeout`);
|
||||
if (handleTimeout) {
|
||||
handleTimeout(socket); // Custom timeout handling
|
||||
} else {
|
||||
// Default: just log, don't close
|
||||
console.warn(`Socket timeout: ${errorPrefix || 'socket'}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user