smart_message 0.0.10 → 0.0.12
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/.github/workflows/deploy-github-pages.yml +38 -0
- data/.gitignore +5 -0
- data/CHANGELOG.md +30 -0
- data/Gemfile.lock +35 -4
- data/README.md +169 -71
- data/Rakefile +29 -4
- data/docs/assets/images/ddq_architecture.svg +130 -0
- data/docs/assets/images/dlq_architecture.svg +115 -0
- data/docs/assets/images/enhanced-dual-publishing.svg +136 -0
- data/docs/assets/images/enhanced-fluent-api.svg +149 -0
- data/docs/assets/images/enhanced-microservices-routing.svg +115 -0
- data/docs/assets/images/enhanced-pattern-matching.svg +107 -0
- data/docs/assets/images/fluent-api-demo.svg +59 -0
- data/docs/assets/images/performance-comparison.svg +161 -0
- data/docs/assets/images/redis-basic-architecture.svg +53 -0
- data/docs/assets/images/redis-enhanced-architecture.svg +88 -0
- data/docs/assets/images/redis-queue-architecture.svg +101 -0
- data/docs/assets/images/smart_message.jpg +0 -0
- data/docs/assets/images/smart_message_walking.jpg +0 -0
- data/docs/assets/images/smartmessage_architecture_overview.svg +173 -0
- data/docs/assets/images/transport-comparison-matrix.svg +171 -0
- data/docs/assets/javascripts/mathjax.js +17 -0
- data/docs/assets/stylesheets/extra.css +51 -0
- data/docs/{addressing.md → core-concepts/addressing.md} +5 -7
- data/docs/{architecture.md → core-concepts/architecture.md} +78 -138
- data/docs/{dispatcher.md → core-concepts/dispatcher.md} +21 -21
- data/docs/{message_filtering.md → core-concepts/message-filtering.md} +2 -3
- data/docs/{message_processing.md → core-concepts/message-processing.md} +17 -17
- data/docs/{troubleshooting.md → development/troubleshooting.md} +7 -7
- data/docs/{examples.md → getting-started/examples.md} +115 -89
- data/docs/{getting-started.md → getting-started/quick-start.md} +47 -18
- data/docs/guides/redis-queue-getting-started.md +697 -0
- data/docs/guides/redis-queue-patterns.md +889 -0
- data/docs/guides/redis-queue-production.md +1091 -0
- data/docs/index.md +64 -0
- data/docs/{dead_letter_queue.md → reference/dead-letter-queue.md} +2 -3
- data/docs/{logging.md → reference/logging.md} +1 -1
- data/docs/{message_deduplication.md → reference/message-deduplication.md} +1 -0
- data/docs/{proc_handlers_summary.md → reference/proc-handlers.md} +7 -6
- data/docs/{serializers.md → reference/serializers.md} +3 -5
- data/docs/{transports.md → reference/transports.md} +133 -11
- data/docs/transports/memory-transport.md +374 -0
- data/docs/transports/redis-enhanced-transport.md +524 -0
- data/docs/transports/redis-queue-transport.md +1304 -0
- data/docs/transports/redis-transport-comparison.md +496 -0
- data/docs/transports/redis-transport.md +509 -0
- data/examples/README.md +98 -5
- data/examples/city_scenario/911_emergency_call_flow.svg +99 -0
- data/examples/city_scenario/README.md +515 -0
- data/examples/city_scenario/ai_visitor_intelligence_flow.svg +108 -0
- data/examples/city_scenario/citizen.rb +195 -0
- data/examples/city_scenario/city_diagram.svg +125 -0
- data/examples/city_scenario/common/health_monitor.rb +80 -0
- data/examples/city_scenario/common/logger.rb +30 -0
- data/examples/city_scenario/emergency_dispatch_center.rb +270 -0
- data/examples/city_scenario/fire_department.rb +446 -0
- data/examples/city_scenario/fire_emergency_flow.svg +95 -0
- data/examples/city_scenario/health_department.rb +100 -0
- data/examples/city_scenario/health_monitoring_system.svg +130 -0
- data/examples/city_scenario/house.rb +244 -0
- data/examples/city_scenario/local_bank.rb +217 -0
- data/examples/city_scenario/messages/emergency_911_message.rb +81 -0
- data/examples/city_scenario/messages/emergency_resolved_message.rb +43 -0
- data/examples/city_scenario/messages/fire_dispatch_message.rb +43 -0
- data/examples/city_scenario/messages/fire_emergency_message.rb +45 -0
- data/examples/city_scenario/messages/health_check_message.rb +22 -0
- data/examples/city_scenario/messages/health_status_message.rb +35 -0
- data/examples/city_scenario/messages/police_dispatch_message.rb +46 -0
- data/examples/city_scenario/messages/silent_alarm_message.rb +38 -0
- data/examples/city_scenario/police_department.rb +316 -0
- data/examples/city_scenario/redis_monitor.rb +129 -0
- data/examples/city_scenario/redis_stats.rb +743 -0
- data/examples/city_scenario/room_for_improvement.md +240 -0
- data/examples/city_scenario/security_emergency_flow.svg +95 -0
- data/examples/city_scenario/service_internal_architecture.svg +154 -0
- data/examples/city_scenario/smart_message_ai_agent.rb +364 -0
- data/examples/city_scenario/start_demo.sh +236 -0
- data/examples/city_scenario/stop_demo.sh +106 -0
- data/examples/city_scenario/visitor.rb +631 -0
- data/examples/{10_message_deduplication.rb → memory/01_message_deduplication_demo.rb} +1 -1
- data/examples/{09_dead_letter_queue_demo.rb → memory/02_dead_letter_queue_demo.rb} +13 -40
- data/examples/{01_point_to_point_orders.rb → memory/03_point_to_point_orders.rb} +1 -1
- data/examples/{02_publish_subscribe_events.rb → memory/04_publish_subscribe_events.rb} +2 -2
- data/examples/{03_many_to_many_chat.rb → memory/05_many_to_many_chat.rb} +4 -4
- data/examples/{show_me.rb → memory/06_pretty_print_demo.rb} +1 -1
- data/examples/{05_proc_handlers.rb → memory/07_proc_handlers_demo.rb} +2 -2
- data/examples/{06_custom_logger_example.rb → memory/08_custom_logger_demo.rb} +17 -14
- data/examples/{07_error_handling_scenarios.rb → memory/09_error_handling_demo.rb} +4 -4
- data/examples/{08_entity_addressing_basic.rb → memory/10_entity_addressing_basic.rb} +8 -8
- data/examples/{08_entity_addressing_with_filtering.rb → memory/11_entity_addressing_with_filtering.rb} +6 -6
- data/examples/{09_regex_filtering_microservices.rb → memory/12_regex_filtering_microservices.rb} +2 -2
- data/examples/{10_header_block_configuration.rb → memory/13_header_block_configuration.rb} +6 -6
- data/examples/{11_global_configuration_example.rb → memory/14_global_configuration_demo.rb} +19 -8
- data/examples/{show_logger.rb → memory/15_logger_demo.rb} +1 -1
- data/examples/memory/README.md +163 -0
- data/examples/memory/memory_transport_architecture.svg +90 -0
- data/examples/memory/point_to_point_pattern.svg +94 -0
- data/examples/memory/publish_subscribe_pattern.svg +125 -0
- data/examples/{04_redis_smart_home_iot.rb → redis/01_smart_home_iot_demo.rb} +5 -5
- data/examples/redis/README.md +230 -0
- data/examples/redis/alert_system_flow.svg +127 -0
- data/examples/redis/dashboard_status_flow.svg +107 -0
- data/examples/redis/device_command_flow.svg +113 -0
- data/examples/redis/redis_transport_architecture.svg +115 -0
- data/examples/{smart_home_iot_dataflow.md → redis/smart_home_iot_dataflow.md} +4 -116
- data/examples/redis/smart_home_system_architecture.svg +133 -0
- data/examples/redis_enhanced/README.md +319 -0
- data/examples/redis_enhanced/enhanced_01_basic_patterns.rb +233 -0
- data/examples/redis_enhanced/enhanced_02_fluent_api.rb +331 -0
- data/examples/redis_enhanced/enhanced_03_dual_publishing.rb +281 -0
- data/examples/redis_enhanced/enhanced_04_advanced_routing.rb +419 -0
- data/examples/redis_queue/01_basic_messaging.rb +221 -0
- data/examples/redis_queue/01_comprehensive_examples.rb +508 -0
- data/examples/redis_queue/02_pattern_routing.rb +405 -0
- data/examples/redis_queue/03_fluent_api.rb +422 -0
- data/examples/redis_queue/04_load_balancing.rb +486 -0
- data/examples/redis_queue/05_microservices.rb +735 -0
- data/examples/redis_queue/06_emergency_alerts.rb +777 -0
- data/examples/redis_queue/07_queue_management.rb +587 -0
- data/examples/redis_queue/README.md +366 -0
- data/examples/redis_queue/enhanced_01_basic_patterns.rb +233 -0
- data/examples/redis_queue/enhanced_02_fluent_api.rb +331 -0
- data/examples/redis_queue/enhanced_03_dual_publishing.rb +281 -0
- data/examples/redis_queue/enhanced_04_advanced_routing.rb +419 -0
- data/examples/redis_queue/redis_queue_architecture.svg +148 -0
- data/ideas/README.md +41 -0
- data/ideas/agents.md +1001 -0
- data/ideas/database_transport.md +980 -0
- data/ideas/improvement.md +359 -0
- data/ideas/meshage.md +1788 -0
- data/ideas/message_discovery.md +178 -0
- data/ideas/message_schema.md +1381 -0
- data/lib/smart_message/.idea/.gitignore +8 -0
- data/lib/smart_message/.idea/markdown.xml +6 -0
- data/lib/smart_message/.idea/misc.xml +4 -0
- data/lib/smart_message/.idea/modules.xml +8 -0
- data/lib/smart_message/.idea/smart_message.iml +16 -0
- data/lib/smart_message/.idea/vcs.xml +6 -0
- data/lib/smart_message/addressing.rb +15 -0
- data/lib/smart_message/base.rb +0 -2
- data/lib/smart_message/configuration.rb +1 -1
- data/lib/smart_message/logger.rb +15 -4
- data/lib/smart_message/plugins.rb +5 -2
- data/lib/smart_message/serializer.rb +14 -0
- data/lib/smart_message/transport/redis_enhanced_transport.rb +399 -0
- data/lib/smart_message/transport/redis_queue_transport.rb +555 -0
- data/lib/smart_message/transport/registry.rb +1 -0
- data/lib/smart_message/transport.rb +34 -1
- data/lib/smart_message/version.rb +1 -1
- data/lib/smart_message.rb +5 -52
- data/mkdocs.yml +184 -0
- data/p2p_plan.md +326 -0
- data/p2p_roadmap.md +287 -0
- data/smart_message.gemspec +2 -0
- data/smart_message.svg +51 -0
- metadata +170 -44
- data/docs/README.md +0 -57
- data/examples/dead_letters.jsonl +0 -12
- data/examples/temp.txt +0 -94
- data/examples/tmux_chat/README.md +0 -283
- data/examples/tmux_chat/bot_agent.rb +0 -278
- data/examples/tmux_chat/human_agent.rb +0 -199
- data/examples/tmux_chat/room_monitor.rb +0 -160
- data/examples/tmux_chat/shared_chat_system.rb +0 -328
- data/examples/tmux_chat/start_chat_demo.sh +0 -190
- data/examples/tmux_chat/stop_chat_demo.sh +0 -22
- /data/docs/{properties.md → core-concepts/properties.md} +0 -0
- /data/docs/{ideas_to_think_about.md → development/ideas.md} +0 -0
@@ -0,0 +1,8 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<project version="4">
|
3
|
+
<component name="ProjectModuleManager">
|
4
|
+
<modules>
|
5
|
+
<module fileurl="file://$PROJECT_DIR$/.idea/smart_message.iml" filepath="$PROJECT_DIR$/.idea/smart_message.iml" />
|
6
|
+
</modules>
|
7
|
+
</component>
|
8
|
+
</project>
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<module type="RUBY_MODULE" version="4">
|
3
|
+
<component name="ModuleRunConfigurationManager">
|
4
|
+
<shared />
|
5
|
+
</component>
|
6
|
+
<component name="NewModuleRootManager">
|
7
|
+
<content url="file://$MODULE_DIR$">
|
8
|
+
<sourceFolder url="file://$MODULE_DIR$/features" isTestSource="true" />
|
9
|
+
<sourceFolder url="file://$MODULE_DIR$/spec" isTestSource="true" />
|
10
|
+
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
|
11
|
+
</content>
|
12
|
+
<orderEntry type="inheritedJdk" />
|
13
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
14
|
+
<orderEntry type="library" scope="PROVIDED" name="bundler (v1.17.2, ruby-2.6.10-p210) [gem]" level="application" />
|
15
|
+
</component>
|
16
|
+
</module>
|
@@ -202,6 +202,11 @@ module SmartMessage
|
|
202
202
|
end
|
203
203
|
end
|
204
204
|
|
205
|
+
# Setter method for from - allows ClassName.from = 'value' syntax
|
206
|
+
def from=(entity_id)
|
207
|
+
from(entity_id)
|
208
|
+
end
|
209
|
+
|
205
210
|
def from_configured?; !from.nil?; end
|
206
211
|
def from_missing?; from.nil?; end
|
207
212
|
def reset_from;
|
@@ -224,6 +229,11 @@ module SmartMessage
|
|
224
229
|
end
|
225
230
|
end
|
226
231
|
|
232
|
+
# Setter method for to - allows ClassName.to = 'value' syntax
|
233
|
+
def to=(entity_id)
|
234
|
+
to(entity_id)
|
235
|
+
end
|
236
|
+
|
227
237
|
def to_configured?; !to.nil?; end
|
228
238
|
def to_missing?; to.nil?; end
|
229
239
|
def reset_to;
|
@@ -246,6 +256,11 @@ module SmartMessage
|
|
246
256
|
end
|
247
257
|
end
|
248
258
|
|
259
|
+
# Setter method for reply_to - allows ClassName.reply_to = 'value' syntax
|
260
|
+
def reply_to=(entity_id)
|
261
|
+
reply_to(entity_id)
|
262
|
+
end
|
263
|
+
|
249
264
|
def reply_to_configured?; !reply_to.nil?; end
|
250
265
|
def reply_to_missing?; reply_to.nil?; end
|
251
266
|
def reset_reply_to;
|
data/lib/smart_message/base.rb
CHANGED
@@ -162,8 +162,6 @@ module SmartMessage
|
|
162
162
|
# @param serialized_message [String] The serialized message content
|
163
163
|
# @return [SmartMessage::Base] The decoded message instance
|
164
164
|
def decode(serialized_message)
|
165
|
-
logger = SmartMessage::Logger.default
|
166
|
-
|
167
165
|
begin
|
168
166
|
(self.logger || SmartMessage::Logger.default).info { "[SmartMessage] Received: #{self.name} (#{serialized_message.bytesize} bytes)" }
|
169
167
|
|
data/lib/smart_message/logger.rb
CHANGED
@@ -9,15 +9,26 @@ require_relative 'logger/lumberjack'
|
|
9
9
|
module SmartMessage
|
10
10
|
module Logger
|
11
11
|
class << self
|
12
|
-
# Global default logger instance
|
13
|
-
def default
|
14
|
-
|
12
|
+
# Global default logger instance - uses configuration if available
|
13
|
+
def default(options = {})
|
14
|
+
# Always check current configuration first (don't cache when config is available)
|
15
|
+
if defined?(SmartMessage.configuration) && SmartMessage.configuration.logger_configured?
|
16
|
+
SmartMessage.configuration.default_logger
|
17
|
+
else
|
18
|
+
# Cache the framework default logger only when no configuration
|
19
|
+
@default ||= Lumberjack.new(**options)
|
20
|
+
end
|
15
21
|
end
|
16
22
|
|
17
23
|
# Set the default logger
|
18
24
|
def default=(logger)
|
19
25
|
@default = logger
|
20
26
|
end
|
27
|
+
|
28
|
+
# Reset the cached default logger
|
29
|
+
def reset!
|
30
|
+
@default = nil
|
31
|
+
end
|
21
32
|
end
|
22
33
|
end
|
23
|
-
end
|
34
|
+
end
|
@@ -105,8 +105,11 @@ module SmartMessage
|
|
105
105
|
end
|
106
106
|
end
|
107
107
|
|
108
|
-
def logger_configured?; !
|
109
|
-
def logger_missing
|
108
|
+
def logger_configured?; !logger_missing?; end
|
109
|
+
def logger_missing?
|
110
|
+
# Check if class-level logger is explicitly configured (without fallback to defaults)
|
111
|
+
(class_variable_get(:@@logger) rescue nil).nil?
|
112
|
+
end
|
110
113
|
def reset_logger; class_variable_set(:@@logger, nil); end
|
111
114
|
|
112
115
|
#########################################################
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# lib/smart_message/serializer.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
module SmartMessage
|
6
|
+
module Serializer
|
7
|
+
class << self
|
8
|
+
def default
|
9
|
+
# Check global configuration first, then fall back to framework default
|
10
|
+
SmartMessage.configuration.default_serializer
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,399 @@
|
|
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
|