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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -1
- data/Gemfile.lock +5 -5
- data/docs/core-concepts/architecture.md +5 -10
- data/docs/getting-started/examples.md +0 -12
- data/docs/getting-started/quick-start.md +4 -9
- data/docs/index.md +4 -4
- data/docs/reference/serializers.md +160 -488
- data/docs/reference/transports.md +1 -125
- data/docs/transports/redis-transport-comparison.md +215 -350
- data/docs/transports/redis-transport.md +3 -22
- data/examples/README.md +6 -9
- data/examples/city_scenario/README.md +1 -1
- data/examples/city_scenario/messages/emergency_911_message.rb +0 -1
- data/examples/city_scenario/messages/emergency_resolved_message.rb +0 -1
- data/examples/city_scenario/messages/fire_dispatch_message.rb +0 -1
- data/examples/city_scenario/messages/fire_emergency_message.rb +0 -1
- data/examples/city_scenario/messages/health_check_message.rb +0 -1
- data/examples/city_scenario/messages/health_status_message.rb +0 -1
- data/examples/city_scenario/messages/police_dispatch_message.rb +0 -1
- data/examples/city_scenario/messages/silent_alarm_message.rb +0 -1
- data/examples/memory/01_message_deduplication_demo.rb +0 -2
- data/examples/memory/02_dead_letter_queue_demo.rb +0 -3
- data/examples/memory/03_point_to_point_orders.rb +0 -2
- data/examples/memory/04_publish_subscribe_events.rb +0 -1
- data/examples/memory/05_many_to_many_chat.rb +0 -3
- data/examples/memory/07_proc_handlers_demo.rb +0 -1
- data/examples/memory/08_custom_logger_demo.rb +0 -4
- data/examples/memory/09_error_handling_demo.rb +0 -3
- data/examples/memory/10_entity_addressing_basic.rb +0 -6
- data/examples/memory/11_entity_addressing_with_filtering.rb +0 -4
- data/examples/memory/12_regex_filtering_microservices.rb +0 -1
- data/examples/memory/13_header_block_configuration.rb +0 -5
- data/examples/memory/14_global_configuration_demo.rb +0 -2
- data/examples/memory/15_logger_demo.rb +0 -1
- data/examples/memory/README.md +3 -3
- data/examples/redis/01_smart_home_iot_demo.rb +0 -4
- data/examples/redis/README.md +0 -2
- data/lib/smart_message/base.rb +19 -10
- data/lib/smart_message/configuration.rb +2 -23
- data/lib/smart_message/dead_letter_queue.rb +1 -1
- data/lib/smart_message/messaging.rb +3 -62
- data/lib/smart_message/plugins.rb +1 -42
- data/lib/smart_message/transport/base.rb +42 -8
- data/lib/smart_message/transport/memory_transport.rb +23 -4
- data/lib/smart_message/transport/redis_transport.rb +11 -0
- data/lib/smart_message/transport/registry.rb +0 -1
- data/lib/smart_message/transport/stdout_transport.rb +28 -10
- data/lib/smart_message/transport.rb +0 -1
- data/lib/smart_message/version.rb +1 -1
- metadata +2 -28
- data/docs/guides/redis-queue-getting-started.md +0 -697
- data/docs/guides/redis-queue-patterns.md +0 -889
- data/docs/guides/redis-queue-production.md +0 -1091
- data/docs/transports/redis-enhanced-transport.md +0 -524
- data/docs/transports/redis-queue-transport.md +0 -1304
- data/examples/redis_enhanced/README.md +0 -319
- data/examples/redis_enhanced/enhanced_01_basic_patterns.rb +0 -233
- data/examples/redis_enhanced/enhanced_02_fluent_api.rb +0 -331
- data/examples/redis_enhanced/enhanced_03_dual_publishing.rb +0 -281
- data/examples/redis_enhanced/enhanced_04_advanced_routing.rb +0 -419
- data/examples/redis_queue/01_basic_messaging.rb +0 -221
- data/examples/redis_queue/01_comprehensive_examples.rb +0 -508
- data/examples/redis_queue/02_pattern_routing.rb +0 -405
- data/examples/redis_queue/03_fluent_api.rb +0 -422
- data/examples/redis_queue/04_load_balancing.rb +0 -486
- data/examples/redis_queue/05_microservices.rb +0 -735
- data/examples/redis_queue/06_emergency_alerts.rb +0 -777
- data/examples/redis_queue/07_queue_management.rb +0 -587
- data/examples/redis_queue/README.md +0 -366
- data/examples/redis_queue/enhanced_01_basic_patterns.rb +0 -233
- data/examples/redis_queue/enhanced_02_fluent_api.rb +0 -331
- data/examples/redis_queue/enhanced_03_dual_publishing.rb +0 -281
- data/examples/redis_queue/enhanced_04_advanced_routing.rb +0 -419
- data/examples/redis_queue/redis_queue_architecture.svg +0 -148
- data/lib/smart_message/transport/redis_enhanced_transport.rb +0 -399
- 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
|