faye-redis-ng 1.0.2 → 1.0.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 699f1995764ed2a5634956a06c84b89daf6b68bdfa54f7f0c0f44d9cbd4f3e03
4
- data.tar.gz: 6fe62df2b60a0d2d43cd001c93147e95f4a19355db7b1ebb3437b1c9ab567ae1
3
+ metadata.gz: d0fea6c56f6598af371ca22722f24899d5aab97ca4a73f52ae3e8a12e28ea20c
4
+ data.tar.gz: 5291d2eb437e9241211c6e8784ee582ad4f792be01ab628ee618f16f85d84c49
5
5
  SHA512:
6
- metadata.gz: 47807e29747264e5bf6847fbe177f4e9a83cb7e7e75b0d0f297b2580579252d81e13588a93daa0efae3b02b4cb30bd91e97f59c63c4f1420dc0f2b9ad1e3a45d
7
- data.tar.gz: 8a3110fdf417cc65b2a0fe98eb57423a5eeffe1e6df7e867a206ee14a71f7b990e4b06aad2a5c5c6c16d028455d5138df17086a04ab3b8f9388ed8af0da0b3f9
6
+ metadata.gz: d22bd343a33dabb655b365dedc1a74bf6b1040604be2b3bd6391309cf6581bc3f0bd17762630ece5fe80f93fb590e653a65b85ba8fa71f09214d8226199ec5bb
7
+ data.tar.gz: 3749cc0435c68f903f401e8598e9952aaa3710b43ef54873b5b9b50e6d229df6253dac88119112dc7f32f2f9c14a1dc7a65061548b202b58fda252d61ffe309d
data/CHANGELOG.md CHANGED
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.3] - 2025-10-06
11
+
12
+ ### Fixed
13
+ - **Memory Leak**: Fixed `MessageQueue.clear` to properly delete message data instead of only clearing queue index
14
+ - **Resource Cleanup**: Fixed `destroy_client` to clear message queue before destroying client
15
+ - **Race Condition**: Fixed `publish` callback timing to wait for all async operations to complete
16
+ - **Pattern Cleanup**: Fixed `SubscriptionManager` to properly clean up wildcard patterns when last subscriber unsubscribes
17
+ - **Thread Safety**: Fixed `PubSubCoordinator` to use array duplication when iterating subscribers to prevent concurrent modification
18
+
19
+ ### Changed
20
+ - **Performance**: Optimized `cleanup_expired` to use batch pipelined operations instead of individual checks
21
+ - Reduces Redis calls from O(n²) to O(n) for n clients
22
+ - Returns count of cleaned clients via callback
23
+
24
+ ### Improved
25
+ - **Test Coverage**: Increased line coverage from 90% to 95.83% (528/551 lines)
26
+ - Added comprehensive tests for cleanup operations and wildcard pattern management
27
+
10
28
  ## [1.0.2] - 2025-10-06
11
29
 
12
30
  ### Fixed
@@ -47,7 +65,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
47
65
  ### Security
48
66
  - Client and message IDs now use `SecureRandom.uuid` instead of predictable time-based generation
49
67
 
50
- [Unreleased]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.2...HEAD
68
+ [Unreleased]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.3...HEAD
69
+ [1.0.3]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.2...v1.0.3
51
70
  [1.0.2]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.1...v1.0.2
52
71
  [1.0.1]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.0...v1.0.1
53
72
  [1.0.0]: https://github.com/7a6163/faye-redis-ng/releases/tag/v1.0.0
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 faye-redis-ng
3
+ Copyright (c) 2025 faye-redis-ng
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -268,4 +268,4 @@ MIT License - see LICENSE file for details
268
268
  ## Acknowledgments
269
269
 
270
270
  - Built for the [Faye](https://faye.jcoglan.com/) messaging system
271
- - Inspired by the original faye-redis gem
271
+ - Inspired by the original [faye-redis](https://github.com/faye/faye-redis-ruby) gem
@@ -110,14 +110,43 @@ module Faye
110
110
  end
111
111
 
112
112
  # Clean up expired clients
113
- def cleanup_expired
113
+ def cleanup_expired(&callback)
114
114
  all do |client_ids|
115
- client_ids.each do |client_id|
116
- exists?(client_id) do |exists|
117
- destroy(client_id) unless exists
115
+ # Check existence in batch using pipelined commands
116
+ results = @connection.with_redis do |redis|
117
+ redis.pipelined do |pipeline|
118
+ client_ids.each do |client_id|
119
+ pipeline.exists?(client_key(client_id))
120
+ end
118
121
  end
119
122
  end
123
+
124
+ # Collect expired client IDs
125
+ expired_clients = []
126
+ client_ids.each_with_index do |client_id, index|
127
+ result = results[index]
128
+ # Redis 5.x returns boolean, older versions return integer
129
+ exists = result.is_a?(Integer) ? result > 0 : result
130
+ expired_clients << client_id unless exists
131
+ end
132
+
133
+ # Batch delete expired clients
134
+ if expired_clients.any?
135
+ @connection.with_redis do |redis|
136
+ redis.pipelined do |pipeline|
137
+ expired_clients.each do |client_id|
138
+ pipeline.del(client_key(client_id))
139
+ pipeline.srem?(clients_index_key, client_id)
140
+ end
141
+ end
142
+ end
143
+ end
144
+
145
+ EventMachine.next_tick { callback.call(expired_clients.size) } if callback
120
146
  end
147
+ rescue => e
148
+ log_error("Failed to cleanup expired clients: #{e.message}")
149
+ EventMachine.next_tick { callback.call(0) } if callback
121
150
  end
122
151
 
123
152
  private
@@ -138,8 +138,19 @@ module Faye
138
138
 
139
139
  # Clear a client's message queue
140
140
  def clear(client_id, &callback)
141
+ # Get all message IDs first
142
+ message_ids = @connection.with_redis do |redis|
143
+ redis.lrange(queue_key(client_id), 0, -1)
144
+ end
145
+
146
+ # Delete queue and all message data
141
147
  @connection.with_redis do |redis|
142
- redis.del(queue_key(client_id))
148
+ redis.pipelined do |pipeline|
149
+ pipeline.del(queue_key(client_id))
150
+ message_ids.each do |message_id|
151
+ pipeline.del(message_key(message_id))
152
+ end
153
+ end
143
154
  end
144
155
 
145
156
  EventMachine.next_tick { callback.call(true) } if callback
@@ -166,9 +166,9 @@ module Faye
166
166
  begin
167
167
  message = JSON.parse(message_json)
168
168
 
169
- # Notify all subscribers
169
+ # Notify all subscribers (use dup to avoid concurrent modification)
170
170
  EventMachine.next_tick do
171
- @subscribers.each do |subscriber|
171
+ @subscribers.dup.each do |subscriber|
172
172
  subscriber.call(channel, message)
173
173
  end
174
174
  end
@@ -56,6 +56,11 @@ module Faye
56
56
  end
57
57
  end
58
58
 
59
+ # Clean up wildcard pattern if no more subscribers
60
+ if channel.include?('*')
61
+ cleanup_pattern_if_unused(channel)
62
+ end
63
+
59
64
  EventMachine.next_tick { callback.call(true) } if callback
60
65
  rescue => e
61
66
  log_error("Failed to unsubscribe client #{client_id} from #{channel}: #{e.message}")
@@ -160,6 +165,20 @@ module Faye
160
165
 
161
166
  private
162
167
 
168
+ def cleanup_pattern_if_unused(pattern)
169
+ subscribers = @connection.with_redis do |redis|
170
+ redis.smembers(channel_subscribers_key(pattern))
171
+ end
172
+
173
+ if subscribers.empty?
174
+ @connection.with_redis do |redis|
175
+ redis.srem(patterns_key, pattern)
176
+ end
177
+ end
178
+ rescue => e
179
+ log_error("Failed to cleanup pattern #{pattern}: #{e.message}")
180
+ end
181
+
163
182
  def client_subscriptions_key(client_id)
164
183
  namespace_key("subscriptions:#{client_id}")
165
184
  end
@@ -1,5 +1,5 @@
1
1
  module Faye
2
2
  class Redis
3
- VERSION = '1.0.2'
3
+ VERSION = '1.0.3'
4
4
  end
5
5
  end
data/lib/faye/redis.rb CHANGED
@@ -66,7 +66,9 @@ module Faye
66
66
  # Destroy a client
67
67
  def destroy_client(client_id, &callback)
68
68
  @subscription_manager.unsubscribe_all(client_id) do
69
- @client_registry.destroy(client_id, &callback)
69
+ @message_queue.clear(client_id) do
70
+ @client_registry.destroy(client_id, &callback)
71
+ end
70
72
  end
71
73
  end
72
74
 
@@ -93,26 +95,49 @@ module Faye
93
95
  # Publish a message to channels
94
96
  def publish(message, channels, &callback)
95
97
  channels = [channels] unless channels.is_a?(Array)
96
- success = true
97
98
 
98
99
  begin
100
+ remaining_operations = channels.size
101
+ success = true
102
+
99
103
  channels.each do |channel|
100
104
  # Store message in queues for subscribed clients
101
105
  @subscription_manager.get_subscribers(channel) do |client_ids|
102
- client_ids.each do |client_id|
103
- @message_queue.enqueue(client_id, message) do |enqueued|
104
- success &&= enqueued
106
+ enqueue_count = client_ids.size
107
+
108
+ if enqueue_count == 0
109
+ # No clients to enqueue, just do pub/sub
110
+ @pubsub_coordinator.publish(channel, message) do |published|
111
+ success &&= published
112
+ remaining_operations -= 1
113
+
114
+ if remaining_operations == 0 && callback
115
+ EventMachine.next_tick { callback.call(success) }
116
+ end
117
+ end
118
+ else
119
+ # Enqueue for all subscribed clients
120
+ client_ids.each do |client_id|
121
+ @message_queue.enqueue(client_id, message) do |enqueued|
122
+ success &&= enqueued
123
+ enqueue_count -= 1
124
+
125
+ # When all enqueues are done, do pub/sub
126
+ if enqueue_count == 0
127
+ @pubsub_coordinator.publish(channel, message) do |published|
128
+ success &&= published
129
+ remaining_operations -= 1
130
+
131
+ if remaining_operations == 0 && callback
132
+ EventMachine.next_tick { callback.call(success) }
133
+ end
134
+ end
135
+ end
136
+ end
105
137
  end
106
138
  end
107
139
  end
108
-
109
- # Publish to Redis pub/sub for cross-server routing
110
- @pubsub_coordinator.publish(channel, message) do |published|
111
- success &&= published
112
- end
113
140
  end
114
-
115
- EventMachine.next_tick { callback.call(success) } if callback
116
141
  rescue => e
117
142
  log_error("Failed to publish message to channels #{channels}: #{e.message}")
118
143
  EventMachine.next_tick { callback.call(false) } if callback
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: faye-redis-ng
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zac