smart_message 0.0.12 → 0.0.13

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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -1
  3. data/Gemfile.lock +5 -5
  4. data/docs/core-concepts/architecture.md +5 -10
  5. data/docs/getting-started/examples.md +0 -12
  6. data/docs/getting-started/quick-start.md +4 -9
  7. data/docs/index.md +4 -4
  8. data/docs/reference/serializers.md +160 -488
  9. data/docs/reference/transports.md +1 -125
  10. data/docs/transports/redis-transport-comparison.md +215 -350
  11. data/docs/transports/redis-transport.md +3 -22
  12. data/examples/README.md +6 -9
  13. data/examples/city_scenario/README.md +1 -1
  14. data/examples/city_scenario/messages/emergency_911_message.rb +0 -1
  15. data/examples/city_scenario/messages/emergency_resolved_message.rb +0 -1
  16. data/examples/city_scenario/messages/fire_dispatch_message.rb +0 -1
  17. data/examples/city_scenario/messages/fire_emergency_message.rb +0 -1
  18. data/examples/city_scenario/messages/health_check_message.rb +0 -1
  19. data/examples/city_scenario/messages/health_status_message.rb +0 -1
  20. data/examples/city_scenario/messages/police_dispatch_message.rb +0 -1
  21. data/examples/city_scenario/messages/silent_alarm_message.rb +0 -1
  22. data/examples/memory/01_message_deduplication_demo.rb +0 -2
  23. data/examples/memory/02_dead_letter_queue_demo.rb +0 -3
  24. data/examples/memory/03_point_to_point_orders.rb +0 -2
  25. data/examples/memory/04_publish_subscribe_events.rb +0 -1
  26. data/examples/memory/05_many_to_many_chat.rb +0 -3
  27. data/examples/memory/07_proc_handlers_demo.rb +0 -1
  28. data/examples/memory/08_custom_logger_demo.rb +0 -4
  29. data/examples/memory/09_error_handling_demo.rb +0 -3
  30. data/examples/memory/10_entity_addressing_basic.rb +0 -6
  31. data/examples/memory/11_entity_addressing_with_filtering.rb +0 -4
  32. data/examples/memory/12_regex_filtering_microservices.rb +0 -1
  33. data/examples/memory/13_header_block_configuration.rb +0 -5
  34. data/examples/memory/14_global_configuration_demo.rb +0 -2
  35. data/examples/memory/15_logger_demo.rb +0 -1
  36. data/examples/memory/README.md +3 -3
  37. data/examples/redis/01_smart_home_iot_demo.rb +0 -4
  38. data/examples/redis/README.md +0 -2
  39. data/lib/smart_message/base.rb +19 -10
  40. data/lib/smart_message/configuration.rb +2 -23
  41. data/lib/smart_message/dead_letter_queue.rb +1 -1
  42. data/lib/smart_message/messaging.rb +3 -62
  43. data/lib/smart_message/plugins.rb +1 -42
  44. data/lib/smart_message/transport/base.rb +42 -8
  45. data/lib/smart_message/transport/memory_transport.rb +23 -4
  46. data/lib/smart_message/transport/redis_transport.rb +11 -0
  47. data/lib/smart_message/transport/registry.rb +0 -1
  48. data/lib/smart_message/transport/stdout_transport.rb +28 -10
  49. data/lib/smart_message/transport.rb +0 -1
  50. data/lib/smart_message/version.rb +1 -1
  51. metadata +2 -28
  52. data/docs/guides/redis-queue-getting-started.md +0 -697
  53. data/docs/guides/redis-queue-patterns.md +0 -889
  54. data/docs/guides/redis-queue-production.md +0 -1091
  55. data/docs/transports/redis-enhanced-transport.md +0 -524
  56. data/docs/transports/redis-queue-transport.md +0 -1304
  57. data/examples/redis_enhanced/README.md +0 -319
  58. data/examples/redis_enhanced/enhanced_01_basic_patterns.rb +0 -233
  59. data/examples/redis_enhanced/enhanced_02_fluent_api.rb +0 -331
  60. data/examples/redis_enhanced/enhanced_03_dual_publishing.rb +0 -281
  61. data/examples/redis_enhanced/enhanced_04_advanced_routing.rb +0 -419
  62. data/examples/redis_queue/01_basic_messaging.rb +0 -221
  63. data/examples/redis_queue/01_comprehensive_examples.rb +0 -508
  64. data/examples/redis_queue/02_pattern_routing.rb +0 -405
  65. data/examples/redis_queue/03_fluent_api.rb +0 -422
  66. data/examples/redis_queue/04_load_balancing.rb +0 -486
  67. data/examples/redis_queue/05_microservices.rb +0 -735
  68. data/examples/redis_queue/06_emergency_alerts.rb +0 -777
  69. data/examples/redis_queue/07_queue_management.rb +0 -587
  70. data/examples/redis_queue/README.md +0 -366
  71. data/examples/redis_queue/enhanced_01_basic_patterns.rb +0 -233
  72. data/examples/redis_queue/enhanced_02_fluent_api.rb +0 -331
  73. data/examples/redis_queue/enhanced_03_dual_publishing.rb +0 -281
  74. data/examples/redis_queue/enhanced_04_advanced_routing.rb +0 -419
  75. data/examples/redis_queue/redis_queue_architecture.svg +0 -148
  76. data/lib/smart_message/transport/redis_enhanced_transport.rb +0 -399
  77. data/lib/smart_message/transport/redis_queue_transport.rb +0 -555
@@ -1,555 +0,0 @@
1
- # lib/smart_message/transport/redis_queue_transport_async.rb
2
- # encoding: utf-8
3
- # frozen_string_literal: true
4
-
5
- require 'async'
6
- require 'async/redis'
7
- require 'json'
8
-
9
- module SmartMessage
10
- module Transport
11
- # Redis Queue Transport - Async-powered routing with RabbitMQ-style patterns
12
- # This transport provides intelligent routing using Redis Lists as queues with pattern matching
13
- # Built on Ruby's Async framework for modern concurrency and testing
14
- #
15
- # Key Features:
16
- # - Async/Fiber-based concurrency (thousands of subscriptions)
17
- # - RabbitMQ-style topic exchange pattern matching (#.*.my_uuid)
18
- # - Load balancing via consumer groups
19
- # - Queue persistence using Redis Lists
20
- # - FIFO message ordering
21
- # - 10x faster than RabbitMQ with same routing intelligence
22
- # - Test-friendly with proper async lifecycle management
23
- #
24
- # Usage:
25
- # Async do
26
- # transport = SmartMessage::Transport::RedisQueueTransport.new
27
- # transport.subscribe_pattern("#.*.my_service") do |msg_class, data|
28
- # puts "Processing: #{msg_class}"
29
- # end
30
- # end
31
- class RedisQueueTransport < Base
32
-
33
- DEFAULT_CONFIG = {
34
- url: 'redis://localhost:6379',
35
- db: 0,
36
- exchange_name: 'smart_message',
37
- queue_prefix: 'smart_message.queue',
38
- routing_prefix: 'smart_message.routing',
39
- consumer_timeout: 1, # 1 second blocking pop timeout
40
- max_queue_size: 10000, # Max messages per queue (circular buffer)
41
- cleanup_on_disconnect: true, # Remove queues on shutdown
42
- reconnect_attempts: 5,
43
- reconnect_delay: 1,
44
- async_timeout: 30 # Global async task timeout
45
- }.freeze
46
-
47
- attr_reader :redis, :exchange_name, :active_queues, :consumer_tasks
48
-
49
- def initialize(**options)
50
- @active_queues = {} # queue_name => consumer_info
51
- @consumer_tasks = {} # queue_name => async task
52
- @routing_table = {} # pattern => [queue_names]
53
- @shutdown = false
54
- # Don't initialize @redis to nil here since super() calls configure which sets it
55
- super(**options)
56
- end
57
-
58
- def configure
59
- @exchange_name = @options[:exchange_name]
60
-
61
- # Setup Redis connection synchronously for immediate availability
62
- begin
63
- redis_client = nil
64
- Async do
65
- endpoint = Async::Redis::Endpoint.parse(@options[:url], db: @options[:db])
66
- redis_client = Async::Redis::Client.new(endpoint)
67
-
68
- # Test connection
69
- redis_client.call('PING')
70
-
71
- # Initialize exchange metadata with local redis client
72
- @redis = redis_client # Set instance variable
73
- setup_exchange
74
-
75
- logger.info { "[RedisQueue] Async transport configured with exchange: #{@exchange_name}" }
76
- end.wait
77
-
78
- # Ensure @redis is set outside the async block too
79
- @redis = redis_client if redis_client
80
- rescue => e
81
- logger.error { "[RedisQueue] Failed to configure transport: #{e.message}" }
82
- @redis = nil
83
- raise
84
- end
85
- end
86
-
87
- def default_options
88
- DEFAULT_CONFIG
89
- end
90
-
91
- # Publish message with intelligent routing to matching queues (Async)
92
- # @param message_class [String] The message class name
93
- # @param serialized_message [String] The serialized message content
94
- def do_publish(message_class, serialized_message)
95
- async_task do
96
- routing_info = extract_routing_info(serialized_message)
97
- routing_key = build_enhanced_routing_key(message_class, routing_info)
98
-
99
- # Find all queues that match this routing key (like RabbitMQ topic exchange)
100
- matching_queues = find_matching_queues(routing_key)
101
-
102
- if matching_queues.empty?
103
- logger.debug { "[RedisQueue] No queues match routing key: #{routing_key}" }
104
- next
105
- end
106
-
107
- # Create message envelope with metadata
108
- message_envelope = {
109
- routing_key: routing_key,
110
- message_class: message_class.to_s,
111
- data: serialized_message,
112
- timestamp: Time.now.to_f,
113
- headers: routing_info
114
- }.to_json
115
-
116
- # Publish to all matching queues atomically using async redis
117
- published_count = 0
118
-
119
- # Use pipelined operations for better performance
120
- commands = []
121
- matching_queues.each do |queue_name|
122
- commands << [:lpush, queue_name, message_envelope]
123
- commands << [:ltrim, queue_name, 0, @options[:max_queue_size] - 1]
124
- published_count += 1
125
- end
126
-
127
- # Execute all commands in pipeline
128
- @redis.pipelined(commands) if commands.any?
129
-
130
- logger.debug { "[RedisQueue] Published to #{published_count} queues with key: #{routing_key}" }
131
- rescue => e
132
- logger.error { "[RedisQueue] Publish error: #{e.message}" }
133
- raise
134
- end
135
- end
136
-
137
- # Subscribe to messages using RabbitMQ-style pattern matching (Async)
138
- # @param pattern [String] Routing pattern (e.g., "#.*.my_service", "order.#.*.*")
139
- # @param process_method [String] Method identifier for processing
140
- # @param filter_options [Hash] Additional filtering options
141
- # @param block [Proc] Optional block for message processing
142
- def subscribe_pattern(pattern, process_method = :process, filter_options = {}, &block)
143
- queue_name = derive_queue_name(pattern, filter_options)
144
-
145
- # Add pattern to routing table (no mutex needed with Fibers)
146
- @routing_table[pattern] ||= []
147
- @routing_table[pattern] << queue_name unless @routing_table[pattern].include?(queue_name)
148
-
149
- # Store queue metadata
150
- @active_queues[queue_name] = {
151
- pattern: pattern,
152
- process_method: process_method,
153
- filter_options: filter_options,
154
- created_at: Time.now,
155
- block_handler: block
156
- }
157
-
158
- # Start async consumer task for this queue (unless in test mode)
159
- start_queue_consumer(queue_name) unless @options[:test_mode]
160
-
161
- logger.info { "[RedisQueue] Subscribed to pattern '#{pattern}' via queue '#{queue_name}'" }
162
- end
163
-
164
- # Subscribe to all messages sent to a specific recipient
165
- # @param recipient_id [String] The recipient identifier
166
- def subscribe_to_recipient(recipient_id, process_method = :process, &block)
167
- pattern = "#.*.#{sanitize_for_routing_key(recipient_id)}"
168
- subscribe_pattern(pattern, process_method, {}, &block)
169
- end
170
-
171
- # Subscribe to all messages from a specific sender
172
- # @param sender_id [String] The sender identifier
173
- def subscribe_from_sender(sender_id, process_method = :process, &block)
174
- pattern = "#.#{sanitize_for_routing_key(sender_id)}.*"
175
- subscribe_pattern(pattern, process_method, {}, &block)
176
- end
177
-
178
- # Subscribe to all messages of a specific type regardless of routing
179
- # @param message_type [String] The message class name
180
- def subscribe_to_type(message_type, process_method = :process, &block)
181
- pattern = "*.#{message_type.to_s.gsub('::', '.').downcase}.*.*"
182
- subscribe_pattern(pattern, process_method, {}, &block)
183
- end
184
-
185
- # Subscribe to all alert/emergency messages
186
- def subscribe_to_alerts(process_method = :process, &block)
187
- patterns = [
188
- "emergency.#.*.*",
189
- "#.alert.*.*",
190
- "#.alarm.*.*",
191
- "#.critical.*.*"
192
- ]
193
-
194
- patterns.each { |pattern| subscribe_pattern(pattern, process_method, {}, &block) }
195
- end
196
-
197
- # Subscribe to all broadcast messages
198
- def subscribe_to_broadcasts(process_method = :process, &block)
199
- pattern = "#.*.broadcast"
200
- subscribe_pattern(pattern, process_method, {}, &block)
201
- end
202
-
203
- # Test-friendly disconnect method
204
- def disconnect
205
- @shutdown = true
206
-
207
- # Stop all consumer tasks
208
- @consumer_tasks.each do |queue_name, task|
209
- task.stop if task&.running?
210
- rescue => e
211
- logger.debug { "[RedisQueue] Error stopping consumer task for #{queue_name}: #{e.message}" }
212
- end
213
-
214
- @consumer_tasks.clear
215
-
216
- # Close Redis connection
217
- async_task do
218
- @redis&.close
219
- @redis = nil
220
- rescue => e
221
- logger.debug { "[RedisQueue] Error closing Redis connection: #{e.message}" }
222
- end
223
- end
224
-
225
- def connected?
226
- return false unless @redis
227
-
228
- # Test connection with async ping
229
- begin
230
- if Async::Task.current?
231
- # Already in async context
232
- return @redis.call('PING') == 'PONG'
233
- else
234
- # Need to create async context
235
- result = Async do
236
- @redis.call('PING') == 'PONG'
237
- end.wait
238
- return result
239
- end
240
- rescue => e
241
- logger.debug { "[RedisQueue] Connection test failed: #{e.message}" }
242
- return false
243
- end
244
- end
245
-
246
- # Get statistics about all active queues
247
- def queue_stats
248
- return {} unless @redis
249
-
250
- stats = {}
251
-
252
- begin
253
- Async do
254
- @active_queues.each do |queue_name, queue_info|
255
- length = @redis.llen(queue_name).to_i
256
- stats[queue_name] = {
257
- length: length,
258
- pattern: queue_info[:pattern],
259
- created_at: queue_info[:created_at],
260
- consumers: @consumer_tasks.key?(queue_name) ? 1 : 0
261
- }
262
- end
263
- end.wait
264
- rescue => e
265
- logger.error { "[RedisQueue] Error getting queue stats: #{e.message}" }
266
- end
267
-
268
- stats
269
- end
270
-
271
- # Get the current routing table
272
- def routing_table
273
- @routing_table.dup
274
- end
275
-
276
- # Fluent API builder for complex subscriptions
277
- def where
278
- RedisQueueSubscriptionBuilder.new(self)
279
- end
280
-
281
- private
282
-
283
- # Ensure async operations run in proper context
284
- def async_task(&block)
285
- if Async::Task.current?
286
- # Already in async context
287
- yield
288
- else
289
- # Need to create async context
290
- Async { yield }
291
- end
292
- end
293
-
294
- # Start async consumer for a queue
295
- def start_queue_consumer(queue_name)
296
- return if @consumer_tasks.key?(queue_name)
297
-
298
- @consumer_tasks[queue_name] = async_task do |task|
299
- begin
300
- consume_from_queue(queue_name)
301
- rescue => e
302
- logger.error { "[RedisQueue] Consumer task error for #{queue_name}: #{e.message}" }
303
- ensure
304
- @consumer_tasks.delete(queue_name)
305
- end
306
- end
307
- end
308
-
309
- # Async consumer loop for a specific queue
310
- def consume_from_queue(queue_name)
311
- queue_info = @active_queues[queue_name]
312
- return unless queue_info
313
-
314
- while !@shutdown
315
- begin
316
- # Use BRPOP for blocking read with timeout (cooperative blocking)
317
- result = @redis.brpop(queue_name, timeout: @options[:consumer_timeout])
318
-
319
- if result && result.length >= 2
320
- _, message_envelope = result
321
- process_queue_message(message_envelope, queue_info)
322
- end
323
-
324
- rescue => e
325
- logger.error { "[RedisQueue] Redis connection error in consumer: #{e.message}" }
326
- # Async will handle reconnection automatically
327
- sleep(1) unless @shutdown
328
- rescue => e
329
- logger.error { "[RedisQueue] Consumer error for #{queue_name}: #{e.message}" }
330
- sleep(1) unless @shutdown
331
- end
332
- end
333
- end
334
-
335
- # Process a message from the queue
336
- def process_queue_message(message_envelope, queue_info)
337
- begin
338
- message_data = JSON.parse(message_envelope)
339
-
340
- message_class = message_data['message_class']
341
- payload = message_data['data']
342
- headers = message_data['headers'] || {}
343
-
344
- # Apply additional filtering if specified
345
- if should_process_message?(headers, queue_info[:filter_options])
346
- # Use block handler if provided, otherwise route through dispatcher
347
- if queue_info[:block_handler]
348
- queue_info[:block_handler].call(message_class, payload)
349
- else
350
- # Route through dispatcher (inherited from Base)
351
- receive(message_class, payload)
352
- end
353
- else
354
- logger.debug { "[RedisQueue] Message filtered out by queue rules" }
355
- end
356
-
357
- rescue JSON::ParserError => e
358
- logger.error { "[RedisQueue] Invalid message envelope: #{e.message}" }
359
- rescue => e
360
- logger.error { "[RedisQueue] Error processing message: #{e.message}" }
361
- end
362
- end
363
-
364
- # Check if message should be processed based on filters
365
- def should_process_message?(headers, filter_options)
366
- return true if filter_options.empty?
367
-
368
- # Apply from/to filters if specified
369
- if filter_options[:from] && headers['from'] != filter_options[:from]
370
- return false
371
- end
372
-
373
- if filter_options[:to] && headers['to'] != filter_options[:to]
374
- return false
375
- end
376
-
377
- true
378
- end
379
-
380
- # Setup exchange metadata in Redis
381
- def setup_exchange
382
- exchange_key = "#{@options[:routing_prefix]}:#{@exchange_name}:metadata"
383
- @redis.hset(exchange_key, 'type', 'topic')
384
- @redis.hset(exchange_key, 'created_at', Time.now.to_f)
385
- @redis.expire(exchange_key, 86400) # Expire in 24 hours
386
- end
387
-
388
- # Generate queue name from pattern and options
389
- def derive_queue_name(pattern, filter_options = {})
390
- base_name = pattern.gsub(/[#*]/, 'wildcard')
391
- filter_suffix = filter_options.empty? ? '' : "_#{filter_options.hash.abs}"
392
- "#{@options[:queue_prefix]}.#{base_name}#{filter_suffix}"
393
- end
394
-
395
- # Extract routing information from message
396
- def extract_routing_info(serialized_message)
397
- data = JSON.parse(serialized_message)
398
- header = data['_sm_header'] || {}
399
- {
400
- from: header['from'],
401
- to: header['to']
402
- }
403
- rescue JSON::ParserError
404
- { from: nil, to: nil }
405
- end
406
-
407
- # Build enhanced routing key similar to RabbitMQ topic exchange
408
- def build_enhanced_routing_key(message_class, routing_info)
409
- # Create hierarchical routing key: namespace.message_type.from.to
410
- parts = []
411
-
412
- # Add exchange name as namespace
413
- parts << @exchange_name
414
-
415
- # Add message class (normalized)
416
- normalized_class = message_class.to_s.gsub('::', '.').downcase
417
- parts << normalized_class
418
-
419
- # Add from/to if present (default to 'any')
420
- parts << (routing_info[:from] || 'any')
421
- parts << (routing_info[:to] || 'any')
422
-
423
- parts.join('.')
424
- end
425
-
426
- # Find queues that match the routing key pattern
427
- def find_matching_queues(routing_key)
428
- matching = []
429
-
430
- @routing_table.each do |pattern, queue_names|
431
- if routing_key_matches_pattern?(routing_key, pattern)
432
- matching.concat(queue_names)
433
- end
434
- end
435
-
436
- matching.uniq
437
- end
438
-
439
- # Check if routing key matches the pattern (RabbitMQ-style)
440
- def routing_key_matches_pattern?(routing_key, pattern)
441
- return false if routing_key.nil? || routing_key.empty?
442
- return false if pattern.nil? || pattern.empty?
443
-
444
- # Split into segments for proper RabbitMQ-style matching
445
- routing_segments = routing_key.split('.')
446
- pattern_segments = pattern.split('.')
447
-
448
- match_segments(routing_segments, pattern_segments)
449
- end
450
-
451
- # Recursively match routing key segments against pattern segments
452
- def match_segments(routing_segments, pattern_segments)
453
- return routing_segments.empty? && pattern_segments.empty? if pattern_segments.empty?
454
- return pattern_segments.all? { |seg| seg == '#' } if routing_segments.empty?
455
-
456
- pattern_seg = pattern_segments[0]
457
-
458
- case pattern_seg
459
- when '#'
460
- # # matches zero or more segments
461
- # Try matching with zero segments (skip the # pattern)
462
- if match_segments(routing_segments, pattern_segments[1..-1])
463
- return true
464
- end
465
- # Try matching with one or more segments (consume one routing segment)
466
- return match_segments(routing_segments[1..-1], pattern_segments)
467
-
468
- when '*'
469
- # * matches exactly one segment
470
- return false if routing_segments.empty?
471
- return match_segments(routing_segments[1..-1], pattern_segments[1..-1])
472
-
473
- else
474
- # Literal match required
475
- return false if routing_segments.empty? || routing_segments[0] != pattern_seg
476
- return match_segments(routing_segments[1..-1], pattern_segments[1..-1])
477
- end
478
- end
479
-
480
- # Sanitize string for use in routing key
481
- def sanitize_for_routing_key(str)
482
- str.to_s.gsub(/[^a-zA-Z0-9_-]/, '_').downcase
483
- end
484
- end
485
-
486
- # Fluent API builder for complex subscription patterns
487
- class RedisQueueSubscriptionBuilder
488
- def initialize(transport)
489
- @transport = transport
490
- @conditions = {}
491
- end
492
-
493
- def from(sender_uuid)
494
- @conditions[:from] = sender_uuid
495
- self
496
- end
497
-
498
- def to(recipient_uuid)
499
- @conditions[:to] = recipient_uuid
500
- self
501
- end
502
-
503
- def type(message_type)
504
- @conditions[:type] = message_type
505
- self
506
- end
507
-
508
- def broadcast
509
- @conditions[:to] = 'broadcast'
510
- self
511
- end
512
-
513
- def alerts
514
- @conditions[:alerts] = true
515
- self
516
- end
517
-
518
- def consumer_group(group_name)
519
- @conditions[:consumer_group] = group_name
520
- self
521
- end
522
-
523
- def build
524
- parts = []
525
-
526
- # Build pattern based on conditions
527
- if @conditions[:alerts]
528
- return "alert.#.*.*"
529
- end
530
-
531
- if @conditions[:type]
532
- parts << "*"
533
- parts << @conditions[:type].to_s.gsub('::', '.').downcase
534
- else
535
- parts << "#"
536
- end
537
-
538
- parts << (@conditions[:from] || "*")
539
- parts << (@conditions[:to] || "*")
540
-
541
- parts.join(".")
542
- end
543
-
544
- def subscribe(process_method = :process, &block)
545
- pattern = build
546
-
547
- filter_options = {}
548
- filter_options[:from] = @conditions[:from] if @conditions[:from]
549
- filter_options[:to] = @conditions[:to] if @conditions[:to]
550
-
551
- @transport.subscribe_pattern(pattern, process_method, filter_options, &block)
552
- end
553
- end
554
- end
555
- end