- 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.
200 lines
6.1 KiB
TypeScript
200 lines
6.1 KiB
TypeScript
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,
|
|
options: CleanupOptions = {}
|
|
): Promise<void> {
|
|
if (!socket || socket.destroyed) return Promise.resolve();
|
|
|
|
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();
|
|
};
|
|
|
|
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();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a cleanup handler for paired sockets (client and server)
|
|
* @param clientSocket The client socket
|
|
* @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,
|
|
serverSocket?: plugins.net.Socket | plugins.tls.TLSSocket | null,
|
|
onCleanup?: (reason: string) => void
|
|
): (reason: string) => void {
|
|
let cleanedUp = false;
|
|
|
|
return (reason: string) => {
|
|
if (cleanedUp) return;
|
|
cleanedUp = true;
|
|
|
|
// Cleanup both sockets (old behavior - too aggressive)
|
|
cleanupSocket(clientSocket, 'client', { immediate: true });
|
|
if (serverSocket) {
|
|
cleanupSocket(serverSocket, 'server', { immediate: true });
|
|
}
|
|
|
|
// Call cleanup callback if provided
|
|
if (onCleanup) {
|
|
onCleanup(reason);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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) => {
|
|
const prefix = errorPrefix || 'Socket';
|
|
handleClose(`${prefix}_error: ${error.message}`);
|
|
});
|
|
|
|
socket.on('close', () => {
|
|
const prefix = errorPrefix || 'socket';
|
|
handleClose(`${prefix}_closed`);
|
|
});
|
|
|
|
socket.on('timeout', () => {
|
|
if (handleTimeout) {
|
|
handleTimeout(socket); // Custom timeout handling
|
|
} else {
|
|
// Default: just log, don't close
|
|
console.warn(`Socket timeout: ${errorPrefix || 'socket'}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Pipe two sockets together with proper cleanup on either end
|
|
* @param socket1 First socket
|
|
* @param socket2 Second socket
|
|
*/
|
|
export function pipeSockets(
|
|
socket1: plugins.net.Socket | plugins.tls.TLSSocket,
|
|
socket2: plugins.net.Socket | plugins.tls.TLSSocket
|
|
): void {
|
|
socket1.pipe(socket2);
|
|
socket2.pipe(socket1);
|
|
} |