@push.rocks/smartproxy 19.5.6 → 19.5.9
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/dist_ts/core/utils/enhanced-connection-pool.js +7 -2
- package/dist_ts/core/utils/lifecycle-component.js +23 -7
- package/dist_ts/core/utils/socket-utils.d.ts +21 -2
- package/dist_ts/core/utils/socket-utils.js +108 -22
- package/dist_ts/forwarding/handlers/http-handler.js +7 -2
- package/dist_ts/forwarding/handlers/https-passthrough-handler.d.ts +1 -1
- package/dist_ts/forwarding/handlers/https-passthrough-handler.js +16 -10
- package/dist_ts/forwarding/handlers/https-terminate-to-http-handler.js +3 -3
- package/dist_ts/forwarding/handlers/https-terminate-to-https-handler.js +3 -3
- package/dist_ts/proxies/http-proxy/connection-pool.js +3 -3
- package/dist_ts/proxies/http-proxy/http-proxy.js +3 -4
- package/dist_ts/proxies/smart-proxy/connection-manager.js +48 -14
- package/dist_ts/proxies/smart-proxy/port-manager.js +2 -2
- package/dist_ts/proxies/smart-proxy/route-connection-handler.js +48 -10
- package/package.json +1 -1
- package/readme.plan.md +246 -1139
- package/ts/core/utils/enhanced-connection-pool.ts +6 -1
- package/ts/core/utils/lifecycle-component.ts +26 -6
- package/ts/core/utils/socket-utils.ts +123 -19
- package/ts/forwarding/handlers/http-handler.ts +6 -1
- package/ts/forwarding/handlers/https-passthrough-handler.ts +28 -16
- package/ts/forwarding/handlers/https-terminate-to-http-handler.ts +2 -2
- package/ts/forwarding/handlers/https-terminate-to-https-handler.ts +2 -2
- package/ts/proxies/http-proxy/connection-pool.ts +2 -2
- package/ts/proxies/http-proxy/http-proxy.ts +4 -3
- package/ts/proxies/smart-proxy/connection-manager.ts +48 -13
- package/ts/proxies/smart-proxy/port-manager.ts +1 -1
- package/ts/proxies/smart-proxy/route-connection-handler.ts +58 -9
package/readme.plan.md
CHANGED
|
@@ -1,1230 +1,337 @@
|
|
|
1
|
-
# SmartProxy
|
|
1
|
+
# SmartProxy Socket Cleanup Fix Plan
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Problem Summary
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
##
|
|
11
|
+
## Root Causes
|
|
8
12
|
|
|
9
|
-
### 1.
|
|
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
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
**
|
|
30
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
###
|
|
184
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
**
|
|
249
|
-
|
|
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
|
-
|
|
42
|
+
## Fix Implementation Plan
|
|
259
43
|
|
|
260
|
-
|
|
44
|
+
### Phase 1: Fix Socket Cleanup (Prevent Premature Closure)
|
|
261
45
|
|
|
262
|
-
|
|
46
|
+
#### 1.1 Modify `cleanupSocket()` to support graceful shutdown
|
|
263
47
|
```typescript
|
|
264
|
-
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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 (
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
//
|
|
437
|
-
if (
|
|
438
|
-
|
|
76
|
+
// Force cleanup after grace period
|
|
77
|
+
if (options.gracePeriod) {
|
|
78
|
+
setTimeout(cleanup, options.gracePeriod);
|
|
439
79
|
}
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
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
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
-
|
|
555
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
147
|
+
#### 2.1 Separate timeout handling from connection closure
|
|
711
148
|
```typescript
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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
|
-
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
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
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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
|
-
|
|
221
|
+
#### 3.2 Update cleanupConnection to check socket states
|
|
860
222
|
```typescript
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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
|
-
|
|
990
|
-
|
|
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
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
248
|
+
### Phase 4: Testing and Validation
|
|
1082
249
|
|
|
1083
|
-
|
|
1084
|
-
1.
|
|
1085
|
-
2.
|
|
1086
|
-
3.
|
|
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
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
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
|
-
|
|
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
|
-
|
|
271
|
+
## Configuration Changes
|
|
1116
272
|
|
|
273
|
+
Add new options to SmartProxyOptions:
|
|
1117
274
|
```typescript
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
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.
|
|
1137
|
-
2.
|
|
1138
|
-
3.
|
|
1139
|
-
4.
|
|
1140
|
-
5.
|
|
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
|
-
|
|
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
|
-
|
|
296
|
+
### Implemented Changes
|
|
1169
297
|
|
|
1170
|
-
|
|
1171
|
-
-
|
|
1172
|
-
-
|
|
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
|
-
|
|
1181
|
-
-
|
|
1182
|
-
-
|
|
1183
|
-
-
|
|
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
|
-
|
|
1191
|
-
-
|
|
1192
|
-
-
|
|
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
|
-
|
|
1198
|
-
-
|
|
1199
|
-
-
|
|
1200
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
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
|
-
|
|
326
|
+
### Test Results
|
|
1216
327
|
|
|
1217
|
-
|
|
1218
|
-
-
|
|
1219
|
-
-
|
|
1220
|
-
-
|
|
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
|
-
|
|
333
|
+
### Notes
|
|
1226
334
|
|
|
1227
|
-
-
|
|
1228
|
-
-
|
|
1229
|
-
-
|
|
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
|