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