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,399 +0,0 @@
1
- # lib/smart_message/transport/redis_transport_enhanced.rb
2
- # Enhanced Redis transport with routing intelligence similar to RabbitMQ
3
-
4
- require_relative 'redis_transport'
5
-
6
- module SmartMessage
7
- module Transport
8
- class RedisEnhancedTransport < RedisTransport
9
-
10
- # Enhanced publish with structured channel names
11
- def do_publish(message_class, serialized_message)
12
- # Extract routing information from message
13
- routing_info = extract_routing_info(serialized_message)
14
-
15
- # Build enhanced channel name with routing info
16
- channel = build_enhanced_channel(message_class, routing_info)
17
-
18
- begin
19
- # Publish to both simple channel (backwards compatibility) and enhanced channel
20
- @redis_pub.publish(message_class, serialized_message) # Original format
21
- @redis_pub.publish(channel, serialized_message) # Enhanced format
22
-
23
- logger.debug { "[Redis Enhanced] Published to channels: #{message_class} and #{channel}" }
24
- rescue Redis::ConnectionError
25
- retry_with_reconnect('publish') do
26
- @redis_pub.publish(message_class, serialized_message)
27
- @redis_pub.publish(channel, serialized_message)
28
- end
29
- end
30
- end
31
-
32
- # Enhanced subscription with pattern support
33
- def subscribe_pattern(pattern)
34
- @mutex.synchronize do
35
- @pattern_subscriptions ||= Set.new
36
- @pattern_subscriptions.add(pattern)
37
- restart_subscriber if @running
38
- end
39
-
40
- logger.info { "[Redis Enhanced] Subscribed to pattern: #{pattern}" }
41
- end
42
-
43
- # Convenience methods similar to RabbitMQ
44
- def subscribe_to_recipient(recipient_id)
45
- pattern = "*.*.#{sanitize_for_channel(recipient_id)}"
46
- subscribe_pattern(pattern)
47
- end
48
-
49
- def subscribe_from_sender(sender_id)
50
- pattern = "*.#{sanitize_for_channel(sender_id)}.*"
51
- subscribe_pattern(pattern)
52
- end
53
-
54
- def subscribe_to_type(message_type)
55
- base_type = message_type.to_s.gsub('::', '.').downcase
56
- pattern = "#{base_type}.*.*"
57
- subscribe_pattern(pattern)
58
- end
59
-
60
- def subscribe_to_alerts
61
- patterns = [
62
- "emergency.*.*",
63
- "*alert*.*.*",
64
- "*alarm*.*.*",
65
- "*critical*.*.*"
66
- ]
67
-
68
- patterns.each { |pattern| subscribe_pattern(pattern) }
69
- end
70
-
71
- def subscribe_to_broadcasts
72
- pattern = "*.*.broadcast"
73
- subscribe_pattern(pattern)
74
- end
75
-
76
- # Fluent API
77
- def where
78
- RedisSubscriptionBuilder.new(self)
79
- end
80
-
81
- private
82
-
83
- def extract_routing_info(serialized_message)
84
- begin
85
- message_data = JSON.parse(serialized_message)
86
- header = message_data['_sm_header'] || {}
87
-
88
- {
89
- from: sanitize_for_channel(header['from'] || 'anonymous'),
90
- to: sanitize_for_channel(header['to'] || 'broadcast')
91
- }
92
- rescue JSON::ParserError
93
- logger.warn { "[Redis Enhanced] Could not parse message for routing info, using defaults" }
94
- { from: 'anonymous', to: 'broadcast' }
95
- end
96
- end
97
-
98
- def build_enhanced_channel(message_class, routing_info)
99
- # Format: message_type.from.to (simplified vs RabbitMQ's 4-part)
100
- base_channel = message_class.to_s.gsub('::', '.').downcase
101
- "#{base_channel}.#{routing_info[:from]}.#{routing_info[:to]}"
102
- end
103
-
104
- def sanitize_for_channel(value)
105
- # Redis channels can contain most characters, but standardize format
106
- value.to_s.gsub(/[^a-zA-Z0-9_\-]/, '_').downcase
107
- end
108
-
109
- # Override to handle both regular and pattern subscriptions
110
- def subscribe_to_channels
111
- channels = @subscribed_channels.to_a
112
- patterns = @pattern_subscriptions&.to_a || []
113
-
114
- return unless channels.any? || patterns.any?
115
-
116
- begin
117
- # Handle both regular subscriptions and pattern subscriptions
118
- if patterns.any?
119
- subscribe_with_patterns(channels, patterns)
120
- elsif channels.any?
121
- @redis_sub.subscribe(*channels) do |on|
122
- setup_subscription_handlers(on)
123
- end
124
- end
125
- rescue => e
126
- logger.error { "[Redis Enhanced] Error in subscription: #{e.class.name} - #{e.message}" }
127
- retry_subscriber if @running
128
- end
129
- end
130
-
131
- def subscribe_with_patterns(channels, patterns)
132
- # Redis doesn't support mixing SUBSCRIBE and PSUBSCRIBE in same connection
133
- # So we handle them in separate threads or use PSUBSCRIBE for everything
134
-
135
- if channels.any?
136
- # Convert regular channels to patterns for unified handling
137
- channel_patterns = channels.map { |ch| ch } # Exact match patterns
138
- all_patterns = patterns + channel_patterns
139
- else
140
- all_patterns = patterns
141
- end
142
-
143
- @redis_sub.psubscribe(*all_patterns) do |on|
144
- on.pmessage do |pattern, channel, serialized_message|
145
- begin
146
- # Determine message class from channel name
147
- message_class = extract_message_class_from_channel(channel)
148
-
149
- # Process the message if we have a handler
150
- if message_class && (@dispatcher.subscribers[message_class] || pattern_matches_handler?(channel))
151
- receive(message_class, serialized_message)
152
- else
153
- logger.debug { "[Redis Enhanced] No handler for channel: #{channel}" }
154
- end
155
- rescue => e
156
- logger.error { "[Redis Enhanced] Error processing pattern message: #{e.message}" }
157
- end
158
- end
159
-
160
- on.psubscribe do |pattern, subscriptions|
161
- logger.debug { "[Redis Enhanced] Subscribed to pattern: #{pattern} (#{subscriptions} total)" }
162
- end
163
-
164
- on.punsubscribe do |pattern, subscriptions|
165
- logger.debug { "[Redis Enhanced] Unsubscribed from pattern: #{pattern} (#{subscriptions} total)" }
166
- end
167
- end
168
- end
169
-
170
- def setup_subscription_handlers(on)
171
- on.message do |channel, serialized_message|
172
- begin
173
- # Handle regular subscription
174
- receive(channel, serialized_message)
175
- rescue => e
176
- logger.error { "[Redis Enhanced] Error processing regular message: #{e.message}" }
177
- end
178
- end
179
-
180
- on.subscribe do |channel, subscriptions|
181
- logger.debug { "[Redis Enhanced] Subscribed to channel: #{channel} (#{subscriptions} total)" }
182
- end
183
-
184
- on.unsubscribe do |channel, subscriptions|
185
- logger.debug { "[Redis Enhanced] Unsubscribed from channel: #{channel} (#{subscriptions} total)" }
186
- end
187
- end
188
-
189
- def extract_message_class_from_channel(channel)
190
- # Handle both original format and enhanced format
191
- parts = channel.split('.')
192
-
193
- if parts.length >= 3
194
- # Enhanced format: message_type.from.to
195
- # Extract just the message type part
196
- message_parts = parts[0..-3] if parts.length > 3
197
- message_parts ||= [parts[0]]
198
-
199
- # Convert back to class name format
200
- message_parts.map(&:capitalize).join('::')
201
- else
202
- # Original format: just use the channel name as class
203
- channel
204
- end
205
- end
206
-
207
- def pattern_matches_handler?(channel)
208
- # Check if any registered patterns would match this channel
209
- return false unless @pattern_subscriptions
210
-
211
- @pattern_subscriptions.any? do |pattern|
212
- File.fnmatch(pattern, channel)
213
- end
214
- end
215
- end
216
-
217
- # Fluent API builder for Redis patterns
218
- class RedisSubscriptionBuilder
219
- def initialize(transport)
220
- @transport = transport
221
- @conditions = {}
222
- end
223
-
224
- def from(sender_id)
225
- @conditions[:from] = sender_id
226
- self
227
- end
228
-
229
- def to(recipient_id)
230
- @conditions[:to] = recipient_id
231
- self
232
- end
233
-
234
- def type(message_type)
235
- @conditions[:type] = message_type
236
- self
237
- end
238
-
239
- def build
240
- pattern_parts = []
241
-
242
- # Build pattern based on conditions (3-part format for Redis)
243
- pattern_parts << (@conditions[:type]&.to_s&.gsub('::', '.')&.downcase || '*')
244
- pattern_parts << (@conditions[:from] || '*')
245
- pattern_parts << (@conditions[:to] || '*')
246
-
247
- pattern_parts.join('.')
248
- end
249
-
250
- def subscribe
251
- pattern = build
252
- @transport.subscribe_pattern(pattern)
253
- end
254
- end
255
- end
256
- end
257
-
258
- # Alternative: Redis Streams implementation
259
- module SmartMessage
260
- module Transport
261
- class RedisStreamsTransport < Base
262
-
263
- def default_options
264
- {
265
- url: 'redis://localhost:6379',
266
- db: 0,
267
- stream_prefix: 'smart_message',
268
- consumer_group: 'smart_message_workers',
269
- consumer_id: Socket.gethostname + '_' + Process.pid.to_s,
270
- max_len: 10000, # Trim streams to prevent unbounded growth
271
- block_time: 1000 # 1 second blocking read
272
- }
273
- end
274
-
275
- def configure
276
- @redis = Redis.new(url: @options[:url], db: @options[:db])
277
- @streams = {}
278
- @consumers = {}
279
- @running = false
280
- end
281
-
282
- def do_publish(message_class, serialized_message)
283
- stream_key = derive_stream_key(message_class)
284
- routing_info = extract_routing_info(serialized_message)
285
-
286
- @redis.xadd(
287
- stream_key,
288
- {
289
- data: serialized_message,
290
- from: routing_info[:from],
291
- to: routing_info[:to],
292
- message_class: message_class.to_s,
293
- timestamp: Time.now.to_f
294
- },
295
- maxlen: @options[:max_len],
296
- approximate: true
297
- )
298
-
299
- logger.debug { "[Redis Streams] Published to stream: #{stream_key}" }
300
- end
301
-
302
- def subscribe(message_class, process_method, filter_options = {})
303
- super(message_class, process_method, filter_options)
304
-
305
- stream_key = derive_stream_key(message_class)
306
- setup_consumer_group(stream_key)
307
- start_consumer(stream_key, message_class, filter_options)
308
- end
309
-
310
- private
311
-
312
- def derive_stream_key(message_class)
313
- "#{@options[:stream_prefix]}:#{message_class.to_s.gsub('::', ':').downcase}"
314
- end
315
-
316
- def setup_consumer_group(stream_key)
317
- begin
318
- @redis.xgroup(
319
- :create,
320
- stream_key,
321
- @options[:consumer_group],
322
- '$', # Start from new messages
323
- mkstream: true
324
- )
325
- rescue Redis::CommandError => e
326
- # Consumer group might already exist
327
- logger.debug { "[Redis Streams] Consumer group exists: #{e.message}" }
328
- end
329
- end
330
-
331
- def start_consumer(stream_key, message_class, filter_options)
332
- return if @consumers[stream_key]
333
-
334
- @consumers[stream_key] = Thread.new do
335
- while @running
336
- begin
337
- # Read from consumer group with blocking
338
- messages = @redis.xread_group(
339
- @options[:consumer_group],
340
- @options[:consumer_id],
341
- { stream_key => '>' },
342
- block: @options[:block_time],
343
- count: 10
344
- )
345
-
346
- messages&.each do |stream, stream_messages|
347
- stream_messages.each do |message_id, fields|
348
- process_stream_message(message_id, fields, message_class, filter_options)
349
- end
350
- end
351
- rescue => e
352
- logger.error { "[Redis Streams] Consumer error: #{e.message}" }
353
- sleep(1)
354
- end
355
- end
356
- end
357
- end
358
-
359
- def process_stream_message(message_id, fields, message_class, filter_options)
360
- # Apply filtering based on from/to if specified
361
- from_filter = filter_options[:from]
362
- to_filter = filter_options[:to]
363
-
364
- if from_filter && fields['from'] != from_filter
365
- @redis.xack(@options[:stream_prefix], @options[:consumer_group], message_id)
366
- return
367
- end
368
-
369
- if to_filter && fields['to'] != to_filter
370
- @redis.xack(@options[:stream_prefix], @options[:consumer_group], message_id)
371
- return
372
- end
373
-
374
- # Process the message
375
- begin
376
- receive(fields['message_class'], fields['data'])
377
- @redis.xack(@options[:stream_prefix], @options[:consumer_group], message_id)
378
- rescue => e
379
- logger.error { "[Redis Streams] Message processing error: #{e.message}" }
380
- # Message will remain unacknowledged and can be retried
381
- end
382
- end
383
-
384
- def extract_routing_info(serialized_message)
385
- begin
386
- message_data = JSON.parse(serialized_message)
387
- header = message_data['_sm_header'] || {}
388
-
389
- {
390
- from: header['from'] || 'anonymous',
391
- to: header['to'] || 'broadcast'
392
- }
393
- rescue JSON::ParserError
394
- { from: 'anonymous', to: 'broadcast' }
395
- end
396
- end
397
- end
398
- end
399
- end