@push.rocks/smartproxy 19.5.23 → 19.5.24
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/package.json +1 -1
- package/readme.connections.md +177 -1
- package/readme.hints.md +39 -1
- package/ts/proxies/smart-proxy/connection-manager.ts +42 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@push.rocks/smartproxy",
|
|
3
|
-
"version": "19.5.
|
|
3
|
+
"version": "19.5.24",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.",
|
|
6
6
|
"main": "dist_ts/index.js",
|
package/readme.connections.md
CHANGED
|
@@ -372,4 +372,180 @@ The connection cleanup mechanisms have been significantly improved in v19.5.20:
|
|
|
372
372
|
2. Immediate routing cleanup handler always destroys outgoing connections
|
|
373
373
|
3. Tests confirm no accumulation in standard scenarios with reachable backends
|
|
374
374
|
|
|
375
|
-
However, the missing connection establishment timeout causes accumulation when backends are unreachable or very slow to connect.
|
|
375
|
+
However, the missing connection establishment timeout causes accumulation when backends are unreachable or very slow to connect.
|
|
376
|
+
|
|
377
|
+
### Outer Proxy Sudden Accumulation After Hours
|
|
378
|
+
|
|
379
|
+
**User Report**: "The counter goes up suddenly after some hours on the outer proxy"
|
|
380
|
+
|
|
381
|
+
**Investigation Findings**:
|
|
382
|
+
|
|
383
|
+
1. **Cleanup Queue Mechanism**:
|
|
384
|
+
- Connections are cleaned up in batches of 100 via a queue
|
|
385
|
+
- If the cleanup timer gets stuck or cleared without restart, connections accumulate
|
|
386
|
+
- The timer is set with `setTimeout` and could be affected by event loop blocking
|
|
387
|
+
|
|
388
|
+
2. **Potential Causes for Sudden Spikes**:
|
|
389
|
+
|
|
390
|
+
a) **Cleanup Timer Failure**:
|
|
391
|
+
```typescript
|
|
392
|
+
// In ConnectionManager, if this timer gets cleared but not restarted:
|
|
393
|
+
this.cleanupTimer = this.setTimeout(() => {
|
|
394
|
+
this.processCleanupQueue();
|
|
395
|
+
}, 100);
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
b) **Memory Pressure**:
|
|
399
|
+
- After hours of operation, memory fragmentation or pressure could cause delays
|
|
400
|
+
- Garbage collection pauses might interfere with timer execution
|
|
401
|
+
|
|
402
|
+
c) **Event Listener Accumulation**:
|
|
403
|
+
- Socket event listeners might accumulate over time
|
|
404
|
+
- Server 'connection' event handlers are particularly important
|
|
405
|
+
|
|
406
|
+
d) **Keep-Alive Connection Cascades**:
|
|
407
|
+
- When many keep-alive connections timeout simultaneously
|
|
408
|
+
- Outer proxy has different timeout than inner proxy
|
|
409
|
+
- Mass disconnection events can overwhelm cleanup queue
|
|
410
|
+
|
|
411
|
+
e) **HttpProxy Component Issues**:
|
|
412
|
+
- If using `useHttpProxy`, the HttpProxy bridge might maintain connection pools
|
|
413
|
+
- These pools might not be properly cleaned after hours
|
|
414
|
+
|
|
415
|
+
3. **Why "Sudden" After Hours**:
|
|
416
|
+
- Not a gradual leak but triggered by specific conditions
|
|
417
|
+
- Likely related to periodic events or thresholds:
|
|
418
|
+
- Inactivity check runs every 30 seconds
|
|
419
|
+
- Keep-alive connections have extended timeouts (6x normal)
|
|
420
|
+
- Parity check has 30-minute timeout for half-closed connections
|
|
421
|
+
|
|
422
|
+
4. **Reproduction Scenarios**:
|
|
423
|
+
- Mass client disconnection/reconnection (network blip)
|
|
424
|
+
- Keep-alive timeout cascade when inner proxy times out first
|
|
425
|
+
- Cleanup timer getting stuck during high load
|
|
426
|
+
- Memory pressure causing event loop delays
|
|
427
|
+
|
|
428
|
+
### Additional Monitoring Recommendations
|
|
429
|
+
|
|
430
|
+
1. **Add Cleanup Queue Monitoring**:
|
|
431
|
+
```typescript
|
|
432
|
+
setInterval(() => {
|
|
433
|
+
const cm = proxy.connectionManager;
|
|
434
|
+
if (cm.cleanupQueue.size > 100 && !cm.cleanupTimer) {
|
|
435
|
+
logger.error('Cleanup queue stuck!', {
|
|
436
|
+
queueSize: cm.cleanupQueue.size,
|
|
437
|
+
hasTimer: !!cm.cleanupTimer
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}, 60000);
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
2. **Track Timer Health**:
|
|
444
|
+
- Monitor if cleanup timer is running
|
|
445
|
+
- Check for event loop blocking
|
|
446
|
+
- Log when batch processing takes too long
|
|
447
|
+
|
|
448
|
+
3. **Memory Monitoring**:
|
|
449
|
+
- Track heap usage over time
|
|
450
|
+
- Monitor for memory leaks in long-running processes
|
|
451
|
+
- Force periodic garbage collection if needed
|
|
452
|
+
|
|
453
|
+
### Immediate Mitigations
|
|
454
|
+
|
|
455
|
+
1. **Restart Cleanup Timer**:
|
|
456
|
+
```typescript
|
|
457
|
+
// Emergency cleanup timer restart
|
|
458
|
+
if (!cm.cleanupTimer && cm.cleanupQueue.size > 0) {
|
|
459
|
+
cm.cleanupTimer = setTimeout(() => {
|
|
460
|
+
cm.processCleanupQueue();
|
|
461
|
+
}, 100);
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
2. **Force Periodic Cleanup**:
|
|
466
|
+
```typescript
|
|
467
|
+
setInterval(() => {
|
|
468
|
+
const cm = connectionManager;
|
|
469
|
+
if (cm.getConnectionCount() > threshold) {
|
|
470
|
+
cm.performOptimizedInactivityCheck();
|
|
471
|
+
// Force process cleanup queue
|
|
472
|
+
cm.processCleanupQueue();
|
|
473
|
+
}
|
|
474
|
+
}, 300000); // Every 5 minutes
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
3. **Connection Age Limits**:
|
|
478
|
+
- Set maximum connection lifetime
|
|
479
|
+
- Force close connections older than threshold
|
|
480
|
+
- More aggressive cleanup for proxy chains
|
|
481
|
+
|
|
482
|
+
## ✅ FIXED: Zombie Connection Detection (January 2025)
|
|
483
|
+
|
|
484
|
+
### Root Cause Identified
|
|
485
|
+
"Zombie connections" occur when sockets are destroyed without triggering their close/error event handlers. This causes connections to remain tracked with both sockets destroyed but `connectionClosed=false`. This is particularly problematic in proxy chains where the inner proxy might close connections in ways that don't trigger proper events on the outer proxy.
|
|
486
|
+
|
|
487
|
+
### Fix Implemented
|
|
488
|
+
Added zombie detection to the periodic inactivity check in ConnectionManager:
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
// In performOptimizedInactivityCheck()
|
|
492
|
+
// Check ALL connections for zombie state
|
|
493
|
+
for (const [connectionId, record] of this.connectionRecords) {
|
|
494
|
+
if (!record.connectionClosed) {
|
|
495
|
+
const incomingDestroyed = record.incoming?.destroyed || false;
|
|
496
|
+
const outgoingDestroyed = record.outgoing?.destroyed || false;
|
|
497
|
+
|
|
498
|
+
// Check for zombie connections: both sockets destroyed but not cleaned up
|
|
499
|
+
if (incomingDestroyed && outgoingDestroyed) {
|
|
500
|
+
logger.log('warn', `Zombie connection detected: ${connectionId} - both sockets destroyed but not cleaned up`, {
|
|
501
|
+
connectionId,
|
|
502
|
+
remoteIP: record.remoteIP,
|
|
503
|
+
age: plugins.prettyMs(now - record.incomingStartTime),
|
|
504
|
+
component: 'connection-manager'
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// Clean up immediately
|
|
508
|
+
this.cleanupConnection(record, 'zombie_cleanup');
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Check for half-zombie: one socket destroyed
|
|
513
|
+
if (incomingDestroyed || outgoingDestroyed) {
|
|
514
|
+
const age = now - record.incomingStartTime;
|
|
515
|
+
// Give it 30 seconds grace period for normal cleanup
|
|
516
|
+
if (age > 30000) {
|
|
517
|
+
logger.log('warn', `Half-zombie connection detected: ${connectionId} - ${incomingDestroyed ? 'incoming' : 'outgoing'} destroyed`, {
|
|
518
|
+
connectionId,
|
|
519
|
+
remoteIP: record.remoteIP,
|
|
520
|
+
age: plugins.prettyMs(age),
|
|
521
|
+
incomingDestroyed,
|
|
522
|
+
outgoingDestroyed,
|
|
523
|
+
component: 'connection-manager'
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// Clean up
|
|
527
|
+
this.cleanupConnection(record, 'half_zombie_cleanup');
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### How It Works
|
|
535
|
+
1. **Full Zombie Detection**: Detects when both incoming and outgoing sockets are destroyed but the connection hasn't been cleaned up
|
|
536
|
+
2. **Half-Zombie Detection**: Detects when only one socket is destroyed, with a 30-second grace period for normal cleanup to occur
|
|
537
|
+
3. **Automatic Cleanup**: Immediately cleans up zombie connections when detected
|
|
538
|
+
4. **Runs Periodically**: Integrated into the existing inactivity check that runs every 30 seconds
|
|
539
|
+
|
|
540
|
+
### Why This Fixes the Outer Proxy Accumulation
|
|
541
|
+
- When inner proxy closes connections abruptly (e.g., due to backend failure), the outer proxy's outgoing socket might be destroyed without firing close/error events
|
|
542
|
+
- These become zombie connections that previously accumulated indefinitely
|
|
543
|
+
- Now they are detected and cleaned up within 30 seconds
|
|
544
|
+
|
|
545
|
+
### Test Results
|
|
546
|
+
Debug scripts confirmed:
|
|
547
|
+
- Zombie connections can be created when sockets are destroyed directly without events
|
|
548
|
+
- The zombie detection successfully identifies and cleans up these connections
|
|
549
|
+
- Both full zombies (both sockets destroyed) and half-zombies (one socket destroyed) are handled
|
|
550
|
+
|
|
551
|
+
This fix addresses the specific issue where "connections that are closed on the inner proxy, always also close on the outer proxy" as requested by the user.
|
package/readme.hints.md
CHANGED
|
@@ -856,4 +856,42 @@ The WrappedSocket class has been implemented as the foundation for PROXY protoco
|
|
|
856
856
|
For detailed information about proxy protocol implementation and proxy chaining:
|
|
857
857
|
- **[Proxy Protocol Guide](./readme.proxy-protocol.md)** - Complete implementation details and configuration
|
|
858
858
|
- **[Proxy Protocol Examples](./readme.proxy-protocol-example.md)** - Code examples and conceptual implementation
|
|
859
|
-
- **[Proxy Chain Summary](./readme.proxy-chain-summary.md)** - Quick reference for proxy chaining setup
|
|
859
|
+
- **[Proxy Chain Summary](./readme.proxy-chain-summary.md)** - Quick reference for proxy chaining setup
|
|
860
|
+
|
|
861
|
+
## Connection Cleanup Edge Cases Investigation (v19.5.20+)
|
|
862
|
+
|
|
863
|
+
### Issue Discovered
|
|
864
|
+
"Zombie connections" can occur when both sockets are destroyed but the connection record hasn't been cleaned up. This happens when sockets are destroyed without triggering their close/error event handlers.
|
|
865
|
+
|
|
866
|
+
### Root Cause
|
|
867
|
+
1. **Event Handler Bypass**: In edge cases (network failures, proxy chain failures, forced socket destruction), sockets can be destroyed without their event handlers being called
|
|
868
|
+
2. **Cleanup Queue Delay**: The `initiateCleanupOnce` method adds connections to a cleanup queue (batch of 100 every 100ms), which may not process fast enough
|
|
869
|
+
3. **Inactivity Check Limitation**: The periodic inactivity check only examines `lastActivity` timestamps, not actual socket states
|
|
870
|
+
|
|
871
|
+
### Test Results
|
|
872
|
+
Debug script (`connection-manager-direct-test.ts`) revealed:
|
|
873
|
+
- **Normal cleanup works**: When socket events fire normally, cleanup is reliable
|
|
874
|
+
- **Zombies ARE created**: Direct socket destruction creates zombies (destroyed sockets, connectionClosed=false)
|
|
875
|
+
- **Manual cleanup works**: Calling `initiateCleanupOnce` on a zombie does clean it up
|
|
876
|
+
- **Inactivity check misses zombies**: The check doesn't detect connections with destroyed sockets
|
|
877
|
+
|
|
878
|
+
### Potential Solutions
|
|
879
|
+
1. **Periodic Zombie Detection**: Add zombie detection to the inactivity check:
|
|
880
|
+
```typescript
|
|
881
|
+
// In performOptimizedInactivityCheck
|
|
882
|
+
if (record.incoming?.destroyed && record.outgoing?.destroyed && !record.connectionClosed) {
|
|
883
|
+
this.cleanupConnection(record, 'zombie_detected');
|
|
884
|
+
}
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
2. **Socket State Monitoring**: Check socket states during connection operations
|
|
888
|
+
3. **Defensive Socket Handling**: Always attach cleanup handlers before any operation that might destroy sockets
|
|
889
|
+
4. **Immediate Cleanup Option**: For critical paths, use `cleanupConnection` instead of `initiateCleanupOnce`
|
|
890
|
+
|
|
891
|
+
### Impact
|
|
892
|
+
- Memory leaks in edge cases (network failures, proxy chain issues)
|
|
893
|
+
- Connection count inaccuracy
|
|
894
|
+
- Potential resource exhaustion over time
|
|
895
|
+
|
|
896
|
+
### Test Files
|
|
897
|
+
- `.nogit/debug/connection-manager-direct-test.ts` - Direct ConnectionManager testing showing zombie creation
|
|
@@ -456,6 +456,48 @@ export class ConnectionManager extends LifecycleComponent {
|
|
|
456
456
|
}
|
|
457
457
|
}
|
|
458
458
|
|
|
459
|
+
// Also check ALL connections for zombie state (destroyed sockets but not cleaned up)
|
|
460
|
+
// This is critical for proxy chains where sockets can be destroyed without events
|
|
461
|
+
for (const [connectionId, record] of this.connectionRecords) {
|
|
462
|
+
if (!record.connectionClosed) {
|
|
463
|
+
const incomingDestroyed = record.incoming?.destroyed || false;
|
|
464
|
+
const outgoingDestroyed = record.outgoing?.destroyed || false;
|
|
465
|
+
|
|
466
|
+
// Check for zombie connections: both sockets destroyed but connection not cleaned up
|
|
467
|
+
if (incomingDestroyed && outgoingDestroyed) {
|
|
468
|
+
logger.log('warn', `Zombie connection detected: ${connectionId} - both sockets destroyed but not cleaned up`, {
|
|
469
|
+
connectionId,
|
|
470
|
+
remoteIP: record.remoteIP,
|
|
471
|
+
age: plugins.prettyMs(now - record.incomingStartTime),
|
|
472
|
+
component: 'connection-manager'
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// Clean up immediately
|
|
476
|
+
this.cleanupConnection(record, 'zombie_cleanup');
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Check for half-zombie: one socket destroyed
|
|
481
|
+
if (incomingDestroyed || outgoingDestroyed) {
|
|
482
|
+
const age = now - record.incomingStartTime;
|
|
483
|
+
// Give it 30 seconds grace period for normal cleanup
|
|
484
|
+
if (age > 30000) {
|
|
485
|
+
logger.log('warn', `Half-zombie connection detected: ${connectionId} - ${incomingDestroyed ? 'incoming' : 'outgoing'} destroyed`, {
|
|
486
|
+
connectionId,
|
|
487
|
+
remoteIP: record.remoteIP,
|
|
488
|
+
age: plugins.prettyMs(age),
|
|
489
|
+
incomingDestroyed,
|
|
490
|
+
outgoingDestroyed,
|
|
491
|
+
component: 'connection-manager'
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// Clean up
|
|
495
|
+
this.cleanupConnection(record, 'half_zombie_cleanup');
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
459
501
|
// Process only connections that need checking
|
|
460
502
|
for (const connectionId of connectionsToCheck) {
|
|
461
503
|
const record = this.connectionRecords.get(connectionId);
|