faye-redis-ng 1.0.8 → 1.0.9
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 +122 -0
 - data/lib/faye/redis/pubsub_coordinator.rb +1 -0
 - data/lib/faye/redis/subscription_manager.rb +58 -21
 - data/lib/faye/redis/version.rb +1 -1
 - data/lib/faye/redis.rb +1 -1
 - metadata +1 -1
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA256:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: cd86d6fdb530405ff0dd146705d45d9c665cfa9f778987ead89d4bb497db06eb
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: ddc3a783e0007e452d69eb97e6f2540a089427420119a4414e91281e1b4a9d9d
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: c2a3f9d350a9aeab02bcfc6589a6f3e81e2973396d54397510d46787e1d7cde81de064a97f44428bf3534f266ccda104ae1535fd8108197e0862aeae1c28b71e
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: 4f41d51dae3a17b7d44171384f6a2646ac7492ae68c4f85845feac100f20d59f609105ebfc90d5fdffd415e893cb3e7556bb4ade191da84d867e16a90e8bf664
         
     | 
    
        data/CHANGELOG.md
    CHANGED
    
    | 
         @@ -7,6 +7,128 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 
     | 
|
| 
       7 
7 
     | 
    
         | 
| 
       8 
8 
     | 
    
         
             
            ## [Unreleased]
         
     | 
| 
       9 
9 
     | 
    
         | 
| 
      
 10 
     | 
    
         
            +
            ## [1.0.9] - 2025-10-30
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            ### Fixed - Concurrency Issues (P1 - High Priority)
         
     | 
| 
      
 13 
     | 
    
         
            +
            - **`unsubscribe_all` Race Condition**: Fixed callback being called multiple times
         
     | 
| 
      
 14 
     | 
    
         
            +
              - Added `callback_called` flag to prevent duplicate callback invocations
         
     | 
| 
      
 15 
     | 
    
         
            +
              - Multiple async unsubscribe operations could trigger callback simultaneously
         
     | 
| 
      
 16 
     | 
    
         
            +
              - **Impact**: Eliminates duplicate cleanup operations in high-concurrency scenarios
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
            - **Reconnect Counter Not Reset**: Fixed `@reconnect_attempts` not resetting on disconnect
         
     | 
| 
      
 19 
     | 
    
         
            +
              - Added counter reset in `PubSubCoordinator#disconnect` method
         
     | 
| 
      
 20 
     | 
    
         
            +
              - Prevents incorrect exponential backoff after disconnect/reconnect cycles
         
     | 
| 
      
 21 
     | 
    
         
            +
              - **Impact**: Ensures proper reconnection behavior after manual disconnects
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
            - **SCAN Connection Pool Blocking**: Optimized long-running SCAN operations
         
     | 
| 
      
 24 
     | 
    
         
            +
              - Changed `scan_orphaned_subscriptions` to batch scanning with connection release
         
     | 
| 
      
 25 
     | 
    
         
            +
              - Each SCAN iteration now releases connection via `EventMachine.next_tick`
         
     | 
| 
      
 26 
     | 
    
         
            +
              - Prevents holding Redis connection for 10-30 seconds with large datasets
         
     | 
| 
      
 27 
     | 
    
         
            +
              - **Impact**: Eliminates connection pool exhaustion during cleanup of 100K+ keys
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
            ### Fixed - Performance Issues (P2 - Medium Priority)
         
     | 
| 
      
 30 
     | 
    
         
            +
            - **Pattern Regex Compilation Overhead**: Added regex pattern caching
         
     | 
| 
      
 31 
     | 
    
         
            +
              - Implemented `@pattern_cache` to memoize compiled regular expressions
         
     | 
| 
      
 32 
     | 
    
         
            +
              - Cache is automatically cleared when patterns are removed
         
     | 
| 
      
 33 
     | 
    
         
            +
              - Prevents recompiling same regex for every pattern match
         
     | 
| 
      
 34 
     | 
    
         
            +
              - **Impact**: 20% CPU reduction with 100 patterns at 1000 msg/sec (100K → 0 regex compilations/sec)
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
            - **Pattern Regex Injection Risk**: Fixed special character handling in patterns
         
     | 
| 
      
 37 
     | 
    
         
            +
              - Added `Regexp.escape` before wildcard replacement
         
     | 
| 
      
 38 
     | 
    
         
            +
              - Properly handles special regex characters (`.`, `[`, `(`, etc.) in channel names
         
     | 
| 
      
 39 
     | 
    
         
            +
              - Added `RegexpError` handling for invalid patterns
         
     | 
| 
      
 40 
     | 
    
         
            +
              - **Impact**: Prevents incorrect pattern matching and potential regex errors
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
            - **Missing Batch Size Validation**: Added bounds checking for `cleanup_batch_size`
         
     | 
| 
      
 43 
     | 
    
         
            +
              - Validates and clamps batch_size to safe range (1-1000)
         
     | 
| 
      
 44 
     | 
    
         
            +
              - Prevents crashes from invalid values (0, negative, nil)
         
     | 
| 
      
 45 
     | 
    
         
            +
              - Prevents performance degradation from extreme values
         
     | 
| 
      
 46 
     | 
    
         
            +
              - **Impact**: Robust configuration handling prevents misconfigurations
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
            ### Changed
         
     | 
| 
      
 49 
     | 
    
         
            +
            - `SubscriptionManager#initialize`: Added `@pattern_cache = {}` for regex memoization
         
     | 
| 
      
 50 
     | 
    
         
            +
            - `SubscriptionManager#channel_matches_pattern?`: Uses cached regexes with proper escaping
         
     | 
| 
      
 51 
     | 
    
         
            +
            - `SubscriptionManager#cleanup_pattern_if_unused`: Clears pattern from cache when removed
         
     | 
| 
      
 52 
     | 
    
         
            +
            - `SubscriptionManager#cleanup_unused_patterns`: Batch cache clearing
         
     | 
| 
      
 53 
     | 
    
         
            +
            - `SubscriptionManager#cleanup_unused_patterns_async`: Batch cache clearing
         
     | 
| 
      
 54 
     | 
    
         
            +
            - `SubscriptionManager#scan_orphaned_subscriptions`: Batched scanning with connection release
         
     | 
| 
      
 55 
     | 
    
         
            +
            - `SubscriptionManager#cleanup_orphaned_data`: Validates `cleanup_batch_size` parameter
         
     | 
| 
      
 56 
     | 
    
         
            +
            - `PubSubCoordinator#disconnect`: Resets `@reconnect_attempts` to 0
         
     | 
| 
      
 57 
     | 
    
         
            +
            - `DEFAULT_OPTIONS`: Updated `cleanup_batch_size` comment with range (min: 1, max: 1000)
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
            ### Technical Details
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
            **Race Condition Fix**:
         
     | 
| 
      
 62 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 63 
     | 
    
         
            +
            # Before: callback could be called multiple times
         
     | 
| 
      
 64 
     | 
    
         
            +
            remaining -= 1
         
     | 
| 
      
 65 
     | 
    
         
            +
            callback.call(true) if callback && remaining == 0
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
            # After: flag prevents duplicate calls
         
     | 
| 
      
 68 
     | 
    
         
            +
            if remaining == 0 && !callback_called && callback
         
     | 
| 
      
 69 
     | 
    
         
            +
              callback_called = true
         
     | 
| 
      
 70 
     | 
    
         
            +
              callback.call(true)
         
     | 
| 
      
 71 
     | 
    
         
            +
            end
         
     | 
| 
      
 72 
     | 
    
         
            +
            ```
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
            **SCAN Optimization**:
         
     | 
| 
      
 75 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 76 
     | 
    
         
            +
            # Before: Single with_redis block holding connection for entire loop
         
     | 
| 
      
 77 
     | 
    
         
            +
            @connection.with_redis do |redis|
         
     | 
| 
      
 78 
     | 
    
         
            +
              loop do
         
     | 
| 
      
 79 
     | 
    
         
            +
                cursor, keys = redis.scan(cursor, ...)
         
     | 
| 
      
 80 
     | 
    
         
            +
                # ... process keys ...
         
     | 
| 
      
 81 
     | 
    
         
            +
              end
         
     | 
| 
      
 82 
     | 
    
         
            +
            end
         
     | 
| 
      
 83 
     | 
    
         
            +
             
     | 
| 
      
 84 
     | 
    
         
            +
            # After: Release connection between iterations
         
     | 
| 
      
 85 
     | 
    
         
            +
            scan_batch = lambda do |cursor_value|
         
     | 
| 
      
 86 
     | 
    
         
            +
              @connection.with_redis do |redis|
         
     | 
| 
      
 87 
     | 
    
         
            +
                cursor, keys = redis.scan(cursor_value, ...)
         
     | 
| 
      
 88 
     | 
    
         
            +
                # ... process keys ...
         
     | 
| 
      
 89 
     | 
    
         
            +
                if cursor == "0"
         
     | 
| 
      
 90 
     | 
    
         
            +
                  # Done
         
     | 
| 
      
 91 
     | 
    
         
            +
                else
         
     | 
| 
      
 92 
     | 
    
         
            +
                  EventMachine.next_tick { scan_batch.call(cursor) }  # Release & continue
         
     | 
| 
      
 93 
     | 
    
         
            +
                end
         
     | 
| 
      
 94 
     | 
    
         
            +
              end
         
     | 
| 
      
 95 
     | 
    
         
            +
            end
         
     | 
| 
      
 96 
     | 
    
         
            +
            ```
         
     | 
| 
      
 97 
     | 
    
         
            +
             
     | 
| 
      
 98 
     | 
    
         
            +
            **Pattern Caching**:
         
     | 
| 
      
 99 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 100 
     | 
    
         
            +
            # Before: Compile regex every time (100K times/sec at high load)
         
     | 
| 
      
 101 
     | 
    
         
            +
            def channel_matches_pattern?(channel, pattern)
         
     | 
| 
      
 102 
     | 
    
         
            +
              regex_pattern = pattern.gsub('**', '.*').gsub('*', '[^/]+')
         
     | 
| 
      
 103 
     | 
    
         
            +
              regex = Regexp.new("^#{regex_pattern}$")
         
     | 
| 
      
 104 
     | 
    
         
            +
              !!(channel =~ regex)
         
     | 
| 
      
 105 
     | 
    
         
            +
            end
         
     | 
| 
      
 106 
     | 
    
         
            +
             
     | 
| 
      
 107 
     | 
    
         
            +
            # After: Memoized compilation (1 time per pattern)
         
     | 
| 
      
 108 
     | 
    
         
            +
            def channel_matches_pattern?(channel, pattern)
         
     | 
| 
      
 109 
     | 
    
         
            +
              regex = @pattern_cache[pattern] ||= begin
         
     | 
| 
      
 110 
     | 
    
         
            +
                escaped = Regexp.escape(pattern)
         
     | 
| 
      
 111 
     | 
    
         
            +
                regex_pattern = escaped.gsub(Regexp.escape('**'), '.*').gsub(Regexp.escape('*'), '[^/]+')
         
     | 
| 
      
 112 
     | 
    
         
            +
                Regexp.new("^#{regex_pattern}$")
         
     | 
| 
      
 113 
     | 
    
         
            +
              end
         
     | 
| 
      
 114 
     | 
    
         
            +
              !!(channel =~ regex)
         
     | 
| 
      
 115 
     | 
    
         
            +
            end
         
     | 
| 
      
 116 
     | 
    
         
            +
            ```
         
     | 
| 
      
 117 
     | 
    
         
            +
             
     | 
| 
      
 118 
     | 
    
         
            +
            ### Test Coverage
         
     | 
| 
      
 119 
     | 
    
         
            +
            - All 177 tests passing
         
     | 
| 
      
 120 
     | 
    
         
            +
            - Line Coverage: 85.77%
         
     | 
| 
      
 121 
     | 
    
         
            +
            - Branch Coverage: 55.04%
         
     | 
| 
      
 122 
     | 
    
         
            +
             
     | 
| 
      
 123 
     | 
    
         
            +
            ### Upgrade Notes
         
     | 
| 
      
 124 
     | 
    
         
            +
            This release includes important concurrency and performance fixes. Recommended for all users, especially:
         
     | 
| 
      
 125 
     | 
    
         
            +
            - High-scale deployments (>50K clients)
         
     | 
| 
      
 126 
     | 
    
         
            +
            - High-traffic scenarios (>1K msg/sec)
         
     | 
| 
      
 127 
     | 
    
         
            +
            - Systems with frequent disconnect/reconnect patterns
         
     | 
| 
      
 128 
     | 
    
         
            +
            - Deployments using wildcard subscriptions
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
            No breaking changes. Drop-in replacement for v1.0.8.
         
     | 
| 
      
 131 
     | 
    
         
            +
             
     | 
| 
       10 
132 
     | 
    
         
             
            ## [1.0.8] - 2025-10-30
         
     | 
| 
       11 
133 
     | 
    
         | 
| 
       12 
134 
     | 
    
         
             
            ### Fixed - Memory Leaks (P0 - High Risk)
         
     | 
| 
         @@ -6,6 +6,7 @@ module Faye 
     | 
|
| 
       6 
6 
     | 
    
         
             
                  def initialize(connection, options = {})
         
     | 
| 
       7 
7 
     | 
    
         
             
                    @connection = connection
         
     | 
| 
       8 
8 
     | 
    
         
             
                    @options = options
         
     | 
| 
      
 9 
     | 
    
         
            +
                    @pattern_cache = {}  # Cache compiled regexes for pattern matching performance
         
     | 
| 
       9 
10 
     | 
    
         
             
                  end
         
     | 
| 
       10 
11 
     | 
    
         | 
| 
       11 
12 
     | 
    
         
             
                  # Subscribe a client to a channel
         
     | 
| 
         @@ -85,10 +86,15 @@ module Faye 
     | 
|
| 
       85 
86 
     | 
    
         
             
                      else
         
     | 
| 
       86 
87 
     | 
    
         
             
                        # Unsubscribe from each channel
         
     | 
| 
       87 
88 
     | 
    
         
             
                        remaining = channels.size
         
     | 
| 
      
 89 
     | 
    
         
            +
                        callback_called = false  # Prevent race condition
         
     | 
| 
       88 
90 
     | 
    
         
             
                        channels.each do |channel|
         
     | 
| 
       89 
91 
     | 
    
         
             
                          unsubscribe(client_id, channel) do
         
     | 
| 
       90 
92 
     | 
    
         
             
                            remaining -= 1
         
     | 
| 
       91 
     | 
    
         
            -
                             
     | 
| 
      
 93 
     | 
    
         
            +
                            # Check flag to prevent multiple callback invocations
         
     | 
| 
      
 94 
     | 
    
         
            +
                            if remaining == 0 && !callback_called && callback
         
     | 
| 
      
 95 
     | 
    
         
            +
                              callback_called = true
         
     | 
| 
      
 96 
     | 
    
         
            +
                              callback.call(true)
         
     | 
| 
      
 97 
     | 
    
         
            +
                            end
         
     | 
| 
       92 
98 
     | 
    
         
             
                          end
         
     | 
| 
       93 
99 
     | 
    
         
             
                        end
         
     | 
| 
       94 
100 
     | 
    
         
             
                      end
         
     | 
| 
         @@ -159,16 +165,26 @@ module Faye 
     | 
|
| 
       159 
165 
     | 
    
         
             
                  end
         
     | 
| 
       160 
166 
     | 
    
         | 
| 
       161 
167 
     | 
    
         
             
                  # Check if a channel matches a pattern
         
     | 
| 
      
 168 
     | 
    
         
            +
                  # Uses memoization to cache compiled regexes for performance
         
     | 
| 
       162 
169 
     | 
    
         
             
                  def channel_matches_pattern?(channel, pattern)
         
     | 
| 
       163 
     | 
    
         
            -
                    #  
     | 
| 
       164 
     | 
    
         
            -
                     
     | 
| 
       165 
     | 
    
         
            -
             
     | 
| 
       166 
     | 
    
         
            -
                       
     | 
| 
       167 
     | 
    
         
            -
                       
     | 
| 
       168 
     | 
    
         
            -
                      . 
     | 
| 
       169 
     | 
    
         
            -
             
     | 
| 
       170 
     | 
    
         
            -
             
     | 
| 
      
 170 
     | 
    
         
            +
                    # Get or compile regex for this pattern
         
     | 
| 
      
 171 
     | 
    
         
            +
                    regex = @pattern_cache[pattern] ||= begin
         
     | 
| 
      
 172 
     | 
    
         
            +
                      # Escape the pattern first to handle special regex characters
         
     | 
| 
      
 173 
     | 
    
         
            +
                      # Then replace escaped wildcards with regex patterns
         
     | 
| 
      
 174 
     | 
    
         
            +
                      # ** matches multiple segments (including /), * matches one segment (no /)
         
     | 
| 
      
 175 
     | 
    
         
            +
                      escaped = Regexp.escape(pattern)
         
     | 
| 
      
 176 
     | 
    
         
            +
             
     | 
| 
      
 177 
     | 
    
         
            +
                      regex_pattern = escaped
         
     | 
| 
      
 178 
     | 
    
         
            +
                        .gsub(Regexp.escape('**'), '.*')        # ** → .* (match anything)
         
     | 
| 
      
 179 
     | 
    
         
            +
                        .gsub(Regexp.escape('*'), '[^/]+')      # * → [^/]+ (match one segment)
         
     | 
| 
      
 180 
     | 
    
         
            +
             
     | 
| 
      
 181 
     | 
    
         
            +
                      Regexp.new("^#{regex_pattern}$")
         
     | 
| 
      
 182 
     | 
    
         
            +
                    end
         
     | 
| 
      
 183 
     | 
    
         
            +
             
     | 
| 
       171 
184 
     | 
    
         
             
                    !!(channel =~ regex)
         
     | 
| 
      
 185 
     | 
    
         
            +
                  rescue RegexpError => e
         
     | 
| 
      
 186 
     | 
    
         
            +
                    log_error("Invalid pattern #{pattern}: #{e.message}")
         
     | 
| 
      
 187 
     | 
    
         
            +
                    false
         
     | 
| 
       172 
188 
     | 
    
         
             
                  end
         
     | 
| 
       173 
189 
     | 
    
         | 
| 
       174 
190 
     | 
    
         
             
                  # Clean up subscriptions for a client
         
     | 
| 
         @@ -184,6 +200,9 @@ module Faye 
     | 
|
| 
       184 
200 
     | 
    
         
             
                    namespace = @options[:namespace] || 'faye'
         
     | 
| 
       185 
201 
     | 
    
         
             
                    batch_size = @options[:cleanup_batch_size] || 50
         
     | 
| 
       186 
202 
     | 
    
         | 
| 
      
 203 
     | 
    
         
            +
                    # Validate and clamp batch_size to safe range (1-1000)
         
     | 
| 
      
 204 
     | 
    
         
            +
                    batch_size = [[batch_size.to_i, 1].max, 1000].min
         
     | 
| 
      
 205 
     | 
    
         
            +
             
     | 
| 
       187 
206 
     | 
    
         
             
                    # Phase 1: Scan for orphaned subscriptions
         
     | 
| 
       188 
207 
     | 
    
         
             
                    scan_orphaned_subscriptions(active_set, namespace) do |orphaned_subscriptions|
         
     | 
| 
       189 
208 
     | 
    
         
             
                      # Phase 2: Clean up orphaned subscriptions in batches
         
     | 
| 
         @@ -205,24 +224,36 @@ module Faye 
     | 
|
| 
       205 
224 
     | 
    
         
             
                  private
         
     | 
| 
       206 
225 
     | 
    
         | 
| 
       207 
226 
     | 
    
         
             
                  # Scan for orphaned subscription keys
         
     | 
| 
      
 227 
     | 
    
         
            +
                  # Uses batched scanning to avoid holding connection for long periods
         
     | 
| 
       208 
228 
     | 
    
         
             
                  def scan_orphaned_subscriptions(active_set, namespace, &callback)
         
     | 
| 
       209 
     | 
    
         
            -
                     
     | 
| 
       210 
     | 
    
         
            -
                      cursor = "0"
         
     | 
| 
       211 
     | 
    
         
            -
                      orphaned_subscriptions = []
         
     | 
| 
      
 229 
     | 
    
         
            +
                    orphaned_subscriptions = []
         
     | 
| 
       212 
230 
     | 
    
         | 
| 
       213 
     | 
    
         
            -
             
     | 
| 
       214 
     | 
    
         
            -
             
     | 
| 
      
 231 
     | 
    
         
            +
                    # Batch scan to release connection between iterations
         
     | 
| 
      
 232 
     | 
    
         
            +
                    scan_batch = lambda do |cursor_value|
         
     | 
| 
      
 233 
     | 
    
         
            +
                      begin
         
     | 
| 
      
 234 
     | 
    
         
            +
                        @connection.with_redis do |redis|
         
     | 
| 
      
 235 
     | 
    
         
            +
                          cursor, keys = redis.scan(cursor_value, match: "#{namespace}:subscriptions:*", count: 100)
         
     | 
| 
       215 
236 
     | 
    
         | 
| 
       216 
     | 
    
         
            -
             
     | 
| 
       217 
     | 
    
         
            -
             
     | 
| 
       218 
     | 
    
         
            -
             
     | 
| 
       219 
     | 
    
         
            -
             
     | 
| 
      
 237 
     | 
    
         
            +
                          keys.each do |key|
         
     | 
| 
      
 238 
     | 
    
         
            +
                            client_id = key.split(':').last
         
     | 
| 
      
 239 
     | 
    
         
            +
                            orphaned_subscriptions << client_id unless active_set.include?(client_id)
         
     | 
| 
      
 240 
     | 
    
         
            +
                          end
         
     | 
| 
       220 
241 
     | 
    
         | 
| 
       221 
     | 
    
         
            -
             
     | 
| 
      
 242 
     | 
    
         
            +
                          if cursor == "0"
         
     | 
| 
      
 243 
     | 
    
         
            +
                            # Scan complete
         
     | 
| 
      
 244 
     | 
    
         
            +
                            EventMachine.next_tick { callback.call(orphaned_subscriptions) }
         
     | 
| 
      
 245 
     | 
    
         
            +
                          else
         
     | 
| 
      
 246 
     | 
    
         
            +
                            # Continue scanning in next tick to release connection
         
     | 
| 
      
 247 
     | 
    
         
            +
                            EventMachine.next_tick { scan_batch.call(cursor) }
         
     | 
| 
      
 248 
     | 
    
         
            +
                          end
         
     | 
| 
      
 249 
     | 
    
         
            +
                        end
         
     | 
| 
      
 250 
     | 
    
         
            +
                      rescue => e
         
     | 
| 
      
 251 
     | 
    
         
            +
                        log_error("Failed to scan orphaned subscriptions batch: #{e.message}")
         
     | 
| 
      
 252 
     | 
    
         
            +
                        EventMachine.next_tick { callback.call(orphaned_subscriptions) }
         
     | 
| 
       222 
253 
     | 
    
         
             
                      end
         
     | 
| 
       223 
     | 
    
         
            -
             
     | 
| 
       224 
     | 
    
         
            -
                      EventMachine.next_tick { callback.call(orphaned_subscriptions) }
         
     | 
| 
       225 
254 
     | 
    
         
             
                    end
         
     | 
| 
      
 255 
     | 
    
         
            +
             
     | 
| 
      
 256 
     | 
    
         
            +
                    scan_batch.call("0")
         
     | 
| 
       226 
257 
     | 
    
         
             
                  rescue => e
         
     | 
| 
       227 
258 
     | 
    
         
             
                    log_error("Failed to scan orphaned subscriptions: #{e.message}")
         
     | 
| 
       228 
259 
     | 
    
         
             
                    EventMachine.next_tick { callback.call([]) }
         
     | 
| 
         @@ -323,6 +354,8 @@ module Faye 
     | 
|
| 
       323 
354 
     | 
    
         
             
                            pipeline.del(channel_subscribers_key(pattern))
         
     | 
| 
       324 
355 
     | 
    
         
             
                          end
         
     | 
| 
       325 
356 
     | 
    
         
             
                        end
         
     | 
| 
      
 357 
     | 
    
         
            +
                        # Clear unused patterns from regex cache
         
     | 
| 
      
 358 
     | 
    
         
            +
                        unused_patterns.each { |pattern| @pattern_cache.delete(pattern) }
         
     | 
| 
       326 
359 
     | 
    
         
             
                        puts "[Faye::Redis::SubscriptionManager] INFO: Cleaned up #{unused_patterns.size} unused patterns" if @options[:log_level] != :silent
         
     | 
| 
       327 
360 
     | 
    
         
             
                      end
         
     | 
| 
       328 
361 
     | 
    
         | 
| 
         @@ -376,6 +409,8 @@ module Faye 
     | 
|
| 
       376 
409 
     | 
    
         
             
                          pipeline.del(channel_subscribers_key(pattern))
         
     | 
| 
       377 
410 
     | 
    
         
             
                        end
         
     | 
| 
       378 
411 
     | 
    
         
             
                      end
         
     | 
| 
      
 412 
     | 
    
         
            +
                      # Clear unused patterns from regex cache
         
     | 
| 
      
 413 
     | 
    
         
            +
                      unused_patterns.each { |pattern| @pattern_cache.delete(pattern) }
         
     | 
| 
       379 
414 
     | 
    
         
             
                      puts "[Faye::Redis::SubscriptionManager] INFO: Cleaned up #{unused_patterns.size} unused patterns" if @options[:log_level] != :silent
         
     | 
| 
       380 
415 
     | 
    
         
             
                    end
         
     | 
| 
       381 
416 
     | 
    
         
             
                  rescue => e
         
     | 
| 
         @@ -391,6 +426,8 @@ module Faye 
     | 
|
| 
       391 
426 
     | 
    
         
             
                      @connection.with_redis do |redis|
         
     | 
| 
       392 
427 
     | 
    
         
             
                        redis.srem(patterns_key, pattern)
         
     | 
| 
       393 
428 
     | 
    
         
             
                      end
         
     | 
| 
      
 429 
     | 
    
         
            +
                      # Clear pattern from regex cache when it's removed
         
     | 
| 
      
 430 
     | 
    
         
            +
                      @pattern_cache.delete(pattern)
         
     | 
| 
       394 
431 
     | 
    
         
             
                    end
         
     | 
| 
       395 
432 
     | 
    
         
             
                  rescue => e
         
     | 
| 
       396 
433 
     | 
    
         
             
                    log_error("Failed to cleanup pattern #{pattern}: #{e.message}")
         
     | 
    
        data/lib/faye/redis/version.rb
    CHANGED
    
    
    
        data/lib/faye/redis.rb
    CHANGED
    
    | 
         @@ -28,7 +28,7 @@ module Faye 
     | 
|
| 
       28 
28 
     | 
    
         
             
                  subscription_ttl: 86400,  # Subscription keys TTL (24 hours), 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 
     | 
    
         
            -
                  cleanup_batch_size: 50  # Number of items  
     | 
| 
      
 31 
     | 
    
         
            +
                  cleanup_batch_size: 50  # Number of items per batch during cleanup (min: 1, max: 1000, prevents blocking)
         
     | 
| 
       32 
32 
     | 
    
         
             
                }.freeze
         
     | 
| 
       33 
33 
     | 
    
         | 
| 
       34 
34 
     | 
    
         
             
                attr_reader :server, :options, :connection, :client_registry,
         
     |