faye-redis-ng 1.0.0

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.
@@ -0,0 +1,207 @@
1
+ require 'json'
2
+ require 'securerandom'
3
+
4
+ module Faye
5
+ class Redis
6
+ class MessageQueue
7
+ attr_reader :connection, :options
8
+
9
+ def initialize(connection, options = {})
10
+ @connection = connection
11
+ @options = options
12
+ end
13
+
14
+ # Enqueue a message for a client
15
+ def enqueue(client_id, message, &callback)
16
+ message_id = generate_message_id
17
+ timestamp = Time.now.to_i
18
+
19
+ message_data = {
20
+ id: message_id,
21
+ channel: message['channel'],
22
+ data: message['data'],
23
+ client_id: message['clientId'],
24
+ timestamp: timestamp
25
+ }
26
+
27
+ @connection.with_redis do |redis|
28
+ redis.multi do |multi|
29
+ # Store message data
30
+ multi.hset(message_key(message_id), message_data.transform_keys(&:to_s).transform_values { |v| v.to_json })
31
+
32
+ # Add message to client's queue
33
+ multi.rpush(queue_key(client_id), message_id)
34
+
35
+ # Set TTL on message
36
+ multi.expire(message_key(message_id), message_ttl)
37
+
38
+ # Set TTL on queue
39
+ multi.expire(queue_key(client_id), message_ttl)
40
+ end
41
+ end
42
+
43
+ EventMachine.next_tick { callback.call(true) } if callback
44
+ rescue => e
45
+ log_error("Failed to enqueue message for client #{client_id}: #{e.message}")
46
+ EventMachine.next_tick { callback.call(false) } if callback
47
+ end
48
+
49
+ # Dequeue all messages for a client
50
+ def dequeue_all(client_id, &callback)
51
+ # Get all message IDs from queue
52
+ message_ids = @connection.with_redis do |redis|
53
+ redis.lrange(queue_key(client_id), 0, -1)
54
+ end
55
+
56
+ # Fetch all messages using pipeline
57
+ messages = []
58
+ unless message_ids.empty?
59
+ @connection.with_redis do |redis|
60
+ redis.pipelined do |pipeline|
61
+ message_ids.each do |message_id|
62
+ pipeline.hgetall(message_key(message_id))
63
+ end
64
+ end.each do |data|
65
+ next if data.nil? || data.empty?
66
+
67
+ # Parse JSON values
68
+ parsed_data = data.transform_values do |v|
69
+ begin
70
+ JSON.parse(v)
71
+ rescue JSON::ParserError
72
+ v
73
+ end
74
+ end
75
+
76
+ # Convert to Faye message format
77
+ messages << {
78
+ 'channel' => parsed_data['channel'],
79
+ 'data' => parsed_data['data'],
80
+ 'clientId' => parsed_data['client_id'],
81
+ 'id' => parsed_data['id']
82
+ }
83
+ end
84
+ end
85
+ end
86
+
87
+ # Delete queue and all message data using pipeline
88
+ unless message_ids.empty?
89
+ @connection.with_redis do |redis|
90
+ redis.pipelined do |pipeline|
91
+ pipeline.del(queue_key(client_id))
92
+ message_ids.each do |message_id|
93
+ pipeline.del(message_key(message_id))
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ EventMachine.next_tick { callback.call(messages) } if callback
100
+ messages
101
+ rescue => e
102
+ log_error("Failed to dequeue messages for client #{client_id}: #{e.message}")
103
+ EventMachine.next_tick { callback.call([]) } if callback
104
+ []
105
+ end
106
+
107
+ # Peek at messages without removing them
108
+ def peek(client_id, limit = 10, &callback)
109
+ message_ids = @connection.with_redis do |redis|
110
+ redis.lrange(queue_key(client_id), 0, limit - 1)
111
+ end
112
+
113
+ messages = message_ids.map do |message_id|
114
+ fetch_message(message_id)
115
+ end.compact
116
+
117
+ EventMachine.next_tick { callback.call(messages) } if callback
118
+ messages
119
+ rescue => e
120
+ log_error("Failed to peek messages for client #{client_id}: #{e.message}")
121
+ EventMachine.next_tick { callback.call([]) } if callback
122
+ []
123
+ end
124
+
125
+ # Get queue size for a client
126
+ def size(client_id, &callback)
127
+ queue_size = @connection.with_redis do |redis|
128
+ redis.llen(queue_key(client_id))
129
+ end
130
+
131
+ EventMachine.next_tick { callback.call(queue_size) } if callback
132
+ queue_size
133
+ rescue => e
134
+ log_error("Failed to get queue size for client #{client_id}: #{e.message}")
135
+ EventMachine.next_tick { callback.call(0) } if callback
136
+ 0
137
+ end
138
+
139
+ # Clear a client's message queue
140
+ def clear(client_id, &callback)
141
+ @connection.with_redis do |redis|
142
+ redis.del(queue_key(client_id))
143
+ end
144
+
145
+ EventMachine.next_tick { callback.call(true) } if callback
146
+ rescue => e
147
+ log_error("Failed to clear queue for client #{client_id}: #{e.message}")
148
+ EventMachine.next_tick { callback.call(false) } if callback
149
+ end
150
+
151
+ private
152
+
153
+ def fetch_message(message_id)
154
+ data = @connection.with_redis do |redis|
155
+ redis.hgetall(message_key(message_id))
156
+ end
157
+
158
+ return nil if data.empty?
159
+
160
+ # Parse JSON values
161
+ parsed_data = data.transform_values do |v|
162
+ begin
163
+ JSON.parse(v)
164
+ rescue JSON::ParserError
165
+ v
166
+ end
167
+ end
168
+
169
+ # Convert to Faye message format
170
+ {
171
+ 'channel' => parsed_data['channel'],
172
+ 'data' => parsed_data['data'],
173
+ 'clientId' => parsed_data['client_id'],
174
+ 'id' => parsed_data['id']
175
+ }
176
+ rescue => e
177
+ log_error("Failed to fetch message #{message_id}: #{e.message}")
178
+ nil
179
+ end
180
+
181
+ def queue_key(client_id)
182
+ namespace_key("messages:#{client_id}")
183
+ end
184
+
185
+ def message_key(message_id)
186
+ namespace_key("message:#{message_id}")
187
+ end
188
+
189
+ def namespace_key(key)
190
+ namespace = @options[:namespace] || 'faye'
191
+ "#{namespace}:#{key}"
192
+ end
193
+
194
+ def message_ttl
195
+ @options[:message_ttl] || 3600
196
+ end
197
+
198
+ def generate_message_id
199
+ SecureRandom.uuid
200
+ end
201
+
202
+ def log_error(message)
203
+ puts "[Faye::Redis::MessageQueue] ERROR: #{message}" if @options[:log_level] != :silent
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,207 @@
1
+ require 'json'
2
+ require 'set'
3
+
4
+ module Faye
5
+ class Redis
6
+ class PubSubCoordinator
7
+ attr_reader :connection, :options
8
+
9
+ def initialize(connection, options = {})
10
+ @connection = connection
11
+ @options = options
12
+ @subscribers = []
13
+ @redis_subscriber = nil
14
+ @subscribed_channels = Set.new
15
+ @subscriber_thread = nil
16
+ @stop_subscriber = false
17
+ @reconnect_attempts = 0
18
+ # Don't setup subscriber immediately - wait until first publish/subscribe
19
+ end
20
+
21
+ # Publish a message to a channel
22
+ def publish(channel, message, &callback)
23
+ # Ensure subscriber is setup
24
+ setup_subscriber unless @subscriber_thread
25
+
26
+ message_json = message.to_json
27
+
28
+ @connection.with_redis do |redis|
29
+ redis.publish(pubsub_channel(channel), message_json)
30
+ end
31
+
32
+ EventMachine.next_tick { callback.call(true) } if callback
33
+ rescue => e
34
+ log_error("Failed to publish message to #{channel}: #{e.message}")
35
+ EventMachine.next_tick { callback.call(false) } if callback
36
+ end
37
+
38
+ # Subscribe to messages from other servers
39
+ def on_message(&block)
40
+ @subscribers << block
41
+ end
42
+
43
+ # Subscribe to a Redis pub/sub channel
44
+ def subscribe_to_channel(channel)
45
+ return if @subscribed_channels.include?(channel)
46
+
47
+ @subscribed_channels.add(channel)
48
+
49
+ if @redis_subscriber
50
+ @redis_subscriber.subscribe(pubsub_channel(channel))
51
+ end
52
+ rescue => e
53
+ log_error("Failed to subscribe to channel #{channel}: #{e.message}")
54
+ end
55
+
56
+ # Unsubscribe from a Redis pub/sub channel
57
+ def unsubscribe_from_channel(channel)
58
+ return unless @subscribed_channels.include?(channel)
59
+
60
+ @subscribed_channels.delete(channel)
61
+
62
+ if @redis_subscriber
63
+ @redis_subscriber.unsubscribe(pubsub_channel(channel))
64
+ end
65
+ rescue => e
66
+ log_error("Failed to unsubscribe from channel #{channel}: #{e.message}")
67
+ end
68
+
69
+ # Disconnect the pub/sub connection
70
+ def disconnect
71
+ @stop_subscriber = true
72
+
73
+ if @subscriber_thread
74
+ @subscriber_thread.kill
75
+ @subscriber_thread = nil
76
+ end
77
+
78
+ if @redis_subscriber
79
+ begin
80
+ @redis_subscriber.quit
81
+ rescue => e
82
+ # Ignore errors during disconnect
83
+ end
84
+ @redis_subscriber = nil
85
+ end
86
+ @subscribed_channels.clear
87
+ @subscribers.clear
88
+ end
89
+
90
+ private
91
+
92
+ def setup_subscriber
93
+ return if @subscriber_thread&.alive?
94
+
95
+ @stop_subscriber = false
96
+ @subscriber_thread = Thread.new do
97
+ run_subscriber_loop
98
+ end
99
+ rescue => e
100
+ log_error("Failed to setup pub/sub subscriber: #{e.message}")
101
+ end
102
+
103
+ def run_subscriber_loop
104
+ max_reconnect_attempts = @options[:pubsub_max_reconnect_attempts] || 10
105
+ base_delay = @options[:pubsub_reconnect_delay] || 1
106
+
107
+ loop do
108
+ break if @stop_subscriber
109
+
110
+ begin
111
+ # Create a dedicated Redis connection for pub/sub
112
+ @redis_subscriber = @connection.create_pubsub_connection
113
+
114
+ # Subscribe to a wildcard pattern to catch all Faye messages
115
+ pattern = pubsub_channel('*')
116
+
117
+ log_info("Starting pub/sub subscriber (attempt #{@reconnect_attempts + 1})")
118
+
119
+ @redis_subscriber.psubscribe(pattern) do |on|
120
+ on.pmessage do |pattern_match, channel, message|
121
+ handle_message(channel, message)
122
+ end
123
+
124
+ on.psubscribe do |channel, subscriptions|
125
+ log_info("Subscribed to pattern: #{channel}")
126
+ @reconnect_attempts = 0 # Reset on successful subscription
127
+ end
128
+
129
+ on.punsubscribe do |channel, subscriptions|
130
+ log_info("Unsubscribed from pattern: #{channel}")
131
+ end
132
+ end
133
+ rescue => e
134
+ break if @stop_subscriber
135
+
136
+ @reconnect_attempts += 1
137
+ log_error("Pub/sub subscriber error: #{e.message} (attempt #{@reconnect_attempts})")
138
+
139
+ # Clean up failed connection
140
+ begin
141
+ @redis_subscriber&.quit
142
+ rescue
143
+ # Ignore cleanup errors
144
+ end
145
+ @redis_subscriber = nil
146
+
147
+ if @reconnect_attempts >= max_reconnect_attempts
148
+ log_error("Max reconnect attempts (#{max_reconnect_attempts}) reached, stopping pub/sub subscriber")
149
+ break
150
+ end
151
+
152
+ # Exponential backoff with jitter
153
+ delay = [base_delay * (2 ** (@reconnect_attempts - 1)), 60].min
154
+ jitter = rand * 0.3 * delay
155
+ sleep(delay + jitter)
156
+
157
+ log_info("Attempting to reconnect pub/sub subscriber...")
158
+ end
159
+ end
160
+ end
161
+
162
+ def handle_message(redis_channel, message_json)
163
+ # Extract the Faye channel from the Redis channel
164
+ channel = extract_channel(redis_channel)
165
+
166
+ begin
167
+ message = JSON.parse(message_json)
168
+
169
+ # Notify all subscribers
170
+ EventMachine.next_tick do
171
+ @subscribers.each do |subscriber|
172
+ subscriber.call(channel, message)
173
+ end
174
+ end
175
+ rescue JSON::ParserError => e
176
+ log_error("Failed to parse message from #{channel}: #{e.message}")
177
+ end
178
+ rescue => e
179
+ log_error("Failed to handle message from #{redis_channel}: #{e.message}")
180
+ end
181
+
182
+ def pubsub_channel(channel)
183
+ namespace_key("publish:#{channel}")
184
+ end
185
+
186
+ def extract_channel(redis_channel)
187
+ # Remove the namespace prefix and 'publish:' prefix
188
+ namespace = @options[:namespace] || 'faye'
189
+ prefix = "#{namespace}:publish:"
190
+ redis_channel.sub(/^#{Regexp.escape(prefix)}/, '')
191
+ end
192
+
193
+ def namespace_key(key)
194
+ namespace = @options[:namespace] || 'faye'
195
+ "#{namespace}:#{key}"
196
+ end
197
+
198
+ def log_error(message)
199
+ puts "[Faye::Redis::PubSubCoordinator] ERROR: #{message}" if @options[:log_level] != :silent
200
+ end
201
+
202
+ def log_info(message)
203
+ puts "[Faye::Redis::PubSubCoordinator] INFO: #{message}" if @options[:log_level] == :debug
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,189 @@
1
+ module Faye
2
+ class Redis
3
+ class SubscriptionManager
4
+ attr_reader :connection, :options
5
+
6
+ def initialize(connection, options = {})
7
+ @connection = connection
8
+ @options = options
9
+ end
10
+
11
+ # Subscribe a client to a channel
12
+ def subscribe(client_id, channel, &callback)
13
+ timestamp = Time.now.to_i
14
+
15
+ @connection.with_redis do |redis|
16
+ redis.multi do |multi|
17
+ # Add channel to client's subscriptions
18
+ multi.sadd(client_subscriptions_key(client_id), channel)
19
+
20
+ # Add client to channel's subscribers
21
+ multi.sadd(channel_subscribers_key(channel), client_id)
22
+
23
+ # Store subscription metadata
24
+ multi.hset(
25
+ subscription_key(client_id, channel),
26
+ 'subscribed_at', timestamp,
27
+ 'channel', channel,
28
+ 'client_id', client_id
29
+ )
30
+
31
+ # Handle wildcard patterns
32
+ if channel.include?('*')
33
+ multi.sadd(patterns_key, channel)
34
+ end
35
+ end
36
+ end
37
+
38
+ EventMachine.next_tick { callback.call(true) } if callback
39
+ rescue => e
40
+ log_error("Failed to subscribe client #{client_id} to #{channel}: #{e.message}")
41
+ EventMachine.next_tick { callback.call(false) } if callback
42
+ end
43
+
44
+ # Unsubscribe a client from a channel
45
+ def unsubscribe(client_id, channel, &callback)
46
+ @connection.with_redis do |redis|
47
+ redis.multi do |multi|
48
+ # Remove channel from client's subscriptions
49
+ multi.srem(client_subscriptions_key(client_id), channel)
50
+
51
+ # Remove client from channel's subscribers
52
+ multi.srem(channel_subscribers_key(channel), client_id)
53
+
54
+ # Delete subscription metadata
55
+ multi.del(subscription_key(client_id, channel))
56
+ end
57
+ end
58
+
59
+ EventMachine.next_tick { callback.call(true) } if callback
60
+ rescue => e
61
+ log_error("Failed to unsubscribe client #{client_id} from #{channel}: #{e.message}")
62
+ EventMachine.next_tick { callback.call(false) } if callback
63
+ end
64
+
65
+ # Unsubscribe a client from all channels
66
+ def unsubscribe_all(client_id, &callback)
67
+ # Get all channels the client is subscribed to
68
+ get_client_subscriptions(client_id) do |channels|
69
+ if channels.empty?
70
+ callback.call(true) if callback
71
+ else
72
+ # Unsubscribe from each channel
73
+ remaining = channels.size
74
+ channels.each do |channel|
75
+ unsubscribe(client_id, channel) do
76
+ remaining -= 1
77
+ callback.call(true) if callback && remaining == 0
78
+ end
79
+ end
80
+ end
81
+ end
82
+ rescue => e
83
+ log_error("Failed to unsubscribe client #{client_id} from all channels: #{e.message}")
84
+ EventMachine.next_tick { callback.call(false) } if callback
85
+ end
86
+
87
+ # Get all channels a client is subscribed to
88
+ def get_client_subscriptions(client_id, &callback)
89
+ channels = @connection.with_redis do |redis|
90
+ redis.smembers(client_subscriptions_key(client_id))
91
+ end
92
+
93
+ EventMachine.next_tick { callback.call(channels) } if callback
94
+ channels
95
+ rescue => e
96
+ log_error("Failed to get subscriptions for client #{client_id}: #{e.message}")
97
+ EventMachine.next_tick { callback.call([]) } if callback
98
+ []
99
+ end
100
+
101
+ # Get all clients subscribed to a channel
102
+ def get_subscribers(channel, &callback)
103
+ # Get direct subscribers
104
+ direct_subscribers = @connection.with_redis do |redis|
105
+ redis.smembers(channel_subscribers_key(channel))
106
+ end
107
+
108
+ # Get pattern subscribers
109
+ pattern_subscribers = get_pattern_subscribers(channel)
110
+
111
+ all_subscribers = (direct_subscribers + pattern_subscribers).uniq
112
+
113
+ EventMachine.next_tick { callback.call(all_subscribers) } if callback
114
+ all_subscribers
115
+ rescue => e
116
+ log_error("Failed to get subscribers for channel #{channel}: #{e.message}")
117
+ EventMachine.next_tick { callback.call([]) } if callback
118
+ []
119
+ end
120
+
121
+ # Get subscribers matching wildcard patterns
122
+ def get_pattern_subscribers(channel)
123
+ patterns = @connection.with_redis do |redis|
124
+ redis.smembers(patterns_key)
125
+ end
126
+
127
+ matching_clients = []
128
+ patterns.each do |pattern|
129
+ if channel_matches_pattern?(channel, pattern)
130
+ clients = @connection.with_redis do |redis|
131
+ redis.smembers(channel_subscribers_key(pattern))
132
+ end
133
+ matching_clients.concat(clients)
134
+ end
135
+ end
136
+
137
+ matching_clients.uniq
138
+ rescue => e
139
+ log_error("Failed to get pattern subscribers for channel #{channel}: #{e.message}")
140
+ []
141
+ end
142
+
143
+ # Check if a channel matches a pattern
144
+ def channel_matches_pattern?(channel, pattern)
145
+ # Convert Faye wildcard pattern to regex
146
+ # * matches one segment, ** matches multiple segments
147
+ regex_pattern = pattern
148
+ .gsub('**', '__DOUBLE_STAR__')
149
+ .gsub('*', '[^/]+')
150
+ .gsub('__DOUBLE_STAR__', '.*')
151
+
152
+ regex = Regexp.new("^#{regex_pattern}$")
153
+ !!(channel =~ regex)
154
+ end
155
+
156
+ # Clean up subscriptions for a client
157
+ def cleanup_client_subscriptions(client_id)
158
+ unsubscribe_all(client_id)
159
+ end
160
+
161
+ private
162
+
163
+ def client_subscriptions_key(client_id)
164
+ namespace_key("subscriptions:#{client_id}")
165
+ end
166
+
167
+ def channel_subscribers_key(channel)
168
+ namespace_key("channels:#{channel}")
169
+ end
170
+
171
+ def subscription_key(client_id, channel)
172
+ namespace_key("subscription:#{client_id}:#{channel}")
173
+ end
174
+
175
+ def patterns_key
176
+ namespace_key('patterns')
177
+ end
178
+
179
+ def namespace_key(key)
180
+ namespace = @options[:namespace] || 'faye'
181
+ "#{namespace}:#{key}"
182
+ end
183
+
184
+ def log_error(message)
185
+ puts "[Faye::Redis::SubscriptionManager] ERROR: #{message}" if @options[:log_level] != :silent
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,5 @@
1
+ module Faye
2
+ class Redis
3
+ VERSION = '1.0.0'
4
+ end
5
+ end