faye-redis-ng 1.0.4 → 1.0.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.
- checksums.yaml +4 -4
 - data/CHANGELOG.md +64 -1
 - data/README.md +144 -0
 - data/lib/faye/redis/version.rb +1 -1
 - data/lib/faye/redis.rb +118 -1
 - metadata +2 -2
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA256:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: f87f5800dede3c5720fc5ab77944c3322f735cd808c1b02edd218fc6d97bca8b
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: 13d579f33d6620ab7ecdc3567b45dd7c4a58264ed0115b25ddc39454662b2e0c
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: fbd69bad2590d4e55b141941952c95e316c3d8dd2a1e37f4ebbe3129ffe00b48957e8ba86c02732ef67aad97200c47b27dc6c663c5c41640cbaa0dd6787e8514
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: 8c08572629040c40e130f03c0881e864f02a1dbfb89b5db9c0509c255d4cf986a9f38bf5267c4834216dcae0ca26ef6c115742b42c4f61095c9e4cb14264824a
         
     | 
    
        data/CHANGELOG.md
    CHANGED
    
    | 
         @@ -7,6 +7,67 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 
     | 
|
| 
       7 
7 
     | 
    
         | 
| 
       8 
8 
     | 
    
         
             
            ## [Unreleased]
         
     | 
| 
       9 
9 
     | 
    
         | 
| 
      
 10 
     | 
    
         
            +
            ## [1.0.6] - 2025-10-30
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            ### Added
         
     | 
| 
      
 13 
     | 
    
         
            +
            - **Automatic Garbage Collection**: Implemented automatic GC timer that runs periodically to clean up expired clients and orphaned data
         
     | 
| 
      
 14 
     | 
    
         
            +
              - New `gc_interval` configuration option (default: 60 seconds)
         
     | 
| 
      
 15 
     | 
    
         
            +
              - Automatically starts when EventMachine is running
         
     | 
| 
      
 16 
     | 
    
         
            +
              - Can be disabled by setting `gc_interval` to 0 or false
         
     | 
| 
      
 17 
     | 
    
         
            +
              - Lazy initialization ensures timer starts even if engine is created before EventMachine starts
         
     | 
| 
      
 18 
     | 
    
         
            +
              - Timer is properly stopped on disconnect to prevent resource leaks
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
            ### Changed
         
     | 
| 
      
 21 
     | 
    
         
            +
            - **Improved User Experience**: No longer requires manual setup of periodic cleanup
         
     | 
| 
      
 22 
     | 
    
         
            +
              - Memory leak prevention is now automatic by default
         
     | 
| 
      
 23 
     | 
    
         
            +
              - Matches behavior of original faye-redis-ruby project
         
     | 
| 
      
 24 
     | 
    
         
            +
              - Users can still manually call `cleanup_expired` if needed
         
     | 
| 
      
 25 
     | 
    
         
            +
              - Custom GC schedules possible by disabling automatic GC
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
            ### Technical Details
         
     | 
| 
      
 28 
     | 
    
         
            +
            The automatic GC timer:
         
     | 
| 
      
 29 
     | 
    
         
            +
            - Runs `cleanup_expired` every 60 seconds by default
         
     | 
| 
      
 30 
     | 
    
         
            +
            - Only starts when EventMachine reactor is running
         
     | 
| 
      
 31 
     | 
    
         
            +
            - Supports lazy initialization for engines created outside EM context
         
     | 
| 
      
 32 
     | 
    
         
            +
            - Properly handles cleanup on disconnect
         
     | 
| 
      
 33 
     | 
    
         
            +
            - Can be customized or disabled via `gc_interval` option
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
            ## [1.0.5] - 2025-10-30
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
            ### Fixed
         
     | 
| 
      
 38 
     | 
    
         
            +
            - **Memory Leak**: Fixed critical memory leak where subscription keys were never cleaned up after client disconnection
         
     | 
| 
      
 39 
     | 
    
         
            +
              - Orphaned `subscriptions:{client_id}` keys remained permanently in Redis
         
     | 
| 
      
 40 
     | 
    
         
            +
              - Orphaned `subscription:{client_id}:{channel}` hash keys accumulated over time
         
     | 
| 
      
 41 
     | 
    
         
            +
              - Orphaned client IDs remained in `channels:{channel}` sets
         
     | 
| 
      
 42 
     | 
    
         
            +
              - Message queues for disconnected clients were not cleaned up
         
     | 
| 
      
 43 
     | 
    
         
            +
              - Could result in hundreds of MB memory leak in production environments
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
            ### Added
         
     | 
| 
      
 46 
     | 
    
         
            +
            - **`cleanup_expired` Method**: New public method to clean up expired clients and orphaned data
         
     | 
| 
      
 47 
     | 
    
         
            +
              - Automatically detects and removes orphaned subscription keys
         
     | 
| 
      
 48 
     | 
    
         
            +
              - Cleans up message queues for disconnected clients
         
     | 
| 
      
 49 
     | 
    
         
            +
              - Removes stale client IDs from channel subscriber lists
         
     | 
| 
      
 50 
     | 
    
         
            +
              - Uses Redis SCAN to avoid blocking operations
         
     | 
| 
      
 51 
     | 
    
         
            +
              - Batch deletion using pipelining for efficiency
         
     | 
| 
      
 52 
     | 
    
         
            +
              - Can be called manually or scheduled as periodic task
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
            ### Changed
         
     | 
| 
      
 55 
     | 
    
         
            +
            - **Improved Cleanup Strategy**: Enhanced cleanup process now handles orphaned data
         
     | 
| 
      
 56 
     | 
    
         
            +
              - `cleanup_expired` now cleans both expired clients AND orphaned subscriptions
         
     | 
| 
      
 57 
     | 
    
         
            +
              - Works even when no expired clients are found
         
     | 
| 
      
 58 
     | 
    
         
            +
              - Prevents memory leaks from abnormal client disconnections
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
            ### Technical Details
         
     | 
| 
      
 61 
     | 
    
         
            +
            Memory leak scenario (before fix):
         
     | 
| 
      
 62 
     | 
    
         
            +
            - 10,000 abnormally disconnected clients × 5 channels each = 50,000+ orphaned keys
         
     | 
| 
      
 63 
     | 
    
         
            +
            - Estimated memory waste: 100-500 MB
         
     | 
| 
      
 64 
     | 
    
         
            +
            - Keys remained permanently without TTL
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
            After fix:
         
     | 
| 
      
 67 
     | 
    
         
            +
            - All orphaned keys cleaned up automatically
         
     | 
| 
      
 68 
     | 
    
         
            +
            - Memory usage remains stable
         
     | 
| 
      
 69 
     | 
    
         
            +
            - Production environments can schedule periodic cleanup
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
       10 
71 
     | 
    
         
             
            ## [1.0.4] - 2025-10-15
         
     | 
| 
       11 
72 
     | 
    
         | 
| 
       12 
73 
     | 
    
         
             
            ### Performance
         
     | 
| 
         @@ -90,7 +151,9 @@ For 100 subscribers receiving one message: 
     | 
|
| 
       90 
151 
     | 
    
         
             
            ### Security
         
     | 
| 
       91 
152 
     | 
    
         
             
            - Client and message IDs now use `SecureRandom.uuid` instead of predictable time-based generation
         
     | 
| 
       92 
153 
     | 
    
         | 
| 
       93 
     | 
    
         
            -
            [Unreleased]: https://github.com/7a6163/faye-redis-ng/compare/v1.0. 
     | 
| 
      
 154 
     | 
    
         
            +
            [Unreleased]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.6...HEAD
         
     | 
| 
      
 155 
     | 
    
         
            +
            [1.0.6]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.5...v1.0.6
         
     | 
| 
      
 156 
     | 
    
         
            +
            [1.0.5]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.4...v1.0.5
         
     | 
| 
       94 
157 
     | 
    
         
             
            [1.0.4]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.3...v1.0.4
         
     | 
| 
       95 
158 
     | 
    
         
             
            [1.0.3]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.2...v1.0.3
         
     | 
| 
       96 
159 
     | 
    
         
             
            [1.0.2]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.1...v1.0.2
         
     | 
    
        data/README.md
    CHANGED
    
    | 
         @@ -81,6 +81,9 @@ bayeux = Faye::RackAdapter.new(app, { 
     | 
|
| 
       81 
81 
     | 
    
         
             
              client_timeout: 60,         # Client session timeout (seconds)
         
     | 
| 
       82 
82 
     | 
    
         
             
              message_ttl: 3600,          # Message TTL (seconds)
         
     | 
| 
       83 
83 
     | 
    
         | 
| 
      
 84 
     | 
    
         
            +
              # Garbage collection
         
     | 
| 
      
 85 
     | 
    
         
            +
              gc_interval: 60,            # Automatic GC interval (seconds), set to 0 or false to disable
         
     | 
| 
      
 86 
     | 
    
         
            +
             
     | 
| 
       84 
87 
     | 
    
         
             
              # Logging
         
     | 
| 
       85 
88 
     | 
    
         
             
              log_level: :info,           # Log level (:silent, :info, :debug)
         
     | 
| 
       86 
89 
     | 
    
         | 
| 
         @@ -234,6 +237,147 @@ The CI/CD pipeline will automatically: 
     | 
|
| 
       234 
237 
     | 
    
         
             
            - Add `RUBYGEMS_API_KEY` to GitHub repository secrets
         
     | 
| 
       235 
238 
     | 
    
         
             
            - The tag must start with 'v' (e.g., v0.1.0, v1.2.3)
         
     | 
| 
       236 
239 
     | 
    
         | 
| 
      
 240 
     | 
    
         
            +
            ## Memory Management
         
     | 
| 
      
 241 
     | 
    
         
            +
             
     | 
| 
      
 242 
     | 
    
         
            +
            ### Automatic Garbage Collection
         
     | 
| 
      
 243 
     | 
    
         
            +
             
     | 
| 
      
 244 
     | 
    
         
            +
            **New in v1.0.6**: faye-redis-ng now includes automatic garbage collection that runs every 60 seconds by default. This automatically cleans up expired clients and orphaned subscription keys, preventing memory leaks without any manual intervention.
         
     | 
| 
      
 245 
     | 
    
         
            +
             
     | 
| 
      
 246 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 247 
     | 
    
         
            +
            bayeux = Faye::RackAdapter.new(app, {
         
     | 
| 
      
 248 
     | 
    
         
            +
              mount: '/faye',
         
     | 
| 
      
 249 
     | 
    
         
            +
              timeout: 25,
         
     | 
| 
      
 250 
     | 
    
         
            +
              engine: {
         
     | 
| 
      
 251 
     | 
    
         
            +
                type: Faye::Redis,
         
     | 
| 
      
 252 
     | 
    
         
            +
                host: 'localhost',
         
     | 
| 
      
 253 
     | 
    
         
            +
                port: 6379,
         
     | 
| 
      
 254 
     | 
    
         
            +
                gc_interval: 60  # Run GC every 60 seconds (default)
         
     | 
| 
      
 255 
     | 
    
         
            +
              }
         
     | 
| 
      
 256 
     | 
    
         
            +
            })
         
     | 
| 
      
 257 
     | 
    
         
            +
            ```
         
     | 
| 
      
 258 
     | 
    
         
            +
             
     | 
| 
      
 259 
     | 
    
         
            +
            To customize the GC interval or disable it:
         
     | 
| 
      
 260 
     | 
    
         
            +
             
     | 
| 
      
 261 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 262 
     | 
    
         
            +
            engine: {
         
     | 
| 
      
 263 
     | 
    
         
            +
              type: Faye::Redis,
         
     | 
| 
      
 264 
     | 
    
         
            +
              host: 'localhost',
         
     | 
| 
      
 265 
     | 
    
         
            +
              port: 6379,
         
     | 
| 
      
 266 
     | 
    
         
            +
              gc_interval: 300  # Run GC every 5 minutes
         
     | 
| 
      
 267 
     | 
    
         
            +
            }
         
     | 
| 
      
 268 
     | 
    
         
            +
             
     | 
| 
      
 269 
     | 
    
         
            +
            # Or disable automatic GC
         
     | 
| 
      
 270 
     | 
    
         
            +
            engine: {
         
     | 
| 
      
 271 
     | 
    
         
            +
              type: Faye::Redis,
         
     | 
| 
      
 272 
     | 
    
         
            +
              host: 'localhost',
         
     | 
| 
      
 273 
     | 
    
         
            +
              port: 6379,
         
     | 
| 
      
 274 
     | 
    
         
            +
              gc_interval: 0  # Disabled - you'll need to call cleanup_expired manually
         
     | 
| 
      
 275 
     | 
    
         
            +
            }
         
     | 
| 
      
 276 
     | 
    
         
            +
            ```
         
     | 
| 
      
 277 
     | 
    
         
            +
             
     | 
| 
      
 278 
     | 
    
         
            +
            ### Manual Cleanup
         
     | 
| 
      
 279 
     | 
    
         
            +
             
     | 
| 
      
 280 
     | 
    
         
            +
            If you've disabled automatic GC, you can manually clean up expired clients:
         
     | 
| 
      
 281 
     | 
    
         
            +
             
     | 
| 
      
 282 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 283 
     | 
    
         
            +
            # Get the engine instance
         
     | 
| 
      
 284 
     | 
    
         
            +
            engine = bayeux.get_engine
         
     | 
| 
      
 285 
     | 
    
         
            +
             
     | 
| 
      
 286 
     | 
    
         
            +
            # Clean up expired clients and orphaned data
         
     | 
| 
      
 287 
     | 
    
         
            +
            engine.cleanup_expired do |expired_count|
         
     | 
| 
      
 288 
     | 
    
         
            +
              puts "Cleaned up #{expired_count} expired clients"
         
     | 
| 
      
 289 
     | 
    
         
            +
            end
         
     | 
| 
      
 290 
     | 
    
         
            +
            ```
         
     | 
| 
      
 291 
     | 
    
         
            +
             
     | 
| 
      
 292 
     | 
    
         
            +
            #### Custom GC Schedule (Optional)
         
     | 
| 
      
 293 
     | 
    
         
            +
             
     | 
| 
      
 294 
     | 
    
         
            +
            If you need more control, you can disable automatic GC and implement your own schedule:
         
     | 
| 
      
 295 
     | 
    
         
            +
             
     | 
| 
      
 296 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 297 
     | 
    
         
            +
            require 'eventmachine'
         
     | 
| 
      
 298 
     | 
    
         
            +
            require 'faye'
         
     | 
| 
      
 299 
     | 
    
         
            +
            require 'faye-redis-ng'
         
     | 
| 
      
 300 
     | 
    
         
            +
             
     | 
| 
      
 301 
     | 
    
         
            +
            bayeux = Faye::RackAdapter.new(app, {
         
     | 
| 
      
 302 
     | 
    
         
            +
              mount: '/faye',
         
     | 
| 
      
 303 
     | 
    
         
            +
              timeout: 25,
         
     | 
| 
      
 304 
     | 
    
         
            +
              engine: {
         
     | 
| 
      
 305 
     | 
    
         
            +
                type: Faye::Redis,
         
     | 
| 
      
 306 
     | 
    
         
            +
                host: 'localhost',
         
     | 
| 
      
 307 
     | 
    
         
            +
                port: 6379,
         
     | 
| 
      
 308 
     | 
    
         
            +
                namespace: 'my-app',
         
     | 
| 
      
 309 
     | 
    
         
            +
                gc_interval: 0  # Disable automatic GC
         
     | 
| 
      
 310 
     | 
    
         
            +
              }
         
     | 
| 
      
 311 
     | 
    
         
            +
            })
         
     | 
| 
      
 312 
     | 
    
         
            +
             
     | 
| 
      
 313 
     | 
    
         
            +
            # Custom cleanup schedule - every 5 minutes
         
     | 
| 
      
 314 
     | 
    
         
            +
            EM.add_periodic_timer(300) do
         
     | 
| 
      
 315 
     | 
    
         
            +
              bayeux.get_engine.cleanup_expired do |count|
         
     | 
| 
      
 316 
     | 
    
         
            +
                puts "[#{Time.now}] Cleaned up #{count} expired clients" if count > 0
         
     | 
| 
      
 317 
     | 
    
         
            +
              end
         
     | 
| 
      
 318 
     | 
    
         
            +
            end
         
     | 
| 
      
 319 
     | 
    
         
            +
             
     | 
| 
      
 320 
     | 
    
         
            +
            run bayeux
         
     | 
| 
      
 321 
     | 
    
         
            +
            ```
         
     | 
| 
      
 322 
     | 
    
         
            +
             
     | 
| 
      
 323 
     | 
    
         
            +
            #### Using Rake Task
         
     | 
| 
      
 324 
     | 
    
         
            +
             
     | 
| 
      
 325 
     | 
    
         
            +
            Create a Rake task for manual or scheduled cleanup:
         
     | 
| 
      
 326 
     | 
    
         
            +
             
     | 
| 
      
 327 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 328 
     | 
    
         
            +
            # lib/tasks/faye_cleanup.rake
         
     | 
| 
      
 329 
     | 
    
         
            +
            namespace :faye do
         
     | 
| 
      
 330 
     | 
    
         
            +
              desc "Clean up expired Faye clients and orphaned subscriptions"
         
     | 
| 
      
 331 
     | 
    
         
            +
              task cleanup: :environment do
         
     | 
| 
      
 332 
     | 
    
         
            +
                require 'eventmachine'
         
     | 
| 
      
 333 
     | 
    
         
            +
             
     | 
| 
      
 334 
     | 
    
         
            +
                EM.run do
         
     | 
| 
      
 335 
     | 
    
         
            +
                  engine = Faye::Redis.new(
         
     | 
| 
      
 336 
     | 
    
         
            +
                    nil,
         
     | 
| 
      
 337 
     | 
    
         
            +
                    host: ENV['REDIS_HOST'] || 'localhost',
         
     | 
| 
      
 338 
     | 
    
         
            +
                    port: ENV['REDIS_PORT']&.to_i || 6379,
         
     | 
| 
      
 339 
     | 
    
         
            +
                    namespace: 'my-app'
         
     | 
| 
      
 340 
     | 
    
         
            +
                  )
         
     | 
| 
      
 341 
     | 
    
         
            +
             
     | 
| 
      
 342 
     | 
    
         
            +
                  engine.cleanup_expired do |count|
         
     | 
| 
      
 343 
     | 
    
         
            +
                    puts "✅ Cleaned up #{count} expired clients"
         
     | 
| 
      
 344 
     | 
    
         
            +
                    engine.disconnect
         
     | 
| 
      
 345 
     | 
    
         
            +
                    EM.stop
         
     | 
| 
      
 346 
     | 
    
         
            +
                  end
         
     | 
| 
      
 347 
     | 
    
         
            +
                end
         
     | 
| 
      
 348 
     | 
    
         
            +
              end
         
     | 
| 
      
 349 
     | 
    
         
            +
            end
         
     | 
| 
      
 350 
     | 
    
         
            +
            ```
         
     | 
| 
      
 351 
     | 
    
         
            +
             
     | 
| 
      
 352 
     | 
    
         
            +
            Then schedule it with cron:
         
     | 
| 
      
 353 
     | 
    
         
            +
             
     | 
| 
      
 354 
     | 
    
         
            +
            ```bash
         
     | 
| 
      
 355 
     | 
    
         
            +
            # Run cleanup every hour
         
     | 
| 
      
 356 
     | 
    
         
            +
            0 * * * * cd /path/to/app && bundle exec rake faye:cleanup
         
     | 
| 
      
 357 
     | 
    
         
            +
            ```
         
     | 
| 
      
 358 
     | 
    
         
            +
             
     | 
| 
      
 359 
     | 
    
         
            +
            ### What Gets Cleaned Up
         
     | 
| 
      
 360 
     | 
    
         
            +
             
     | 
| 
      
 361 
     | 
    
         
            +
            The `cleanup_expired` method removes:
         
     | 
| 
      
 362 
     | 
    
         
            +
             
     | 
| 
      
 363 
     | 
    
         
            +
            1. **Expired client keys** (`clients:{client_id}`)
         
     | 
| 
      
 364 
     | 
    
         
            +
            2. **Orphaned subscription lists** (`subscriptions:{client_id}`)
         
     | 
| 
      
 365 
     | 
    
         
            +
            3. **Orphaned subscription metadata** (`subscription:{client_id}:{channel}`)
         
     | 
| 
      
 366 
     | 
    
         
            +
            4. **Stale client IDs from channel subscribers** (`channels:{channel}`)
         
     | 
| 
      
 367 
     | 
    
         
            +
            5. **Orphaned message queues** (`messages:{client_id}`)
         
     | 
| 
      
 368 
     | 
    
         
            +
             
     | 
| 
      
 369 
     | 
    
         
            +
            ### Memory Leak Prevention
         
     | 
| 
      
 370 
     | 
    
         
            +
             
     | 
| 
      
 371 
     | 
    
         
            +
            **v1.0.6+**: Automatic garbage collection is now enabled by default, preventing memory leaks from orphaned keys without any configuration needed.
         
     | 
| 
      
 372 
     | 
    
         
            +
             
     | 
| 
      
 373 
     | 
    
         
            +
            Without GC, abnormal client disconnections (crashes, network failures, etc.) can cause orphaned keys to accumulate:
         
     | 
| 
      
 374 
     | 
    
         
            +
             
     | 
| 
      
 375 
     | 
    
         
            +
            - **Before v1.0.5**: 10,000 orphaned clients × 5 channels = 50,000+ keys = 100-500 MB leaked
         
     | 
| 
      
 376 
     | 
    
         
            +
            - **v1.0.5**: Manual cleanup required via `cleanup_expired` method
         
     | 
| 
      
 377 
     | 
    
         
            +
            - **v1.0.6+**: Automatic GC runs every 60 seconds by default - no manual intervention needed
         
     | 
| 
      
 378 
     | 
    
         
            +
             
     | 
| 
      
 379 
     | 
    
         
            +
            The automatic GC ensures memory usage remains stable even with frequent client disconnections.
         
     | 
| 
      
 380 
     | 
    
         
            +
             
     | 
| 
       237 
381 
     | 
    
         
             
            ## Troubleshooting
         
     | 
| 
       238 
382 
     | 
    
         | 
| 
       239 
383 
     | 
    
         
             
            ### Connection Issues
         
     | 
    
        data/lib/faye/redis/version.rb
    CHANGED
    
    
    
        data/lib/faye/redis.rb
    CHANGED
    
    | 
         @@ -1,4 +1,5 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            require 'securerandom'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'set'
         
     | 
| 
       2 
3 
     | 
    
         
             
            require_relative 'redis/version'
         
     | 
| 
       3 
4 
     | 
    
         
             
            require_relative 'redis/logger'
         
     | 
| 
       4 
5 
     | 
    
         
             
            require_relative 'redis/connection'
         
     | 
| 
         @@ -24,7 +25,8 @@ module Faye 
     | 
|
| 
       24 
25 
     | 
    
         
             
                  retry_delay: 1,
         
     | 
| 
       25 
26 
     | 
    
         
             
                  client_timeout: 60,
         
     | 
| 
       26 
27 
     | 
    
         
             
                  message_ttl: 3600,
         
     | 
| 
       27 
     | 
    
         
            -
                  namespace: 'faye'
         
     | 
| 
      
 28 
     | 
    
         
            +
                  namespace: 'faye',
         
     | 
| 
      
 29 
     | 
    
         
            +
                  gc_interval: 60  # Automatic garbage collection interval (seconds), set to 0 or false to disable
         
     | 
| 
       28 
30 
     | 
    
         
             
                }.freeze
         
     | 
| 
       29 
31 
     | 
    
         | 
| 
       30 
32 
     | 
    
         
             
                attr_reader :server, :options, :connection, :client_registry,
         
     | 
| 
         @@ -49,10 +51,16 @@ module Faye 
     | 
|
| 
       49 
51 
     | 
    
         | 
| 
       50 
52 
     | 
    
         
             
                  # Set up message routing
         
     | 
| 
       51 
53 
     | 
    
         
             
                  setup_message_routing
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                  # Start automatic garbage collection timer
         
     | 
| 
      
 56 
     | 
    
         
            +
                  start_gc_timer
         
     | 
| 
       52 
57 
     | 
    
         
             
                end
         
     | 
| 
       53 
58 
     | 
    
         | 
| 
       54 
59 
     | 
    
         
             
                # Create a new client
         
     | 
| 
       55 
60 
     | 
    
         
             
                def create_client(&callback)
         
     | 
| 
      
 61 
     | 
    
         
            +
                  # Ensure GC timer is started (lazy initialization)
         
     | 
| 
      
 62 
     | 
    
         
            +
                  ensure_gc_timer_started
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
       56 
64 
     | 
    
         
             
                  client_id = generate_client_id
         
     | 
| 
       57 
65 
     | 
    
         
             
                  @client_registry.create(client_id) do |success|
         
     | 
| 
       58 
66 
     | 
    
         
             
                    if success
         
     | 
| 
         @@ -135,10 +143,26 @@ module Faye 
     | 
|
| 
       135 
143 
     | 
    
         | 
| 
       136 
144 
     | 
    
         
             
                # Disconnect the engine
         
     | 
| 
       137 
145 
     | 
    
         
             
                def disconnect
         
     | 
| 
      
 146 
     | 
    
         
            +
                  # Stop GC timer if running
         
     | 
| 
      
 147 
     | 
    
         
            +
                  stop_gc_timer
         
     | 
| 
      
 148 
     | 
    
         
            +
             
     | 
| 
       138 
149 
     | 
    
         
             
                  @pubsub_coordinator.disconnect
         
     | 
| 
       139 
150 
     | 
    
         
             
                  @connection.disconnect
         
     | 
| 
       140 
151 
     | 
    
         
             
                end
         
     | 
| 
       141 
152 
     | 
    
         | 
| 
      
 153 
     | 
    
         
            +
                # Clean up expired clients and their associated data
         
     | 
| 
      
 154 
     | 
    
         
            +
                def cleanup_expired(&callback)
         
     | 
| 
      
 155 
     | 
    
         
            +
                  @client_registry.cleanup_expired do |expired_count|
         
     | 
| 
      
 156 
     | 
    
         
            +
                    @logger.info("Cleaned up #{expired_count} expired clients") if expired_count > 0
         
     | 
| 
      
 157 
     | 
    
         
            +
             
     | 
| 
      
 158 
     | 
    
         
            +
                    # Always clean up orphaned subscription keys (even if no expired clients)
         
     | 
| 
      
 159 
     | 
    
         
            +
                    # This handles cases where subscriptions were orphaned due to crashes
         
     | 
| 
      
 160 
     | 
    
         
            +
                    cleanup_orphaned_subscriptions do
         
     | 
| 
      
 161 
     | 
    
         
            +
                      callback.call(expired_count) if callback
         
     | 
| 
      
 162 
     | 
    
         
            +
                    end
         
     | 
| 
      
 163 
     | 
    
         
            +
                  end
         
     | 
| 
      
 164 
     | 
    
         
            +
                end
         
     | 
| 
      
 165 
     | 
    
         
            +
             
     | 
| 
       142 
166 
     | 
    
         
             
                private
         
     | 
| 
       143 
167 
     | 
    
         | 
| 
       144 
168 
     | 
    
         
             
                def generate_client_id
         
     | 
| 
         @@ -171,6 +195,62 @@ module Faye 
     | 
|
| 
       171 
195 
     | 
    
         
             
                  end
         
     | 
| 
       172 
196 
     | 
    
         
             
                end
         
     | 
| 
       173 
197 
     | 
    
         | 
| 
      
 198 
     | 
    
         
            +
                def cleanup_orphaned_subscriptions(&callback)
         
     | 
| 
      
 199 
     | 
    
         
            +
                  # Get all active client IDs
         
     | 
| 
      
 200 
     | 
    
         
            +
                  @client_registry.all do |active_clients|
         
     | 
| 
      
 201 
     | 
    
         
            +
                    active_set = active_clients.to_set
         
     | 
| 
      
 202 
     | 
    
         
            +
                    namespace = @options[:namespace] || 'faye'
         
     | 
| 
      
 203 
     | 
    
         
            +
             
     | 
| 
      
 204 
     | 
    
         
            +
                    # Scan for subscription keys and clean up orphaned ones
         
     | 
| 
      
 205 
     | 
    
         
            +
                    @connection.with_redis do |redis|
         
     | 
| 
      
 206 
     | 
    
         
            +
                      cursor = "0"
         
     | 
| 
      
 207 
     | 
    
         
            +
                      orphaned_keys = []
         
     | 
| 
      
 208 
     | 
    
         
            +
             
     | 
| 
      
 209 
     | 
    
         
            +
                      loop do
         
     | 
| 
      
 210 
     | 
    
         
            +
                        cursor, keys = redis.scan(cursor, match: "#{namespace}:subscriptions:*", count: 100)
         
     | 
| 
      
 211 
     | 
    
         
            +
             
     | 
| 
      
 212 
     | 
    
         
            +
                        keys.each do |key|
         
     | 
| 
      
 213 
     | 
    
         
            +
                          # Extract client_id from key (format: namespace:subscriptions:client_id)
         
     | 
| 
      
 214 
     | 
    
         
            +
                          client_id = key.split(':').last
         
     | 
| 
      
 215 
     | 
    
         
            +
                          orphaned_keys << client_id unless active_set.include?(client_id)
         
     | 
| 
      
 216 
     | 
    
         
            +
                        end
         
     | 
| 
      
 217 
     | 
    
         
            +
             
     | 
| 
      
 218 
     | 
    
         
            +
                        break if cursor == "0"
         
     | 
| 
      
 219 
     | 
    
         
            +
                      end
         
     | 
| 
      
 220 
     | 
    
         
            +
             
     | 
| 
      
 221 
     | 
    
         
            +
                      # Clean up orphaned subscription data
         
     | 
| 
      
 222 
     | 
    
         
            +
                      if orphaned_keys.any?
         
     | 
| 
      
 223 
     | 
    
         
            +
                        @logger.info("Cleaning up #{orphaned_keys.size} orphaned subscription sets")
         
     | 
| 
      
 224 
     | 
    
         
            +
             
     | 
| 
      
 225 
     | 
    
         
            +
                        orphaned_keys.each do |client_id|
         
     | 
| 
      
 226 
     | 
    
         
            +
                          # Get channels for this orphaned client
         
     | 
| 
      
 227 
     | 
    
         
            +
                          channels = redis.smembers("#{namespace}:subscriptions:#{client_id}")
         
     | 
| 
      
 228 
     | 
    
         
            +
             
     | 
| 
      
 229 
     | 
    
         
            +
                          # Remove in batch
         
     | 
| 
      
 230 
     | 
    
         
            +
                          redis.pipelined do |pipeline|
         
     | 
| 
      
 231 
     | 
    
         
            +
                            # Delete client's subscription list
         
     | 
| 
      
 232 
     | 
    
         
            +
                            pipeline.del("#{namespace}:subscriptions:#{client_id}")
         
     | 
| 
      
 233 
     | 
    
         
            +
             
     | 
| 
      
 234 
     | 
    
         
            +
                            # Delete each subscription metadata and remove from channel subscribers
         
     | 
| 
      
 235 
     | 
    
         
            +
                            channels.each do |channel|
         
     | 
| 
      
 236 
     | 
    
         
            +
                              pipeline.del("#{namespace}:subscription:#{client_id}:#{channel}")
         
     | 
| 
      
 237 
     | 
    
         
            +
                              pipeline.srem("#{namespace}:channels:#{channel}", client_id)
         
     | 
| 
      
 238 
     | 
    
         
            +
                            end
         
     | 
| 
      
 239 
     | 
    
         
            +
             
     | 
| 
      
 240 
     | 
    
         
            +
                            # Delete message queue if exists
         
     | 
| 
      
 241 
     | 
    
         
            +
                            pipeline.del("#{namespace}:messages:#{client_id}")
         
     | 
| 
      
 242 
     | 
    
         
            +
                          end
         
     | 
| 
      
 243 
     | 
    
         
            +
                        end
         
     | 
| 
      
 244 
     | 
    
         
            +
                      end
         
     | 
| 
      
 245 
     | 
    
         
            +
                    end
         
     | 
| 
      
 246 
     | 
    
         
            +
             
     | 
| 
      
 247 
     | 
    
         
            +
                    EventMachine.next_tick { callback.call } if callback
         
     | 
| 
      
 248 
     | 
    
         
            +
                  end
         
     | 
| 
      
 249 
     | 
    
         
            +
                rescue => e
         
     | 
| 
      
 250 
     | 
    
         
            +
                  log_error("Failed to cleanup orphaned subscriptions: #{e.message}")
         
     | 
| 
      
 251 
     | 
    
         
            +
                  EventMachine.next_tick { callback.call } if callback
         
     | 
| 
      
 252 
     | 
    
         
            +
                end
         
     | 
| 
      
 253 
     | 
    
         
            +
             
     | 
| 
       174 
254 
     | 
    
         
             
                def setup_message_routing
         
     | 
| 
       175 
255 
     | 
    
         
             
                  # Subscribe to message events from other servers
         
     | 
| 
       176 
256 
     | 
    
         
             
                  @pubsub_coordinator.on_message do |channel, message|
         
     | 
| 
         @@ -184,5 +264,42 @@ module Faye 
     | 
|
| 
       184 
264 
     | 
    
         
             
                def log_error(message)
         
     | 
| 
       185 
265 
     | 
    
         
             
                  @logger.error(message)
         
     | 
| 
       186 
266 
     | 
    
         
             
                end
         
     | 
| 
      
 267 
     | 
    
         
            +
             
     | 
| 
      
 268 
     | 
    
         
            +
                # Start automatic garbage collection timer
         
     | 
| 
      
 269 
     | 
    
         
            +
                def start_gc_timer
         
     | 
| 
      
 270 
     | 
    
         
            +
                  gc_interval = @options[:gc_interval]
         
     | 
| 
      
 271 
     | 
    
         
            +
             
     | 
| 
      
 272 
     | 
    
         
            +
                  # Skip if GC is disabled (0, false, or nil)
         
     | 
| 
      
 273 
     | 
    
         
            +
                  return if !gc_interval || gc_interval == 0
         
     | 
| 
      
 274 
     | 
    
         
            +
             
     | 
| 
      
 275 
     | 
    
         
            +
                  # Only start timer if EventMachine is running
         
     | 
| 
      
 276 
     | 
    
         
            +
                  return unless EventMachine.reactor_running?
         
     | 
| 
      
 277 
     | 
    
         
            +
             
     | 
| 
      
 278 
     | 
    
         
            +
                  @logger.info("Starting automatic GC timer with interval: #{gc_interval} seconds")
         
     | 
| 
      
 279 
     | 
    
         
            +
             
     | 
| 
      
 280 
     | 
    
         
            +
                  @gc_timer = EventMachine.add_periodic_timer(gc_interval) do
         
     | 
| 
      
 281 
     | 
    
         
            +
                    @logger.debug("Running automatic garbage collection")
         
     | 
| 
      
 282 
     | 
    
         
            +
                    cleanup_expired do |count|
         
     | 
| 
      
 283 
     | 
    
         
            +
                      @logger.debug("GC completed: #{count} expired clients cleaned") if count > 0
         
     | 
| 
      
 284 
     | 
    
         
            +
                    end
         
     | 
| 
      
 285 
     | 
    
         
            +
                  end
         
     | 
| 
      
 286 
     | 
    
         
            +
                end
         
     | 
| 
      
 287 
     | 
    
         
            +
             
     | 
| 
      
 288 
     | 
    
         
            +
                # Ensure GC timer is started (called lazily on first operation)
         
     | 
| 
      
 289 
     | 
    
         
            +
                def ensure_gc_timer_started
         
     | 
| 
      
 290 
     | 
    
         
            +
                  return if @gc_timer  # Already started
         
     | 
| 
      
 291 
     | 
    
         
            +
                  return if !@options[:gc_interval] || @options[:gc_interval] == 0  # Disabled
         
     | 
| 
      
 292 
     | 
    
         
            +
             
     | 
| 
      
 293 
     | 
    
         
            +
                  start_gc_timer
         
     | 
| 
      
 294 
     | 
    
         
            +
                end
         
     | 
| 
      
 295 
     | 
    
         
            +
             
     | 
| 
      
 296 
     | 
    
         
            +
                # Stop automatic garbage collection timer
         
     | 
| 
      
 297 
     | 
    
         
            +
                def stop_gc_timer
         
     | 
| 
      
 298 
     | 
    
         
            +
                  if @gc_timer
         
     | 
| 
      
 299 
     | 
    
         
            +
                    EventMachine.cancel_timer(@gc_timer)
         
     | 
| 
      
 300 
     | 
    
         
            +
                    @gc_timer = nil
         
     | 
| 
      
 301 
     | 
    
         
            +
                    @logger.info("Stopped automatic GC timer")
         
     | 
| 
      
 302 
     | 
    
         
            +
                  end
         
     | 
| 
      
 303 
     | 
    
         
            +
                end
         
     | 
| 
       187 
304 
     | 
    
         
             
              end
         
     | 
| 
       188 
305 
     | 
    
         
             
            end
         
     | 
    
        metadata
    CHANGED
    
    | 
         @@ -1,14 +1,14 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            --- !ruby/object:Gem::Specification
         
     | 
| 
       2 
2 
     | 
    
         
             
            name: faye-redis-ng
         
     | 
| 
       3 
3 
     | 
    
         
             
            version: !ruby/object:Gem::Version
         
     | 
| 
       4 
     | 
    
         
            -
              version: 1.0. 
     | 
| 
      
 4 
     | 
    
         
            +
              version: 1.0.6
         
     | 
| 
       5 
5 
     | 
    
         
             
            platform: ruby
         
     | 
| 
       6 
6 
     | 
    
         
             
            authors:
         
     | 
| 
       7 
7 
     | 
    
         
             
            - Zac
         
     | 
| 
       8 
8 
     | 
    
         
             
            autorequire:
         
     | 
| 
       9 
9 
     | 
    
         
             
            bindir: bin
         
     | 
| 
       10 
10 
     | 
    
         
             
            cert_chain: []
         
     | 
| 
       11 
     | 
    
         
            -
            date: 2025-10- 
     | 
| 
      
 11 
     | 
    
         
            +
            date: 2025-10-30 00:00:00.000000000 Z
         
     | 
| 
       12 
12 
     | 
    
         
             
            dependencies:
         
     | 
| 
       13 
13 
     | 
    
         
             
            - !ruby/object:Gem::Dependency
         
     | 
| 
       14 
14 
     | 
    
         
             
              name: redis
         
     |