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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fde6220c9baee883a47eced86a4cd7245ecca48a05a55c906660a5286260c401
4
- data.tar.gz: dd3e0d6190cc019070da513d57ac076537b7d8aa1626e29b17c1f7fb91910b79
3
+ metadata.gz: f87f5800dede3c5720fc5ab77944c3322f735cd808c1b02edd218fc6d97bca8b
4
+ data.tar.gz: 13d579f33d6620ab7ecdc3567b45dd7c4a58264ed0115b25ddc39454662b2e0c
5
5
  SHA512:
6
- metadata.gz: 968761695fa0cc17df7c810a345068bf0de18437fd909eba1ed803f784daff107734f846106f67154cfe1c5a3295905c61f1863de40a59a5304199147144fd80
7
- data.tar.gz: 8137b865b357b20024edbbb870ff51b76b44779eebe3db18e77167ef326a64322adae9beec35adad04c54028236f6b30c87736fa67642b2684ac9ef73a951c13
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.4...HEAD
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
@@ -1,5 +1,5 @@
1
1
  module Faye
2
2
  class Redis
3
- VERSION = '1.0.4'
3
+ VERSION = '1.0.6'
4
4
  end
5
5
  end
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
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-15 00:00:00.000000000 Z
11
+ date: 2025-10-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis