faye-redis-ng 1.0.6 → 1.0.7
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 +50 -1
 - data/lib/faye/redis/pubsub_coordinator.rb +14 -4
 - data/lib/faye/redis/version.rb +1 -1
 - data/lib/faye/redis.rb +81 -15
 - 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: 4a2f33a6f83547306e5a0e52a70e2e8e06a236703c5a56bffc7cf85a829d0a54
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: 0e62be0064f4307be4a87d94ddce9e424d7ee4dbad9a85fdf41c03c7ed5db854
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: f67fd292dd0bf0b9fb90a3af34fc7b8ae15627569090c303a06b58f89702124319a8ad96f75603cc38570c5881aa0f4ecaf0e0df39a43ad96ddb9483c3bac51e
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: 697a5b1bbd62ebd8f87da936ffc0aa3dc4cad84c9a010f4537dfd5e21c1ea1847ce0af757085bb3a4c7dc6f3a2952cf4c53e01d4a6f33e2fa4e714faffec61be
         
     | 
    
        data/CHANGELOG.md
    CHANGED
    
    | 
         @@ -7,6 +7,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 
     | 
|
| 
       7 
7 
     | 
    
         | 
| 
       8 
8 
     | 
    
         
             
            ## [Unreleased]
         
     | 
| 
       9 
9 
     | 
    
         | 
| 
      
 10 
     | 
    
         
            +
            ## [1.0.7] - 2025-10-30
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            ### Fixed
         
     | 
| 
      
 13 
     | 
    
         
            +
            - **Critical: Publish Race Condition**: Fixed race condition in `publish` method where callback could be called multiple times
         
     | 
| 
      
 14 
     | 
    
         
            +
              - Added `callback_called` flag to prevent duplicate callback invocations
         
     | 
| 
      
 15 
     | 
    
         
            +
              - Properly track completion of all async operations before calling final callback
         
     | 
| 
      
 16 
     | 
    
         
            +
              - Ensures `success` status is correctly aggregated from all operations
         
     | 
| 
      
 17 
     | 
    
         
            +
              - **Impact**: Eliminates unreliable message delivery status in high-concurrency scenarios
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
            - **Critical: Thread Safety Issue**: Fixed thread safety issue in PubSubCoordinator message handling
         
     | 
| 
      
 20 
     | 
    
         
            +
              - Changed `EventMachine.next_tick` to `EventMachine.schedule` for cross-thread safety
         
     | 
| 
      
 21 
     | 
    
         
            +
              - Added reactor running check before scheduling
         
     | 
| 
      
 22 
     | 
    
         
            +
              - Added error handling for subscriber callbacks
         
     | 
| 
      
 23 
     | 
    
         
            +
              - **Impact**: Prevents undefined behavior when messages arrive from Redis pub/sub thread
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
            - **Message Deduplication**: Fixed duplicate message enqueue issue
         
     | 
| 
      
 26 
     | 
    
         
            +
              - Local published messages were being enqueued twice (local + pub/sub echo)
         
     | 
| 
      
 27 
     | 
    
         
            +
              - Added message ID tracking to filter out locally published messages from pub/sub
         
     | 
| 
      
 28 
     | 
    
         
            +
              - Messages now include unique IDs for deduplication
         
     | 
| 
      
 29 
     | 
    
         
            +
              - **Impact**: Eliminates duplicate messages in single-server deployments
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
            - **Batch Enqueue Logic**: Fixed `enqueue_messages_batch` to handle nil callbacks correctly
         
     | 
| 
      
 32 
     | 
    
         
            +
              - Separated empty client list check from callback check
         
     | 
| 
      
 33 
     | 
    
         
            +
              - Allows batch enqueue without callback (used by setup_message_routing)
         
     | 
| 
      
 34 
     | 
    
         
            +
              - **Impact**: Fixes NoMethodError when enqueue is called without callback
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
            ### Added
         
     | 
| 
      
 37 
     | 
    
         
            +
            - **Concurrency Test Suite**: Added comprehensive concurrency tests (spec/faye/redis_concurrency_spec.rb)
         
     | 
| 
      
 38 
     | 
    
         
            +
              - Tests for callback guarantee (single invocation)
         
     | 
| 
      
 39 
     | 
    
         
            +
              - Tests for concurrent publish operations
         
     | 
| 
      
 40 
     | 
    
         
            +
              - Tests for multi-channel publishing
         
     | 
| 
      
 41 
     | 
    
         
            +
              - Tests for error handling
         
     | 
| 
      
 42 
     | 
    
         
            +
              - Stress test with 50 rapid publishes
         
     | 
| 
      
 43 
     | 
    
         
            +
              - Thread safety tests
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
            ### Technical Details
         
     | 
| 
      
 46 
     | 
    
         
            +
            **Publish Race Condition Fix**:
         
     | 
| 
      
 47 
     | 
    
         
            +
            - Before: Multiple async callbacks could decrement counter and call callback multiple times
         
     | 
| 
      
 48 
     | 
    
         
            +
            - After: Track completion with callback_called flag, ensure atomic callback invocation
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
            **Thread Safety Fix**:
         
     | 
| 
      
 51 
     | 
    
         
            +
            - Before: `EventMachine.next_tick` called from Redis subscriber thread (unsafe)
         
     | 
| 
      
 52 
     | 
    
         
            +
            - After: `EventMachine.schedule` safely queues work from any thread to EM reactor
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
            **Message Deduplication**:
         
     | 
| 
      
 55 
     | 
    
         
            +
            - Before: Message published locally → enqueued → published to Redis → received back → enqueued again
         
     | 
| 
      
 56 
     | 
    
         
            +
            - After: Track local message IDs, filter out self-published messages from pub/sub
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
       10 
58 
     | 
    
         
             
            ## [1.0.6] - 2025-10-30
         
     | 
| 
       11 
59 
     | 
    
         | 
| 
       12 
60 
     | 
    
         
             
            ### Added
         
     | 
| 
         @@ -151,7 +199,8 @@ For 100 subscribers receiving one message: 
     | 
|
| 
       151 
199 
     | 
    
         
             
            ### Security
         
     | 
| 
       152 
200 
     | 
    
         
             
            - Client and message IDs now use `SecureRandom.uuid` instead of predictable time-based generation
         
     | 
| 
       153 
201 
     | 
    
         | 
| 
       154 
     | 
    
         
            -
            [Unreleased]: https://github.com/7a6163/faye-redis-ng/compare/v1.0. 
     | 
| 
      
 202 
     | 
    
         
            +
            [Unreleased]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.7...HEAD
         
     | 
| 
      
 203 
     | 
    
         
            +
            [1.0.7]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.6...v1.0.7
         
     | 
| 
       155 
204 
     | 
    
         
             
            [1.0.6]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.5...v1.0.6
         
     | 
| 
       156 
205 
     | 
    
         
             
            [1.0.5]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.4...v1.0.5
         
     | 
| 
       157 
206 
     | 
    
         
             
            [1.0.4]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.3...v1.0.4
         
     | 
| 
         @@ -166,11 +166,21 @@ module Faye 
     | 
|
| 
       166 
166 
     | 
    
         
             
                    begin
         
     | 
| 
       167 
167 
     | 
    
         
             
                      message = JSON.parse(message_json)
         
     | 
| 
       168 
168 
     | 
    
         | 
| 
       169 
     | 
    
         
            -
                      # Notify all subscribers 
     | 
| 
       170 
     | 
    
         
            -
                      EventMachine. 
     | 
| 
       171 
     | 
    
         
            -
             
     | 
| 
       172 
     | 
    
         
            -
             
     | 
| 
      
 169 
     | 
    
         
            +
                      # Notify all subscribers
         
     | 
| 
      
 170 
     | 
    
         
            +
                      # Use EventMachine.schedule to safely call from non-EM thread
         
     | 
| 
      
 171 
     | 
    
         
            +
                      # (handle_message is called from subscriber_thread, not EM reactor thread)
         
     | 
| 
      
 172 
     | 
    
         
            +
                      if EventMachine.reactor_running?
         
     | 
| 
      
 173 
     | 
    
         
            +
                        EventMachine.schedule do
         
     | 
| 
      
 174 
     | 
    
         
            +
                          @subscribers.dup.each do |subscriber|
         
     | 
| 
      
 175 
     | 
    
         
            +
                            begin
         
     | 
| 
      
 176 
     | 
    
         
            +
                              subscriber.call(channel, message)
         
     | 
| 
      
 177 
     | 
    
         
            +
                            rescue => e
         
     | 
| 
      
 178 
     | 
    
         
            +
                              log_error("Subscriber callback error for #{channel}: #{e.message}")
         
     | 
| 
      
 179 
     | 
    
         
            +
                            end
         
     | 
| 
      
 180 
     | 
    
         
            +
                          end
         
     | 
| 
       173 
181 
     | 
    
         
             
                        end
         
     | 
| 
      
 182 
     | 
    
         
            +
                      else
         
     | 
| 
      
 183 
     | 
    
         
            +
                        log_error("Cannot handle message: EventMachine reactor not running")
         
     | 
| 
       174 
184 
     | 
    
         
             
                      end
         
     | 
| 
       175 
185 
     | 
    
         
             
                    rescue JSON::ParserError => e
         
     | 
| 
       176 
186 
     | 
    
         
             
                      log_error("Failed to parse message from #{channel}: #{e.message}")
         
     | 
    
        data/lib/faye/redis/version.rb
    CHANGED
    
    
    
        data/lib/faye/redis.rb
    CHANGED
    
    | 
         @@ -105,34 +105,68 @@ module Faye 
     | 
|
| 
       105 
105 
     | 
    
         
             
                  channels = [channels] unless channels.is_a?(Array)
         
     | 
| 
       106 
106 
     | 
    
         | 
| 
       107 
107 
     | 
    
         
             
                  begin
         
     | 
| 
       108 
     | 
    
         
            -
                     
     | 
| 
       109 
     | 
    
         
            -
                     
     | 
| 
      
 108 
     | 
    
         
            +
                    # Ensure message has an ID for deduplication
         
     | 
| 
      
 109 
     | 
    
         
            +
                    message = message.dup unless message.frozen?
         
     | 
| 
      
 110 
     | 
    
         
            +
                    message['id'] ||= generate_message_id
         
     | 
| 
      
 111 
     | 
    
         
            +
             
     | 
| 
      
 112 
     | 
    
         
            +
                    # Track this message as locally published
         
     | 
| 
      
 113 
     | 
    
         
            +
                    if @local_message_ids
         
     | 
| 
      
 114 
     | 
    
         
            +
                      if @local_message_ids_mutex
         
     | 
| 
      
 115 
     | 
    
         
            +
                        @local_message_ids_mutex.synchronize { @local_message_ids.add(message['id']) }
         
     | 
| 
      
 116 
     | 
    
         
            +
                      else
         
     | 
| 
      
 117 
     | 
    
         
            +
                        @local_message_ids.add(message['id'])
         
     | 
| 
      
 118 
     | 
    
         
            +
                      end
         
     | 
| 
      
 119 
     | 
    
         
            +
                    end
         
     | 
| 
      
 120 
     | 
    
         
            +
             
     | 
| 
      
 121 
     | 
    
         
            +
                    total_channels = channels.size
         
     | 
| 
      
 122 
     | 
    
         
            +
                    completed_channels = 0
         
     | 
| 
      
 123 
     | 
    
         
            +
                    callback_called = false
         
     | 
| 
      
 124 
     | 
    
         
            +
                    all_success = true
         
     | 
| 
       110 
125 
     | 
    
         | 
| 
       111 
126 
     | 
    
         
             
                    channels.each do |channel|
         
     | 
| 
       112 
127 
     | 
    
         
             
                      # Get subscribers and process in parallel
         
     | 
| 
       113 
128 
     | 
    
         
             
                      @subscription_manager.get_subscribers(channel) do |client_ids|
         
     | 
| 
       114 
     | 
    
         
            -
                        #  
     | 
| 
      
 129 
     | 
    
         
            +
                        # Track operations for this channel
         
     | 
| 
      
 130 
     | 
    
         
            +
                        pending_ops = 2  # pubsub + enqueue
         
     | 
| 
      
 131 
     | 
    
         
            +
                        channel_success = true
         
     | 
| 
      
 132 
     | 
    
         
            +
                        ops_completed = 0
         
     | 
| 
      
 133 
     | 
    
         
            +
             
     | 
| 
      
 134 
     | 
    
         
            +
                        complete_channel = lambda do
         
     | 
| 
      
 135 
     | 
    
         
            +
                          ops_completed += 1
         
     | 
| 
      
 136 
     | 
    
         
            +
                          if ops_completed == pending_ops
         
     | 
| 
      
 137 
     | 
    
         
            +
                            # This channel is complete
         
     | 
| 
      
 138 
     | 
    
         
            +
                            all_success &&= channel_success
         
     | 
| 
      
 139 
     | 
    
         
            +
                            completed_channels += 1
         
     | 
| 
      
 140 
     | 
    
         
            +
             
     | 
| 
      
 141 
     | 
    
         
            +
                            # Call final callback when all channels are done
         
     | 
| 
      
 142 
     | 
    
         
            +
                            if completed_channels == total_channels && !callback_called && callback
         
     | 
| 
      
 143 
     | 
    
         
            +
                              callback_called = true
         
     | 
| 
      
 144 
     | 
    
         
            +
                              EventMachine.next_tick { callback.call(all_success) }
         
     | 
| 
      
 145 
     | 
    
         
            +
                            end
         
     | 
| 
      
 146 
     | 
    
         
            +
                          end
         
     | 
| 
      
 147 
     | 
    
         
            +
                        end
         
     | 
| 
      
 148 
     | 
    
         
            +
             
     | 
| 
      
 149 
     | 
    
         
            +
                        # Publish to pub/sub
         
     | 
| 
       115 
150 
     | 
    
         
             
                        @pubsub_coordinator.publish(channel, message) do |published|
         
     | 
| 
       116 
     | 
    
         
            -
                           
     | 
| 
      
 151 
     | 
    
         
            +
                          channel_success &&= published
         
     | 
| 
      
 152 
     | 
    
         
            +
                          complete_channel.call
         
     | 
| 
       117 
153 
     | 
    
         
             
                        end
         
     | 
| 
       118 
154 
     | 
    
         | 
| 
       119 
     | 
    
         
            -
                        # Enqueue for all subscribed clients 
     | 
| 
      
 155 
     | 
    
         
            +
                        # Enqueue for all subscribed clients
         
     | 
| 
       120 
156 
     | 
    
         
             
                        if client_ids.any?
         
     | 
| 
       121 
157 
     | 
    
         
             
                          enqueue_messages_batch(client_ids, message) do |enqueued|
         
     | 
| 
       122 
     | 
    
         
            -
                             
     | 
| 
      
 158 
     | 
    
         
            +
                            channel_success &&= enqueued
         
     | 
| 
      
 159 
     | 
    
         
            +
                            complete_channel.call
         
     | 
| 
       123 
160 
     | 
    
         
             
                          end
         
     | 
| 
       124 
     | 
    
         
            -
                         
     | 
| 
       125 
     | 
    
         
            -
             
     | 
| 
       126 
     | 
    
         
            -
             
     | 
| 
       127 
     | 
    
         
            -
                        remaining_operations -= 1
         
     | 
| 
       128 
     | 
    
         
            -
                        if remaining_operations == 0 && callback
         
     | 
| 
       129 
     | 
    
         
            -
                          EventMachine.next_tick { callback.call(success) }
         
     | 
| 
      
 161 
     | 
    
         
            +
                        else
         
     | 
| 
      
 162 
     | 
    
         
            +
                          # No clients, but still need to complete
         
     | 
| 
      
 163 
     | 
    
         
            +
                          complete_channel.call
         
     | 
| 
       130 
164 
     | 
    
         
             
                        end
         
     | 
| 
       131 
165 
     | 
    
         
             
                      end
         
     | 
| 
       132 
166 
     | 
    
         
             
                    end
         
     | 
| 
       133 
167 
     | 
    
         
             
                  rescue => e
         
     | 
| 
       134 
168 
     | 
    
         
             
                    log_error("Failed to publish message to channels #{channels}: #{e.message}")
         
     | 
| 
       135 
     | 
    
         
            -
                    EventMachine.next_tick { callback.call(false) } if callback
         
     | 
| 
      
 169 
     | 
    
         
            +
                    EventMachine.next_tick { callback.call(false) } if callback && !callback_called
         
     | 
| 
       136 
170 
     | 
    
         
             
                  end
         
     | 
| 
       137 
171 
     | 
    
         
             
                end
         
     | 
| 
       138 
172 
     | 
    
         | 
| 
         @@ -169,9 +203,20 @@ module Faye 
     | 
|
| 
       169 
203 
     | 
    
         
             
                  SecureRandom.uuid
         
     | 
| 
       170 
204 
     | 
    
         
             
                end
         
     | 
| 
       171 
205 
     | 
    
         | 
| 
      
 206 
     | 
    
         
            +
                def generate_message_id
         
     | 
| 
      
 207 
     | 
    
         
            +
                  SecureRandom.uuid
         
     | 
| 
      
 208 
     | 
    
         
            +
                end
         
     | 
| 
      
 209 
     | 
    
         
            +
             
     | 
| 
       172 
210 
     | 
    
         
             
                # Batch enqueue messages to multiple clients using a single Redis pipeline
         
     | 
| 
       173 
211 
     | 
    
         
             
                def enqueue_messages_batch(client_ids, message, &callback)
         
     | 
| 
       174 
     | 
    
         
            -
                   
     | 
| 
      
 212 
     | 
    
         
            +
                  # Handle empty client list
         
     | 
| 
      
 213 
     | 
    
         
            +
                  if client_ids.empty?
         
     | 
| 
      
 214 
     | 
    
         
            +
                    EventMachine.next_tick { callback.call(true) } if callback
         
     | 
| 
      
 215 
     | 
    
         
            +
                    return
         
     | 
| 
      
 216 
     | 
    
         
            +
                  end
         
     | 
| 
      
 217 
     | 
    
         
            +
             
     | 
| 
      
 218 
     | 
    
         
            +
                  # No callback provided, but still need to enqueue
         
     | 
| 
      
 219 
     | 
    
         
            +
                  # (setup_message_routing calls this without callback)
         
     | 
| 
       175 
220 
     | 
    
         | 
| 
       176 
221 
     | 
    
         
             
                  message_json = message.to_json
         
     | 
| 
       177 
222 
     | 
    
         
             
                  message_ttl = @options[:message_ttl] || 3600
         
     | 
| 
         @@ -252,10 +297,31 @@ module Faye 
     | 
|
| 
       252 
297 
     | 
    
         
             
                end
         
     | 
| 
       253 
298 
     | 
    
         | 
| 
       254 
299 
     | 
    
         
             
                def setup_message_routing
         
     | 
| 
      
 300 
     | 
    
         
            +
                  # Track locally published message IDs to avoid duplicate enqueue
         
     | 
| 
      
 301 
     | 
    
         
            +
                  @local_message_ids = Set.new
         
     | 
| 
      
 302 
     | 
    
         
            +
                  @local_message_ids_mutex = Mutex.new if defined?(Mutex)
         
     | 
| 
      
 303 
     | 
    
         
            +
             
     | 
| 
       255 
304 
     | 
    
         
             
                  # Subscribe to message events from other servers
         
     | 
| 
       256 
305 
     | 
    
         
             
                  @pubsub_coordinator.on_message do |channel, message|
         
     | 
| 
      
 306 
     | 
    
         
            +
                    # Skip if this is a message we just published locally
         
     | 
| 
      
 307 
     | 
    
         
            +
                    # (Redis pub/sub echoes back messages to the publisher)
         
     | 
| 
      
 308 
     | 
    
         
            +
                    message_id = message['id']
         
     | 
| 
      
 309 
     | 
    
         
            +
                    is_local = false
         
     | 
| 
      
 310 
     | 
    
         
            +
             
     | 
| 
      
 311 
     | 
    
         
            +
                    if message_id
         
     | 
| 
      
 312 
     | 
    
         
            +
                      if @local_message_ids_mutex
         
     | 
| 
      
 313 
     | 
    
         
            +
                        @local_message_ids_mutex.synchronize do
         
     | 
| 
      
 314 
     | 
    
         
            +
                          is_local = @local_message_ids.delete(message_id)
         
     | 
| 
      
 315 
     | 
    
         
            +
                        end
         
     | 
| 
      
 316 
     | 
    
         
            +
                      else
         
     | 
| 
      
 317 
     | 
    
         
            +
                        is_local = @local_message_ids.delete(message_id)
         
     | 
| 
      
 318 
     | 
    
         
            +
                      end
         
     | 
| 
      
 319 
     | 
    
         
            +
                    end
         
     | 
| 
      
 320 
     | 
    
         
            +
             
     | 
| 
      
 321 
     | 
    
         
            +
                    next if is_local
         
     | 
| 
      
 322 
     | 
    
         
            +
             
     | 
| 
      
 323 
     | 
    
         
            +
                    # Enqueue for remote servers' messages only
         
     | 
| 
       257 
324 
     | 
    
         
             
                    @subscription_manager.get_subscribers(channel) do |client_ids|
         
     | 
| 
       258 
     | 
    
         
            -
                      # Use batch enqueue for better performance
         
     | 
| 
       259 
325 
     | 
    
         
             
                      enqueue_messages_batch(client_ids, message) if client_ids.any?
         
     | 
| 
       260 
326 
     | 
    
         
             
                    end
         
     | 
| 
       261 
327 
     | 
    
         
             
                  end
         
     |