@push.rocks/smartproxy 19.5.4 → 19.5.6
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/async-utils.d.ts +81 -0
- package/dist_ts/core/utils/async-utils.js +216 -0
- package/dist_ts/core/utils/binary-heap.d.ts +73 -0
- package/dist_ts/core/utils/binary-heap.js +193 -0
- package/dist_ts/core/utils/enhanced-connection-pool.d.ts +110 -0
- package/dist_ts/core/utils/enhanced-connection-pool.js +320 -0
- package/dist_ts/core/utils/fs-utils.d.ts +144 -0
- package/dist_ts/core/utils/fs-utils.js +252 -0
- package/dist_ts/core/utils/index.d.ts +6 -2
- package/dist_ts/core/utils/index.js +7 -3
- package/dist_ts/core/utils/lifecycle-component.d.ts +59 -0
- package/dist_ts/core/utils/lifecycle-component.js +195 -0
- package/dist_ts/core/utils/socket-utils.d.ts +28 -0
- package/dist_ts/core/utils/socket-utils.js +77 -0
- package/dist_ts/forwarding/handlers/http-handler.js +7 -4
- package/dist_ts/forwarding/handlers/https-passthrough-handler.js +14 -55
- package/dist_ts/forwarding/handlers/https-terminate-to-http-handler.js +52 -40
- package/dist_ts/forwarding/handlers/https-terminate-to-https-handler.js +31 -43
- package/dist_ts/plugins.d.ts +2 -1
- package/dist_ts/plugins.js +3 -2
- package/dist_ts/proxies/http-proxy/certificate-manager.d.ts +15 -0
- package/dist_ts/proxies/http-proxy/certificate-manager.js +49 -2
- package/dist_ts/proxies/http-proxy/connection-pool.js +4 -19
- package/dist_ts/proxies/http-proxy/http-proxy.js +3 -7
- package/dist_ts/proxies/nftables-proxy/nftables-proxy.d.ts +10 -0
- package/dist_ts/proxies/nftables-proxy/nftables-proxy.js +53 -43
- package/dist_ts/proxies/smart-proxy/cert-store.js +22 -20
- package/dist_ts/proxies/smart-proxy/connection-manager.d.ts +35 -9
- package/dist_ts/proxies/smart-proxy/connection-manager.js +243 -189
- package/dist_ts/proxies/smart-proxy/http-proxy-bridge.js +13 -2
- package/dist_ts/proxies/smart-proxy/port-manager.js +3 -3
- package/dist_ts/proxies/smart-proxy/route-connection-handler.js +35 -4
- package/package.json +2 -2
- package/readme.hints.md +96 -1
- package/readme.plan.md +1135 -221
- package/readme.problems.md +167 -83
- package/ts/core/utils/async-utils.ts +275 -0
- package/ts/core/utils/binary-heap.ts +225 -0
- package/ts/core/utils/enhanced-connection-pool.ts +420 -0
- package/ts/core/utils/fs-utils.ts +270 -0
- package/ts/core/utils/index.ts +6 -2
- package/ts/core/utils/lifecycle-component.ts +231 -0
- package/ts/core/utils/socket-utils.ts +96 -0
- package/ts/forwarding/handlers/http-handler.ts +7 -3
- package/ts/forwarding/handlers/https-passthrough-handler.ts +13 -62
- package/ts/forwarding/handlers/https-terminate-to-http-handler.ts +58 -46
- package/ts/forwarding/handlers/https-terminate-to-https-handler.ts +38 -53
- package/ts/plugins.ts +2 -1
- package/ts/proxies/http-proxy/certificate-manager.ts +52 -1
- package/ts/proxies/http-proxy/connection-pool.ts +3 -16
- package/ts/proxies/http-proxy/http-proxy.ts +2 -5
- package/ts/proxies/nftables-proxy/nftables-proxy.ts +64 -79
- package/ts/proxies/smart-proxy/cert-store.ts +26 -20
- package/ts/proxies/smart-proxy/connection-manager.ts +277 -197
- package/ts/proxies/smart-proxy/http-proxy-bridge.ts +15 -1
- package/ts/proxies/smart-proxy/port-manager.ts +2 -2
- package/ts/proxies/smart-proxy/route-connection-handler.ts +39 -4
- package/readme.plan2.md +0 -764
- package/ts/common/eventUtils.ts +0 -34
- package/ts/common/types.ts +0 -91
- package/ts/core/utils/event-system.ts +0 -376
- package/ts/core/utils/event-utils.ts +0 -25
|
@@ -3,22 +3,45 @@ import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.
|
|
|
3
3
|
import { SecurityManager } from './security-manager.js';
|
|
4
4
|
import { TimeoutManager } from './timeout-manager.js';
|
|
5
5
|
import { logger } from '../../core/utils/logger.js';
|
|
6
|
+
import { LifecycleComponent } from '../../core/utils/lifecycle-component.js';
|
|
7
|
+
import { cleanupSocket } from '../../core/utils/socket-utils.js';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
|
-
* Manages connection lifecycle, tracking, and cleanup
|
|
10
|
+
* Manages connection lifecycle, tracking, and cleanup with performance optimizations
|
|
9
11
|
*/
|
|
10
|
-
export class ConnectionManager {
|
|
12
|
+
export class ConnectionManager extends LifecycleComponent {
|
|
11
13
|
private connectionRecords: Map<string, IConnectionRecord> = new Map();
|
|
12
14
|
private terminationStats: {
|
|
13
15
|
incoming: Record<string, number>;
|
|
14
16
|
outgoing: Record<string, number>;
|
|
15
17
|
} = { incoming: {}, outgoing: {} };
|
|
18
|
+
|
|
19
|
+
// Performance optimization: Track connections needing inactivity check
|
|
20
|
+
private nextInactivityCheck: Map<string, number> = new Map();
|
|
21
|
+
|
|
22
|
+
// Connection limits
|
|
23
|
+
private readonly maxConnections: number;
|
|
24
|
+
private readonly cleanupBatchSize: number = 100;
|
|
25
|
+
|
|
26
|
+
// Cleanup queue for batched processing
|
|
27
|
+
private cleanupQueue: Set<string> = new Set();
|
|
28
|
+
private cleanupTimer: NodeJS.Timeout | null = null;
|
|
16
29
|
|
|
17
30
|
constructor(
|
|
18
31
|
private settings: ISmartProxyOptions,
|
|
19
32
|
private securityManager: SecurityManager,
|
|
20
33
|
private timeoutManager: TimeoutManager
|
|
21
|
-
) {
|
|
34
|
+
) {
|
|
35
|
+
super();
|
|
36
|
+
|
|
37
|
+
// Set reasonable defaults for connection limits
|
|
38
|
+
this.maxConnections = settings.defaults?.security?.maxConnections || 10000;
|
|
39
|
+
|
|
40
|
+
// Start inactivity check timer if not disabled
|
|
41
|
+
if (!settings.disableInactivityCheck) {
|
|
42
|
+
this.startInactivityCheckTimer();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
22
45
|
|
|
23
46
|
/**
|
|
24
47
|
* Generate a unique connection ID
|
|
@@ -31,17 +54,29 @@ export class ConnectionManager {
|
|
|
31
54
|
/**
|
|
32
55
|
* Create and track a new connection
|
|
33
56
|
*/
|
|
34
|
-
public createConnection(socket: plugins.net.Socket): IConnectionRecord {
|
|
57
|
+
public createConnection(socket: plugins.net.Socket): IConnectionRecord | null {
|
|
58
|
+
// Enforce connection limit
|
|
59
|
+
if (this.connectionRecords.size >= this.maxConnections) {
|
|
60
|
+
logger.log('warn', `Connection limit reached (${this.maxConnections}). Rejecting new connection.`, {
|
|
61
|
+
currentConnections: this.connectionRecords.size,
|
|
62
|
+
maxConnections: this.maxConnections,
|
|
63
|
+
component: 'connection-manager'
|
|
64
|
+
});
|
|
65
|
+
socket.destroy();
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
35
69
|
const connectionId = this.generateConnectionId();
|
|
36
70
|
const remoteIP = socket.remoteAddress || '';
|
|
37
71
|
const localPort = socket.localPort || 0;
|
|
72
|
+
const now = Date.now();
|
|
38
73
|
|
|
39
74
|
const record: IConnectionRecord = {
|
|
40
75
|
id: connectionId,
|
|
41
76
|
incoming: socket,
|
|
42
77
|
outgoing: null,
|
|
43
|
-
incomingStartTime:
|
|
44
|
-
lastActivity:
|
|
78
|
+
incomingStartTime: now,
|
|
79
|
+
lastActivity: now,
|
|
45
80
|
connectionClosed: false,
|
|
46
81
|
pendingData: [],
|
|
47
82
|
pendingDataSize: 0,
|
|
@@ -70,6 +105,42 @@ export class ConnectionManager {
|
|
|
70
105
|
public trackConnection(connectionId: string, record: IConnectionRecord): void {
|
|
71
106
|
this.connectionRecords.set(connectionId, record);
|
|
72
107
|
this.securityManager.trackConnectionByIP(record.remoteIP, connectionId);
|
|
108
|
+
|
|
109
|
+
// Schedule inactivity check
|
|
110
|
+
if (!this.settings.disableInactivityCheck) {
|
|
111
|
+
this.scheduleInactivityCheck(connectionId, record);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Schedule next inactivity check for a connection
|
|
117
|
+
*/
|
|
118
|
+
private scheduleInactivityCheck(connectionId: string, record: IConnectionRecord): void {
|
|
119
|
+
let timeout = this.settings.inactivityTimeout!;
|
|
120
|
+
|
|
121
|
+
if (record.hasKeepAlive) {
|
|
122
|
+
if (this.settings.keepAliveTreatment === 'immortal') {
|
|
123
|
+
// Don't schedule check for immortal connections
|
|
124
|
+
return;
|
|
125
|
+
} else if (this.settings.keepAliveTreatment === 'extended') {
|
|
126
|
+
const multiplier = this.settings.keepAliveInactivityMultiplier || 6;
|
|
127
|
+
timeout = timeout * multiplier;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const checkTime = Date.now() + timeout;
|
|
132
|
+
this.nextInactivityCheck.set(connectionId, checkTime);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Start the inactivity check timer
|
|
137
|
+
*/
|
|
138
|
+
private startInactivityCheckTimer(): void {
|
|
139
|
+
// Check every 30 seconds for connections that need inactivity check
|
|
140
|
+
this.setInterval(() => {
|
|
141
|
+
this.performOptimizedInactivityCheck();
|
|
142
|
+
}, 30000);
|
|
143
|
+
// Note: LifecycleComponent's setInterval already calls unref()
|
|
73
144
|
}
|
|
74
145
|
|
|
75
146
|
/**
|
|
@@ -98,18 +169,65 @@ export class ConnectionManager {
|
|
|
98
169
|
*/
|
|
99
170
|
public initiateCleanupOnce(record: IConnectionRecord, reason: string = 'normal'): void {
|
|
100
171
|
if (this.settings.enableDetailedLogging) {
|
|
101
|
-
logger.log('info', `Connection cleanup initiated`, {
|
|
172
|
+
logger.log('info', `Connection cleanup initiated`, {
|
|
173
|
+
connectionId: record.id,
|
|
174
|
+
remoteIP: record.remoteIP,
|
|
175
|
+
reason,
|
|
176
|
+
component: 'connection-manager'
|
|
177
|
+
});
|
|
102
178
|
}
|
|
103
179
|
|
|
104
|
-
if (
|
|
105
|
-
record.incomingTerminationReason === null ||
|
|
106
|
-
record.incomingTerminationReason === undefined
|
|
107
|
-
) {
|
|
180
|
+
if (record.incomingTerminationReason == null) {
|
|
108
181
|
record.incomingTerminationReason = reason;
|
|
109
182
|
this.incrementTerminationStat('incoming', reason);
|
|
110
183
|
}
|
|
111
184
|
|
|
112
|
-
|
|
185
|
+
// Add to cleanup queue for batched processing
|
|
186
|
+
this.queueCleanup(record.id);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Queue a connection for cleanup
|
|
191
|
+
*/
|
|
192
|
+
private queueCleanup(connectionId: string): void {
|
|
193
|
+
this.cleanupQueue.add(connectionId);
|
|
194
|
+
|
|
195
|
+
// Process immediately if queue is getting large
|
|
196
|
+
if (this.cleanupQueue.size >= this.cleanupBatchSize) {
|
|
197
|
+
this.processCleanupQueue();
|
|
198
|
+
} else if (!this.cleanupTimer) {
|
|
199
|
+
// Otherwise, schedule batch processing
|
|
200
|
+
this.cleanupTimer = this.setTimeout(() => {
|
|
201
|
+
this.processCleanupQueue();
|
|
202
|
+
}, 100);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Process the cleanup queue in batches
|
|
208
|
+
*/
|
|
209
|
+
private processCleanupQueue(): void {
|
|
210
|
+
if (this.cleanupTimer) {
|
|
211
|
+
this.clearTimeout(this.cleanupTimer);
|
|
212
|
+
this.cleanupTimer = null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const toCleanup = Array.from(this.cleanupQueue).slice(0, this.cleanupBatchSize);
|
|
216
|
+
this.cleanupQueue.clear();
|
|
217
|
+
|
|
218
|
+
for (const connectionId of toCleanup) {
|
|
219
|
+
const record = this.connectionRecords.get(connectionId);
|
|
220
|
+
if (record) {
|
|
221
|
+
this.cleanupConnection(record, record.incomingTerminationReason || 'normal');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// If there are more in queue, schedule next batch
|
|
226
|
+
if (this.cleanupQueue.size > 0) {
|
|
227
|
+
this.cleanupTimer = this.setTimeout(() => {
|
|
228
|
+
this.processCleanupQueue();
|
|
229
|
+
}, 10);
|
|
230
|
+
}
|
|
113
231
|
}
|
|
114
232
|
|
|
115
233
|
/**
|
|
@@ -119,6 +237,9 @@ export class ConnectionManager {
|
|
|
119
237
|
if (!record.connectionClosed) {
|
|
120
238
|
record.connectionClosed = true;
|
|
121
239
|
|
|
240
|
+
// Remove from inactivity check
|
|
241
|
+
this.nextInactivityCheck.delete(record.id);
|
|
242
|
+
|
|
122
243
|
// Track connection termination
|
|
123
244
|
this.securityManager.removeConnectionByIP(record.remoteIP, record.id);
|
|
124
245
|
|
|
@@ -127,29 +248,41 @@ export class ConnectionManager {
|
|
|
127
248
|
record.cleanupTimer = undefined;
|
|
128
249
|
}
|
|
129
250
|
|
|
130
|
-
//
|
|
251
|
+
// Calculate metrics once
|
|
131
252
|
const duration = Date.now() - record.incomingStartTime;
|
|
132
|
-
const
|
|
133
|
-
|
|
253
|
+
const logData = {
|
|
254
|
+
connectionId: record.id,
|
|
255
|
+
remoteIP: record.remoteIP,
|
|
256
|
+
localPort: record.localPort,
|
|
257
|
+
reason,
|
|
258
|
+
duration: plugins.prettyMs(duration),
|
|
259
|
+
bytes: { in: record.bytesReceived, out: record.bytesSent },
|
|
260
|
+
tls: record.isTLS,
|
|
261
|
+
keepAlive: record.hasKeepAlive,
|
|
262
|
+
usingNetworkProxy: record.usingNetworkProxy,
|
|
263
|
+
domainSwitches: record.domainSwitches || 0,
|
|
264
|
+
component: 'connection-manager'
|
|
265
|
+
};
|
|
134
266
|
|
|
135
267
|
// Remove all data handlers to make sure we clean up properly
|
|
136
268
|
if (record.incoming) {
|
|
137
269
|
try {
|
|
138
|
-
// Remove our safe data handler
|
|
139
270
|
record.incoming.removeAllListeners('data');
|
|
140
|
-
// Reset the handler references
|
|
141
271
|
record.renegotiationHandler = undefined;
|
|
142
272
|
} catch (err) {
|
|
143
|
-
logger.log('error', `Error removing data handlers
|
|
273
|
+
logger.log('error', `Error removing data handlers: ${err}`, {
|
|
274
|
+
connectionId: record.id,
|
|
275
|
+
error: err,
|
|
276
|
+
component: 'connection-manager'
|
|
277
|
+
});
|
|
144
278
|
}
|
|
145
279
|
}
|
|
146
280
|
|
|
147
|
-
// Handle
|
|
148
|
-
|
|
281
|
+
// Handle socket cleanup without delay
|
|
282
|
+
cleanupSocket(record.incoming, `${record.id}-incoming`);
|
|
149
283
|
|
|
150
|
-
// Handle outgoing socket
|
|
151
284
|
if (record.outgoing) {
|
|
152
|
-
|
|
285
|
+
cleanupSocket(record.outgoing, `${record.id}-outgoing`);
|
|
153
286
|
}
|
|
154
287
|
|
|
155
288
|
// Clear pendingData to avoid memory leaks
|
|
@@ -162,28 +295,13 @@ export class ConnectionManager {
|
|
|
162
295
|
// Log connection details
|
|
163
296
|
if (this.settings.enableDetailedLogging) {
|
|
164
297
|
logger.log('info',
|
|
165
|
-
`Connection
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
`${record.usingNetworkProxy ? ', Using NetworkProxy' : ''}` +
|
|
169
|
-
`${record.domainSwitches ? `, Domain switches: ${record.domainSwitches}` : ''}`,
|
|
170
|
-
{
|
|
171
|
-
connectionId: record.id,
|
|
172
|
-
remoteIP: record.remoteIP,
|
|
173
|
-
localPort: record.localPort,
|
|
174
|
-
reason,
|
|
175
|
-
duration: plugins.prettyMs(duration),
|
|
176
|
-
bytes: { in: bytesReceived, out: bytesSent },
|
|
177
|
-
tls: record.isTLS,
|
|
178
|
-
keepAlive: record.hasKeepAlive,
|
|
179
|
-
usingNetworkProxy: record.usingNetworkProxy,
|
|
180
|
-
domainSwitches: record.domainSwitches || 0,
|
|
181
|
-
component: 'connection-manager'
|
|
182
|
-
}
|
|
298
|
+
`Connection terminated: ${record.remoteIP}:${record.localPort} (${reason}) - ` +
|
|
299
|
+
`${plugins.prettyMs(duration)}, IN: ${record.bytesReceived}B, OUT: ${record.bytesSent}B`,
|
|
300
|
+
logData
|
|
183
301
|
);
|
|
184
302
|
} else {
|
|
185
303
|
logger.log('info',
|
|
186
|
-
`Connection
|
|
304
|
+
`Connection terminated: ${record.remoteIP} (${reason}). Active: ${this.connectionRecords.size}`,
|
|
187
305
|
{
|
|
188
306
|
connectionId: record.id,
|
|
189
307
|
remoteIP: record.remoteIP,
|
|
@@ -196,40 +314,6 @@ export class ConnectionManager {
|
|
|
196
314
|
}
|
|
197
315
|
}
|
|
198
316
|
|
|
199
|
-
/**
|
|
200
|
-
* Helper method to clean up a socket
|
|
201
|
-
*/
|
|
202
|
-
private cleanupSocket(record: IConnectionRecord, side: 'incoming' | 'outgoing', socket: plugins.net.Socket): void {
|
|
203
|
-
try {
|
|
204
|
-
if (!socket.destroyed) {
|
|
205
|
-
// Try graceful shutdown first, then force destroy after a short timeout
|
|
206
|
-
socket.end();
|
|
207
|
-
const socketTimeout = setTimeout(() => {
|
|
208
|
-
try {
|
|
209
|
-
if (!socket.destroyed) {
|
|
210
|
-
socket.destroy();
|
|
211
|
-
}
|
|
212
|
-
} catch (err) {
|
|
213
|
-
logger.log('error', `Error destroying ${side} socket for connection ${record.id}: ${err}`, { connectionId: record.id, side, error: err, component: 'connection-manager' });
|
|
214
|
-
}
|
|
215
|
-
}, 1000);
|
|
216
|
-
|
|
217
|
-
// Ensure the timeout doesn't block Node from exiting
|
|
218
|
-
if (socketTimeout.unref) {
|
|
219
|
-
socketTimeout.unref();
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
} catch (err) {
|
|
223
|
-
logger.log('error', `Error closing ${side} socket for connection ${record.id}: ${err}`, { connectionId: record.id, side, error: err, component: 'connection-manager' });
|
|
224
|
-
try {
|
|
225
|
-
if (!socket.destroyed) {
|
|
226
|
-
socket.destroy();
|
|
227
|
-
}
|
|
228
|
-
} catch (destroyErr) {
|
|
229
|
-
logger.log('error', `Error destroying ${side} socket for connection ${record.id}: ${destroyErr}`, { connectionId: record.id, side, error: destroyErr, component: 'connection-manager' });
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
317
|
|
|
234
318
|
/**
|
|
235
319
|
* Creates a generic error handler for incoming or outgoing sockets
|
|
@@ -238,49 +322,44 @@ export class ConnectionManager {
|
|
|
238
322
|
return (err: Error) => {
|
|
239
323
|
const code = (err as any).code;
|
|
240
324
|
let reason = 'error';
|
|
241
|
-
|
|
325
|
+
|
|
242
326
|
const now = Date.now();
|
|
243
327
|
const connectionDuration = now - record.incomingStartTime;
|
|
244
328
|
const lastActivityAge = now - record.lastActivity;
|
|
245
329
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
error: err.message,
|
|
274
|
-
duration: plugins.prettyMs(connectionDuration),
|
|
275
|
-
lastActivity: plugins.prettyMs(lastActivityAge),
|
|
276
|
-
component: 'connection-manager'
|
|
277
|
-
});
|
|
330
|
+
// Update activity tracking
|
|
331
|
+
if (side === 'incoming') {
|
|
332
|
+
record.lastActivity = now;
|
|
333
|
+
this.scheduleInactivityCheck(record.id, record);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const errorData = {
|
|
337
|
+
connectionId: record.id,
|
|
338
|
+
side,
|
|
339
|
+
remoteIP: record.remoteIP,
|
|
340
|
+
error: err.message,
|
|
341
|
+
duration: plugins.prettyMs(connectionDuration),
|
|
342
|
+
lastActivity: plugins.prettyMs(lastActivityAge),
|
|
343
|
+
component: 'connection-manager'
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
switch (code) {
|
|
347
|
+
case 'ECONNRESET':
|
|
348
|
+
reason = 'econnreset';
|
|
349
|
+
logger.log('warn', `ECONNRESET on ${side}: ${record.remoteIP}`, errorData);
|
|
350
|
+
break;
|
|
351
|
+
case 'ETIMEDOUT':
|
|
352
|
+
reason = 'etimedout';
|
|
353
|
+
logger.log('warn', `ETIMEDOUT on ${side}: ${record.remoteIP}`, errorData);
|
|
354
|
+
break;
|
|
355
|
+
default:
|
|
356
|
+
logger.log('error', `Error on ${side}: ${record.remoteIP} - ${err.message}`, errorData);
|
|
278
357
|
}
|
|
279
358
|
|
|
280
|
-
if (side === 'incoming' && record.incomingTerminationReason
|
|
359
|
+
if (side === 'incoming' && record.incomingTerminationReason == null) {
|
|
281
360
|
record.incomingTerminationReason = reason;
|
|
282
361
|
this.incrementTerminationStat('incoming', reason);
|
|
283
|
-
} else if (side === 'outgoing' && record.outgoingTerminationReason
|
|
362
|
+
} else if (side === 'outgoing' && record.outgoingTerminationReason == null) {
|
|
284
363
|
record.outgoingTerminationReason = reason;
|
|
285
364
|
this.incrementTerminationStat('outgoing', reason);
|
|
286
365
|
}
|
|
@@ -303,13 +382,12 @@ export class ConnectionManager {
|
|
|
303
382
|
});
|
|
304
383
|
}
|
|
305
384
|
|
|
306
|
-
if (side === 'incoming' && record.incomingTerminationReason
|
|
385
|
+
if (side === 'incoming' && record.incomingTerminationReason == null) {
|
|
307
386
|
record.incomingTerminationReason = 'normal';
|
|
308
387
|
this.incrementTerminationStat('incoming', 'normal');
|
|
309
|
-
} else if (side === 'outgoing' && record.outgoingTerminationReason
|
|
388
|
+
} else if (side === 'outgoing' && record.outgoingTerminationReason == null) {
|
|
310
389
|
record.outgoingTerminationReason = 'normal';
|
|
311
390
|
this.incrementTerminationStat('outgoing', 'normal');
|
|
312
|
-
// Record the time when outgoing socket closed.
|
|
313
391
|
record.outgoingClosedTime = Date.now();
|
|
314
392
|
}
|
|
315
393
|
|
|
@@ -332,26 +410,29 @@ export class ConnectionManager {
|
|
|
332
410
|
}
|
|
333
411
|
|
|
334
412
|
/**
|
|
335
|
-
*
|
|
413
|
+
* Optimized inactivity check - only checks connections that are due
|
|
336
414
|
*/
|
|
337
|
-
|
|
415
|
+
private performOptimizedInactivityCheck(): void {
|
|
338
416
|
const now = Date.now();
|
|
339
|
-
const
|
|
417
|
+
const connectionsToCheck: string[] = [];
|
|
340
418
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
if (
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
419
|
+
// Find connections that need checking
|
|
420
|
+
for (const [connectionId, checkTime] of this.nextInactivityCheck) {
|
|
421
|
+
if (checkTime <= now) {
|
|
422
|
+
connectionsToCheck.push(connectionId);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Process only connections that need checking
|
|
427
|
+
for (const connectionId of connectionsToCheck) {
|
|
428
|
+
const record = this.connectionRecords.get(connectionId);
|
|
429
|
+
if (!record || record.connectionClosed) {
|
|
430
|
+
this.nextInactivityCheck.delete(connectionId);
|
|
350
431
|
continue;
|
|
351
432
|
}
|
|
352
433
|
|
|
353
434
|
const inactivityTime = now - record.lastActivity;
|
|
354
|
-
|
|
435
|
+
|
|
355
436
|
// Use extended timeout for extended-treatment keep-alive connections
|
|
356
437
|
let effectiveTimeout = this.settings.inactivityTimeout!;
|
|
357
438
|
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
|
|
@@ -359,37 +440,37 @@ export class ConnectionManager {
|
|
|
359
440
|
effectiveTimeout = effectiveTimeout * multiplier;
|
|
360
441
|
}
|
|
361
442
|
|
|
362
|
-
if (inactivityTime > effectiveTimeout
|
|
443
|
+
if (inactivityTime > effectiveTimeout) {
|
|
363
444
|
// For keep-alive connections, issue a warning first
|
|
364
445
|
if (record.hasKeepAlive && !record.inactivityWarningIssued) {
|
|
365
|
-
logger.log('warn', `Keep-alive connection
|
|
366
|
-
connectionId
|
|
446
|
+
logger.log('warn', `Keep-alive connection inactive: ${record.remoteIP}`, {
|
|
447
|
+
connectionId,
|
|
367
448
|
remoteIP: record.remoteIP,
|
|
368
449
|
inactiveFor: plugins.prettyMs(inactivityTime),
|
|
369
|
-
closureWarning: '10 minutes',
|
|
370
450
|
component: 'connection-manager'
|
|
371
451
|
});
|
|
372
452
|
|
|
373
|
-
// Set warning flag and add grace period
|
|
374
453
|
record.inactivityWarningIssued = true;
|
|
375
|
-
|
|
454
|
+
|
|
455
|
+
// Reschedule check for 10 minutes later
|
|
456
|
+
this.nextInactivityCheck.set(connectionId, now + 600000);
|
|
376
457
|
|
|
377
458
|
// Try to stimulate activity with a probe packet
|
|
378
459
|
if (record.outgoing && !record.outgoing.destroyed) {
|
|
379
460
|
try {
|
|
380
461
|
record.outgoing.write(Buffer.alloc(0));
|
|
381
|
-
|
|
382
|
-
if (this.settings.enableDetailedLogging) {
|
|
383
|
-
logger.log('info', `Sent probe packet to test keep-alive connection ${id}`, { connectionId: id, component: 'connection-manager' });
|
|
384
|
-
}
|
|
385
462
|
} catch (err) {
|
|
386
|
-
logger.log('error', `Error sending probe packet
|
|
463
|
+
logger.log('error', `Error sending probe packet: ${err}`, {
|
|
464
|
+
connectionId,
|
|
465
|
+
error: err,
|
|
466
|
+
component: 'connection-manager'
|
|
467
|
+
});
|
|
387
468
|
}
|
|
388
469
|
}
|
|
389
470
|
} else {
|
|
390
|
-
//
|
|
391
|
-
logger.log('warn', `Closing inactive connection ${
|
|
392
|
-
connectionId
|
|
471
|
+
// Close the connection
|
|
472
|
+
logger.log('warn', `Closing inactive connection: ${record.remoteIP}`, {
|
|
473
|
+
connectionId,
|
|
393
474
|
remoteIP: record.remoteIP,
|
|
394
475
|
inactiveFor: plugins.prettyMs(inactivityTime),
|
|
395
476
|
hasKeepAlive: record.hasKeepAlive,
|
|
@@ -397,15 +478,9 @@ export class ConnectionManager {
|
|
|
397
478
|
});
|
|
398
479
|
this.cleanupConnection(record, 'inactivity');
|
|
399
480
|
}
|
|
400
|
-
} else
|
|
401
|
-
//
|
|
402
|
-
|
|
403
|
-
logger.log('info', `Connection ${id} activity detected after inactivity warning`, {
|
|
404
|
-
connectionId: id,
|
|
405
|
-
component: 'connection-manager'
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
record.inactivityWarningIssued = false;
|
|
481
|
+
} else {
|
|
482
|
+
// Reschedule next check
|
|
483
|
+
this.scheduleInactivityCheck(connectionId, record);
|
|
409
484
|
}
|
|
410
485
|
|
|
411
486
|
// Parity check: if outgoing socket closed and incoming remains active
|
|
@@ -415,8 +490,8 @@ export class ConnectionManager {
|
|
|
415
490
|
!record.connectionClosed &&
|
|
416
491
|
now - record.outgoingClosedTime > 120000
|
|
417
492
|
) {
|
|
418
|
-
logger.log('warn', `Parity check:
|
|
419
|
-
connectionId
|
|
493
|
+
logger.log('warn', `Parity check failed: ${record.remoteIP}`, {
|
|
494
|
+
connectionId,
|
|
420
495
|
remoteIP: record.remoteIP,
|
|
421
496
|
timeElapsed: plugins.prettyMs(now - record.outgoingClosedTime),
|
|
422
497
|
component: 'connection-manager'
|
|
@@ -426,68 +501,73 @@ export class ConnectionManager {
|
|
|
426
501
|
}
|
|
427
502
|
}
|
|
428
503
|
|
|
504
|
+
/**
|
|
505
|
+
* Legacy method for backward compatibility
|
|
506
|
+
*/
|
|
507
|
+
public performInactivityCheck(): void {
|
|
508
|
+
this.performOptimizedInactivityCheck();
|
|
509
|
+
}
|
|
510
|
+
|
|
429
511
|
/**
|
|
430
512
|
* Clear all connections (for shutdown)
|
|
431
513
|
*/
|
|
432
|
-
public clearConnections(): void {
|
|
433
|
-
//
|
|
434
|
-
|
|
514
|
+
public async clearConnections(): Promise<void> {
|
|
515
|
+
// Delegate to LifecycleComponent's cleanup
|
|
516
|
+
await this.cleanup();
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Override LifecycleComponent's onCleanup method
|
|
521
|
+
*/
|
|
522
|
+
protected async onCleanup(): Promise<void> {
|
|
435
523
|
|
|
436
|
-
//
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
524
|
+
// Process connections in batches to avoid blocking
|
|
525
|
+
const connections = Array.from(this.connectionRecords.values());
|
|
526
|
+
const batchSize = 100;
|
|
527
|
+
let index = 0;
|
|
528
|
+
|
|
529
|
+
const processBatch = () => {
|
|
530
|
+
const batch = connections.slice(index, index + batchSize);
|
|
531
|
+
|
|
532
|
+
for (const record of batch) {
|
|
440
533
|
try {
|
|
441
|
-
// Clear any timers
|
|
442
534
|
if (record.cleanupTimer) {
|
|
443
535
|
clearTimeout(record.cleanupTimer);
|
|
444
536
|
record.cleanupTimer = undefined;
|
|
445
537
|
}
|
|
446
538
|
|
|
447
|
-
//
|
|
448
|
-
if (record.incoming
|
|
449
|
-
record.incoming.
|
|
539
|
+
// Immediate destruction using socket-utils
|
|
540
|
+
if (record.incoming) {
|
|
541
|
+
cleanupSocket(record.incoming, `${record.id}-incoming-shutdown`);
|
|
450
542
|
}
|
|
451
543
|
|
|
452
|
-
if (record.outgoing
|
|
453
|
-
record.outgoing.
|
|
544
|
+
if (record.outgoing) {
|
|
545
|
+
cleanupSocket(record.outgoing, `${record.id}-outgoing-shutdown`);
|
|
454
546
|
}
|
|
455
547
|
} catch (err) {
|
|
456
|
-
logger.log('error', `Error during
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
// Short delay to allow graceful ends to process
|
|
462
|
-
setTimeout(() => {
|
|
463
|
-
// Second pass: Force destroy everything
|
|
464
|
-
for (const id of connectionIds) {
|
|
465
|
-
const record = this.connectionRecords.get(id);
|
|
466
|
-
if (record) {
|
|
467
|
-
try {
|
|
468
|
-
// Remove all listeners to prevent memory leaks
|
|
469
|
-
if (record.incoming) {
|
|
470
|
-
record.incoming.removeAllListeners();
|
|
471
|
-
if (!record.incoming.destroyed) {
|
|
472
|
-
record.incoming.destroy();
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
if (record.outgoing) {
|
|
477
|
-
record.outgoing.removeAllListeners();
|
|
478
|
-
if (!record.outgoing.destroyed) {
|
|
479
|
-
record.outgoing.destroy();
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
} catch (err) {
|
|
483
|
-
logger.log('error', `Error during forced destruction of connection ${id}: ${err}`, { connectionId: id, error: err, component: 'connection-manager' });
|
|
484
|
-
}
|
|
548
|
+
logger.log('error', `Error during connection cleanup: ${err}`, {
|
|
549
|
+
connectionId: record.id,
|
|
550
|
+
error: err,
|
|
551
|
+
component: 'connection-manager'
|
|
552
|
+
});
|
|
485
553
|
}
|
|
486
554
|
}
|
|
487
555
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
556
|
+
index += batchSize;
|
|
557
|
+
|
|
558
|
+
// Continue with next batch if needed
|
|
559
|
+
if (index < connections.length) {
|
|
560
|
+
setImmediate(processBatch);
|
|
561
|
+
} else {
|
|
562
|
+
// Clear all maps
|
|
563
|
+
this.connectionRecords.clear();
|
|
564
|
+
this.nextInactivityCheck.clear();
|
|
565
|
+
this.cleanupQueue.clear();
|
|
566
|
+
this.terminationStats = { incoming: {}, outgoing: {} };
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
// Start batch processing
|
|
571
|
+
setImmediate(processBatch);
|
|
492
572
|
}
|
|
493
573
|
}
|