@push.rocks/smartproxy 19.5.7 → 19.5.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/readme.plan.md CHANGED
@@ -1,1230 +1,337 @@
1
- # SmartProxy Performance Optimization Plan
1
+ # SmartProxy Socket Cleanup Fix Plan
2
2
 
3
- ## Executive Summary
3
+ ## Problem Summary
4
4
 
5
- This plan addresses critical performance issues in SmartProxy that impact scalability, responsiveness, and stability. The approach is phased, starting with critical event loop blockers and progressing to long-term architectural improvements.
5
+ The current socket cleanup implementation is too aggressive and closes long-lived connections prematurely. This affects:
6
+ - WebSocket connections in HTTPS passthrough
7
+ - Long-lived HTTP connections (SSE, streaming)
8
+ - Database connections
9
+ - Any connection that should remain open for hours
6
10
 
7
- ## Phase 1: Critical Issues (Week 1) ✅ COMPLETE
11
+ ## Root Causes
8
12
 
9
- ### 1.1 Eliminate Busy Wait Loop ✅
10
-
11
- **Issue**: `ts/proxies/nftables-proxy/nftables-proxy.ts:235-238` blocks the entire event loop
12
-
13
- **Solution**:
13
+ ### 1. **Bilateral Socket Cleanup**
14
+ When one socket closes, both sockets are immediately destroyed:
14
15
  ```typescript
15
- // Create utility function in ts/core/utils/async-utils.ts
16
- export async function delay(ms: number): Promise<void> {
17
- return new Promise(resolve => setTimeout(resolve, ms));
18
- }
19
-
20
- // Replace busy wait in nftables-proxy.ts
21
- // OLD:
22
- const waitUntil = Date.now() + retryDelayMs;
23
- while (Date.now() < waitUntil) { }
24
-
25
- // NEW:
26
- await delay(retryDelayMs);
16
+ // In createSocketCleanupHandler
17
+ cleanupSocket(clientSocket, 'client');
18
+ cleanupSocket(serverSocket, 'server'); // Both destroyed together!
27
19
  ```
28
20
 
29
- **Implementation**:
30
- 1. Create `async-utils.ts` with common async utilities
31
- 2. Replace all synchronous sleeps with async delay
32
- 3. Ensure all calling functions are async
33
-
34
- ### 1.2 Async Filesystem Operations ✅
35
-
36
- **Issue**: Multiple synchronous filesystem operations blocking the event loop
37
-
38
- **Solution Architecture**:
21
+ ### 2. **Aggressive Timeout Handling**
22
+ Timeout events immediately trigger connection cleanup:
39
23
  ```typescript
40
- // Create ts/core/utils/fs-utils.ts
41
- import * as plugins from '../../plugins.js';
42
-
43
- export class AsyncFileSystem {
44
- static async exists(path: string): Promise<boolean> {
45
- try {
46
- await plugins.fs.promises.access(path);
47
- return true;
48
- } catch {
49
- return false;
50
- }
51
- }
52
-
53
- static async ensureDir(path: string): Promise<void> {
54
- await plugins.fs.promises.mkdir(path, { recursive: true });
55
- }
56
-
57
- static async readFile(path: string): Promise<string> {
58
- return plugins.fs.promises.readFile(path, 'utf8');
59
- }
60
-
61
- static async writeFile(path: string, data: string): Promise<void> {
62
- // Ensure directory exists
63
- const dir = plugins.path.dirname(path);
64
- await this.ensureDir(dir);
65
- await plugins.fs.promises.writeFile(path, data);
66
- }
67
-
68
- static async remove(path: string): Promise<void> {
69
- try {
70
- await plugins.fs.promises.unlink(path);
71
- } catch (error: any) {
72
- if (error.code !== 'ENOENT') throw error;
73
- }
74
- }
75
-
76
- static async readJSON<T>(path: string): Promise<T> {
77
- const content = await this.readFile(path);
78
- return JSON.parse(content);
79
- }
80
-
81
- static async writeJSON(path: string, data: any): Promise<void> {
82
- await this.writeFile(path, JSON.stringify(data, null, 2));
83
- }
84
- }
85
- ```
86
-
87
- **Migration Strategy**:
88
-
89
- 1. **Certificate Manager** (`ts/proxies/http-proxy/certificate-manager.ts`)
90
- ```typescript
91
- // OLD:
92
- constructor(private options: IHttpProxyOptions) {
93
- if (!fs.existsSync(this.certDir)) {
94
- fs.mkdirSync(this.certDir, { recursive: true });
95
- }
96
- }
97
-
98
- // NEW:
99
- private initialized = false;
100
-
101
- constructor(private options: IHttpProxyOptions) {}
102
-
103
- async initialize(): Promise<void> {
104
- if (this.initialized) return;
105
- await AsyncFileSystem.ensureDir(this.certDir);
106
- this.initialized = true;
107
- }
108
-
109
- async getCertificate(domain: string): Promise<{ cert: string; key: string } | null> {
110
- await this.initialize();
111
- const certPath = path.join(this.certDir, `${domain}.crt`);
112
- const keyPath = path.join(this.certDir, `${domain}.key`);
113
-
114
- if (await AsyncFileSystem.exists(certPath) && await AsyncFileSystem.exists(keyPath)) {
115
- const [cert, key] = await Promise.all([
116
- AsyncFileSystem.readFile(certPath),
117
- AsyncFileSystem.readFile(keyPath)
118
- ]);
119
- return { cert, key };
120
- }
121
- return null;
122
- }
123
- ```
124
-
125
- 2. **Certificate Store** (`ts/proxies/smart-proxy/cert-store.ts`)
126
- ```typescript
127
- // Convert all methods to async
128
- export class CertStore {
129
- constructor(private storePath: string) {}
130
-
131
- async init(): Promise<void> {
132
- await AsyncFileSystem.ensureDir(this.storePath);
133
- }
134
-
135
- async hasCertificate(domain: string): Promise<boolean> {
136
- const certPath = this.getCertificatePath(domain);
137
- return AsyncFileSystem.exists(certPath);
138
- }
139
-
140
- async getCertificate(domain: string): Promise<ICertificateInfo | null> {
141
- if (!await this.hasCertificate(domain)) return null;
142
-
143
- const metaPath = path.join(this.getCertificatePath(domain), 'meta.json');
144
- return AsyncFileSystem.readJSON<ICertificateInfo>(metaPath);
145
- }
146
- }
147
- ```
148
-
149
- 3. **NFTables Proxy** (`ts/proxies/nftables-proxy/nftables-proxy.ts`)
150
- ```typescript
151
- // Replace execSync with execAsync
152
- private async execNftCommand(command: string): Promise<string> {
153
- const maxRetries = 3;
154
- let lastError: Error | null = null;
155
-
156
- for (let i = 0; i < maxRetries; i++) {
157
- try {
158
- const { stdout } = await this.execAsync(command);
159
- return stdout;
160
- } catch (err: any) {
161
- lastError = err;
162
- if (i < maxRetries - 1) {
163
- await delay(this.retryDelayMs);
164
- }
165
- }
166
- }
167
-
168
- throw new NftExecutionError(`Failed after ${maxRetries} attempts: ${lastError?.message}`);
169
- }
170
- ```
171
-
172
- ## Dependencies Between Phases
173
-
174
- ### Critical Path
175
- ```
176
- Phase 1.1 (Busy Wait) ─┐
177
- ├─> Phase 2.1 (Timer Management) ─> Phase 3.2 (Worker Threads)
178
- Phase 1.2 (Async FS) ──┘ │
179
- ├─> Phase 4.1 (Monitoring)
180
- Phase 2.2 (Connection Pool) ────────────────────────────┘
24
+ socket.on('timeout', () => {
25
+ handleClose(`${prefix}_timeout`); // Destroys both sockets!
26
+ });
181
27
  ```
182
28
 
183
- ### Phase Dependencies
184
- - **Phase 1** must complete before Phase 2 (foundation for async operations)
185
- - **Phase 2.1** enables proper cleanup for Phase 3.2 worker threads
186
- - **Phase 3** optimizations depend on stable async foundation
187
- - **Phase 4** monitoring requires all components to be instrumented
188
-
189
- ## Phase 2: Resource Management (Week 2) 🔨 IN PROGRESS
190
-
191
- ### 2.1 Timer Lifecycle Management
192
-
193
- **Issue**: Timers created without cleanup references causing memory leaks
194
-
195
- **Solution Pattern**:
29
+ ### 3. **Parity Check Forces Closure**
30
+ If one socket closes but the other remains open for >2 minutes, connection is forcefully terminated:
196
31
  ```typescript
197
- // Create base class in ts/core/utils/lifecycle-component.ts
198
- export abstract class LifecycleComponent {
199
- private timers: Set<NodeJS.Timeout> = new Set();
200
- private listeners: Array<{ target: any, event: string, handler: Function }> = [];
201
- protected isShuttingDown = false;
202
-
203
- protected setInterval(handler: Function, timeout: number): NodeJS.Timeout {
204
- const timer = setInterval(() => {
205
- if (!this.isShuttingDown) {
206
- handler();
207
- }
208
- }, timeout);
209
- this.timers.add(timer);
210
- return timer;
211
- }
212
-
213
- protected setTimeout(handler: Function, timeout: number): NodeJS.Timeout {
214
- const timer = setTimeout(() => {
215
- this.timers.delete(timer);
216
- if (!this.isShuttingDown) {
217
- handler();
218
- }
219
- }, timeout);
220
- this.timers.add(timer);
221
- return timer;
222
- }
223
-
224
- protected addEventListener(target: any, event: string, handler: Function): void {
225
- target.on(event, handler);
226
- this.listeners.push({ target, event, handler });
227
- }
228
-
229
- protected async cleanup(): Promise<void> {
230
- this.isShuttingDown = true;
231
-
232
- // Clear all timers
233
- for (const timer of this.timers) {
234
- clearInterval(timer);
235
- clearTimeout(timer);
236
- }
237
- this.timers.clear();
238
-
239
- // Remove all listeners
240
- for (const { target, event, handler } of this.listeners) {
241
- target.removeListener(event, handler);
242
- }
243
- this.listeners = [];
244
- }
32
+ if (record.outgoingClosedTime &&
33
+ !record.incoming.destroyed &&
34
+ now - record.outgoingClosedTime > 120000) {
35
+ this.cleanupConnection(record, 'parity_check');
245
36
  }
246
37
  ```
247
38
 
248
- **Implementation**:
249
- 1. Extend LifecycleComponent in:
250
- - `HttpProxy`
251
- - `SmartProxy`
252
- - `ConnectionManager`
253
- - `RequestHandler`
254
- - `SharedSecurityManager`
255
-
256
- 2. Replace direct timer/listener usage with lifecycle methods
39
+ ### 4. **No Half-Open Connection Support**
40
+ The proxy doesn't support TCP half-open connections where one side closes while the other continues sending.
257
41
 
258
- ### 2.2 Connection Pool Enhancement
42
+ ## Fix Implementation Plan
259
43
 
260
- **Issue**: No backpressure mechanism and synchronous operations
44
+ ### Phase 1: Fix Socket Cleanup (Prevent Premature Closure)
261
45
 
262
- **Solution**:
46
+ #### 1.1 Modify `cleanupSocket()` to support graceful shutdown
263
47
  ```typescript
264
- // First, implement efficient BinaryHeap for O(log n) operations
265
- // Create ts/core/utils/binary-heap.ts
266
- export class BinaryHeap<T> {
267
- private heap: T[] = [];
268
-
269
- constructor(
270
- private compareFn: (a: T, b: T) => number,
271
- private extractKey?: (item: T) => string
272
- ) {}
273
-
274
- insert(item: T): void {
275
- this.heap.push(item);
276
- this.bubbleUp(this.heap.length - 1);
277
- }
278
-
279
- extract(): T | undefined {
280
- if (this.heap.length === 0) return undefined;
281
- if (this.heap.length === 1) return this.heap.pop();
282
-
283
- const result = this.heap[0];
284
- this.heap[0] = this.heap.pop()!;
285
- this.bubbleDown(0);
286
- return result;
287
- }
288
-
289
- extractIf(predicate: (item: T) => boolean): T | undefined {
290
- const index = this.heap.findIndex(predicate);
291
- if (index === -1) return undefined;
292
-
293
- if (index === this.heap.length - 1) return this.heap.pop();
294
-
295
- const result = this.heap[index];
296
- this.heap[index] = this.heap.pop()!;
297
-
298
- // Restore heap property
299
- this.bubbleUp(index);
300
- this.bubbleDown(index);
301
- return result;
302
- }
303
-
304
- sizeFor(key: string): number {
305
- if (!this.extractKey) return this.heap.length;
306
- return this.heap.filter(item => this.extractKey!(item) === key).length;
307
- }
308
-
309
- private bubbleUp(index: number): void {
310
- while (index > 0) {
311
- const parentIndex = Math.floor((index - 1) / 2);
312
- if (this.compareFn(this.heap[index], this.heap[parentIndex]) >= 0) break;
313
-
314
- [this.heap[index], this.heap[parentIndex]] =
315
- [this.heap[parentIndex], this.heap[index]];
316
- index = parentIndex;
317
- }
318
- }
319
-
320
- private bubbleDown(index: number): void {
321
- while (true) {
322
- let minIndex = index;
323
- const leftChild = 2 * index + 1;
324
- const rightChild = 2 * index + 2;
325
-
326
- if (leftChild < this.heap.length &&
327
- this.compareFn(this.heap[leftChild], this.heap[minIndex]) < 0) {
328
- minIndex = leftChild;
329
- }
330
-
331
- if (rightChild < this.heap.length &&
332
- this.compareFn(this.heap[rightChild], this.heap[minIndex]) < 0) {
333
- minIndex = rightChild;
334
- }
335
-
336
- if (minIndex === index) break;
337
-
338
- [this.heap[index], this.heap[minIndex]] =
339
- [this.heap[minIndex], this.heap[index]];
340
- index = minIndex;
341
- }
342
- }
48
+ export interface CleanupOptions {
49
+ immediate?: boolean; // Force immediate destruction
50
+ allowDrain?: boolean; // Allow write buffer to drain
51
+ gracePeriod?: number; // Ms to wait before force close
343
52
  }
344
53
 
345
- // Enhanced connection pool with queue and heap
346
- export class EnhancedConnectionPool extends LifecycleComponent {
347
- private connectionQueue: Array<{
348
- resolve: (socket: net.Socket) => void;
349
- reject: (error: Error) => void;
350
- host: string;
351
- port: number;
352
- timestamp: number;
353
- }> = [];
54
+ export function cleanupSocket(
55
+ socket: Socket | TLSSocket | null,
56
+ socketName?: string,
57
+ options: CleanupOptions = {}
58
+ ): Promise<void> {
59
+ if (!socket || socket.destroyed) return Promise.resolve();
354
60
 
355
- private connectionHeap: BinaryHeap<IConnectionEntry>;
356
- private metricsCollector: ConnectionMetrics;
357
-
358
- constructor(options: IConnectionPoolOptions) {
359
- super();
360
-
361
- // Priority: least recently used connections first
362
- this.connectionHeap = new BinaryHeap(
363
- (a, b) => a.lastUsed - b.lastUsed,
364
- (item) => item.poolKey
365
- );
366
-
367
- this.metricsCollector = new ConnectionMetrics();
368
- this.startQueueProcessor();
369
- }
370
-
371
- private startQueueProcessor(): void {
372
- // Process queue periodically to handle timeouts and retries
373
- this.setInterval(() => {
374
- const now = Date.now();
375
- const timeout = this.options.connectionQueueTimeout || 30000;
376
-
377
- // Remove timed out requests
378
- this.connectionQueue = this.connectionQueue.filter(item => {
379
- if (now - item.timestamp > timeout) {
380
- item.reject(new Error(`Connection pool timeout for ${item.host}:${item.port}`));
381
- this.metricsCollector.recordTimeout();
382
- return false;
383
- }
384
- return true;
385
- });
386
-
387
- // Try to fulfill queued requests
388
- this.processQueue();
389
- }, 1000);
390
- }
391
-
392
- private processQueue(): void {
393
- if (this.connectionQueue.length === 0) return;
394
-
395
- // Group by destination
396
- const grouped = new Map<string, typeof this.connectionQueue>();
397
-
398
- for (const item of this.connectionQueue) {
399
- const key = `${item.host}:${item.port}`;
400
- if (!grouped.has(key)) grouped.set(key, []);
401
- grouped.get(key)!.push(item);
402
- }
403
-
404
- // Try to fulfill requests for each destination
405
- for (const [poolKey, requests] of grouped) {
406
- const available = this.connectionHeap.extractIf(
407
- conn => conn.poolKey === poolKey && conn.isIdle && !conn.socket.destroyed
408
- );
409
-
410
- if (available) {
411
- const request = requests.shift()!;
412
- this.connectionQueue = this.connectionQueue.filter(item => item !== request);
413
-
414
- available.isIdle = false;
415
- available.lastUsed = Date.now();
416
- request.resolve(available.socket);
417
-
418
- this.metricsCollector.recordReuse();
61
+ return new Promise<void>((resolve) => {
62
+ const cleanup = () => {
63
+ socket.removeAllListeners();
64
+ if (!socket.destroyed) {
65
+ socket.destroy();
419
66
  }
420
- }
421
- }
422
-
423
- async getConnection(host: string, port: number): Promise<net.Socket> {
424
- const poolKey = `${host}:${port}`;
425
-
426
- // Try to get existing connection
427
- let connection = this.connectionHeap.extractIf(
428
- conn => conn.poolKey === poolKey && conn.isIdle && !conn.socket.destroyed
429
- );
67
+ resolve();
68
+ };
430
69
 
431
- if (connection) {
432
- connection.isIdle = false;
433
- connection.lastUsed = Date.now();
434
- this.metricsCollector.recordReuse();
70
+ if (options.immediate) {
71
+ cleanup();
72
+ } else if (options.allowDrain && socket.writable) {
73
+ // Allow pending writes to complete
74
+ socket.end(() => cleanup());
435
75
 
436
- // Validate connection is still alive
437
- if (await this.validateConnection(connection.socket)) {
438
- return connection.socket;
76
+ // Force cleanup after grace period
77
+ if (options.gracePeriod) {
78
+ setTimeout(cleanup, options.gracePeriod);
439
79
  }
440
-
441
- // Connection is dead, try another
442
- connection.socket.destroy();
443
- return this.getConnection(host, port);
444
- }
445
-
446
- // Check pool size
447
- const poolSize = this.connectionHeap.sizeFor(poolKey);
448
- if (poolSize < this.options.connectionPoolSize) {
449
- return this.createConnection(host, port);
450
- }
451
-
452
- // Queue the request
453
- return this.queueConnectionRequest(host, port);
454
- }
455
-
456
- private async validateConnection(socket: net.Socket): Promise<boolean> {
457
- return new Promise((resolve) => {
458
- if (socket.destroyed || !socket.readable || !socket.writable) {
459
- resolve(false);
460
- return;
461
- }
462
-
463
- // Try to write a TCP keepalive probe
464
- const originalWrite = socket.write;
465
- let writeError = false;
466
-
467
- socket.write = function(data: any, encoding?: any, cb?: any) {
468
- writeError = true;
469
- return false;
470
- };
471
-
472
- socket.setNoDelay(true);
473
- socket.setNoDelay(false);
474
-
475
- socket.write = originalWrite;
476
-
477
- resolve(!writeError);
478
- });
479
- }
480
-
481
- returnConnection(socket: net.Socket, host: string, port: number): void {
482
- const poolKey = `${host}:${port}`;
483
-
484
- // Check for queued requests first
485
- const queuedIndex = this.connectionQueue.findIndex(
486
- item => item.host === host && item.port === port
487
- );
488
-
489
- if (queuedIndex >= 0) {
490
- const queued = this.connectionQueue.splice(queuedIndex, 1)[0];
491
- queued.resolve(socket);
492
- this.metricsCollector.recordDirectHandoff();
493
- return;
80
+ } else {
81
+ cleanup();
494
82
  }
495
-
496
- // Return to pool
497
- this.connectionHeap.insert({
498
- socket,
499
- poolKey,
500
- lastUsed: Date.now(),
501
- isIdle: true,
502
- created: Date.now()
503
- });
504
- }
505
-
506
- getMetrics(): IConnectionPoolMetrics {
507
- return {
508
- ...this.metricsCollector.getMetrics(),
509
- poolSize: this.connectionHeap.size(),
510
- queueLength: this.connectionQueue.length
511
- };
512
- }
83
+ });
513
84
  }
514
85
  ```
515
86
 
516
- ## Phase 3: Performance Optimizations (Week 3)
517
-
518
- ### 3.1 JSON Operations Optimization
519
-
520
- **Issue**: Frequent JSON.stringify for cache keys
521
-
522
- **Solution**:
87
+ #### 1.2 Implement Independent Socket Tracking
523
88
  ```typescript
524
- // Create ts/core/utils/hash-utils.ts
525
- import * as crypto from 'crypto';
526
-
527
- export class HashUtils {
528
- private static readonly objectCache = new WeakMap<object, string>();
529
-
530
- static hashObject(obj: any): string {
531
- // Check cache first
532
- if (typeof obj === 'object' && obj !== null) {
533
- const cached = this.objectCache.get(obj);
534
- if (cached) return cached;
535
- }
536
-
537
- // Create stable string representation
538
- const str = this.stableStringify(obj);
539
- const hash = crypto.createHash('sha256').update(str).digest('hex').slice(0, 16);
540
-
541
- // Cache if object
542
- if (typeof obj === 'object' && obj !== null) {
543
- this.objectCache.set(obj, hash);
89
+ export function createIndependentSocketHandlers(
90
+ clientSocket: Socket,
91
+ serverSocket: Socket,
92
+ onBothClosed: (reason: string) => void
93
+ ): { cleanupClient: () => void, cleanupServer: () => void } {
94
+ let clientClosed = false;
95
+ let serverClosed = false;
96
+ let clientReason = '';
97
+ let serverReason = '';
98
+
99
+ const checkBothClosed = () => {
100
+ if (clientClosed && serverClosed) {
101
+ onBothClosed(`client: ${clientReason}, server: ${serverReason}`);
544
102
  }
545
-
546
- return hash;
547
- }
548
-
549
- private static stableStringify(obj: any): string {
550
- if (obj === null || typeof obj !== 'object') {
551
- return JSON.stringify(obj);
103
+ };
104
+
105
+ const cleanupClient = async (reason: string) => {
106
+ if (clientClosed) return;
107
+ clientClosed = true;
108
+ clientReason = reason;
109
+
110
+ // Allow server to continue if still active
111
+ if (!serverClosed && serverSocket.writable) {
112
+ // Half-close: stop reading from client, let server finish
113
+ clientSocket.pause();
114
+ clientSocket.unpipe(serverSocket);
115
+ await cleanupSocket(clientSocket, 'client', { allowDrain: true });
116
+ } else {
117
+ await cleanupSocket(clientSocket, 'client');
552
118
  }
553
119
 
554
- if (Array.isArray(obj)) {
555
- return '[' + obj.map(item => this.stableStringify(item)).join(',') + ']';
120
+ checkBothClosed();
121
+ };
122
+
123
+ const cleanupServer = async (reason: string) => {
124
+ if (serverClosed) return;
125
+ serverClosed = true;
126
+ serverReason = reason;
127
+
128
+ // Allow client to continue if still active
129
+ if (!clientClosed && clientSocket.writable) {
130
+ // Half-close: stop reading from server, let client finish
131
+ serverSocket.pause();
132
+ serverSocket.unpipe(clientSocket);
133
+ await cleanupSocket(serverSocket, 'server', { allowDrain: true });
134
+ } else {
135
+ await cleanupSocket(serverSocket, 'server');
556
136
  }
557
137
 
558
- const keys = Object.keys(obj).sort();
559
- const pairs = keys.map(key => `"${key}":${this.stableStringify(obj[key])}`);
560
- return '{' + pairs.join(',') + '}';
561
- }
562
- }
563
-
564
- // Update function-cache.ts
565
- private computeContextHash(context: IRouteContext): string {
566
- return HashUtils.hashObject({
567
- domain: context.domain,
568
- path: context.path,
569
- clientIp: context.clientIp
570
- });
571
- }
572
- ```
573
-
574
- ### 3.2 Worker Thread Integration
575
-
576
- **Issue**: CPU-intensive operations blocking event loop
577
-
578
- **Solution Architecture**:
579
- ```typescript
580
- // Create ts/core/workers/worker-pool.ts
581
- import { Worker } from 'worker_threads';
582
-
583
- export class WorkerPool {
584
- private workers: Worker[] = [];
585
- private queue: Array<{
586
- task: any;
587
- resolve: Function;
588
- reject: Function;
589
- }> = [];
590
- private busyWorkers = new Set<Worker>();
591
-
592
- constructor(
593
- private workerScript: string,
594
- private poolSize: number = 4
595
- ) {
596
- this.initializeWorkers();
597
- }
598
-
599
- async execute<T>(task: any): Promise<T> {
600
- const worker = await this.getAvailableWorker();
601
-
602
- return new Promise((resolve, reject) => {
603
- const messageHandler = (result: any) => {
604
- worker.off('message', messageHandler);
605
- worker.off('error', errorHandler);
606
- this.releaseWorker(worker);
607
- resolve(result);
608
- };
609
-
610
- const errorHandler = (error: Error) => {
611
- worker.off('message', messageHandler);
612
- worker.off('error', errorHandler);
613
- this.releaseWorker(worker);
614
- reject(error);
615
- };
616
-
617
- worker.on('message', messageHandler);
618
- worker.on('error', errorHandler);
619
- worker.postMessage(task);
620
- });
621
- }
622
- }
623
-
624
- // Create ts/core/workers/nftables-worker.ts
625
- import { parentPort } from 'worker_threads';
626
- import { exec } from 'child_process';
627
- import { promisify } from 'util';
628
-
629
- const execAsync = promisify(exec);
630
-
631
- parentPort?.on('message', async (task) => {
632
- try {
633
- const result = await execAsync(task.command);
634
- parentPort?.postMessage({ success: true, result });
635
- } catch (error) {
636
- parentPort?.postMessage({ success: false, error: error.message });
637
- }
638
- });
639
- ```
640
-
641
- ## Phase 4: Monitoring & Metrics (Week 4)
642
-
643
- ### 4.1 Event Loop Monitoring
644
-
645
- ```typescript
646
- // Create ts/core/monitoring/performance-monitor.ts
647
- export class PerformanceMonitor extends LifecycleComponent {
648
- private metrics = {
649
- eventLoopLag: [] as number[],
650
- activeConnections: 0,
651
- memoryUsage: {} as NodeJS.MemoryUsage,
652
- cpuUsage: {} as NodeJS.CpuUsage
138
+ checkBothClosed();
653
139
  };
654
-
655
- start() {
656
- // Monitor event loop lag
657
- let lastCheck = process.hrtime.bigint();
658
-
659
- this.setInterval(() => {
660
- const now = process.hrtime.bigint();
661
- const expectedInterval = 100n * 1000000n; // 100ms in nanoseconds
662
- const actualInterval = now - lastCheck;
663
- const lag = Number(actualInterval - expectedInterval) / 1000000; // Convert to ms
664
-
665
- this.metrics.eventLoopLag.push(lag);
666
- if (this.metrics.eventLoopLag.length > 100) {
667
- this.metrics.eventLoopLag.shift();
668
- }
669
-
670
- lastCheck = now;
671
- }, 100);
672
-
673
- // Monitor system resources
674
- this.setInterval(() => {
675
- this.metrics.memoryUsage = process.memoryUsage();
676
- this.metrics.cpuUsage = process.cpuUsage();
677
- }, 5000);
678
- }
679
-
680
- getMetrics() {
681
- const avgLag = this.metrics.eventLoopLag.reduce((a, b) => a + b, 0)
682
- / this.metrics.eventLoopLag.length;
683
-
684
- return {
685
- eventLoopLag: {
686
- current: this.metrics.eventLoopLag[this.metrics.eventLoopLag.length - 1],
687
- average: avgLag,
688
- max: Math.max(...this.metrics.eventLoopLag)
689
- },
690
- memory: this.metrics.memoryUsage,
691
- cpu: this.metrics.cpuUsage,
692
- activeConnections: this.metrics.activeConnections
693
- };
694
- }
140
+
141
+ return { cleanupClient, cleanupServer };
695
142
  }
696
143
  ```
697
144
 
698
- ## Testing Strategy
699
-
700
- ### Unit Tests
701
- 1. Create tests for each new utility class
702
- 2. Mock filesystem and network operations
703
- 3. Test error scenarios and edge cases
704
-
705
- ### Integration Tests
706
- 1. Test async migration with real filesystem
707
- 2. Verify timer cleanup on shutdown
708
- 3. Test connection pool under load
145
+ ### Phase 2: Fix Timeout Handling
709
146
 
710
- ### Performance Tests
147
+ #### 2.1 Separate timeout handling from connection closure
711
148
  ```typescript
712
- // Create test/performance/event-loop-test.ts
713
- import { tap, expect } from '@git.zone/tstest/tapbundle';
714
-
715
- tap.test('should not block event loop', async () => {
716
- const intervals: number[] = [];
717
- let lastTime = Date.now();
149
+ export function setupSocketHandlers(
150
+ socket: Socket | TLSSocket,
151
+ handleClose: (reason: string) => void,
152
+ handleTimeout?: (socket: Socket) => void, // New optional handler
153
+ errorPrefix?: string
154
+ ): void {
155
+ socket.on('error', (error) => {
156
+ const prefix = errorPrefix || 'Socket';
157
+ handleClose(`${prefix}_error: ${error.message}`);
158
+ });
718
159
 
719
- const timer = setInterval(() => {
720
- const now = Date.now();
721
- intervals.push(now - lastTime);
722
- lastTime = now;
723
- }, 10);
724
-
725
- // Run operations that might block
726
- await runPotentiallyBlockingOperation();
727
-
728
- clearInterval(timer);
160
+ socket.on('close', () => {
161
+ const prefix = errorPrefix || 'socket';
162
+ handleClose(`${prefix}_closed`);
163
+ });
729
164
 
730
- // Check that no interval exceeded 50ms (allowing some tolerance)
731
- const maxInterval = Math.max(...intervals);
732
- expect(maxInterval).toBeLessThan(50);
733
- });
165
+ socket.on('timeout', () => {
166
+ if (handleTimeout) {
167
+ handleTimeout(socket); // Custom timeout handling
168
+ } else {
169
+ // Default: just log, don't close
170
+ console.warn(`Socket timeout: ${errorPrefix || 'socket'}`);
171
+ }
172
+ });
173
+ }
734
174
  ```
735
175
 
736
- ## Migration Timeline
737
-
738
- ### Week 1: Critical Fixes
739
- - Day 1-2: Fix busy wait loop
740
- - Day 3-4: Convert critical sync operations
741
- - Day 5: Testing and validation
742
-
743
- ### Week 2: Resource Management
744
- - Day 1-2: Implement LifecycleComponent
745
- - Day 3-4: Migrate components
746
- - Day 5: Connection pool enhancement
747
-
748
- ### Week 3: Optimizations
749
- - Day 1-2: JSON operation optimization
750
- - Day 3-4: Worker thread integration
751
- - Day 5: Performance testing
752
-
753
- ### Week 4: Monitoring & Polish
754
- - Day 1-2: Performance monitoring
755
- - Day 3-4: Load testing
756
- - Day 5: Documentation and release
757
-
758
- ## Error Handling Strategy
759
-
760
- ### Graceful Degradation
176
+ #### 2.2 Update HTTPS passthrough handler
761
177
  ```typescript
762
- // Create ts/core/utils/error-handler.ts
763
- export class ErrorHandler {
764
- private static errorCounts = new Map<string, number>();
765
- private static circuitBreakers = new Map<string, CircuitBreaker>();
766
-
767
- static async withFallback<T>(
768
- operation: () => Promise<T>,
769
- fallback: () => Promise<T>,
770
- context: string
771
- ): Promise<T> {
772
- const breaker = this.getCircuitBreaker(context);
773
-
774
- if (breaker.isOpen()) {
775
- return fallback();
776
- }
777
-
778
- try {
779
- const result = await operation();
780
- breaker.recordSuccess();
781
- return result;
782
- } catch (error) {
783
- breaker.recordFailure();
784
- this.recordError(context, error);
785
-
786
- if (breaker.isOpen()) {
787
- logger.warn(`Circuit breaker opened for ${context}`);
788
- }
789
-
790
- return fallback();
791
- }
178
+ // In https-passthrough-handler.ts
179
+ const { cleanupClient, cleanupServer } = createIndependentSocketHandlers(
180
+ clientSocket,
181
+ serverSocket,
182
+ (reason) => {
183
+ this.emit(ForwardingHandlerEvents.DISCONNECTED, {
184
+ remoteAddress,
185
+ bytesSent,
186
+ bytesReceived,
187
+ reason
188
+ });
792
189
  }
190
+ );
793
191
 
794
- private static getCircuitBreaker(context: string): CircuitBreaker {
795
- if (!this.circuitBreakers.has(context)) {
796
- this.circuitBreakers.set(context, new CircuitBreaker({
797
- failureThreshold: 5,
798
- resetTimeout: 60000
799
- }));
800
- }
801
- return this.circuitBreakers.get(context)!;
802
- }
803
- }
192
+ // Setup handlers with custom timeout handling
193
+ setupSocketHandlers(clientSocket, cleanupClient, (socket) => {
194
+ // Just reset timeout, don't close
195
+ socket.setTimeout(timeout);
196
+ }, 'client');
804
197
 
805
- // Usage example in Certificate Manager
806
- async getCertificate(domain: string): Promise<CertificateInfo | null> {
807
- return ErrorHandler.withFallback(
808
- // Try async operation
809
- async () => {
810
- await this.initialize();
811
- return this.loadCertificateAsync(domain);
812
- },
813
- // Fallback to sync if needed
814
- async () => {
815
- logger.warn(`Falling back to sync certificate load for ${domain}`);
816
- return this.loadCertificateSync(domain);
817
- },
818
- 'certificate-load'
819
- );
820
- }
198
+ setupSocketHandlers(serverSocket, cleanupServer, (socket) => {
199
+ // Just reset timeout, don't close
200
+ socket.setTimeout(timeout);
201
+ }, 'server');
821
202
  ```
822
203
 
823
- ## Backward Compatibility
824
-
825
- ### API Preservation
826
- 1. **Maintain existing interfaces** - All public APIs remain unchanged
827
- 2. **Progressive enhancement** - New features are opt-in via configuration
828
- 3. **Sync method wrappers** - Provide sync-looking APIs that use async internally
204
+ ### Phase 3: Fix Connection Manager
829
205
 
206
+ #### 3.1 Remove aggressive parity check
830
207
  ```typescript
831
- // Example: Maintaining backward compatibility
832
- export class CertStore {
833
- // Old sync API (deprecated but maintained)
834
- getCertificateSync(domain: string): ICertificateInfo | null {
835
- console.warn('getCertificateSync is deprecated, use getCertificate');
836
- return this.syncFallbackGetCertificate(domain);
837
- }
838
-
839
- // New async API
840
- async getCertificate(domain: string): Promise<ICertificateInfo | null> {
841
- return this.asyncGetCertificate(domain);
842
- }
843
-
844
- // Smart detection for gradual migration
845
- getCertificateAuto(domain: string, callback?: (err: Error | null, cert: ICertificateInfo | null) => void) {
846
- if (callback) {
847
- // Callback style for compatibility
848
- this.getCertificate(domain)
849
- .then(cert => callback(null, cert))
850
- .catch(err => callback(err, null));
851
- } else {
852
- // Return promise for modern usage
853
- return this.getCertificate(domain);
854
- }
208
+ // Remove or significantly increase the parity check timeout
209
+ // From 2 minutes to 30 minutes for long-lived connections
210
+ if (record.outgoingClosedTime &&
211
+ !record.incoming.destroyed &&
212
+ !record.connectionClosed &&
213
+ now - record.outgoingClosedTime > 1800000) { // 30 minutes
214
+ // Only close if no data activity
215
+ if (now - record.lastActivity > 600000) { // 10 minutes of inactivity
216
+ this.cleanupConnection(record, 'parity_check');
855
217
  }
856
218
  }
857
219
  ```
858
220
 
859
- ### Configuration Compatibility
221
+ #### 3.2 Update cleanupConnection to check socket states
860
222
  ```typescript
861
- // Support both old and new configuration formats
862
- interface SmartProxyOptions {
863
- // Old options (maintained)
864
- preserveSourceIP?: boolean;
865
- defaultAllowedIPs?: string[];
866
-
867
- // New performance options (added)
868
- performance?: {
869
- asyncFilesystem?: boolean;
870
- enhancedConnectionPool?: boolean;
871
- workerThreads?: boolean;
872
- };
873
- }
874
- ```
875
-
876
- ## Monitoring Dashboard
877
-
878
- ### Real-time Metrics Visualization
879
- ```typescript
880
- // Create ts/core/monitoring/dashboard-server.ts
881
- export class MonitoringDashboard {
882
- private httpServer: http.Server;
883
- private wsServer: WebSocket.Server;
884
- private metricsHistory: MetricsHistory;
885
-
886
- async start(port: number = 9090): Promise<void> {
887
- this.httpServer = http.createServer(this.handleRequest.bind(this));
888
- this.wsServer = new WebSocket.Server({ server: this.httpServer });
223
+ public cleanupConnection(record: IConnectionRecord, reason: string = 'normal'): void {
224
+ if (!record.connectionClosed) {
225
+ record.connectionClosed = true;
889
226
 
890
- this.wsServer.on('connection', (ws) => {
891
- // Send current metrics
892
- ws.send(JSON.stringify({
893
- type: 'initial',
894
- data: this.metricsHistory.getLast(100)
895
- }));
896
-
897
- // Subscribe to updates
898
- const interval = setInterval(() => {
899
- if (ws.readyState === WebSocket.OPEN) {
900
- ws.send(JSON.stringify({
901
- type: 'update',
902
- data: this.performanceMonitor.getMetrics()
903
- }));
904
- }
905
- }, 1000);
906
-
907
- ws.on('close', () => clearInterval(interval));
908
- });
909
-
910
- this.httpServer.listen(port);
911
- logger.info(`Monitoring dashboard available at http://localhost:${port}`);
912
- }
913
-
914
- private handleRequest(req: http.IncomingMessage, res: http.ServerResponse) {
915
- if (req.url === '/') {
916
- res.writeHead(200, { 'Content-Type': 'text/html' });
917
- res.end(this.getDashboardHTML());
918
- } else if (req.url === '/metrics') {
919
- res.writeHead(200, { 'Content-Type': 'application/json' });
920
- res.end(JSON.stringify(this.performanceMonitor.getMetrics()));
227
+ // Only cleanup sockets that are actually closed or inactive
228
+ if (record.incoming && (!record.incoming.writable || record.incoming.destroyed)) {
229
+ cleanupSocket(record.incoming, `${record.id}-incoming`, { immediate: true });
921
230
  }
922
- }
923
-
924
- private getDashboardHTML(): string {
925
- return `
926
- <!DOCTYPE html>
927
- <html>
928
- <head>
929
- <title>SmartProxy Performance Monitor</title>
930
- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
931
- <style>
932
- body { font-family: Arial, sans-serif; padding: 20px; }
933
- .metric { display: inline-block; margin: 10px; padding: 15px; background: #f0f0f0; }
934
- .chart-container { width: 45%; display: inline-block; margin: 2%; }
935
- </style>
936
- </head>
937
- <body>
938
- <h1>SmartProxy Performance Monitor</h1>
939
-
940
- <div id="metrics">
941
- <div class="metric">
942
- <h3>Event Loop Lag</h3>
943
- <div id="eventLoopLag">--</div>
944
- </div>
945
- <div class="metric">
946
- <h3>Active Connections</h3>
947
- <div id="activeConnections">--</div>
948
- </div>
949
- <div class="metric">
950
- <h3>Memory Usage</h3>
951
- <div id="memoryUsage">--</div>
952
- </div>
953
- <div class="metric">
954
- <h3>Connection Pool</h3>
955
- <div id="connectionPool">--</div>
956
- </div>
957
- </div>
958
-
959
- <div class="chart-container">
960
- <canvas id="eventLoopChart"></canvas>
961
- </div>
962
- <div class="chart-container">
963
- <canvas id="connectionChart"></canvas>
964
- </div>
965
-
966
- <script>
967
- const ws = new WebSocket('ws://localhost:9090');
968
- const eventLoopData = [];
969
- const connectionData = [];
970
-
971
- // Initialize charts
972
- const eventLoopChart = new Chart(document.getElementById('eventLoopChart'), {
973
- type: 'line',
974
- data: {
975
- labels: [],
976
- datasets: [{
977
- label: 'Event Loop Lag (ms)',
978
- data: eventLoopData,
979
- borderColor: 'rgb(255, 99, 132)',
980
- tension: 0.1
981
- }]
982
- },
983
- options: {
984
- responsive: true,
985
- scales: { y: { beginAtZero: true } }
986
- }
987
- });
988
231
 
989
- ws.onmessage = (event) => {
990
- const message = JSON.parse(event.data);
991
- updateMetrics(message.data);
992
- };
993
-
994
- function updateMetrics(metrics) {
995
- document.getElementById('eventLoopLag').textContent =
996
- metrics.eventLoopLag.current.toFixed(2) + ' ms';
997
- document.getElementById('activeConnections').textContent =
998
- metrics.activeConnections;
999
- document.getElementById('memoryUsage').textContent =
1000
- (metrics.memory.heapUsed / 1024 / 1024).toFixed(2) + ' MB';
1001
-
1002
- // Update charts
1003
- const now = new Date().toLocaleTimeString();
1004
- eventLoopData.push(metrics.eventLoopLag.current);
1005
- eventLoopChart.data.labels.push(now);
1006
-
1007
- if (eventLoopData.length > 60) {
1008
- eventLoopData.shift();
1009
- eventLoopChart.data.labels.shift();
1010
- }
1011
-
1012
- eventLoopChart.update();
232
+ if (record.outgoing && (!record.outgoing.writable || record.outgoing.destroyed)) {
233
+ cleanupSocket(record.outgoing, `${record.id}-outgoing`, { immediate: true });
1013
234
  }
1014
- </script>
1015
- </body>
1016
- </html>
1017
- `;
1018
- }
1019
- }
1020
- ```
1021
-
1022
- ## Performance Benchmarking
1023
-
1024
- ### Benchmark Suite
1025
- ```typescript
1026
- // Create test/performance/benchmark.ts
1027
- import { SmartProxy } from '../../ts/index.js';
1028
-
1029
- export class PerformanceBenchmark {
1030
- async runConnectionStresTest(): Promise<BenchmarkResult> {
1031
- const proxy = new SmartProxy({ /* config */ });
1032
- await proxy.start();
1033
-
1034
- const results = {
1035
- connectionRate: 0,
1036
- avgLatency: 0,
1037
- maxConnections: 0,
1038
- eventLoopLag: []
1039
- };
1040
-
1041
- // Monitor event loop during test
1042
- const lagSamples: number[] = [];
1043
- let lastCheck = process.hrtime.bigint();
1044
- const monitor = setInterval(() => {
1045
- const now = process.hrtime.bigint();
1046
- const lag = Number(now - lastCheck - 100_000_000n) / 1_000_000;
1047
- lagSamples.push(lag);
1048
- lastCheck = now;
1049
- }, 100);
1050
-
1051
- // Create connections with increasing rate
1052
- const startTime = Date.now();
1053
- let connectionCount = 0;
1054
235
 
1055
- for (let rate = 100; rate <= 10000; rate += 100) {
1056
- const connections = await this.createConnections(rate);
1057
- connectionCount += connections.length;
1058
-
1059
- // Check if performance degrades
1060
- const avgLag = lagSamples.slice(-10).reduce((a, b) => a + b) / 10;
1061
- if (avgLag > 50) {
1062
- results.maxConnections = connectionCount;
1063
- break;
1064
- }
1065
-
1066
- await this.delay(1000);
236
+ // If either socket is still active, don't remove the record yet
237
+ if ((record.incoming && record.incoming.writable) ||
238
+ (record.outgoing && record.outgoing.writable)) {
239
+ record.connectionClosed = false; // Reset flag
240
+ return; // Don't finish cleanup
1067
241
  }
1068
242
 
1069
- clearInterval(monitor);
1070
- await proxy.stop();
1071
-
1072
- results.connectionRate = connectionCount / ((Date.now() - startTime) / 1000);
1073
- results.avgLatency = this.calculateAvgLatency();
1074
- results.eventLoopLag = lagSamples;
1075
-
1076
- return results;
243
+ // Continue with full cleanup...
1077
244
  }
1078
245
  }
1079
246
  ```
1080
247
 
1081
- ## Documentation Updates
248
+ ### Phase 4: Testing and Validation
1082
249
 
1083
- ### API Documentation
1084
- 1. **Update JSDoc comments** for all modified methods
1085
- 2. **Add migration guide** for async transitions
1086
- 3. **Performance tuning guide** with recommended settings
250
+ #### 4.1 Test Cases to Implement
251
+ 1. WebSocket connection should stay open for >1 hour
252
+ 2. HTTP streaming response should continue after request closes
253
+ 3. Half-open connections should work correctly
254
+ 4. Verify no socket leaks with long-running connections
255
+ 5. Test graceful shutdown with pending data
1087
256
 
1088
- ### Example Updates
1089
- ```typescript
1090
- /**
1091
- * Gets a certificate for the specified domain
1092
- * @param domain - The domain to get certificate for
1093
- * @returns Promise resolving to certificate info or null
1094
- * @since v20.0.0 - Now returns Promise (breaking change)
1095
- * @example
1096
- * // Old way (deprecated)
1097
- * const cert = certStore.getCertificateSync('example.com');
1098
- *
1099
- * // New way
1100
- * const cert = await certStore.getCertificate('example.com');
1101
- *
1102
- * // Compatibility mode
1103
- * certStore.getCertificateAuto('example.com', (err, cert) => {
1104
- * if (err) console.error(err);
1105
- * else console.log(cert);
1106
- * });
1107
- */
1108
- async getCertificate(domain: string): Promise<ICertificateInfo | null> {
1109
- // Implementation
1110
- }
1111
- ```
257
+ #### 4.2 Socket Leak Prevention
258
+ - Ensure all event listeners are tracked and removed
259
+ - Use WeakMap for socket metadata to prevent memory leaks
260
+ - Implement connection count monitoring
261
+ - Add periodic health checks for orphaned sockets
262
+
263
+ ## Implementation Order
1112
264
 
1113
- ## Rollback Strategy
265
+ 1. **Day 1**: Implement graceful `cleanupSocket()` and independent socket handlers
266
+ 2. **Day 2**: Update all handlers to use new cleanup mechanism
267
+ 3. **Day 3**: Fix timeout handling to not close connections
268
+ 4. **Day 4**: Update connection manager parity check and cleanup logic
269
+ 5. **Day 5**: Comprehensive testing and leak detection
1114
270
 
1115
- Each phase is designed to be independently deployable with feature flags:
271
+ ## Configuration Changes
1116
272
 
273
+ Add new options to SmartProxyOptions:
1117
274
  ```typescript
1118
- export const PerformanceFlags = {
1119
- useAsyncFilesystem: process.env.SMARTPROXY_ASYNC_FS !== 'false',
1120
- useEnhancedPool: process.env.SMARTPROXY_ENHANCED_POOL === 'true',
1121
- useWorkerThreads: process.env.SMARTPROXY_WORKERS === 'true',
1122
- enableMonitoring: process.env.SMARTPROXY_MONITORING === 'true'
1123
- };
275
+ interface ISmartProxyOptions {
276
+ // Existing options...
277
+
278
+ // New options for long-lived connections
279
+ socketCleanupGracePeriod?: number; // Default: 5000ms
280
+ allowHalfOpenConnections?: boolean; // Default: true
281
+ parityCheckTimeout?: number; // Default: 1800000ms (30 min)
282
+ timeoutBehavior?: 'close' | 'reset' | 'ignore'; // Default: 'reset'
283
+ }
1124
284
  ```
1125
285
 
1126
- ### Gradual Rollout Plan
1127
- 1. **Development**: All flags enabled
1128
- 2. **Staging**: Monitor metrics for 1 week
1129
- 3. **Production**:
1130
- - 10% traffic → 25% → 50% → 100%
1131
- - Monitor key metrics at each stage
1132
- - Rollback if metrics degrade
1133
-
1134
286
  ## Success Metrics
1135
287
 
1136
- 1. **Event Loop Lag**: < 10ms average, < 50ms max
1137
- 2. **Connection Handling**: Support 10k+ concurrent connections
1138
- 3. **Memory Usage**: Stable under sustained load
1139
- 4. **CPU Usage**: Efficient utilization across cores
1140
- 5. **Response Time**: < 5ms overhead for proxy operations
1141
-
1142
- ## Risk Mitigation
1143
-
1144
- 1. **Backward Compatibility**: Maintain existing APIs
1145
- 2. **Gradual Rollout**: Use feature flags
1146
- 3. **Monitoring**: Track metrics before/after changes
1147
- 4. **Testing**: Comprehensive test coverage
1148
- 5. **Documentation**: Update all API docs
1149
-
1150
- ## Summary of Key Optimizations
1151
-
1152
- ### Immediate Impact (Phase 1)
1153
- 1. **Eliminate busy wait loop** - Unblocks event loop immediately
1154
- 2. **Async filesystem operations** - Prevents I/O blocking
1155
- 3. **Proper error handling** - Graceful degradation with fallbacks
1156
-
1157
- ### Performance Enhancements (Phase 2-3)
1158
- 1. **Enhanced connection pooling** - O(log n) operations with BinaryHeap
1159
- 2. **Resource lifecycle management** - Prevents memory leaks
1160
- 3. **Worker threads** - Offloads CPU-intensive operations
1161
- 4. **Optimized JSON operations** - Reduces parsing overhead
288
+ 1. WebSocket connections remain stable for 24+ hours
289
+ 2. No premature connection closures reported
290
+ 3. Memory usage remains stable (no socket leaks)
291
+ 4. Half-open connections work correctly
292
+ 5. Graceful shutdown completes within grace period
1162
293
 
1163
- ### Monitoring & Validation (Phase 4)
1164
- 1. **Real-time dashboard** - Visual performance monitoring
1165
- 2. **Event loop lag tracking** - Early warning system
1166
- 3. **Automated benchmarking** - Regression prevention
294
+ ## Implementation Status: COMPLETED
1167
295
 
1168
- ## Implementation Checklist
296
+ ### Implemented Changes
1169
297
 
1170
- ### Phase 1: Critical Fixes (Priority: URGENT)
1171
- - [ ] Create `ts/core/utils/async-utils.ts` with delay function
1172
- - [ ] Fix busy wait loop in `nftables-proxy.ts`
1173
- - [ ] Create `ts/core/utils/fs-utils.ts` with AsyncFileSystem class
1174
- - [ ] Migrate `certificate-manager.ts` to async operations
1175
- - [ ] Migrate `cert-store.ts` to async operations
1176
- - [ ] Replace `execSync` with `execAsync` in `nftables-proxy.ts`
1177
- - [ ] Add comprehensive unit tests for async operations
1178
- - [ ] Performance test to verify event loop improvements
298
+ 1. **Modified `cleanupSocket()` in `socket-utils.ts`**
299
+ - Added `CleanupOptions` interface with `immediate`, `allowDrain`, and `gracePeriod` options
300
+ - Implemented graceful shutdown support with write buffer draining
1179
301
 
1180
- ### Phase 2: Resource Management
1181
- - [x] Implement LifecycleComponent base class
1182
- - [ ] Migrate components to extend LifecycleComponent
1183
- - [x] Implement BinaryHeap data structure
1184
- - [x] Create EnhancedConnectionPool with queue support
1185
- - [x] Add connection validation and health checks
1186
- - [ ] Implement proper timer cleanup in all components
1187
- - [ ] Add integration tests for resource management
1188
- - [x] Clean up legacy code (removed ts/common/, event-utils.ts, event-system.ts)
302
+ 2. **Created `createIndependentSocketHandlers()` in `socket-utils.ts`**
303
+ - Tracks socket states independently
304
+ - Supports half-open connections where one side can close while the other remains open
305
+ - Only triggers full cleanup when both sockets are closed
1189
306
 
1190
- ### Phase 3: Performance Optimizations
1191
- - [ ] Implement HashUtils for efficient object hashing
1192
- - [ ] Create WorkerPool for CPU-intensive operations
1193
- - [ ] Migrate NFTables operations to worker threads
1194
- - [ ] Optimize JSON operations with caching
1195
- - [ ] Add performance benchmarks
307
+ 3. **Updated `setupSocketHandlers()` in `socket-utils.ts`**
308
+ - Added optional `handleTimeout` parameter to customize timeout behavior
309
+ - Prevents automatic connection closure on timeout events
1196
310
 
1197
- ### Phase 4: Monitoring & Polish
1198
- - [ ] Implement PerformanceMonitor class
1199
- - [ ] Create monitoring dashboard with WebSocket updates
1200
- - [ ] Add comprehensive metrics collection
1201
- - [ ] Document all API changes
1202
- - [ ] Create migration guide
1203
- - [ ] Update examples and tutorials
311
+ 4. **Updated HTTPS Passthrough Handler**
312
+ - Now uses `createIndependentSocketHandlers` for half-open support
313
+ - Custom timeout handling that resets timer instead of closing connection
314
+ - Manual data forwarding with backpressure handling
1204
315
 
1205
- ## Next Steps
316
+ 5. **Updated Connection Manager**
317
+ - Extended parity check from 2 minutes to 30 minutes
318
+ - Added activity check before closing (10 minutes of inactivity required)
319
+ - Modified cleanup to check socket states before destroying
1206
320
 
1207
- 1. **Immediate Action**: Fix the busy wait loop (blocks entire event loop)
1208
- 2. **Code Review**: Review this plan with the team
1209
- 3. **Feature Branch**: Create `feature/performance-optimization`
1210
- 4. **Phase 1 Implementation**: Complete within 1 week
1211
- 5. **Staging Deployment**: Test with real traffic patterns
1212
- 6. **Gradual Rollout**: Use feature flags for production
1213
- 7. **Monitor & Iterate**: Track metrics and adjust as needed
321
+ 6. **Updated Basic Forwarding in Route Connection Handler**
322
+ - Replaced simple `pipe()` with independent socket handlers
323
+ - Added manual data forwarding with backpressure support
324
+ - Removed bilateral close handlers to prevent premature cleanup
1214
325
 
1215
- ## Expected Outcomes
326
+ ### Test Results
1216
327
 
1217
- After implementing all phases:
1218
- - **10x improvement** in concurrent connection handling
1219
- - **90% reduction** in event loop blocking
1220
- - **50% reduction** in memory usage under load
1221
- - **Zero memory leaks** with proper resource cleanup
1222
- - **Real-time visibility** into performance metrics
1223
- - **Graceful degradation** under extreme load
328
+ All tests passing:
329
+ - Long-lived connection test: Connection stayed open for 61+ seconds with periodic keep-alive
330
+ - Half-open connection test: One side closed while the other continued to send data
331
+ - No socket leaks or premature closures
1224
332
 
1225
- ## Version Plan
333
+ ### Notes
1226
334
 
1227
- - **v19.6.0**: Phase 1 (Critical fixes) - Backward compatible
1228
- - **v19.7.0**: Phase 2 (Resource management) - Backward compatible
1229
- - **v19.8.0**: Phase 3 (Optimizations) - Backward compatible
1230
- - **v20.0.0**: Phase 4 (Full async) - Breaking changes with migration path
335
+ - The fix maintains backward compatibility
336
+ - No configuration changes required for existing deployments
337
+ - Long-lived connections now work correctly in both HTTPS passthrough and basic forwarding modes