faye-redis-ng 1.0.3 → 1.0.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.
- checksums.yaml +4 -4
 - data/CHANGELOG.md +64 -1
 - data/README.md +103 -0
 - data/lib/faye/redis/message_queue.rb +35 -107
 - data/lib/faye/redis/version.rb +1 -1
 - data/lib/faye/redis.rb +113 -34
 - 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: 02a75e3e557cd2916224537b1d688333c2661e1006a836ca92afde12f59c04bb
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: 33a2a61187282c3801de1ac648f4d55fa0ad2a46d5ef1c1f964f1e5db5df7ed7
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: 79f1cdaeb24197a454fbcf9ffa2a2a0678ebe2e09c0ab82a694e9e6b643fba4a43fa14d836c1cb3eeca91c9f4091d63faec489d60c7237e8652b42679c8a811f
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: d96e691b5bb66c86c64852474b4bbb9cb1797420adfed71210f64c27f0e695b5489dec71c18d6636daa3ecdcea6e6a1580f90cba850f140e1c7ebc5734d615b8
         
     | 
    
        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.5] - 2025-10-30
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            ### Fixed
         
     | 
| 
      
 13 
     | 
    
         
            +
            - **Memory Leak**: Fixed critical memory leak where subscription keys were never cleaned up after client disconnection
         
     | 
| 
      
 14 
     | 
    
         
            +
              - Orphaned `subscriptions:{client_id}` keys remained permanently in Redis
         
     | 
| 
      
 15 
     | 
    
         
            +
              - Orphaned `subscription:{client_id}:{channel}` hash keys accumulated over time
         
     | 
| 
      
 16 
     | 
    
         
            +
              - Orphaned client IDs remained in `channels:{channel}` sets
         
     | 
| 
      
 17 
     | 
    
         
            +
              - Message queues for disconnected clients were not cleaned up
         
     | 
| 
      
 18 
     | 
    
         
            +
              - Could result in hundreds of MB memory leak in production environments
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
            ### Added
         
     | 
| 
      
 21 
     | 
    
         
            +
            - **`cleanup_expired` Method**: New public method to clean up expired clients and orphaned data
         
     | 
| 
      
 22 
     | 
    
         
            +
              - Automatically detects and removes orphaned subscription keys
         
     | 
| 
      
 23 
     | 
    
         
            +
              - Cleans up message queues for disconnected clients
         
     | 
| 
      
 24 
     | 
    
         
            +
              - Removes stale client IDs from channel subscriber lists
         
     | 
| 
      
 25 
     | 
    
         
            +
              - Uses Redis SCAN to avoid blocking operations
         
     | 
| 
      
 26 
     | 
    
         
            +
              - Batch deletion using pipelining for efficiency
         
     | 
| 
      
 27 
     | 
    
         
            +
              - Can be called manually or scheduled as periodic task
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
            ### Changed
         
     | 
| 
      
 30 
     | 
    
         
            +
            - **Improved Cleanup Strategy**: Enhanced cleanup process now handles orphaned data
         
     | 
| 
      
 31 
     | 
    
         
            +
              - `cleanup_expired` now cleans both expired clients AND orphaned subscriptions
         
     | 
| 
      
 32 
     | 
    
         
            +
              - Works even when no expired clients are found
         
     | 
| 
      
 33 
     | 
    
         
            +
              - Prevents memory leaks from abnormal client disconnections
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
            ### Technical Details
         
     | 
| 
      
 36 
     | 
    
         
            +
            Memory leak scenario (before fix):
         
     | 
| 
      
 37 
     | 
    
         
            +
            - 10,000 abnormally disconnected clients × 5 channels each = 50,000+ orphaned keys
         
     | 
| 
      
 38 
     | 
    
         
            +
            - Estimated memory waste: 100-500 MB
         
     | 
| 
      
 39 
     | 
    
         
            +
            - Keys remained permanently without TTL
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
            After fix:
         
     | 
| 
      
 42 
     | 
    
         
            +
            - All orphaned keys cleaned up automatically
         
     | 
| 
      
 43 
     | 
    
         
            +
            - Memory usage remains stable
         
     | 
| 
      
 44 
     | 
    
         
            +
            - Production environments can schedule periodic cleanup
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
            ## [1.0.4] - 2025-10-15
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
            ### Performance
         
     | 
| 
      
 49 
     | 
    
         
            +
            - **Major Message Delivery Optimization**: Significantly improved message publishing and delivery performance
         
     | 
| 
      
 50 
     | 
    
         
            +
              - Reduced Redis operations for message enqueue from 4 to 2 per message (50% reduction)
         
     | 
| 
      
 51 
     | 
    
         
            +
              - Reduced Redis operations for message dequeue from 2N+1 to 2 atomic operations (90%+ reduction for N messages)
         
     | 
| 
      
 52 
     | 
    
         
            +
              - Changed publish flow from sequential to parallel execution
         
     | 
| 
      
 53 
     | 
    
         
            +
              - Added batch enqueue operation using Redis pipelining for multiple clients
         
     | 
| 
      
 54 
     | 
    
         
            +
              - Reduced network round trips from N to 1 when publishing to multiple clients
         
     | 
| 
      
 55 
     | 
    
         
            +
              - **Overall latency improvement: 60-80% faster message delivery** (depending on subscriber count)
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
            ### Changed
         
     | 
| 
      
 58 
     | 
    
         
            +
            - **Message Storage**: Simplified message storage structure
         
     | 
| 
      
 59 
     | 
    
         
            +
              - Messages now stored directly as JSON in Redis lists instead of using separate hash + list
         
     | 
| 
      
 60 
     | 
    
         
            +
              - Maintains message UUID for uniqueness and traceability
         
     | 
| 
      
 61 
     | 
    
         
            +
              - More efficient use of Redis memory and operations
         
     | 
| 
      
 62 
     | 
    
         
            +
            - **Publish Mechanism**: Refactored publish method to execute pub/sub and enqueue operations in parallel
         
     | 
| 
      
 63 
     | 
    
         
            +
              - Eliminates sequential waiting bottleneck
         
     | 
| 
      
 64 
     | 
    
         
            +
              - Uses single Redis pipeline for batch client enqueue operations
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
            ### Technical Details
         
     | 
| 
      
 67 
     | 
    
         
            +
            For 100 subscribers receiving one message:
         
     | 
| 
      
 68 
     | 
    
         
            +
            - Before: 400 Redis operations (sequential), 100 network round trips, ~200-500ms latency
         
     | 
| 
      
 69 
     | 
    
         
            +
            - After: 200 Redis operations (parallel + pipelined), 1 network round trip, ~20-50ms latency
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
       10 
71 
     | 
    
         
             
            ## [1.0.3] - 2025-10-06
         
     | 
| 
       11 
72 
     | 
    
         | 
| 
       12 
73 
     | 
    
         
             
            ### Fixed
         
     | 
| 
         @@ -65,7 +126,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 
     | 
|
| 
       65 
126 
     | 
    
         
             
            ### Security
         
     | 
| 
       66 
127 
     | 
    
         
             
            - Client and message IDs now use `SecureRandom.uuid` instead of predictable time-based generation
         
     | 
| 
       67 
128 
     | 
    
         | 
| 
       68 
     | 
    
         
            -
            [Unreleased]: https://github.com/7a6163/faye-redis-ng/compare/v1.0. 
     | 
| 
      
 129 
     | 
    
         
            +
            [Unreleased]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.5...HEAD
         
     | 
| 
      
 130 
     | 
    
         
            +
            [1.0.5]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.4...v1.0.5
         
     | 
| 
      
 131 
     | 
    
         
            +
            [1.0.4]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.3...v1.0.4
         
     | 
| 
       69 
132 
     | 
    
         
             
            [1.0.3]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.2...v1.0.3
         
     | 
| 
       70 
133 
     | 
    
         
             
            [1.0.2]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.1...v1.0.2
         
     | 
| 
       71 
134 
     | 
    
         
             
            [1.0.1]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.0...v1.0.1
         
     | 
    
        data/README.md
    CHANGED
    
    | 
         @@ -234,6 +234,109 @@ The CI/CD pipeline will automatically: 
     | 
|
| 
       234 
234 
     | 
    
         
             
            - Add `RUBYGEMS_API_KEY` to GitHub repository secrets
         
     | 
| 
       235 
235 
     | 
    
         
             
            - The tag must start with 'v' (e.g., v0.1.0, v1.2.3)
         
     | 
| 
       236 
236 
     | 
    
         | 
| 
      
 237 
     | 
    
         
            +
            ## Memory Management
         
     | 
| 
      
 238 
     | 
    
         
            +
             
     | 
| 
      
 239 
     | 
    
         
            +
            ### Cleaning Up Expired Clients
         
     | 
| 
      
 240 
     | 
    
         
            +
             
     | 
| 
      
 241 
     | 
    
         
            +
            To prevent memory leaks from orphaned subscription keys, you should periodically clean up expired clients:
         
     | 
| 
      
 242 
     | 
    
         
            +
             
     | 
| 
      
 243 
     | 
    
         
            +
            #### Manual Cleanup
         
     | 
| 
      
 244 
     | 
    
         
            +
             
     | 
| 
      
 245 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 246 
     | 
    
         
            +
            # Get the engine instance
         
     | 
| 
      
 247 
     | 
    
         
            +
            engine = bayeux.get_engine
         
     | 
| 
      
 248 
     | 
    
         
            +
             
     | 
| 
      
 249 
     | 
    
         
            +
            # Clean up expired clients and orphaned data
         
     | 
| 
      
 250 
     | 
    
         
            +
            engine.cleanup_expired do |expired_count|
         
     | 
| 
      
 251 
     | 
    
         
            +
              puts "Cleaned up #{expired_count} expired clients"
         
     | 
| 
      
 252 
     | 
    
         
            +
            end
         
     | 
| 
      
 253 
     | 
    
         
            +
            ```
         
     | 
| 
      
 254 
     | 
    
         
            +
             
     | 
| 
      
 255 
     | 
    
         
            +
            #### Automatic Periodic Cleanup (Recommended)
         
     | 
| 
      
 256 
     | 
    
         
            +
             
     | 
| 
      
 257 
     | 
    
         
            +
            Add this to your Faye server setup:
         
     | 
| 
      
 258 
     | 
    
         
            +
             
     | 
| 
      
 259 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 260 
     | 
    
         
            +
            require 'eventmachine'
         
     | 
| 
      
 261 
     | 
    
         
            +
            require 'faye'
         
     | 
| 
      
 262 
     | 
    
         
            +
            require 'faye-redis-ng'
         
     | 
| 
      
 263 
     | 
    
         
            +
             
     | 
| 
      
 264 
     | 
    
         
            +
            bayeux = Faye::RackAdapter.new(app, {
         
     | 
| 
      
 265 
     | 
    
         
            +
              mount: '/faye',
         
     | 
| 
      
 266 
     | 
    
         
            +
              timeout: 25,
         
     | 
| 
      
 267 
     | 
    
         
            +
              engine: {
         
     | 
| 
      
 268 
     | 
    
         
            +
                type: Faye::Redis,
         
     | 
| 
      
 269 
     | 
    
         
            +
                host: 'localhost',
         
     | 
| 
      
 270 
     | 
    
         
            +
                port: 6379,
         
     | 
| 
      
 271 
     | 
    
         
            +
                namespace: 'my-app'
         
     | 
| 
      
 272 
     | 
    
         
            +
              }
         
     | 
| 
      
 273 
     | 
    
         
            +
            })
         
     | 
| 
      
 274 
     | 
    
         
            +
             
     | 
| 
      
 275 
     | 
    
         
            +
            # Schedule automatic cleanup every 5 minutes
         
     | 
| 
      
 276 
     | 
    
         
            +
            EM.add_periodic_timer(300) do
         
     | 
| 
      
 277 
     | 
    
         
            +
              bayeux.get_engine.cleanup_expired do |count|
         
     | 
| 
      
 278 
     | 
    
         
            +
                puts "[#{Time.now}] Cleaned up #{count} expired clients" if count > 0
         
     | 
| 
      
 279 
     | 
    
         
            +
              end
         
     | 
| 
      
 280 
     | 
    
         
            +
            end
         
     | 
| 
      
 281 
     | 
    
         
            +
             
     | 
| 
      
 282 
     | 
    
         
            +
            run bayeux
         
     | 
| 
      
 283 
     | 
    
         
            +
            ```
         
     | 
| 
      
 284 
     | 
    
         
            +
             
     | 
| 
      
 285 
     | 
    
         
            +
            #### Using Rake Task
         
     | 
| 
      
 286 
     | 
    
         
            +
             
     | 
| 
      
 287 
     | 
    
         
            +
            Create a Rake task for manual or scheduled cleanup:
         
     | 
| 
      
 288 
     | 
    
         
            +
             
     | 
| 
      
 289 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 290 
     | 
    
         
            +
            # lib/tasks/faye_cleanup.rake
         
     | 
| 
      
 291 
     | 
    
         
            +
            namespace :faye do
         
     | 
| 
      
 292 
     | 
    
         
            +
              desc "Clean up expired Faye clients and orphaned subscriptions"
         
     | 
| 
      
 293 
     | 
    
         
            +
              task cleanup: :environment do
         
     | 
| 
      
 294 
     | 
    
         
            +
                require 'eventmachine'
         
     | 
| 
      
 295 
     | 
    
         
            +
             
     | 
| 
      
 296 
     | 
    
         
            +
                EM.run do
         
     | 
| 
      
 297 
     | 
    
         
            +
                  engine = Faye::Redis.new(
         
     | 
| 
      
 298 
     | 
    
         
            +
                    nil,
         
     | 
| 
      
 299 
     | 
    
         
            +
                    host: ENV['REDIS_HOST'] || 'localhost',
         
     | 
| 
      
 300 
     | 
    
         
            +
                    port: ENV['REDIS_PORT']&.to_i || 6379,
         
     | 
| 
      
 301 
     | 
    
         
            +
                    namespace: 'my-app'
         
     | 
| 
      
 302 
     | 
    
         
            +
                  )
         
     | 
| 
      
 303 
     | 
    
         
            +
             
     | 
| 
      
 304 
     | 
    
         
            +
                  engine.cleanup_expired do |count|
         
     | 
| 
      
 305 
     | 
    
         
            +
                    puts "✅ Cleaned up #{count} expired clients"
         
     | 
| 
      
 306 
     | 
    
         
            +
                    engine.disconnect
         
     | 
| 
      
 307 
     | 
    
         
            +
                    EM.stop
         
     | 
| 
      
 308 
     | 
    
         
            +
                  end
         
     | 
| 
      
 309 
     | 
    
         
            +
                end
         
     | 
| 
      
 310 
     | 
    
         
            +
              end
         
     | 
| 
      
 311 
     | 
    
         
            +
            end
         
     | 
| 
      
 312 
     | 
    
         
            +
            ```
         
     | 
| 
      
 313 
     | 
    
         
            +
             
     | 
| 
      
 314 
     | 
    
         
            +
            Then schedule it with cron:
         
     | 
| 
      
 315 
     | 
    
         
            +
             
     | 
| 
      
 316 
     | 
    
         
            +
            ```bash
         
     | 
| 
      
 317 
     | 
    
         
            +
            # Run cleanup every hour
         
     | 
| 
      
 318 
     | 
    
         
            +
            0 * * * * cd /path/to/app && bundle exec rake faye:cleanup
         
     | 
| 
      
 319 
     | 
    
         
            +
            ```
         
     | 
| 
      
 320 
     | 
    
         
            +
             
     | 
| 
      
 321 
     | 
    
         
            +
            ### What Gets Cleaned Up
         
     | 
| 
      
 322 
     | 
    
         
            +
             
     | 
| 
      
 323 
     | 
    
         
            +
            The `cleanup_expired` method removes:
         
     | 
| 
      
 324 
     | 
    
         
            +
             
     | 
| 
      
 325 
     | 
    
         
            +
            1. **Expired client keys** (`clients:{client_id}`)
         
     | 
| 
      
 326 
     | 
    
         
            +
            2. **Orphaned subscription lists** (`subscriptions:{client_id}`)
         
     | 
| 
      
 327 
     | 
    
         
            +
            3. **Orphaned subscription metadata** (`subscription:{client_id}:{channel}`)
         
     | 
| 
      
 328 
     | 
    
         
            +
            4. **Stale client IDs from channel subscribers** (`channels:{channel}`)
         
     | 
| 
      
 329 
     | 
    
         
            +
            5. **Orphaned message queues** (`messages:{client_id}`)
         
     | 
| 
      
 330 
     | 
    
         
            +
             
     | 
| 
      
 331 
     | 
    
         
            +
            ### Memory Leak Prevention
         
     | 
| 
      
 332 
     | 
    
         
            +
             
     | 
| 
      
 333 
     | 
    
         
            +
            Without periodic cleanup, abnormal client disconnections (crashes, network failures, etc.) can cause orphaned keys to accumulate:
         
     | 
| 
      
 334 
     | 
    
         
            +
             
     | 
| 
      
 335 
     | 
    
         
            +
            - **Before fix**: 10,000 orphaned clients × 5 channels = 50,000+ keys = 100-500 MB leaked
         
     | 
| 
      
 336 
     | 
    
         
            +
            - **After fix**: All orphaned keys are cleaned up automatically
         
     | 
| 
      
 337 
     | 
    
         
            +
             
     | 
| 
      
 338 
     | 
    
         
            +
            **Recommendation**: Schedule cleanup every 5-10 minutes in production environments.
         
     | 
| 
      
 339 
     | 
    
         
            +
             
     | 
| 
       237 
340 
     | 
    
         
             
            ## Troubleshooting
         
     | 
| 
       238 
341 
     | 
    
         | 
| 
       239 
342 
     | 
    
         
             
            ### Connection Issues
         
     | 
| 
         @@ -13,30 +13,20 @@ module Faye 
     | 
|
| 
       13 
13 
     | 
    
         | 
| 
       14 
14 
     | 
    
         
             
                  # Enqueue a message for a client
         
     | 
| 
       15 
15 
     | 
    
         
             
                  def enqueue(client_id, message, &callback)
         
     | 
| 
       16 
     | 
    
         
            -
                     
     | 
| 
       17 
     | 
    
         
            -
                     
     | 
| 
      
 16 
     | 
    
         
            +
                    # Add unique ID if not present (for message deduplication)
         
     | 
| 
      
 17 
     | 
    
         
            +
                    message_with_id = message.dup
         
     | 
| 
      
 18 
     | 
    
         
            +
                    message_with_id['id'] ||= generate_message_id
         
     | 
| 
       18 
19 
     | 
    
         | 
| 
       19 
     | 
    
         
            -
                     
     | 
| 
       20 
     | 
    
         
            -
             
     | 
| 
       21 
     | 
    
         
            -
                      channel: message['channel'],
         
     | 
| 
       22 
     | 
    
         
            -
                      data: message['data'],
         
     | 
| 
       23 
     | 
    
         
            -
                      client_id: message['clientId'],
         
     | 
| 
       24 
     | 
    
         
            -
                      timestamp: timestamp
         
     | 
| 
       25 
     | 
    
         
            -
                    }
         
     | 
| 
      
 20 
     | 
    
         
            +
                    # Store message directly as JSON
         
     | 
| 
      
 21 
     | 
    
         
            +
                    message_json = message_with_id.to_json
         
     | 
| 
       26 
22 
     | 
    
         | 
| 
       27 
23 
     | 
    
         
             
                    @connection.with_redis do |redis|
         
     | 
| 
       28 
     | 
    
         
            -
                       
     | 
| 
       29 
     | 
    
         
            -
             
     | 
| 
       30 
     | 
    
         
            -
                        multi.hset(message_key(message_id), message_data.transform_keys(&:to_s).transform_values { |v| v.to_json })
         
     | 
| 
       31 
     | 
    
         
            -
             
     | 
| 
      
 24 
     | 
    
         
            +
                      # Use RPUSH with EXPIRE in a single pipeline
         
     | 
| 
      
 25 
     | 
    
         
            +
                      redis.pipelined do |pipeline|
         
     | 
| 
       32 
26 
     | 
    
         
             
                        # Add message to client's queue
         
     | 
| 
       33 
     | 
    
         
            -
                         
     | 
| 
       34 
     | 
    
         
            -
             
     | 
| 
       35 
     | 
    
         
            -
                         
     | 
| 
       36 
     | 
    
         
            -
                        multi.expire(message_key(message_id), message_ttl)
         
     | 
| 
       37 
     | 
    
         
            -
             
     | 
| 
       38 
     | 
    
         
            -
                        # Set TTL on queue
         
     | 
| 
       39 
     | 
    
         
            -
                        multi.expire(queue_key(client_id), message_ttl)
         
     | 
| 
      
 27 
     | 
    
         
            +
                        pipeline.rpush(queue_key(client_id), message_json)
         
     | 
| 
      
 28 
     | 
    
         
            +
                        # Set TTL on queue (only if it doesn't already have one)
         
     | 
| 
      
 29 
     | 
    
         
            +
                        pipeline.expire(queue_key(client_id), message_ttl)
         
     | 
| 
       40 
30 
     | 
    
         
             
                      end
         
     | 
| 
       41 
31 
     | 
    
         
             
                    end
         
     | 
| 
       42 
32 
     | 
    
         | 
| 
         @@ -48,50 +38,25 @@ module Faye 
     | 
|
| 
       48 
38 
     | 
    
         | 
| 
       49 
39 
     | 
    
         
             
                  # Dequeue all messages for a client
         
     | 
| 
       50 
40 
     | 
    
         
             
                  def dequeue_all(client_id, &callback)
         
     | 
| 
       51 
     | 
    
         
            -
                    # Get all  
     | 
| 
       52 
     | 
    
         
            -
                     
     | 
| 
       53 
     | 
    
         
            -
                      redis.lrange(queue_key(client_id), 0, -1)
         
     | 
| 
       54 
     | 
    
         
            -
                    end
         
     | 
| 
      
 41 
     | 
    
         
            +
                    # Get all messages and delete queue in a single atomic operation
         
     | 
| 
      
 42 
     | 
    
         
            +
                    key = queue_key(client_id)
         
     | 
| 
       55 
43 
     | 
    
         | 
| 
       56 
     | 
    
         
            -
                     
     | 
| 
       57 
     | 
    
         
            -
             
     | 
| 
       58 
     | 
    
         
            -
             
     | 
| 
       59 
     | 
    
         
            -
             
     | 
| 
       60 
     | 
    
         
            -
                         
     | 
| 
       61 
     | 
    
         
            -
                          message_ids.each do |message_id|
         
     | 
| 
       62 
     | 
    
         
            -
                            pipeline.hgetall(message_key(message_id))
         
     | 
| 
       63 
     | 
    
         
            -
                          end
         
     | 
| 
       64 
     | 
    
         
            -
                        end.each do |data|
         
     | 
| 
       65 
     | 
    
         
            -
                          next if data.nil? || data.empty?
         
     | 
| 
       66 
     | 
    
         
            -
             
     | 
| 
       67 
     | 
    
         
            -
                          # Parse JSON values
         
     | 
| 
       68 
     | 
    
         
            -
                          parsed_data = data.transform_values do |v|
         
     | 
| 
       69 
     | 
    
         
            -
                            begin
         
     | 
| 
       70 
     | 
    
         
            -
                              JSON.parse(v)
         
     | 
| 
       71 
     | 
    
         
            -
                            rescue JSON::ParserError
         
     | 
| 
       72 
     | 
    
         
            -
                              v
         
     | 
| 
       73 
     | 
    
         
            -
                            end
         
     | 
| 
       74 
     | 
    
         
            -
                          end
         
     | 
| 
       75 
     | 
    
         
            -
             
     | 
| 
       76 
     | 
    
         
            -
                          # Convert to Faye message format
         
     | 
| 
       77 
     | 
    
         
            -
                          messages << {
         
     | 
| 
       78 
     | 
    
         
            -
                            'channel' => parsed_data['channel'],
         
     | 
| 
       79 
     | 
    
         
            -
                            'data' => parsed_data['data'],
         
     | 
| 
       80 
     | 
    
         
            -
                            'clientId' => parsed_data['client_id'],
         
     | 
| 
       81 
     | 
    
         
            -
                            'id' => parsed_data['id']
         
     | 
| 
       82 
     | 
    
         
            -
                          }
         
     | 
| 
       83 
     | 
    
         
            -
                        end
         
     | 
| 
      
 44 
     | 
    
         
            +
                    json_messages = @connection.with_redis do |redis|
         
     | 
| 
      
 45 
     | 
    
         
            +
                      # Use MULTI/EXEC to atomically get and delete
         
     | 
| 
      
 46 
     | 
    
         
            +
                      redis.multi do |multi|
         
     | 
| 
      
 47 
     | 
    
         
            +
                        multi.lrange(key, 0, -1)
         
     | 
| 
      
 48 
     | 
    
         
            +
                        multi.del(key)
         
     | 
| 
       84 
49 
     | 
    
         
             
                      end
         
     | 
| 
       85 
50 
     | 
    
         
             
                    end
         
     | 
| 
       86 
51 
     | 
    
         | 
| 
       87 
     | 
    
         
            -
                    #  
     | 
| 
       88 
     | 
    
         
            -
                     
     | 
| 
       89 
     | 
    
         
            -
             
     | 
| 
       90 
     | 
    
         
            -
             
     | 
| 
       91 
     | 
    
         
            -
             
     | 
| 
       92 
     | 
    
         
            -
                           
     | 
| 
       93 
     | 
    
         
            -
             
     | 
| 
       94 
     | 
    
         
            -
                           
     | 
| 
      
 52 
     | 
    
         
            +
                    # Parse messages from JSON
         
     | 
| 
      
 53 
     | 
    
         
            +
                    messages = []
         
     | 
| 
      
 54 
     | 
    
         
            +
                    if json_messages && json_messages[0]
         
     | 
| 
      
 55 
     | 
    
         
            +
                      json_messages[0].each do |json|
         
     | 
| 
      
 56 
     | 
    
         
            +
                        begin
         
     | 
| 
      
 57 
     | 
    
         
            +
                          messages << JSON.parse(json)
         
     | 
| 
      
 58 
     | 
    
         
            +
                        rescue JSON::ParserError => e
         
     | 
| 
      
 59 
     | 
    
         
            +
                          log_error("Failed to parse message JSON: #{e.message}")
         
     | 
| 
       95 
60 
     | 
    
         
             
                        end
         
     | 
| 
       96 
61 
     | 
    
         
             
                      end
         
     | 
| 
       97 
62 
     | 
    
         
             
                    end
         
     | 
| 
         @@ -106,12 +71,17 @@ module Faye 
     | 
|
| 
       106 
71 
     | 
    
         | 
| 
       107 
72 
     | 
    
         
             
                  # Peek at messages without removing them
         
     | 
| 
       108 
73 
     | 
    
         
             
                  def peek(client_id, limit = 10, &callback)
         
     | 
| 
       109 
     | 
    
         
            -
                     
     | 
| 
      
 74 
     | 
    
         
            +
                    json_messages = @connection.with_redis do |redis|
         
     | 
| 
       110 
75 
     | 
    
         
             
                      redis.lrange(queue_key(client_id), 0, limit - 1)
         
     | 
| 
       111 
76 
     | 
    
         
             
                    end
         
     | 
| 
       112 
77 
     | 
    
         | 
| 
       113 
     | 
    
         
            -
                    messages =  
     | 
| 
       114 
     | 
    
         
            -
                       
     | 
| 
      
 78 
     | 
    
         
            +
                    messages = json_messages.map do |json|
         
     | 
| 
      
 79 
     | 
    
         
            +
                      begin
         
     | 
| 
      
 80 
     | 
    
         
            +
                        JSON.parse(json)
         
     | 
| 
      
 81 
     | 
    
         
            +
                      rescue JSON::ParserError => e
         
     | 
| 
      
 82 
     | 
    
         
            +
                        log_error("Failed to parse message JSON: #{e.message}")
         
     | 
| 
      
 83 
     | 
    
         
            +
                        nil
         
     | 
| 
      
 84 
     | 
    
         
            +
                      end
         
     | 
| 
       115 
85 
     | 
    
         
             
                    end.compact
         
     | 
| 
       116 
86 
     | 
    
         | 
| 
       117 
87 
     | 
    
         
             
                    EventMachine.next_tick { callback.call(messages) } if callback
         
     | 
| 
         @@ -138,19 +108,9 @@ module Faye 
     | 
|
| 
       138 
108 
     | 
    
         | 
| 
       139 
109 
     | 
    
         
             
                  # Clear a client's message queue
         
     | 
| 
       140 
110 
     | 
    
         
             
                  def clear(client_id, &callback)
         
     | 
| 
       141 
     | 
    
         
            -
                    #  
     | 
| 
       142 
     | 
    
         
            -
                    message_ids = @connection.with_redis do |redis|
         
     | 
| 
       143 
     | 
    
         
            -
                      redis.lrange(queue_key(client_id), 0, -1)
         
     | 
| 
       144 
     | 
    
         
            -
                    end
         
     | 
| 
       145 
     | 
    
         
            -
             
     | 
| 
       146 
     | 
    
         
            -
                    # Delete queue and all message data
         
     | 
| 
      
 111 
     | 
    
         
            +
                    # Simply delete the queue
         
     | 
| 
       147 
112 
     | 
    
         
             
                    @connection.with_redis do |redis|
         
     | 
| 
       148 
     | 
    
         
            -
                      redis. 
     | 
| 
       149 
     | 
    
         
            -
                        pipeline.del(queue_key(client_id))
         
     | 
| 
       150 
     | 
    
         
            -
                        message_ids.each do |message_id|
         
     | 
| 
       151 
     | 
    
         
            -
                          pipeline.del(message_key(message_id))
         
     | 
| 
       152 
     | 
    
         
            -
                        end
         
     | 
| 
       153 
     | 
    
         
            -
                      end
         
     | 
| 
      
 113 
     | 
    
         
            +
                      redis.del(queue_key(client_id))
         
     | 
| 
       154 
114 
     | 
    
         
             
                    end
         
     | 
| 
       155 
115 
     | 
    
         | 
| 
       156 
116 
     | 
    
         
             
                    EventMachine.next_tick { callback.call(true) } if callback
         
     | 
| 
         @@ -161,42 +121,10 @@ module Faye 
     | 
|
| 
       161 
121 
     | 
    
         | 
| 
       162 
122 
     | 
    
         
             
                  private
         
     | 
| 
       163 
123 
     | 
    
         | 
| 
       164 
     | 
    
         
            -
                  def fetch_message(message_id)
         
     | 
| 
       165 
     | 
    
         
            -
                    data = @connection.with_redis do |redis|
         
     | 
| 
       166 
     | 
    
         
            -
                      redis.hgetall(message_key(message_id))
         
     | 
| 
       167 
     | 
    
         
            -
                    end
         
     | 
| 
       168 
     | 
    
         
            -
             
     | 
| 
       169 
     | 
    
         
            -
                    return nil if data.empty?
         
     | 
| 
       170 
     | 
    
         
            -
             
     | 
| 
       171 
     | 
    
         
            -
                    # Parse JSON values
         
     | 
| 
       172 
     | 
    
         
            -
                    parsed_data = data.transform_values do |v|
         
     | 
| 
       173 
     | 
    
         
            -
                      begin
         
     | 
| 
       174 
     | 
    
         
            -
                        JSON.parse(v)
         
     | 
| 
       175 
     | 
    
         
            -
                      rescue JSON::ParserError
         
     | 
| 
       176 
     | 
    
         
            -
                        v
         
     | 
| 
       177 
     | 
    
         
            -
                      end
         
     | 
| 
       178 
     | 
    
         
            -
                    end
         
     | 
| 
       179 
     | 
    
         
            -
             
     | 
| 
       180 
     | 
    
         
            -
                    # Convert to Faye message format
         
     | 
| 
       181 
     | 
    
         
            -
                    {
         
     | 
| 
       182 
     | 
    
         
            -
                      'channel' => parsed_data['channel'],
         
     | 
| 
       183 
     | 
    
         
            -
                      'data' => parsed_data['data'],
         
     | 
| 
       184 
     | 
    
         
            -
                      'clientId' => parsed_data['client_id'],
         
     | 
| 
       185 
     | 
    
         
            -
                      'id' => parsed_data['id']
         
     | 
| 
       186 
     | 
    
         
            -
                    }
         
     | 
| 
       187 
     | 
    
         
            -
                  rescue => e
         
     | 
| 
       188 
     | 
    
         
            -
                    log_error("Failed to fetch message #{message_id}: #{e.message}")
         
     | 
| 
       189 
     | 
    
         
            -
                    nil
         
     | 
| 
       190 
     | 
    
         
            -
                  end
         
     | 
| 
       191 
     | 
    
         
            -
             
     | 
| 
       192 
124 
     | 
    
         
             
                  def queue_key(client_id)
         
     | 
| 
       193 
125 
     | 
    
         
             
                    namespace_key("messages:#{client_id}")
         
     | 
| 
       194 
126 
     | 
    
         
             
                  end
         
     | 
| 
       195 
127 
     | 
    
         | 
| 
       196 
     | 
    
         
            -
                  def message_key(message_id)
         
     | 
| 
       197 
     | 
    
         
            -
                    namespace_key("message:#{message_id}")
         
     | 
| 
       198 
     | 
    
         
            -
                  end
         
     | 
| 
       199 
     | 
    
         
            -
             
     | 
| 
       200 
128 
     | 
    
         
             
                  def namespace_key(key)
         
     | 
| 
       201 
129 
     | 
    
         
             
                    namespace = @options[:namespace] || 'faye'
         
     | 
| 
       202 
130 
     | 
    
         
             
                    "#{namespace}:#{key}"
         
     | 
    
        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'
         
     | 
| 
         @@ -101,41 +102,25 @@ module Faye 
     | 
|
| 
       101 
102 
     | 
    
         
             
                    success = true
         
     | 
| 
       102 
103 
     | 
    
         | 
| 
       103 
104 
     | 
    
         
             
                    channels.each do |channel|
         
     | 
| 
       104 
     | 
    
         
            -
                      #  
     | 
| 
      
 105 
     | 
    
         
            +
                      # Get subscribers and process in parallel
         
     | 
| 
       105 
106 
     | 
    
         
             
                      @subscription_manager.get_subscribers(channel) do |client_ids|
         
     | 
| 
       106 
     | 
    
         
            -
                         
     | 
| 
       107 
     | 
    
         
            -
             
     | 
| 
       108 
     | 
    
         
            -
             
     | 
| 
       109 
     | 
    
         
            -
             
     | 
| 
       110 
     | 
    
         
            -
                          @pubsub_coordinator.publish(channel, message) do |published|
         
     | 
| 
       111 
     | 
    
         
            -
                            success &&= published
         
     | 
| 
       112 
     | 
    
         
            -
                            remaining_operations -= 1
         
     | 
| 
      
 107 
     | 
    
         
            +
                        # Immediately publish to pub/sub (don't wait for enqueue)
         
     | 
| 
      
 108 
     | 
    
         
            +
                        @pubsub_coordinator.publish(channel, message) do |published|
         
     | 
| 
      
 109 
     | 
    
         
            +
                          success &&= published
         
     | 
| 
      
 110 
     | 
    
         
            +
                        end
         
     | 
| 
       113 
111 
     | 
    
         | 
| 
       114 
     | 
    
         
            -
             
     | 
| 
       115 
     | 
    
         
            -
             
     | 
| 
       116 
     | 
    
         
            -
             
     | 
| 
       117 
     | 
    
         
            -
             
     | 
| 
       118 
     | 
    
         
            -
                        else
         
     | 
| 
       119 
     | 
    
         
            -
                          # Enqueue for all subscribed clients
         
     | 
| 
       120 
     | 
    
         
            -
                          client_ids.each do |client_id|
         
     | 
| 
       121 
     | 
    
         
            -
                            @message_queue.enqueue(client_id, message) do |enqueued|
         
     | 
| 
       122 
     | 
    
         
            -
                              success &&= enqueued
         
     | 
| 
       123 
     | 
    
         
            -
                              enqueue_count -= 1
         
     | 
| 
       124 
     | 
    
         
            -
             
     | 
| 
       125 
     | 
    
         
            -
                              # When all enqueues are done, do pub/sub
         
     | 
| 
       126 
     | 
    
         
            -
                              if enqueue_count == 0
         
     | 
| 
       127 
     | 
    
         
            -
                                @pubsub_coordinator.publish(channel, message) do |published|
         
     | 
| 
       128 
     | 
    
         
            -
                                  success &&= published
         
     | 
| 
       129 
     | 
    
         
            -
                                  remaining_operations -= 1
         
     | 
| 
       130 
     | 
    
         
            -
             
     | 
| 
       131 
     | 
    
         
            -
                                  if remaining_operations == 0 && callback
         
     | 
| 
       132 
     | 
    
         
            -
                                    EventMachine.next_tick { callback.call(success) }
         
     | 
| 
       133 
     | 
    
         
            -
                                  end
         
     | 
| 
       134 
     | 
    
         
            -
                                end
         
     | 
| 
       135 
     | 
    
         
            -
                              end
         
     | 
| 
       136 
     | 
    
         
            -
                            end
         
     | 
| 
      
 112 
     | 
    
         
            +
                        # Enqueue for all subscribed clients in parallel (batch operation)
         
     | 
| 
      
 113 
     | 
    
         
            +
                        if client_ids.any?
         
     | 
| 
      
 114 
     | 
    
         
            +
                          enqueue_messages_batch(client_ids, message) do |enqueued|
         
     | 
| 
      
 115 
     | 
    
         
            +
                            success &&= enqueued
         
     | 
| 
       137 
116 
     | 
    
         
             
                          end
         
     | 
| 
       138 
117 
     | 
    
         
             
                        end
         
     | 
| 
      
 118 
     | 
    
         
            +
             
     | 
| 
      
 119 
     | 
    
         
            +
                        # Track completion
         
     | 
| 
      
 120 
     | 
    
         
            +
                        remaining_operations -= 1
         
     | 
| 
      
 121 
     | 
    
         
            +
                        if remaining_operations == 0 && callback
         
     | 
| 
      
 122 
     | 
    
         
            +
                          EventMachine.next_tick { callback.call(success) }
         
     | 
| 
      
 123 
     | 
    
         
            +
                        end
         
     | 
| 
       139 
124 
     | 
    
         
             
                      end
         
     | 
| 
       140 
125 
     | 
    
         
             
                    end
         
     | 
| 
       141 
126 
     | 
    
         
             
                  rescue => e
         
     | 
| 
         @@ -155,19 +140,113 @@ module Faye 
     | 
|
| 
       155 
140 
     | 
    
         
             
                  @connection.disconnect
         
     | 
| 
       156 
141 
     | 
    
         
             
                end
         
     | 
| 
       157 
142 
     | 
    
         | 
| 
      
 143 
     | 
    
         
            +
                # Clean up expired clients and their associated data
         
     | 
| 
      
 144 
     | 
    
         
            +
                def cleanup_expired(&callback)
         
     | 
| 
      
 145 
     | 
    
         
            +
                  @client_registry.cleanup_expired do |expired_count|
         
     | 
| 
      
 146 
     | 
    
         
            +
                    @logger.info("Cleaned up #{expired_count} expired clients") if expired_count > 0
         
     | 
| 
      
 147 
     | 
    
         
            +
             
     | 
| 
      
 148 
     | 
    
         
            +
                    # Always clean up orphaned subscription keys (even if no expired clients)
         
     | 
| 
      
 149 
     | 
    
         
            +
                    # This handles cases where subscriptions were orphaned due to crashes
         
     | 
| 
      
 150 
     | 
    
         
            +
                    cleanup_orphaned_subscriptions do
         
     | 
| 
      
 151 
     | 
    
         
            +
                      callback.call(expired_count) if callback
         
     | 
| 
      
 152 
     | 
    
         
            +
                    end
         
     | 
| 
      
 153 
     | 
    
         
            +
                  end
         
     | 
| 
      
 154 
     | 
    
         
            +
                end
         
     | 
| 
      
 155 
     | 
    
         
            +
             
     | 
| 
       158 
156 
     | 
    
         
             
                private
         
     | 
| 
       159 
157 
     | 
    
         | 
| 
       160 
158 
     | 
    
         
             
                def generate_client_id
         
     | 
| 
       161 
159 
     | 
    
         
             
                  SecureRandom.uuid
         
     | 
| 
       162 
160 
     | 
    
         
             
                end
         
     | 
| 
       163 
161 
     | 
    
         | 
| 
      
 162 
     | 
    
         
            +
                # Batch enqueue messages to multiple clients using a single Redis pipeline
         
     | 
| 
      
 163 
     | 
    
         
            +
                def enqueue_messages_batch(client_ids, message, &callback)
         
     | 
| 
      
 164 
     | 
    
         
            +
                  return EventMachine.next_tick { callback.call(true) } if client_ids.empty? || !callback
         
     | 
| 
      
 165 
     | 
    
         
            +
             
     | 
| 
      
 166 
     | 
    
         
            +
                  message_json = message.to_json
         
     | 
| 
      
 167 
     | 
    
         
            +
                  message_ttl = @options[:message_ttl] || 3600
         
     | 
| 
      
 168 
     | 
    
         
            +
                  namespace = @options[:namespace] || 'faye'
         
     | 
| 
      
 169 
     | 
    
         
            +
             
     | 
| 
      
 170 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 171 
     | 
    
         
            +
                    @connection.with_redis do |redis|
         
     | 
| 
      
 172 
     | 
    
         
            +
                      redis.pipelined do |pipeline|
         
     | 
| 
      
 173 
     | 
    
         
            +
                        client_ids.each do |client_id|
         
     | 
| 
      
 174 
     | 
    
         
            +
                          queue_key = "#{namespace}:messages:#{client_id}"
         
     | 
| 
      
 175 
     | 
    
         
            +
                          pipeline.rpush(queue_key, message_json)
         
     | 
| 
      
 176 
     | 
    
         
            +
                          pipeline.expire(queue_key, message_ttl)
         
     | 
| 
      
 177 
     | 
    
         
            +
                        end
         
     | 
| 
      
 178 
     | 
    
         
            +
                      end
         
     | 
| 
      
 179 
     | 
    
         
            +
                    end
         
     | 
| 
      
 180 
     | 
    
         
            +
             
     | 
| 
      
 181 
     | 
    
         
            +
                    EventMachine.next_tick { callback.call(true) } if callback
         
     | 
| 
      
 182 
     | 
    
         
            +
                  rescue => e
         
     | 
| 
      
 183 
     | 
    
         
            +
                    log_error("Failed to batch enqueue messages: #{e.message}")
         
     | 
| 
      
 184 
     | 
    
         
            +
                    EventMachine.next_tick { callback.call(false) } if callback
         
     | 
| 
      
 185 
     | 
    
         
            +
                  end
         
     | 
| 
      
 186 
     | 
    
         
            +
                end
         
     | 
| 
      
 187 
     | 
    
         
            +
             
     | 
| 
      
 188 
     | 
    
         
            +
                def cleanup_orphaned_subscriptions(&callback)
         
     | 
| 
      
 189 
     | 
    
         
            +
                  # Get all active client IDs
         
     | 
| 
      
 190 
     | 
    
         
            +
                  @client_registry.all do |active_clients|
         
     | 
| 
      
 191 
     | 
    
         
            +
                    active_set = active_clients.to_set
         
     | 
| 
      
 192 
     | 
    
         
            +
                    namespace = @options[:namespace] || 'faye'
         
     | 
| 
      
 193 
     | 
    
         
            +
             
     | 
| 
      
 194 
     | 
    
         
            +
                    # Scan for subscription keys and clean up orphaned ones
         
     | 
| 
      
 195 
     | 
    
         
            +
                    @connection.with_redis do |redis|
         
     | 
| 
      
 196 
     | 
    
         
            +
                      cursor = "0"
         
     | 
| 
      
 197 
     | 
    
         
            +
                      orphaned_keys = []
         
     | 
| 
      
 198 
     | 
    
         
            +
             
     | 
| 
      
 199 
     | 
    
         
            +
                      loop do
         
     | 
| 
      
 200 
     | 
    
         
            +
                        cursor, keys = redis.scan(cursor, match: "#{namespace}:subscriptions:*", count: 100)
         
     | 
| 
      
 201 
     | 
    
         
            +
             
     | 
| 
      
 202 
     | 
    
         
            +
                        keys.each do |key|
         
     | 
| 
      
 203 
     | 
    
         
            +
                          # Extract client_id from key (format: namespace:subscriptions:client_id)
         
     | 
| 
      
 204 
     | 
    
         
            +
                          client_id = key.split(':').last
         
     | 
| 
      
 205 
     | 
    
         
            +
                          orphaned_keys << client_id unless active_set.include?(client_id)
         
     | 
| 
      
 206 
     | 
    
         
            +
                        end
         
     | 
| 
      
 207 
     | 
    
         
            +
             
     | 
| 
      
 208 
     | 
    
         
            +
                        break if cursor == "0"
         
     | 
| 
      
 209 
     | 
    
         
            +
                      end
         
     | 
| 
      
 210 
     | 
    
         
            +
             
     | 
| 
      
 211 
     | 
    
         
            +
                      # Clean up orphaned subscription data
         
     | 
| 
      
 212 
     | 
    
         
            +
                      if orphaned_keys.any?
         
     | 
| 
      
 213 
     | 
    
         
            +
                        @logger.info("Cleaning up #{orphaned_keys.size} orphaned subscription sets")
         
     | 
| 
      
 214 
     | 
    
         
            +
             
     | 
| 
      
 215 
     | 
    
         
            +
                        orphaned_keys.each do |client_id|
         
     | 
| 
      
 216 
     | 
    
         
            +
                          # Get channels for this orphaned client
         
     | 
| 
      
 217 
     | 
    
         
            +
                          channels = redis.smembers("#{namespace}:subscriptions:#{client_id}")
         
     | 
| 
      
 218 
     | 
    
         
            +
             
     | 
| 
      
 219 
     | 
    
         
            +
                          # Remove in batch
         
     | 
| 
      
 220 
     | 
    
         
            +
                          redis.pipelined do |pipeline|
         
     | 
| 
      
 221 
     | 
    
         
            +
                            # Delete client's subscription list
         
     | 
| 
      
 222 
     | 
    
         
            +
                            pipeline.del("#{namespace}:subscriptions:#{client_id}")
         
     | 
| 
      
 223 
     | 
    
         
            +
             
     | 
| 
      
 224 
     | 
    
         
            +
                            # Delete each subscription metadata and remove from channel subscribers
         
     | 
| 
      
 225 
     | 
    
         
            +
                            channels.each do |channel|
         
     | 
| 
      
 226 
     | 
    
         
            +
                              pipeline.del("#{namespace}:subscription:#{client_id}:#{channel}")
         
     | 
| 
      
 227 
     | 
    
         
            +
                              pipeline.srem("#{namespace}:channels:#{channel}", client_id)
         
     | 
| 
      
 228 
     | 
    
         
            +
                            end
         
     | 
| 
      
 229 
     | 
    
         
            +
             
     | 
| 
      
 230 
     | 
    
         
            +
                            # Delete message queue if exists
         
     | 
| 
      
 231 
     | 
    
         
            +
                            pipeline.del("#{namespace}:messages:#{client_id}")
         
     | 
| 
      
 232 
     | 
    
         
            +
                          end
         
     | 
| 
      
 233 
     | 
    
         
            +
                        end
         
     | 
| 
      
 234 
     | 
    
         
            +
                      end
         
     | 
| 
      
 235 
     | 
    
         
            +
                    end
         
     | 
| 
      
 236 
     | 
    
         
            +
             
     | 
| 
      
 237 
     | 
    
         
            +
                    EventMachine.next_tick { callback.call } if callback
         
     | 
| 
      
 238 
     | 
    
         
            +
                  end
         
     | 
| 
      
 239 
     | 
    
         
            +
                rescue => e
         
     | 
| 
      
 240 
     | 
    
         
            +
                  log_error("Failed to cleanup orphaned subscriptions: #{e.message}")
         
     | 
| 
      
 241 
     | 
    
         
            +
                  EventMachine.next_tick { callback.call } if callback
         
     | 
| 
      
 242 
     | 
    
         
            +
                end
         
     | 
| 
      
 243 
     | 
    
         
            +
             
     | 
| 
       164 
244 
     | 
    
         
             
                def setup_message_routing
         
     | 
| 
       165 
245 
     | 
    
         
             
                  # Subscribe to message events from other servers
         
     | 
| 
       166 
246 
     | 
    
         
             
                  @pubsub_coordinator.on_message do |channel, message|
         
     | 
| 
       167 
247 
     | 
    
         
             
                    @subscription_manager.get_subscribers(channel) do |client_ids|
         
     | 
| 
       168 
     | 
    
         
            -
                       
     | 
| 
       169 
     | 
    
         
            -
             
     | 
| 
       170 
     | 
    
         
            -
                      end
         
     | 
| 
      
 248 
     | 
    
         
            +
                      # Use batch enqueue for better performance
         
     | 
| 
      
 249 
     | 
    
         
            +
                      enqueue_messages_batch(client_ids, message) if client_ids.any?
         
     | 
| 
       171 
250 
     | 
    
         
             
                    end
         
     | 
| 
       172 
251 
     | 
    
         
             
                  end
         
     | 
| 
       173 
252 
     | 
    
         
             
                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.5
         
     | 
| 
       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
         
     |