@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartproxy",
3
- "version": "19.5.23",
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",
@@ -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);