faye-redis-ng 1.0.9 → 1.0.11
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 +75 -3
 - data/README.md +1 -0
 - data/lib/faye/redis/message_queue.rb +11 -7
 - data/lib/faye/redis/subscription_manager.rb +123 -32
 - data/lib/faye/redis/version.rb +1 -1
 - data/lib/faye/redis.rb +1 -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: 5a2266042c42929dc9203cf1160bbfd64d429c87cdb076b3369cd55bbf77032e
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: 51509e6fb5fb775bfbdcd4db0dada3dfa26f6bee5f5729d2b3fa165b5b417644
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: 79c6d4bee6b55572a772a4977fd5b5a5857746c415bac1f6f0a802ad69b16e137642b90d20ec02ec21abb95f69e5fab9ab830f018c6aaa974ad56b2c3202f269
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: 38c29ca6cd4330c5a862951b2810e8e095e4dec2939691bd8379ef714f947ad05a0e875bcf55a53de42d35913357d2f05c38f08b0ce16da12e553e2c7a22f1b1
         
     | 
    
        data/CHANGELOG.md
    CHANGED
    
    | 
         @@ -7,6 +7,75 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 
     | 
|
| 
       7 
7 
     | 
    
         | 
| 
       8 
8 
     | 
    
         
             
            ## [Unreleased]
         
     | 
| 
       9 
9 
     | 
    
         | 
| 
      
 10 
     | 
    
         
            +
            ## [1.0.11] - 2025-10-31
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            ### Changed - Optimized Subscription TTL
         
     | 
| 
      
 13 
     | 
    
         
            +
            - **Reduced subscription_ttl from 24 hours to 5 minutes**: More aggressive cleanup of orphaned subscription data
         
     | 
| 
      
 14 
     | 
    
         
            +
              - **Previous**: `subscription_ttl: 86400` (24 hours)
         
     | 
| 
      
 15 
     | 
    
         
            +
              - **New**: `subscription_ttl: 300` (5 minutes = 5x client_timeout)
         
     | 
| 
      
 16 
     | 
    
         
            +
              - **Rationale**: Client keys expire after 60 seconds, keeping subscription data for 24 hours was excessive
         
     | 
| 
      
 17 
     | 
    
         
            +
              - **Impact**: Faster memory reclamation for disconnected clients while maintaining safety net
         
     | 
| 
      
 18 
     | 
    
         
            +
              - **Benefits**:
         
     | 
| 
      
 19 
     | 
    
         
            +
                - Reduces memory footprint by cleaning up orphaned subscriptions faster
         
     | 
| 
      
 20 
     | 
    
         
            +
                - Maintains 5-minute safety window (5 GC cycles) for proper cleanup
         
     | 
| 
      
 21 
     | 
    
         
            +
                - Allows short-lived reconnections within 5 minutes to reuse subscription data
         
     | 
| 
      
 22 
     | 
    
         
            +
              - **Affected keys**:
         
     | 
| 
      
 23 
     | 
    
         
            +
                - `{namespace}:subscriptions:{client_id}` (SET)
         
     | 
| 
      
 24 
     | 
    
         
            +
                - `{namespace}:channels:{channel}` (SET)
         
     | 
| 
      
 25 
     | 
    
         
            +
                - `{namespace}:subscription:{client_id}:{channel}` (Hash)
         
     | 
| 
      
 26 
     | 
    
         
            +
                - `{namespace}:patterns` (SET)
         
     | 
| 
      
 27 
     | 
    
         
            +
              - **Backward compatibility**: Users can still override with custom `subscription_ttl` option
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
            ### Notes
         
     | 
| 
      
 30 
     | 
    
         
            +
            - This change is safe because automatic GC runs every 60 seconds by default
         
     | 
| 
      
 31 
     | 
    
         
            +
            - The 5-minute TTL provides sufficient buffer (5 GC cycles) for cleanup
         
     | 
| 
      
 32 
     | 
    
         
            +
            - TTL-safe implementation (v1.0.10) ensures active subscriptions maintain their original TTL
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
            ## [1.0.10] - 2025-10-30
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
            ### Fixed - Critical Memory Leak (P0 - Critical Priority)
         
     | 
| 
      
 37 
     | 
    
         
            +
            - **TTL Reset on Every Operation**: Fixed message queue and subscription TTL being reset on every operation
         
     | 
| 
      
 38 
     | 
    
         
            +
              - **Problem**: `EXPIRE` was called on every `enqueue` and `subscribe`, resetting TTL to full duration
         
     | 
| 
      
 39 
     | 
    
         
            +
              - **Impact**: Hot/active queues and subscriptions never expired, causing unbounded memory growth
         
     | 
| 
      
 40 
     | 
    
         
            +
              - **Solution**: Implemented Lua scripts to only set TTL if key has no TTL (TTL == -1)
         
     | 
| 
      
 41 
     | 
    
         
            +
              - Message queues: `enqueue` now checks TTL before setting expiration
         
     | 
| 
      
 42 
     | 
    
         
            +
              - Subscriptions: `subscribe` now checks TTL before setting expiration on all keys:
         
     | 
| 
      
 43 
     | 
    
         
            +
                - `faye:subscriptions:{client_id}` (SET)
         
     | 
| 
      
 44 
     | 
    
         
            +
                - `faye:channels:{channel}` (SET)
         
     | 
| 
      
 45 
     | 
    
         
            +
                - `faye:subscription:{client_id}:{channel}` (Hash)
         
     | 
| 
      
 46 
     | 
    
         
            +
                - `faye:patterns` (SET)
         
     | 
| 
      
 47 
     | 
    
         
            +
              - **Impact**: Prevents memory leak for active clients with frequent messages/re-subscriptions
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
            - **Orphaned Message Queue Cleanup**: Added dedicated cleanup for orphaned message queues
         
     | 
| 
      
 50 
     | 
    
         
            +
              - Added `cleanup_orphaned_message_queues_async` to scan and remove orphaned message queues
         
     | 
| 
      
 51 
     | 
    
         
            +
              - Added `cleanup_message_queues_batched` for batched deletion with EventMachine yielding
         
     | 
| 
      
 52 
     | 
    
         
            +
              - Integrated into `cleanup_orphaned_data` workflow (Phase 3)
         
     | 
| 
      
 53 
     | 
    
         
            +
              - **Impact**: Ensures message queues are cleaned up even if subscription cleanup misses them
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
            ### Added
         
     | 
| 
      
 56 
     | 
    
         
            +
            - Lua script-based TTL management for atomic operations
         
     | 
| 
      
 57 
     | 
    
         
            +
            - Comprehensive TTL behavior tests (3 new tests):
         
     | 
| 
      
 58 
     | 
    
         
            +
              - `sets TTL on first enqueue`
         
     | 
| 
      
 59 
     | 
    
         
            +
              - `does not reset TTL on subsequent enqueues`
         
     | 
| 
      
 60 
     | 
    
         
            +
              - `sets TTL again after queue expires and is recreated`
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
            ### Changed
         
     | 
| 
      
 63 
     | 
    
         
            +
            - `MessageQueue#enqueue`: Uses Lua script to prevent TTL reset
         
     | 
| 
      
 64 
     | 
    
         
            +
            - `SubscriptionManager#subscribe`: Uses Lua script to prevent TTL reset on all subscription keys
         
     | 
| 
      
 65 
     | 
    
         
            +
            - `SubscriptionManager#cleanup_orphaned_data`: Added Phase 3 for message queue cleanup
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
            ### Technical Details
         
     | 
| 
      
 68 
     | 
    
         
            +
            **Lua Script Approach**:
         
     | 
| 
      
 69 
     | 
    
         
            +
            ```lua
         
     | 
| 
      
 70 
     | 
    
         
            +
            redis.call('RPUSH', KEYS[1], ARGV[1])
         
     | 
| 
      
 71 
     | 
    
         
            +
            local ttl = redis.call('TTL', KEYS[1])
         
     | 
| 
      
 72 
     | 
    
         
            +
            if ttl == -1 then  -- Only set if no TTL exists
         
     | 
| 
      
 73 
     | 
    
         
            +
              redis.call('EXPIRE', KEYS[1], tonumber(ARGV[2]))
         
     | 
| 
      
 74 
     | 
    
         
            +
            end
         
     | 
| 
      
 75 
     | 
    
         
            +
            ```
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
            **Test Coverage**: 213 examples, 0 failures, 87.22% line coverage
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
       10 
79 
     | 
    
         
             
            ## [1.0.9] - 2025-10-30
         
     | 
| 
       11 
80 
     | 
    
         | 
| 
       12 
81 
     | 
    
         
             
            ### Fixed - Concurrency Issues (P1 - High Priority)
         
     | 
| 
         @@ -116,9 +185,12 @@ end 
     | 
|
| 
       116 
185 
     | 
    
         
             
            ```
         
     | 
| 
       117 
186 
     | 
    
         | 
| 
       118 
187 
     | 
    
         
             
            ### Test Coverage
         
     | 
| 
       119 
     | 
    
         
            -
            -  
     | 
| 
       120 
     | 
    
         
            -
            - Line Coverage:  
     | 
| 
       121 
     | 
    
         
            -
            - Branch Coverage:  
     | 
| 
      
 188 
     | 
    
         
            +
            - **210 tests passing** (+33 new tests, +18.6%)
         
     | 
| 
      
 189 
     | 
    
         
            +
            - **Line Coverage: 89.69%** (+3.92% from v1.0.8)
         
     | 
| 
      
 190 
     | 
    
         
            +
            - **Branch Coverage: 60.08%** (+5.04% from v1.0.8)
         
     | 
| 
      
 191 
     | 
    
         
            +
            - Added comprehensive tests for all P1/P2 fixes
         
     | 
| 
      
 192 
     | 
    
         
            +
            - Added edge case and error handling tests
         
     | 
| 
      
 193 
     | 
    
         
            +
            - All new features have corresponding test coverage
         
     | 
| 
       122 
194 
     | 
    
         | 
| 
       123 
195 
     | 
    
         
             
            ### Upgrade Notes
         
     | 
| 
       124 
196 
     | 
    
         
             
            This release includes important concurrency and performance fixes. Recommended for all users, especially:
         
     | 
    
        data/README.md
    CHANGED
    
    | 
         @@ -80,6 +80,7 @@ bayeux = Faye::RackAdapter.new(app, { 
     | 
|
| 
       80 
80 
     | 
    
         
             
              # Data expiration
         
     | 
| 
       81 
81 
     | 
    
         
             
              client_timeout: 60,         # Client session timeout (seconds)
         
     | 
| 
       82 
82 
     | 
    
         
             
              message_ttl: 3600,          # Message TTL (seconds)
         
     | 
| 
      
 83 
     | 
    
         
            +
              subscription_ttl: 300,      # Subscription keys TTL (seconds, default: 5 minutes)
         
     | 
| 
       83 
84 
     | 
    
         | 
| 
       84 
85 
     | 
    
         
             
              # Garbage collection
         
     | 
| 
       85 
86 
     | 
    
         
             
              gc_interval: 60,            # Automatic GC interval (seconds), set to 0 or false to disable
         
     | 
| 
         @@ -19,15 +19,19 @@ module Faye 
     | 
|
| 
       19 
19 
     | 
    
         | 
| 
       20 
20 
     | 
    
         
             
                    # Store message directly as JSON
         
     | 
| 
       21 
21 
     | 
    
         
             
                    message_json = message_with_id.to_json
         
     | 
| 
      
 22 
     | 
    
         
            +
                    key = queue_key(client_id)
         
     | 
| 
       22 
23 
     | 
    
         | 
| 
       23 
24 
     | 
    
         
             
                    @connection.with_redis do |redis|
         
     | 
| 
       24 
     | 
    
         
            -
                      # Use RPUSH  
     | 
| 
       25 
     | 
    
         
            -
                       
     | 
| 
       26 
     | 
    
         
            -
             
     | 
| 
       27 
     | 
    
         
            -
                         
     | 
| 
       28 
     | 
    
         
            -
                         
     | 
| 
       29 
     | 
    
         
            -
                         
     | 
| 
       30 
     | 
    
         
            -
             
     | 
| 
      
 25 
     | 
    
         
            +
                      # Use Lua script to atomically RPUSH and set TTL only if key has no TTL
         
     | 
| 
      
 26 
     | 
    
         
            +
                      # This prevents resetting TTL on every enqueue for hot queues
         
     | 
| 
      
 27 
     | 
    
         
            +
                      redis.eval(<<~LUA, keys: [key], argv: [message_json, message_ttl.to_s])
         
     | 
| 
      
 28 
     | 
    
         
            +
                        redis.call('RPUSH', KEYS[1], ARGV[1])
         
     | 
| 
      
 29 
     | 
    
         
            +
                        local ttl = redis.call('TTL', KEYS[1])
         
     | 
| 
      
 30 
     | 
    
         
            +
                        if ttl == -1 then
         
     | 
| 
      
 31 
     | 
    
         
            +
                          redis.call('EXPIRE', KEYS[1], tonumber(ARGV[2]))
         
     | 
| 
      
 32 
     | 
    
         
            +
                        end
         
     | 
| 
      
 33 
     | 
    
         
            +
                        return 1
         
     | 
| 
      
 34 
     | 
    
         
            +
                      LUA
         
     | 
| 
       31 
35 
     | 
    
         
             
                    end
         
     | 
| 
       32 
36 
     | 
    
         | 
| 
       33 
37 
     | 
    
         
             
                    EventMachine.next_tick { callback.call(true) } if callback
         
     | 
| 
         @@ -12,36 +12,50 @@ module Faye 
     | 
|
| 
       12 
12 
     | 
    
         
             
                  # Subscribe a client to a channel
         
     | 
| 
       13 
13 
     | 
    
         
             
                  def subscribe(client_id, channel, &callback)
         
     | 
| 
       14 
14 
     | 
    
         
             
                    timestamp = Time.now.to_i
         
     | 
| 
       15 
     | 
    
         
            -
                    subscription_ttl = @options[:subscription_ttl] ||  
     | 
| 
      
 15 
     | 
    
         
            +
                    subscription_ttl = @options[:subscription_ttl] || 300  # 5 minutes default (5x client_timeout)
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                    client_subs_key = client_subscriptions_key(client_id)
         
     | 
| 
      
 18 
     | 
    
         
            +
                    channel_subs_key = channel_subscribers_key(channel)
         
     | 
| 
      
 19 
     | 
    
         
            +
                    sub_key = subscription_key(client_id, channel)
         
     | 
| 
       16 
20 
     | 
    
         | 
| 
       17 
21 
     | 
    
         
             
                    @connection.with_redis do |redis|
         
     | 
| 
       18 
     | 
    
         
            -
                       
     | 
| 
       19 
     | 
    
         
            -
             
     | 
| 
       20 
     | 
    
         
            -
             
     | 
| 
       21 
     | 
    
         
            -
                         
     | 
| 
       22 
     | 
    
         
            -
                         
     | 
| 
       23 
     | 
    
         
            -
             
     | 
| 
       24 
     | 
    
         
            -
                         
     | 
| 
       25 
     | 
    
         
            -
             
     | 
| 
       26 
     | 
    
         
            -
                         
     | 
| 
       27 
     | 
    
         
            -
             
     | 
| 
       28 
     | 
    
         
            -
             
     | 
| 
       29 
     | 
    
         
            -
                         
     | 
| 
       30 
     | 
    
         
            -
                         
     | 
| 
       31 
     | 
    
         
            -
             
     | 
| 
       32 
     | 
    
         
            -
                          ' 
     | 
| 
       33 
     | 
    
         
            -
                          'channel', channel,
         
     | 
| 
       34 
     | 
    
         
            -
                          'client_id', client_id
         
     | 
| 
       35 
     | 
    
         
            -
                        )
         
     | 
| 
       36 
     | 
    
         
            -
                        # Set TTL for subscription metadata
         
     | 
| 
       37 
     | 
    
         
            -
                        multi.expire(subscription_key(client_id, channel), subscription_ttl)
         
     | 
| 
       38 
     | 
    
         
            -
             
     | 
| 
       39 
     | 
    
         
            -
                        # Handle wildcard patterns
         
     | 
| 
       40 
     | 
    
         
            -
                        if channel.include?('*')
         
     | 
| 
       41 
     | 
    
         
            -
                          multi.sadd?(patterns_key, channel)
         
     | 
| 
       42 
     | 
    
         
            -
                          # Set/refresh TTL for patterns set
         
     | 
| 
       43 
     | 
    
         
            -
                          multi.expire(patterns_key, subscription_ttl)
         
     | 
| 
      
 22 
     | 
    
         
            +
                      # Use Lua script to atomically add subscriptions and set TTL only if keys have no TTL
         
     | 
| 
      
 23 
     | 
    
         
            +
                      # This prevents resetting TTL on re-subscription
         
     | 
| 
      
 24 
     | 
    
         
            +
                      redis.eval(<<-LUA, keys: [client_subs_key, channel_subs_key, sub_key], argv: [channel, client_id, timestamp.to_s, subscription_ttl])
         
     | 
| 
      
 25 
     | 
    
         
            +
                        -- Add channel to client's subscriptions
         
     | 
| 
      
 26 
     | 
    
         
            +
                        redis.call('SADD', KEYS[1], ARGV[1])
         
     | 
| 
      
 27 
     | 
    
         
            +
                        local ttl1 = redis.call('TTL', KEYS[1])
         
     | 
| 
      
 28 
     | 
    
         
            +
                        if ttl1 == -1 then
         
     | 
| 
      
 29 
     | 
    
         
            +
                          redis.call('EXPIRE', KEYS[1], ARGV[4])
         
     | 
| 
      
 30 
     | 
    
         
            +
                        end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                        -- Add client to channel's subscribers
         
     | 
| 
      
 33 
     | 
    
         
            +
                        redis.call('SADD', KEYS[2], ARGV[2])
         
     | 
| 
      
 34 
     | 
    
         
            +
                        local ttl2 = redis.call('TTL', KEYS[2])
         
     | 
| 
      
 35 
     | 
    
         
            +
                        if ttl2 == -1 then
         
     | 
| 
      
 36 
     | 
    
         
            +
                          redis.call('EXPIRE', KEYS[2], ARGV[4])
         
     | 
| 
       44 
37 
     | 
    
         
             
                        end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                        -- Store subscription metadata
         
     | 
| 
      
 40 
     | 
    
         
            +
                        redis.call('HSET', KEYS[3], 'subscribed_at', ARGV[3], 'channel', ARGV[1], 'client_id', ARGV[2])
         
     | 
| 
      
 41 
     | 
    
         
            +
                        local ttl3 = redis.call('TTL', KEYS[3])
         
     | 
| 
      
 42 
     | 
    
         
            +
                        if ttl3 == -1 then
         
     | 
| 
      
 43 
     | 
    
         
            +
                          redis.call('EXPIRE', KEYS[3], ARGV[4])
         
     | 
| 
      
 44 
     | 
    
         
            +
                        end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                        return 1
         
     | 
| 
      
 47 
     | 
    
         
            +
                      LUA
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                      # Handle wildcard patterns separately
         
     | 
| 
      
 50 
     | 
    
         
            +
                      if channel.include?('*')
         
     | 
| 
      
 51 
     | 
    
         
            +
                        redis.eval(<<-LUA, keys: [patterns_key], argv: [channel, subscription_ttl])
         
     | 
| 
      
 52 
     | 
    
         
            +
                          redis.call('SADD', KEYS[1], ARGV[1])
         
     | 
| 
      
 53 
     | 
    
         
            +
                          local ttl = redis.call('TTL', KEYS[1])
         
     | 
| 
      
 54 
     | 
    
         
            +
                          if ttl == -1 then
         
     | 
| 
      
 55 
     | 
    
         
            +
                            redis.call('EXPIRE', KEYS[1], ARGV[2])
         
     | 
| 
      
 56 
     | 
    
         
            +
                          end
         
     | 
| 
      
 57 
     | 
    
         
            +
                          return 1
         
     | 
| 
      
 58 
     | 
    
         
            +
                        LUA
         
     | 
| 
       45 
59 
     | 
    
         
             
                      end
         
     | 
| 
       46 
60 
     | 
    
         
             
                    end
         
     | 
| 
       47 
61 
     | 
    
         | 
| 
         @@ -207,11 +221,14 @@ module Faye 
     | 
|
| 
       207 
221 
     | 
    
         
             
                    scan_orphaned_subscriptions(active_set, namespace) do |orphaned_subscriptions|
         
     | 
| 
       208 
222 
     | 
    
         
             
                      # Phase 2: Clean up orphaned subscriptions in batches
         
     | 
| 
       209 
223 
     | 
    
         
             
                      cleanup_orphaned_subscriptions_batched(orphaned_subscriptions, namespace, batch_size) do
         
     | 
| 
       210 
     | 
    
         
            -
                        # Phase 3: Clean up  
     | 
| 
       211 
     | 
    
         
            -
                         
     | 
| 
       212 
     | 
    
         
            -
                          # Phase 4: Clean up  
     | 
| 
       213 
     | 
    
         
            -
                           
     | 
| 
       214 
     | 
    
         
            -
                             
     | 
| 
      
 224 
     | 
    
         
            +
                        # Phase 3: Clean up orphaned message queues
         
     | 
| 
      
 225 
     | 
    
         
            +
                        cleanup_orphaned_message_queues_async(active_set, namespace, batch_size) do
         
     | 
| 
      
 226 
     | 
    
         
            +
                          # Phase 4: Clean up empty channels (yields between operations)
         
     | 
| 
      
 227 
     | 
    
         
            +
                          cleanup_empty_channels_async(namespace) do
         
     | 
| 
      
 228 
     | 
    
         
            +
                            # Phase 5: Clean up unused patterns
         
     | 
| 
      
 229 
     | 
    
         
            +
                            cleanup_unused_patterns_async do
         
     | 
| 
      
 230 
     | 
    
         
            +
                              callback.call if callback
         
     | 
| 
      
 231 
     | 
    
         
            +
                            end
         
     | 
| 
       215 
232 
     | 
    
         
             
                          end
         
     | 
| 
       216 
233 
     | 
    
         
             
                        end
         
     | 
| 
       217 
234 
     | 
    
         
             
                      end
         
     | 
| 
         @@ -305,6 +322,80 @@ module Faye 
     | 
|
| 
       305 
322 
     | 
    
         
             
                    EventMachine.next_tick { callback.call }
         
     | 
| 
       306 
323 
     | 
    
         
             
                  end
         
     | 
| 
       307 
324 
     | 
    
         | 
| 
      
 325 
     | 
    
         
            +
                  # Clean up orphaned message queues for non-existent clients
         
     | 
| 
      
 326 
     | 
    
         
            +
                  # Scans for message queues that belong to clients not in the active set
         
     | 
| 
      
 327 
     | 
    
         
            +
                  def cleanup_orphaned_message_queues_async(active_set, namespace, batch_size, &callback)
         
     | 
| 
      
 328 
     | 
    
         
            +
                    orphaned_queues = []
         
     | 
| 
      
 329 
     | 
    
         
            +
             
     | 
| 
      
 330 
     | 
    
         
            +
                    # Batch scan to avoid holding connection
         
     | 
| 
      
 331 
     | 
    
         
            +
                    scan_batch = lambda do |cursor_value|
         
     | 
| 
      
 332 
     | 
    
         
            +
                      begin
         
     | 
| 
      
 333 
     | 
    
         
            +
                        @connection.with_redis do |redis|
         
     | 
| 
      
 334 
     | 
    
         
            +
                          cursor, keys = redis.scan(cursor_value, match: "#{namespace}:messages:*", count: 100)
         
     | 
| 
      
 335 
     | 
    
         
            +
             
     | 
| 
      
 336 
     | 
    
         
            +
                          keys.each do |key|
         
     | 
| 
      
 337 
     | 
    
         
            +
                            client_id = key.split(':').last
         
     | 
| 
      
 338 
     | 
    
         
            +
                            orphaned_queues << key unless active_set.include?(client_id)
         
     | 
| 
      
 339 
     | 
    
         
            +
                          end
         
     | 
| 
      
 340 
     | 
    
         
            +
             
     | 
| 
      
 341 
     | 
    
         
            +
                          if cursor == "0"
         
     | 
| 
      
 342 
     | 
    
         
            +
                            # Scan complete, now clean up in batches
         
     | 
| 
      
 343 
     | 
    
         
            +
                            if orphaned_queues.any?
         
     | 
| 
      
 344 
     | 
    
         
            +
                              cleanup_message_queues_batched(orphaned_queues, batch_size) do
         
     | 
| 
      
 345 
     | 
    
         
            +
                                EventMachine.next_tick { callback.call }
         
     | 
| 
      
 346 
     | 
    
         
            +
                              end
         
     | 
| 
      
 347 
     | 
    
         
            +
                            else
         
     | 
| 
      
 348 
     | 
    
         
            +
                              EventMachine.next_tick { callback.call }
         
     | 
| 
      
 349 
     | 
    
         
            +
                            end
         
     | 
| 
      
 350 
     | 
    
         
            +
                          else
         
     | 
| 
      
 351 
     | 
    
         
            +
                            # Continue scanning
         
     | 
| 
      
 352 
     | 
    
         
            +
                            EventMachine.next_tick { scan_batch.call(cursor) }
         
     | 
| 
      
 353 
     | 
    
         
            +
                          end
         
     | 
| 
      
 354 
     | 
    
         
            +
                        end
         
     | 
| 
      
 355 
     | 
    
         
            +
                      rescue => e
         
     | 
| 
      
 356 
     | 
    
         
            +
                        log_error("Failed to scan orphaned message queues: #{e.message}")
         
     | 
| 
      
 357 
     | 
    
         
            +
                        EventMachine.next_tick { callback.call }
         
     | 
| 
      
 358 
     | 
    
         
            +
                      end
         
     | 
| 
      
 359 
     | 
    
         
            +
                    end
         
     | 
| 
      
 360 
     | 
    
         
            +
             
     | 
| 
      
 361 
     | 
    
         
            +
                    scan_batch.call("0")
         
     | 
| 
      
 362 
     | 
    
         
            +
                  rescue => e
         
     | 
| 
      
 363 
     | 
    
         
            +
                    log_error("Failed to cleanup orphaned message queues: #{e.message}")
         
     | 
| 
      
 364 
     | 
    
         
            +
                    EventMachine.next_tick { callback.call }
         
     | 
| 
      
 365 
     | 
    
         
            +
                  end
         
     | 
| 
      
 366 
     | 
    
         
            +
             
     | 
| 
      
 367 
     | 
    
         
            +
                  # Delete message queues in batches
         
     | 
| 
      
 368 
     | 
    
         
            +
                  def cleanup_message_queues_batched(queue_keys, batch_size, &callback)
         
     | 
| 
      
 369 
     | 
    
         
            +
                    return EventMachine.next_tick { callback.call } if queue_keys.empty?
         
     | 
| 
      
 370 
     | 
    
         
            +
             
     | 
| 
      
 371 
     | 
    
         
            +
                    total = queue_keys.size
         
     | 
| 
      
 372 
     | 
    
         
            +
                    batches = queue_keys.each_slice(batch_size).to_a
         
     | 
| 
      
 373 
     | 
    
         
            +
             
     | 
| 
      
 374 
     | 
    
         
            +
                    process_batch = lambda do |batch_index|
         
     | 
| 
      
 375 
     | 
    
         
            +
                      if batch_index >= batches.size
         
     | 
| 
      
 376 
     | 
    
         
            +
                        puts "[Faye::Redis::SubscriptionManager] INFO: Cleaned up #{total} orphaned message queues" if @options[:log_level] != :silent
         
     | 
| 
      
 377 
     | 
    
         
            +
                        EventMachine.next_tick { callback.call }
         
     | 
| 
      
 378 
     | 
    
         
            +
                        return
         
     | 
| 
      
 379 
     | 
    
         
            +
                      end
         
     | 
| 
      
 380 
     | 
    
         
            +
             
     | 
| 
      
 381 
     | 
    
         
            +
                      batch = batches[batch_index]
         
     | 
| 
      
 382 
     | 
    
         
            +
             
     | 
| 
      
 383 
     | 
    
         
            +
                      @connection.with_redis do |redis|
         
     | 
| 
      
 384 
     | 
    
         
            +
                        redis.pipelined do |pipeline|
         
     | 
| 
      
 385 
     | 
    
         
            +
                          batch.each { |key| pipeline.del(key) }
         
     | 
| 
      
 386 
     | 
    
         
            +
                        end
         
     | 
| 
      
 387 
     | 
    
         
            +
                      end
         
     | 
| 
      
 388 
     | 
    
         
            +
             
     | 
| 
      
 389 
     | 
    
         
            +
                      # Yield control between batches
         
     | 
| 
      
 390 
     | 
    
         
            +
                      EventMachine.next_tick { process_batch.call(batch_index + 1) }
         
     | 
| 
      
 391 
     | 
    
         
            +
                    end
         
     | 
| 
      
 392 
     | 
    
         
            +
             
     | 
| 
      
 393 
     | 
    
         
            +
                    process_batch.call(0)
         
     | 
| 
      
 394 
     | 
    
         
            +
                  rescue => e
         
     | 
| 
      
 395 
     | 
    
         
            +
                    log_error("Failed to cleanup message queues batch: #{e.message}")
         
     | 
| 
      
 396 
     | 
    
         
            +
                    EventMachine.next_tick { callback.call }
         
     | 
| 
      
 397 
     | 
    
         
            +
                  end
         
     | 
| 
      
 398 
     | 
    
         
            +
             
     | 
| 
       308 
399 
     | 
    
         
             
                  # Async version of cleanup_empty_channels that yields between operations
         
     | 
| 
       309 
400 
     | 
    
         
             
                  def cleanup_empty_channels_async(namespace, &callback)
         
     | 
| 
       310 
401 
     | 
    
         
             
                    @connection.with_redis do |redis|
         
     | 
    
        data/lib/faye/redis/version.rb
    CHANGED
    
    
    
        data/lib/faye/redis.rb
    CHANGED
    
    | 
         @@ -25,7 +25,7 @@ module Faye 
     | 
|
| 
       25 
25 
     | 
    
         
             
                  retry_delay: 1,
         
     | 
| 
       26 
26 
     | 
    
         
             
                  client_timeout: 60,
         
     | 
| 
       27 
27 
     | 
    
         
             
                  message_ttl: 3600,
         
     | 
| 
       28 
     | 
    
         
            -
                  subscription_ttl:  
     | 
| 
      
 28 
     | 
    
         
            +
                  subscription_ttl: 300,  # Subscription keys TTL (5 minutes = 5x client_timeout), provides safety net if GC fails
         
     | 
| 
       29 
29 
     | 
    
         
             
                  namespace: 'faye',
         
     | 
| 
       30 
30 
     | 
    
         
             
                  gc_interval: 60,  # Automatic garbage collection interval (seconds), set to 0 or false to disable
         
     | 
| 
       31 
31 
     | 
    
         
             
                  cleanup_batch_size: 50  # Number of items per batch during cleanup (min: 1, max: 1000, prevents blocking)
         
     | 
    
        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.11
         
     | 
| 
       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-31 00:00:00.000000000 Z
         
     | 
| 
       12 
12 
     | 
    
         
             
            dependencies:
         
     | 
| 
       13 
13 
     | 
    
         
             
            - !ruby/object:Gem::Dependency
         
     | 
| 
       14 
14 
     | 
    
         
             
              name: redis
         
     |