faye-redis-ng 1.0.4 → 1.0.6
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 +64 -1
- data/README.md +144 -0
- data/lib/faye/redis/version.rb +1 -1
- data/lib/faye/redis.rb +118 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f87f5800dede3c5720fc5ab77944c3322f735cd808c1b02edd218fc6d97bca8b
|
|
4
|
+
data.tar.gz: 13d579f33d6620ab7ecdc3567b45dd7c4a58264ed0115b25ddc39454662b2e0c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fbd69bad2590d4e55b141941952c95e316c3d8dd2a1e37f4ebbe3129ffe00b48957e8ba86c02732ef67aad97200c47b27dc6c663c5c41640cbaa0dd6787e8514
|
|
7
|
+
data.tar.gz: 8c08572629040c40e130f03c0881e864f02a1dbfb89b5db9c0509c255d4cf986a9f38bf5267c4834216dcae0ca26ef6c115742b42c4f61095c9e4cb14264824a
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,67 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.0.6] - 2025-10-30
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Automatic Garbage Collection**: Implemented automatic GC timer that runs periodically to clean up expired clients and orphaned data
|
|
14
|
+
- New `gc_interval` configuration option (default: 60 seconds)
|
|
15
|
+
- Automatically starts when EventMachine is running
|
|
16
|
+
- Can be disabled by setting `gc_interval` to 0 or false
|
|
17
|
+
- Lazy initialization ensures timer starts even if engine is created before EventMachine starts
|
|
18
|
+
- Timer is properly stopped on disconnect to prevent resource leaks
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- **Improved User Experience**: No longer requires manual setup of periodic cleanup
|
|
22
|
+
- Memory leak prevention is now automatic by default
|
|
23
|
+
- Matches behavior of original faye-redis-ruby project
|
|
24
|
+
- Users can still manually call `cleanup_expired` if needed
|
|
25
|
+
- Custom GC schedules possible by disabling automatic GC
|
|
26
|
+
|
|
27
|
+
### Technical Details
|
|
28
|
+
The automatic GC timer:
|
|
29
|
+
- Runs `cleanup_expired` every 60 seconds by default
|
|
30
|
+
- Only starts when EventMachine reactor is running
|
|
31
|
+
- Supports lazy initialization for engines created outside EM context
|
|
32
|
+
- Properly handles cleanup on disconnect
|
|
33
|
+
- Can be customized or disabled via `gc_interval` option
|
|
34
|
+
|
|
35
|
+
## [1.0.5] - 2025-10-30
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
- **Memory Leak**: Fixed critical memory leak where subscription keys were never cleaned up after client disconnection
|
|
39
|
+
- Orphaned `subscriptions:{client_id}` keys remained permanently in Redis
|
|
40
|
+
- Orphaned `subscription:{client_id}:{channel}` hash keys accumulated over time
|
|
41
|
+
- Orphaned client IDs remained in `channels:{channel}` sets
|
|
42
|
+
- Message queues for disconnected clients were not cleaned up
|
|
43
|
+
- Could result in hundreds of MB memory leak in production environments
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
- **`cleanup_expired` Method**: New public method to clean up expired clients and orphaned data
|
|
47
|
+
- Automatically detects and removes orphaned subscription keys
|
|
48
|
+
- Cleans up message queues for disconnected clients
|
|
49
|
+
- Removes stale client IDs from channel subscriber lists
|
|
50
|
+
- Uses Redis SCAN to avoid blocking operations
|
|
51
|
+
- Batch deletion using pipelining for efficiency
|
|
52
|
+
- Can be called manually or scheduled as periodic task
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
- **Improved Cleanup Strategy**: Enhanced cleanup process now handles orphaned data
|
|
56
|
+
- `cleanup_expired` now cleans both expired clients AND orphaned subscriptions
|
|
57
|
+
- Works even when no expired clients are found
|
|
58
|
+
- Prevents memory leaks from abnormal client disconnections
|
|
59
|
+
|
|
60
|
+
### Technical Details
|
|
61
|
+
Memory leak scenario (before fix):
|
|
62
|
+
- 10,000 abnormally disconnected clients × 5 channels each = 50,000+ orphaned keys
|
|
63
|
+
- Estimated memory waste: 100-500 MB
|
|
64
|
+
- Keys remained permanently without TTL
|
|
65
|
+
|
|
66
|
+
After fix:
|
|
67
|
+
- All orphaned keys cleaned up automatically
|
|
68
|
+
- Memory usage remains stable
|
|
69
|
+
- Production environments can schedule periodic cleanup
|
|
70
|
+
|
|
10
71
|
## [1.0.4] - 2025-10-15
|
|
11
72
|
|
|
12
73
|
### Performance
|
|
@@ -90,7 +151,9 @@ For 100 subscribers receiving one message:
|
|
|
90
151
|
### Security
|
|
91
152
|
- Client and message IDs now use `SecureRandom.uuid` instead of predictable time-based generation
|
|
92
153
|
|
|
93
|
-
[Unreleased]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.
|
|
154
|
+
[Unreleased]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.6...HEAD
|
|
155
|
+
[1.0.6]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.5...v1.0.6
|
|
156
|
+
[1.0.5]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.4...v1.0.5
|
|
94
157
|
[1.0.4]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.3...v1.0.4
|
|
95
158
|
[1.0.3]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.2...v1.0.3
|
|
96
159
|
[1.0.2]: https://github.com/7a6163/faye-redis-ng/compare/v1.0.1...v1.0.2
|
data/README.md
CHANGED
|
@@ -81,6 +81,9 @@ bayeux = Faye::RackAdapter.new(app, {
|
|
|
81
81
|
client_timeout: 60, # Client session timeout (seconds)
|
|
82
82
|
message_ttl: 3600, # Message TTL (seconds)
|
|
83
83
|
|
|
84
|
+
# Garbage collection
|
|
85
|
+
gc_interval: 60, # Automatic GC interval (seconds), set to 0 or false to disable
|
|
86
|
+
|
|
84
87
|
# Logging
|
|
85
88
|
log_level: :info, # Log level (:silent, :info, :debug)
|
|
86
89
|
|
|
@@ -234,6 +237,147 @@ The CI/CD pipeline will automatically:
|
|
|
234
237
|
- Add `RUBYGEMS_API_KEY` to GitHub repository secrets
|
|
235
238
|
- The tag must start with 'v' (e.g., v0.1.0, v1.2.3)
|
|
236
239
|
|
|
240
|
+
## Memory Management
|
|
241
|
+
|
|
242
|
+
### Automatic Garbage Collection
|
|
243
|
+
|
|
244
|
+
**New in v1.0.6**: faye-redis-ng now includes automatic garbage collection that runs every 60 seconds by default. This automatically cleans up expired clients and orphaned subscription keys, preventing memory leaks without any manual intervention.
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
bayeux = Faye::RackAdapter.new(app, {
|
|
248
|
+
mount: '/faye',
|
|
249
|
+
timeout: 25,
|
|
250
|
+
engine: {
|
|
251
|
+
type: Faye::Redis,
|
|
252
|
+
host: 'localhost',
|
|
253
|
+
port: 6379,
|
|
254
|
+
gc_interval: 60 # Run GC every 60 seconds (default)
|
|
255
|
+
}
|
|
256
|
+
})
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
To customize the GC interval or disable it:
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
engine: {
|
|
263
|
+
type: Faye::Redis,
|
|
264
|
+
host: 'localhost',
|
|
265
|
+
port: 6379,
|
|
266
|
+
gc_interval: 300 # Run GC every 5 minutes
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
# Or disable automatic GC
|
|
270
|
+
engine: {
|
|
271
|
+
type: Faye::Redis,
|
|
272
|
+
host: 'localhost',
|
|
273
|
+
port: 6379,
|
|
274
|
+
gc_interval: 0 # Disabled - you'll need to call cleanup_expired manually
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Manual Cleanup
|
|
279
|
+
|
|
280
|
+
If you've disabled automatic GC, you can manually clean up expired clients:
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
# Get the engine instance
|
|
284
|
+
engine = bayeux.get_engine
|
|
285
|
+
|
|
286
|
+
# Clean up expired clients and orphaned data
|
|
287
|
+
engine.cleanup_expired do |expired_count|
|
|
288
|
+
puts "Cleaned up #{expired_count} expired clients"
|
|
289
|
+
end
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
#### Custom GC Schedule (Optional)
|
|
293
|
+
|
|
294
|
+
If you need more control, you can disable automatic GC and implement your own schedule:
|
|
295
|
+
|
|
296
|
+
```ruby
|
|
297
|
+
require 'eventmachine'
|
|
298
|
+
require 'faye'
|
|
299
|
+
require 'faye-redis-ng'
|
|
300
|
+
|
|
301
|
+
bayeux = Faye::RackAdapter.new(app, {
|
|
302
|
+
mount: '/faye',
|
|
303
|
+
timeout: 25,
|
|
304
|
+
engine: {
|
|
305
|
+
type: Faye::Redis,
|
|
306
|
+
host: 'localhost',
|
|
307
|
+
port: 6379,
|
|
308
|
+
namespace: 'my-app',
|
|
309
|
+
gc_interval: 0 # Disable automatic GC
|
|
310
|
+
}
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
# Custom cleanup schedule - every 5 minutes
|
|
314
|
+
EM.add_periodic_timer(300) do
|
|
315
|
+
bayeux.get_engine.cleanup_expired do |count|
|
|
316
|
+
puts "[#{Time.now}] Cleaned up #{count} expired clients" if count > 0
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
run bayeux
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
#### Using Rake Task
|
|
324
|
+
|
|
325
|
+
Create a Rake task for manual or scheduled cleanup:
|
|
326
|
+
|
|
327
|
+
```ruby
|
|
328
|
+
# lib/tasks/faye_cleanup.rake
|
|
329
|
+
namespace :faye do
|
|
330
|
+
desc "Clean up expired Faye clients and orphaned subscriptions"
|
|
331
|
+
task cleanup: :environment do
|
|
332
|
+
require 'eventmachine'
|
|
333
|
+
|
|
334
|
+
EM.run do
|
|
335
|
+
engine = Faye::Redis.new(
|
|
336
|
+
nil,
|
|
337
|
+
host: ENV['REDIS_HOST'] || 'localhost',
|
|
338
|
+
port: ENV['REDIS_PORT']&.to_i || 6379,
|
|
339
|
+
namespace: 'my-app'
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
engine.cleanup_expired do |count|
|
|
343
|
+
puts "✅ Cleaned up #{count} expired clients"
|
|
344
|
+
engine.disconnect
|
|
345
|
+
EM.stop
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Then schedule it with cron:
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
# Run cleanup every hour
|
|
356
|
+
0 * * * * cd /path/to/app && bundle exec rake faye:cleanup
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### What Gets Cleaned Up
|
|
360
|
+
|
|
361
|
+
The `cleanup_expired` method removes:
|
|
362
|
+
|
|
363
|
+
1. **Expired client keys** (`clients:{client_id}`)
|
|
364
|
+
2. **Orphaned subscription lists** (`subscriptions:{client_id}`)
|
|
365
|
+
3. **Orphaned subscription metadata** (`subscription:{client_id}:{channel}`)
|
|
366
|
+
4. **Stale client IDs from channel subscribers** (`channels:{channel}`)
|
|
367
|
+
5. **Orphaned message queues** (`messages:{client_id}`)
|
|
368
|
+
|
|
369
|
+
### Memory Leak Prevention
|
|
370
|
+
|
|
371
|
+
**v1.0.6+**: Automatic garbage collection is now enabled by default, preventing memory leaks from orphaned keys without any configuration needed.
|
|
372
|
+
|
|
373
|
+
Without GC, abnormal client disconnections (crashes, network failures, etc.) can cause orphaned keys to accumulate:
|
|
374
|
+
|
|
375
|
+
- **Before v1.0.5**: 10,000 orphaned clients × 5 channels = 50,000+ keys = 100-500 MB leaked
|
|
376
|
+
- **v1.0.5**: Manual cleanup required via `cleanup_expired` method
|
|
377
|
+
- **v1.0.6+**: Automatic GC runs every 60 seconds by default - no manual intervention needed
|
|
378
|
+
|
|
379
|
+
The automatic GC ensures memory usage remains stable even with frequent client disconnections.
|
|
380
|
+
|
|
237
381
|
## Troubleshooting
|
|
238
382
|
|
|
239
383
|
### Connection Issues
|
data/lib/faye/redis/version.rb
CHANGED
data/lib/faye/redis.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'securerandom'
|
|
2
|
+
require 'set'
|
|
2
3
|
require_relative 'redis/version'
|
|
3
4
|
require_relative 'redis/logger'
|
|
4
5
|
require_relative 'redis/connection'
|
|
@@ -24,7 +25,8 @@ module Faye
|
|
|
24
25
|
retry_delay: 1,
|
|
25
26
|
client_timeout: 60,
|
|
26
27
|
message_ttl: 3600,
|
|
27
|
-
namespace: 'faye'
|
|
28
|
+
namespace: 'faye',
|
|
29
|
+
gc_interval: 60 # Automatic garbage collection interval (seconds), set to 0 or false to disable
|
|
28
30
|
}.freeze
|
|
29
31
|
|
|
30
32
|
attr_reader :server, :options, :connection, :client_registry,
|
|
@@ -49,10 +51,16 @@ module Faye
|
|
|
49
51
|
|
|
50
52
|
# Set up message routing
|
|
51
53
|
setup_message_routing
|
|
54
|
+
|
|
55
|
+
# Start automatic garbage collection timer
|
|
56
|
+
start_gc_timer
|
|
52
57
|
end
|
|
53
58
|
|
|
54
59
|
# Create a new client
|
|
55
60
|
def create_client(&callback)
|
|
61
|
+
# Ensure GC timer is started (lazy initialization)
|
|
62
|
+
ensure_gc_timer_started
|
|
63
|
+
|
|
56
64
|
client_id = generate_client_id
|
|
57
65
|
@client_registry.create(client_id) do |success|
|
|
58
66
|
if success
|
|
@@ -135,10 +143,26 @@ module Faye
|
|
|
135
143
|
|
|
136
144
|
# Disconnect the engine
|
|
137
145
|
def disconnect
|
|
146
|
+
# Stop GC timer if running
|
|
147
|
+
stop_gc_timer
|
|
148
|
+
|
|
138
149
|
@pubsub_coordinator.disconnect
|
|
139
150
|
@connection.disconnect
|
|
140
151
|
end
|
|
141
152
|
|
|
153
|
+
# Clean up expired clients and their associated data
|
|
154
|
+
def cleanup_expired(&callback)
|
|
155
|
+
@client_registry.cleanup_expired do |expired_count|
|
|
156
|
+
@logger.info("Cleaned up #{expired_count} expired clients") if expired_count > 0
|
|
157
|
+
|
|
158
|
+
# Always clean up orphaned subscription keys (even if no expired clients)
|
|
159
|
+
# This handles cases where subscriptions were orphaned due to crashes
|
|
160
|
+
cleanup_orphaned_subscriptions do
|
|
161
|
+
callback.call(expired_count) if callback
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
142
166
|
private
|
|
143
167
|
|
|
144
168
|
def generate_client_id
|
|
@@ -171,6 +195,62 @@ module Faye
|
|
|
171
195
|
end
|
|
172
196
|
end
|
|
173
197
|
|
|
198
|
+
def cleanup_orphaned_subscriptions(&callback)
|
|
199
|
+
# Get all active client IDs
|
|
200
|
+
@client_registry.all do |active_clients|
|
|
201
|
+
active_set = active_clients.to_set
|
|
202
|
+
namespace = @options[:namespace] || 'faye'
|
|
203
|
+
|
|
204
|
+
# Scan for subscription keys and clean up orphaned ones
|
|
205
|
+
@connection.with_redis do |redis|
|
|
206
|
+
cursor = "0"
|
|
207
|
+
orphaned_keys = []
|
|
208
|
+
|
|
209
|
+
loop do
|
|
210
|
+
cursor, keys = redis.scan(cursor, match: "#{namespace}:subscriptions:*", count: 100)
|
|
211
|
+
|
|
212
|
+
keys.each do |key|
|
|
213
|
+
# Extract client_id from key (format: namespace:subscriptions:client_id)
|
|
214
|
+
client_id = key.split(':').last
|
|
215
|
+
orphaned_keys << client_id unless active_set.include?(client_id)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
break if cursor == "0"
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Clean up orphaned subscription data
|
|
222
|
+
if orphaned_keys.any?
|
|
223
|
+
@logger.info("Cleaning up #{orphaned_keys.size} orphaned subscription sets")
|
|
224
|
+
|
|
225
|
+
orphaned_keys.each do |client_id|
|
|
226
|
+
# Get channels for this orphaned client
|
|
227
|
+
channels = redis.smembers("#{namespace}:subscriptions:#{client_id}")
|
|
228
|
+
|
|
229
|
+
# Remove in batch
|
|
230
|
+
redis.pipelined do |pipeline|
|
|
231
|
+
# Delete client's subscription list
|
|
232
|
+
pipeline.del("#{namespace}:subscriptions:#{client_id}")
|
|
233
|
+
|
|
234
|
+
# Delete each subscription metadata and remove from channel subscribers
|
|
235
|
+
channels.each do |channel|
|
|
236
|
+
pipeline.del("#{namespace}:subscription:#{client_id}:#{channel}")
|
|
237
|
+
pipeline.srem("#{namespace}:channels:#{channel}", client_id)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Delete message queue if exists
|
|
241
|
+
pipeline.del("#{namespace}:messages:#{client_id}")
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
EventMachine.next_tick { callback.call } if callback
|
|
248
|
+
end
|
|
249
|
+
rescue => e
|
|
250
|
+
log_error("Failed to cleanup orphaned subscriptions: #{e.message}")
|
|
251
|
+
EventMachine.next_tick { callback.call } if callback
|
|
252
|
+
end
|
|
253
|
+
|
|
174
254
|
def setup_message_routing
|
|
175
255
|
# Subscribe to message events from other servers
|
|
176
256
|
@pubsub_coordinator.on_message do |channel, message|
|
|
@@ -184,5 +264,42 @@ module Faye
|
|
|
184
264
|
def log_error(message)
|
|
185
265
|
@logger.error(message)
|
|
186
266
|
end
|
|
267
|
+
|
|
268
|
+
# Start automatic garbage collection timer
|
|
269
|
+
def start_gc_timer
|
|
270
|
+
gc_interval = @options[:gc_interval]
|
|
271
|
+
|
|
272
|
+
# Skip if GC is disabled (0, false, or nil)
|
|
273
|
+
return if !gc_interval || gc_interval == 0
|
|
274
|
+
|
|
275
|
+
# Only start timer if EventMachine is running
|
|
276
|
+
return unless EventMachine.reactor_running?
|
|
277
|
+
|
|
278
|
+
@logger.info("Starting automatic GC timer with interval: #{gc_interval} seconds")
|
|
279
|
+
|
|
280
|
+
@gc_timer = EventMachine.add_periodic_timer(gc_interval) do
|
|
281
|
+
@logger.debug("Running automatic garbage collection")
|
|
282
|
+
cleanup_expired do |count|
|
|
283
|
+
@logger.debug("GC completed: #{count} expired clients cleaned") if count > 0
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Ensure GC timer is started (called lazily on first operation)
|
|
289
|
+
def ensure_gc_timer_started
|
|
290
|
+
return if @gc_timer # Already started
|
|
291
|
+
return if !@options[:gc_interval] || @options[:gc_interval] == 0 # Disabled
|
|
292
|
+
|
|
293
|
+
start_gc_timer
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Stop automatic garbage collection timer
|
|
297
|
+
def stop_gc_timer
|
|
298
|
+
if @gc_timer
|
|
299
|
+
EventMachine.cancel_timer(@gc_timer)
|
|
300
|
+
@gc_timer = nil
|
|
301
|
+
@logger.info("Stopped automatic GC timer")
|
|
302
|
+
end
|
|
303
|
+
end
|
|
187
304
|
end
|
|
188
305
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: faye-redis-ng
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Zac
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-10-
|
|
11
|
+
date: 2025-10-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: redis
|