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 +4 -4
- data/CHANGELOG.md +20 -1
- data/LICENSE +1 -1
- data/README.md +1 -1
- data/lib/faye/redis/client_registry.rb +33 -4
- data/lib/faye/redis/message_queue.rb +12 -1
- data/lib/faye/redis/pubsub_coordinator.rb +2 -2
- data/lib/faye/redis/subscription_manager.rb +19 -0
- data/lib/faye/redis/version.rb +1 -1
- data/lib/faye/redis.rb +37 -12
- 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: d0fea6c56f6598af371ca22722f24899d5aab97ca4a73f52ae3e8a12e28ea20c
|
4
|
+
data.tar.gz: 5291d2eb437e9241211c6e8784ee582ad4f792be01ab628ee618f16f85d84c49
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
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
|
-
|
116
|
-
|
117
|
-
|
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.
|
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
|
data/lib/faye/redis/version.rb
CHANGED
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
|
-
@
|
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.
|
103
|
-
|
104
|
-
|
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
|