smart_message 0.0.8 → 0.0.9
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/.gitignore +1 -0
- data/.irbrc +24 -0
- data/CHANGELOG.md +96 -0
- data/Gemfile.lock +6 -1
- data/README.md +289 -15
- data/docs/README.md +3 -1
- data/docs/addressing.md +119 -13
- data/docs/architecture.md +68 -0
- data/docs/dead_letter_queue.md +673 -0
- data/docs/dispatcher.md +87 -0
- data/docs/examples.md +59 -1
- data/docs/getting-started.md +8 -1
- data/docs/logging.md +382 -326
- data/docs/message_filtering.md +451 -0
- data/examples/01_point_to_point_orders.rb +54 -53
- data/examples/02_publish_subscribe_events.rb +14 -10
- data/examples/03_many_to_many_chat.rb +16 -8
- data/examples/04_redis_smart_home_iot.rb +20 -10
- data/examples/05_proc_handlers.rb +12 -11
- data/examples/06_custom_logger_example.rb +95 -100
- data/examples/07_error_handling_scenarios.rb +4 -2
- data/examples/08_entity_addressing_basic.rb +18 -6
- data/examples/08_entity_addressing_with_filtering.rb +27 -9
- data/examples/09_dead_letter_queue_demo.rb +559 -0
- data/examples/09_regex_filtering_microservices.rb +407 -0
- data/examples/10_header_block_configuration.rb +263 -0
- data/examples/11_global_configuration_example.rb +219 -0
- data/examples/README.md +102 -0
- data/examples/dead_letters.jsonl +12 -0
- data/examples/performance_metrics/benchmark_results_ractor_20250818_205603.json +135 -0
- data/examples/performance_metrics/benchmark_results_ractor_20250818_205831.json +135 -0
- data/examples/performance_metrics/benchmark_results_test_20250818_204942.json +130 -0
- data/examples/performance_metrics/benchmark_results_threadpool_20250818_204942.json +130 -0
- data/examples/performance_metrics/benchmark_results_threadpool_20250818_204959.json +130 -0
- data/examples/performance_metrics/benchmark_results_threadpool_20250818_205044.json +130 -0
- data/examples/performance_metrics/benchmark_results_threadpool_20250818_205109.json +130 -0
- data/examples/performance_metrics/benchmark_results_threadpool_20250818_205252.json +130 -0
- data/examples/performance_metrics/benchmark_results_unknown_20250819_172852.json +130 -0
- data/examples/performance_metrics/compare_benchmarks.rb +519 -0
- data/examples/performance_metrics/dead_letters.jsonl +3100 -0
- data/examples/performance_metrics/performance_benchmark.rb +344 -0
- data/examples/show_logger.rb +367 -0
- data/examples/show_me.rb +145 -0
- data/examples/temp.txt +94 -0
- data/examples/tmux_chat/bot_agent.rb +4 -2
- data/examples/tmux_chat/human_agent.rb +4 -2
- data/examples/tmux_chat/room_monitor.rb +4 -2
- data/examples/tmux_chat/shared_chat_system.rb +6 -3
- data/lib/smart_message/addressing.rb +259 -0
- data/lib/smart_message/base.rb +121 -599
- data/lib/smart_message/circuit_breaker.rb +2 -1
- data/lib/smart_message/configuration.rb +199 -0
- data/lib/smart_message/dead_letter_queue.rb +27 -10
- data/lib/smart_message/dispatcher.rb +90 -49
- data/lib/smart_message/header.rb +5 -0
- data/lib/smart_message/logger/base.rb +21 -1
- data/lib/smart_message/logger/default.rb +88 -138
- data/lib/smart_message/logger/lumberjack.rb +324 -0
- data/lib/smart_message/logger/null.rb +81 -0
- data/lib/smart_message/logger.rb +17 -9
- data/lib/smart_message/messaging.rb +100 -0
- data/lib/smart_message/plugins.rb +132 -0
- data/lib/smart_message/serializer/base.rb +25 -8
- data/lib/smart_message/serializer/json.rb +5 -4
- data/lib/smart_message/subscription.rb +193 -0
- data/lib/smart_message/transport/base.rb +72 -41
- data/lib/smart_message/transport/memory_transport.rb +7 -5
- data/lib/smart_message/transport/redis_transport.rb +15 -45
- data/lib/smart_message/transport/stdout_transport.rb +18 -8
- data/lib/smart_message/transport.rb +1 -34
- data/lib/smart_message/utilities.rb +142 -0
- data/lib/smart_message/version.rb +1 -1
- data/lib/smart_message/versioning.rb +85 -0
- data/lib/smart_message/wrapper.rb.bak +132 -0
- data/lib/smart_message.rb +74 -28
- data/smart_message.gemspec +3 -0
- metadata +76 -3
- data/lib/smart_message/serializer.rb +0 -10
- data/lib/smart_message/wrapper.rb +0 -43
@@ -182,7 +182,8 @@ module SmartMessage
|
|
182
182
|
sent_to_dlq = true
|
183
183
|
rescue => dlq_error
|
184
184
|
# DLQ storage failed - log but don't raise
|
185
|
-
|
185
|
+
# Note: Logger might not be available in circuit breaker context
|
186
|
+
warn "Warning: Failed to store message in DLQ: #{dlq_error.message}"
|
186
187
|
end
|
187
188
|
end
|
188
189
|
|
@@ -0,0 +1,199 @@
|
|
1
|
+
# lib/smart_message/configuration.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
module SmartMessage
|
6
|
+
# Global configuration class for SmartMessage framework
|
7
|
+
#
|
8
|
+
# This class provides a centralized way for applications to configure
|
9
|
+
# default behavior for all SmartMessage classes. Applications can set
|
10
|
+
# global defaults for logger, transport, and serializer, which will be
|
11
|
+
# used by all message classes unless explicitly overridden.
|
12
|
+
#
|
13
|
+
# IMPORTANT: No configuration = NO LOGGING
|
14
|
+
# Applications must explicitly configure logging if they want it.
|
15
|
+
#
|
16
|
+
# Usage:
|
17
|
+
# # No configuration block = NO LOGGING (default behavior)
|
18
|
+
#
|
19
|
+
# # Use framework default logger (Lumberjack) with custom log file:
|
20
|
+
# SmartMessage.configure do |config|
|
21
|
+
# config.logger = "log/my_app.log" # String path = Lumberjack logger
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# # Use framework default logger with STDOUT/STDERR:
|
25
|
+
# SmartMessage.configure do |config|
|
26
|
+
# config.logger = STDOUT # Log to STDOUT
|
27
|
+
# config.logger = STDERR # Log to STDERR
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# # Use framework default logger with default file (log/smart_message.log):
|
31
|
+
# SmartMessage.configure do |config|
|
32
|
+
# config.logger = :default # Framework default
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# # Configure Lumberjack logger options:
|
36
|
+
# SmartMessage.configure do |config|
|
37
|
+
# config.logger = :default # Use framework default
|
38
|
+
# config.log_level = :debug # :debug, :info, :warn, :error, :fatal
|
39
|
+
# config.log_format = :json # :text or :json
|
40
|
+
# config.log_include_source = true # Include file:line source info
|
41
|
+
# config.log_structured_data = true # Include structured message data
|
42
|
+
# config.log_colorize = true # Enable colorized output (console only)
|
43
|
+
# config.log_options = { # Additional Lumberjack options
|
44
|
+
# roll_by_date: true, # Enable date-based log rolling
|
45
|
+
# date_pattern: '%Y-%m-%d', # Date pattern for rolling
|
46
|
+
# roll_by_size: true, # Enable size-based log rolling
|
47
|
+
# max_file_size: 50 * 1024 * 1024, # Max file size before rolling (50 MB)
|
48
|
+
# keep_files: 10 # Number of rolled files to keep
|
49
|
+
# }
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# # Use custom logger:
|
53
|
+
# SmartMessage.configure do |config|
|
54
|
+
# config.logger = MyApp::Logger.new # Custom logger object
|
55
|
+
# config.transport = MyApp::Transport.new
|
56
|
+
# config.serializer = MyApp::Serializer.new
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# # Explicitly disable logging:
|
60
|
+
# SmartMessage.configure do |config|
|
61
|
+
# config.logger = nil # Explicit no logging
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# # Individual message classes use these defaults automatically
|
65
|
+
# class OrderMessage < SmartMessage::Base
|
66
|
+
# property :order_id
|
67
|
+
# # No config block needed - uses global defaults
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# # Override global defaults when needed
|
71
|
+
# class SpecialMessage < SmartMessage::Base
|
72
|
+
# config do
|
73
|
+
# logger MyApp::SpecialLogger.new # Override just the logger
|
74
|
+
# # transport and serializer still use global defaults
|
75
|
+
# end
|
76
|
+
# end
|
77
|
+
class Configuration
|
78
|
+
attr_accessor :transport, :serializer, :log_level, :log_format, :log_include_source, :log_structured_data, :log_colorize, :log_options
|
79
|
+
attr_reader :logger
|
80
|
+
|
81
|
+
def initialize
|
82
|
+
@logger = nil
|
83
|
+
@transport = nil
|
84
|
+
@serializer = nil
|
85
|
+
@logger_explicitly_set_to_nil = false
|
86
|
+
@log_level = nil
|
87
|
+
@log_format = nil
|
88
|
+
@log_include_source = nil
|
89
|
+
@log_structured_data = nil
|
90
|
+
@log_colorize = nil
|
91
|
+
@log_options = {}
|
92
|
+
end
|
93
|
+
|
94
|
+
# Custom logger setter to track explicit nil assignment
|
95
|
+
def logger=(value)
|
96
|
+
@logger = value
|
97
|
+
@logger_explicitly_set_to_nil = value.nil?
|
98
|
+
end
|
99
|
+
|
100
|
+
# Reset configuration to defaults
|
101
|
+
def reset!
|
102
|
+
@logger = nil
|
103
|
+
@transport = nil
|
104
|
+
@serializer = nil
|
105
|
+
@logger_explicitly_set_to_nil = false
|
106
|
+
@log_level = nil
|
107
|
+
@log_format = nil
|
108
|
+
@log_include_source = nil
|
109
|
+
@log_structured_data = nil
|
110
|
+
@log_colorize = nil
|
111
|
+
@log_options = {}
|
112
|
+
end
|
113
|
+
|
114
|
+
# Check if logger is configured (including explicit nil for no logging)
|
115
|
+
def logger_configured?
|
116
|
+
!@logger.nil? || @logger_explicitly_set_to_nil || @logger == :default || @logger.is_a?(String) || @logger == STDOUT || @logger == STDERR
|
117
|
+
end
|
118
|
+
|
119
|
+
# Check if transport is configured
|
120
|
+
def transport_configured?
|
121
|
+
!@transport.nil?
|
122
|
+
end
|
123
|
+
|
124
|
+
# Check if serializer is configured
|
125
|
+
def serializer_configured?
|
126
|
+
!@serializer.nil?
|
127
|
+
end
|
128
|
+
|
129
|
+
# Get the configured logger or no logging
|
130
|
+
def default_logger
|
131
|
+
case @logger
|
132
|
+
when nil
|
133
|
+
# If explicitly set to nil, use null logger (no logging)
|
134
|
+
if @logger_explicitly_set_to_nil
|
135
|
+
SmartMessage::Logger::Null.new
|
136
|
+
else
|
137
|
+
# Not configured, NO LOGGING
|
138
|
+
SmartMessage::Logger::Null.new
|
139
|
+
end
|
140
|
+
when :default
|
141
|
+
# Explicitly requested framework default
|
142
|
+
framework_default_logger
|
143
|
+
when String
|
144
|
+
# String path means use Lumberjack logger with that file path
|
145
|
+
SmartMessage::Logger::Lumberjack.new(**logger_options.merge(log_file: @logger))
|
146
|
+
when STDOUT, STDERR
|
147
|
+
# STDOUT/STDERR constants mean use Lumberjack logger with that output
|
148
|
+
SmartMessage::Logger::Lumberjack.new(**logger_options.merge(log_file: @logger))
|
149
|
+
else
|
150
|
+
@logger
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Get the configured transport or framework default
|
155
|
+
def default_transport
|
156
|
+
@transport || framework_default_transport
|
157
|
+
end
|
158
|
+
|
159
|
+
# Get the configured serializer or framework default
|
160
|
+
def default_serializer
|
161
|
+
@serializer || framework_default_serializer
|
162
|
+
end
|
163
|
+
|
164
|
+
private
|
165
|
+
|
166
|
+
# Framework's built-in default logger (Lumberjack)
|
167
|
+
def framework_default_logger
|
168
|
+
SmartMessage::Logger::Lumberjack.new(**logger_options)
|
169
|
+
end
|
170
|
+
|
171
|
+
# Build logger options from configuration
|
172
|
+
def logger_options
|
173
|
+
options = {}
|
174
|
+
options[:level] = @log_level if @log_level
|
175
|
+
options[:format] = @log_format if @log_format
|
176
|
+
options[:include_source] = @log_include_source unless @log_include_source.nil?
|
177
|
+
options[:structured_data] = @log_structured_data unless @log_structured_data.nil?
|
178
|
+
options[:colorize] = @log_colorize unless @log_colorize.nil?
|
179
|
+
|
180
|
+
# Merge in log_options (for roll_by_date, roll_by_size, max_file_size, etc.)
|
181
|
+
options.merge!(@log_options) if @log_options && @log_options.is_a?(Hash)
|
182
|
+
|
183
|
+
options
|
184
|
+
end
|
185
|
+
|
186
|
+
# Framework's built-in default transport (Redis)
|
187
|
+
def framework_default_transport
|
188
|
+
SmartMessage::Transport::RedisTransport.new
|
189
|
+
end
|
190
|
+
|
191
|
+
# Framework's built-in default serializer (JSON)
|
192
|
+
def framework_default_serializer
|
193
|
+
SmartMessage::Serializer::Json.new
|
194
|
+
rescue => e
|
195
|
+
# Fallback if JSON serializer is not available
|
196
|
+
nil
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
@@ -26,20 +26,35 @@ module SmartMessage
|
|
26
26
|
@file_path = File.expand_path(file_path)
|
27
27
|
@mutex = Mutex.new
|
28
28
|
ensure_directory_exists
|
29
|
+
|
30
|
+
logger.debug { "[SmartMessage::DeadLetterQueue] Initialized with file path: #{@file_path}" }
|
31
|
+
rescue => e
|
32
|
+
logger&.error { "[SmartMessage] Error in dead letter queue initialization: #{e.class.name} - #{e.message}" }
|
33
|
+
raise
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def logger
|
39
|
+
@logger ||= SmartMessage::Logger.default
|
29
40
|
end
|
41
|
+
|
42
|
+
public
|
30
43
|
|
31
44
|
# Core FIFO queue operations
|
32
45
|
|
33
46
|
# Add a failed message to the dead letter queue
|
34
|
-
# @param
|
35
|
-
# @param message_payload [String] The serialized message payload
|
47
|
+
# @param message [SmartMessage::Base] The message instance
|
36
48
|
# @param error_info [Hash] Error details including :error, :retry_count, :transport, etc.
|
37
|
-
def enqueue(
|
49
|
+
def enqueue(message, error_info = {})
|
50
|
+
message_header = message._sm_header
|
51
|
+
message_payload = message.encode
|
52
|
+
|
38
53
|
entry = {
|
39
54
|
timestamp: Time.now.iso8601,
|
40
55
|
header: message_header.to_hash,
|
41
56
|
payload: message_payload,
|
42
|
-
payload_format: error_info[:serializer] || 'json',
|
57
|
+
payload_format: error_info[:serializer] || 'json',
|
43
58
|
error: error_info[:error] || 'Unknown error',
|
44
59
|
retry_count: error_info[:retry_count] || 0,
|
45
60
|
transport: error_info[:transport],
|
@@ -77,7 +92,7 @@ module SmartMessage
|
|
77
92
|
oldest_entry
|
78
93
|
end
|
79
94
|
rescue JSON::ParserError => e
|
80
|
-
|
95
|
+
logger.warn { "[SmartMessage] Warning: Corrupted DLQ entry skipped: #{e.message}" }
|
81
96
|
nil
|
82
97
|
end
|
83
98
|
|
@@ -209,8 +224,10 @@ module SmartMessage
|
|
209
224
|
read_entries_with_filter do |entry|
|
210
225
|
stats[:total] += 1
|
211
226
|
|
212
|
-
|
213
|
-
|
227
|
+
full_class_name = entry.dig(:header, :message_class) || 'Unknown'
|
228
|
+
# Extract short class name (everything after the last ::)
|
229
|
+
short_class_name = full_class_name.split('::').last || full_class_name
|
230
|
+
stats[:by_class][short_class_name] += 1
|
214
231
|
|
215
232
|
error = entry[:error] || 'Unknown error'
|
216
233
|
stats[:by_error][error] += 1
|
@@ -286,11 +303,11 @@ module SmartMessage
|
|
286
303
|
JSON.parse(payload, symbolize_names: true)
|
287
304
|
else
|
288
305
|
# For unknown formats, assume JSON as fallback but log warning
|
289
|
-
|
306
|
+
logger.warn { "[SmartMessage] Warning: Unknown payload format '#{format}', attempting JSON" }
|
290
307
|
JSON.parse(payload, symbolize_names: true)
|
291
308
|
end
|
292
309
|
rescue JSON::ParserError => e
|
293
|
-
|
310
|
+
logger.error { "[SmartMessage] Error in payload deserialization: #{e.class.name} - #{e.message}" }
|
294
311
|
nil
|
295
312
|
end
|
296
313
|
|
@@ -310,7 +327,7 @@ module SmartMessage
|
|
310
327
|
message._sm_header.to = header_data[:to] if header_data[:to]
|
311
328
|
message._sm_header.reply_to = header_data[:reply_to] if header_data[:reply_to]
|
312
329
|
rescue => e
|
313
|
-
|
330
|
+
logger.warn { "[SmartMessage] Warning: Failed to restore some header fields: #{e.message}" }
|
314
331
|
end
|
315
332
|
|
316
333
|
# Generic file reading iterator with error handling
|
@@ -15,15 +15,28 @@ module SmartMessage
|
|
15
15
|
# TODO: setup forwardable for some @router_pool methods
|
16
16
|
|
17
17
|
def initialize(circuit_breaker_options = {})
|
18
|
-
@subscribers = Hash.new
|
18
|
+
@subscribers = Hash.new { |h, k| h[k] = [] }
|
19
19
|
@router_pool = Concurrent::CachedThreadPool.new
|
20
|
-
|
20
|
+
|
21
21
|
# Configure circuit breakers
|
22
22
|
configure_circuit_breakers(circuit_breaker_options)
|
23
23
|
at_exit do
|
24
24
|
shutdown_pool
|
25
25
|
end
|
26
|
+
|
27
|
+
logger.debug { "[SmartMessage::Dispatcher] Initialized with circuit breaker options: #{circuit_breaker_options}" }
|
28
|
+
rescue => e
|
29
|
+
logger.error { "[SmartMessage] Error in dispatcher initialization: #{e.class.name} - #{e.message}" }
|
30
|
+
raise
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def logger
|
36
|
+
@logger ||= SmartMessage::Logger.default
|
26
37
|
end
|
38
|
+
|
39
|
+
public
|
27
40
|
|
28
41
|
|
29
42
|
def what_can_i_do?
|
@@ -84,20 +97,20 @@ module SmartMessage
|
|
84
97
|
|
85
98
|
def add(message_class, process_method_as_string, filter_options = {})
|
86
99
|
klass = String(message_class)
|
87
|
-
|
100
|
+
|
88
101
|
# Create subscription entry with filter options
|
89
102
|
subscription = {
|
90
103
|
process_method: process_method_as_string,
|
91
104
|
filters: filter_options
|
92
105
|
}
|
93
|
-
|
106
|
+
|
94
107
|
# Check if this exact subscription already exists
|
95
108
|
existing_subscription = @subscribers[klass].find do |sub|
|
96
109
|
sub[:process_method] == process_method_as_string && sub[:filters] == filter_options
|
97
110
|
end
|
98
|
-
|
111
|
+
|
99
112
|
unless existing_subscription
|
100
|
-
@subscribers[klass]
|
113
|
+
@subscribers[klass] << subscription
|
101
114
|
end
|
102
115
|
end
|
103
116
|
|
@@ -117,25 +130,27 @@ module SmartMessage
|
|
117
130
|
|
118
131
|
# complete reset all subscriptions
|
119
132
|
def drop_all!
|
120
|
-
@subscribers = Hash.new
|
133
|
+
@subscribers = Hash.new { |h, k| h[k] = [] }
|
121
134
|
end
|
122
135
|
|
123
136
|
|
124
|
-
#
|
125
|
-
#
|
126
|
-
|
127
|
-
|
137
|
+
# Route a decoded message to appropriate message processors
|
138
|
+
# @param decoded_message [SmartMessage::Base] The decoded message instance
|
139
|
+
def route(decoded_message)
|
140
|
+
message_header = decoded_message._sm_header
|
128
141
|
message_klass = message_header.message_class
|
129
|
-
|
130
|
-
|
142
|
+
logger.debug { "[SmartMessage::Dispatcher] Routing message #{message_klass} to #{@subscribers[message_klass]&.size || 0} subscribers" }
|
143
|
+
logger.debug { "[SmartMessage::Dispatcher] Available subscribers: #{@subscribers.keys}" }
|
144
|
+
return nil if @subscribers[message_klass].nil? || @subscribers[message_klass].empty?
|
145
|
+
|
131
146
|
@subscribers[message_klass].each do |subscription|
|
132
147
|
# Extract subscription details
|
133
148
|
message_processor = subscription[:process_method]
|
134
149
|
filters = subscription[:filters]
|
135
|
-
|
150
|
+
|
136
151
|
# Check if message matches filters
|
137
152
|
next unless message_matches_filters?(message_header, filters)
|
138
|
-
|
153
|
+
|
139
154
|
SS.add(message_klass, message_processor, 'routed' )
|
140
155
|
@router_pool.post do
|
141
156
|
# Use circuit breaker to protect message processing
|
@@ -143,21 +158,21 @@ module SmartMessage
|
|
143
158
|
# Check if this is a proc handler or a regular method call
|
144
159
|
if proc_handler?(message_processor)
|
145
160
|
# Call the proc handler via SmartMessage::Base
|
146
|
-
SmartMessage::Base.call_proc_handler(message_processor,
|
161
|
+
SmartMessage::Base.call_proc_handler(message_processor, decoded_message)
|
147
162
|
else
|
148
|
-
#
|
163
|
+
# Method call logic with decoded message
|
149
164
|
parts = message_processor.split('.')
|
150
165
|
target_klass = parts[0]
|
151
166
|
class_method = parts[1]
|
152
167
|
target_klass.constantize
|
153
168
|
.method(class_method)
|
154
|
-
.call(
|
169
|
+
.call(decoded_message)
|
155
170
|
end
|
156
171
|
end
|
157
|
-
|
172
|
+
|
158
173
|
# Handle circuit breaker fallback responses
|
159
174
|
if circuit_result.is_a?(Hash) && circuit_result[:circuit_breaker]
|
160
|
-
handle_circuit_breaker_fallback(circuit_result,
|
175
|
+
handle_circuit_breaker_fallback(circuit_result, decoded_message, message_processor)
|
161
176
|
end
|
162
177
|
end
|
163
178
|
end
|
@@ -167,7 +182,7 @@ module SmartMessage
|
|
167
182
|
# @return [Hash] Circuit breaker statistics
|
168
183
|
def circuit_breaker_stats
|
169
184
|
stats = {}
|
170
|
-
|
185
|
+
|
171
186
|
begin
|
172
187
|
if respond_to?(:circuit)
|
173
188
|
breaker = circuit(:message_processor)
|
@@ -186,7 +201,7 @@ module SmartMessage
|
|
186
201
|
rescue => e
|
187
202
|
stats[:error] = "Failed to get circuit breaker stats: #{e.message}"
|
188
203
|
end
|
189
|
-
|
204
|
+
|
190
205
|
stats
|
191
206
|
end
|
192
207
|
|
@@ -204,7 +219,7 @@ module SmartMessage
|
|
204
219
|
# Shutdown the router pool with timeout and fallback
|
205
220
|
def shutdown_pool
|
206
221
|
@router_pool.shutdown
|
207
|
-
|
222
|
+
|
208
223
|
# Wait for graceful shutdown, force kill if timeout
|
209
224
|
unless @router_pool.wait_for_termination(3)
|
210
225
|
@router_pool.kill
|
@@ -218,26 +233,46 @@ module SmartMessage
|
|
218
233
|
def message_matches_filters?(message_header, filters)
|
219
234
|
# If no filters specified, accept all messages (backward compatibility)
|
220
235
|
return true if filters.nil? || filters.empty? || filters.values.all?(&:nil?)
|
221
|
-
|
236
|
+
|
222
237
|
# Check from filter
|
223
238
|
if filters[:from]
|
224
|
-
from_match = filters[:from]
|
239
|
+
from_match = filter_value_matches?(message_header.from, filters[:from])
|
225
240
|
return false unless from_match
|
226
241
|
end
|
227
|
-
|
242
|
+
|
228
243
|
# Check to/broadcast filters (OR logic between them)
|
229
244
|
if filters[:broadcast] || filters[:to]
|
230
245
|
broadcast_match = filters[:broadcast] && message_header.to.nil?
|
231
|
-
to_match = filters[:to] && filters[:to]
|
232
|
-
|
246
|
+
to_match = filters[:to] && filter_value_matches?(message_header.to, filters[:to])
|
247
|
+
|
233
248
|
# If either broadcast or to filter is specified, at least one must match
|
234
249
|
combined_match = (broadcast_match || to_match)
|
235
250
|
return false unless combined_match
|
236
251
|
end
|
237
|
-
|
252
|
+
|
238
253
|
true
|
239
254
|
end
|
240
255
|
|
256
|
+
# Check if a value matches any of the filter criteria
|
257
|
+
# Supports both exact string matching and regex pattern matching
|
258
|
+
# @param value [String, nil] The value to match against
|
259
|
+
# @param filter_array [Array] Array of strings and/or regexps to match against
|
260
|
+
# @return [Boolean] True if the value matches any filter in the array
|
261
|
+
def filter_value_matches?(value, filter_array)
|
262
|
+
return false if value.nil? || filter_array.nil?
|
263
|
+
|
264
|
+
filter_array.any? do |filter|
|
265
|
+
case filter
|
266
|
+
when String
|
267
|
+
filter == value
|
268
|
+
when Regexp
|
269
|
+
filter.match?(value)
|
270
|
+
else
|
271
|
+
false
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
241
276
|
# Check if a message processor is a proc handler
|
242
277
|
# @param message_processor [String] The message processor identifier
|
243
278
|
# @return [Boolean] True if this is a proc handler
|
@@ -250,19 +285,19 @@ module SmartMessage
|
|
250
285
|
def configure_circuit_breakers(options = {})
|
251
286
|
# Ensure CircuitBreaker module is available
|
252
287
|
return unless defined?(SmartMessage::CircuitBreaker::DEFAULT_CONFIGS)
|
253
|
-
|
288
|
+
|
254
289
|
# Configure message processor circuit breaker
|
255
290
|
default_config = SmartMessage::CircuitBreaker::DEFAULT_CONFIGS[:message_processor]
|
256
291
|
return unless default_config
|
257
|
-
|
292
|
+
|
258
293
|
processor_config = default_config.merge(options[:message_processor] || {})
|
259
|
-
|
294
|
+
|
260
295
|
# Define the circuit using the class-level DSL
|
261
296
|
self.class.circuit :message_processor do
|
262
|
-
threshold failures: processor_config[:threshold][:failures],
|
297
|
+
threshold failures: processor_config[:threshold][:failures],
|
263
298
|
within: processor_config[:threshold][:within].seconds
|
264
299
|
reset_after processor_config[:reset_after].seconds
|
265
|
-
|
300
|
+
|
266
301
|
# Configure storage backend
|
267
302
|
case processor_config[:storage]
|
268
303
|
when :redis
|
@@ -281,7 +316,7 @@ module SmartMessage
|
|
281
316
|
else
|
282
317
|
storage BreakerMachines::Storage::Memory.new
|
283
318
|
end
|
284
|
-
|
319
|
+
|
285
320
|
# Default fallback for message processing failures
|
286
321
|
fallback do |exception|
|
287
322
|
{
|
@@ -296,27 +331,33 @@ module SmartMessage
|
|
296
331
|
}
|
297
332
|
end
|
298
333
|
end
|
299
|
-
|
334
|
+
|
300
335
|
end
|
301
336
|
|
302
337
|
# Handle circuit breaker fallback responses
|
303
338
|
# @param circuit_result [Hash] The circuit breaker fallback result
|
304
|
-
# @param
|
305
|
-
# @param message_payload [String] The message payload
|
339
|
+
# @param decoded_message [SmartMessage::Base] The decoded message instance
|
306
340
|
# @param message_processor [String] The processor that failed
|
307
|
-
def handle_circuit_breaker_fallback(circuit_result,
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
341
|
+
def handle_circuit_breaker_fallback(circuit_result, decoded_message, message_processor)
|
342
|
+
message_header = decoded_message._sm_header
|
343
|
+
|
344
|
+
# Always log circuit breaker activation for debugging
|
345
|
+
error_msg = circuit_result[:circuit_breaker][:error]
|
346
|
+
logger.error { "[SmartMessage::Dispatcher] Circuit breaker activated for processor: #{message_processor}" }
|
347
|
+
logger.error { "[SmartMessage::Dispatcher] Error: #{error_msg}" }
|
348
|
+
logger.error { "[SmartMessage::Dispatcher] Message: #{message_header.message_class} from #{message_header.from}" }
|
349
|
+
|
350
|
+
# Send to dead letter queue
|
351
|
+
SmartMessage::DeadLetterQueue.default.enqueue(decoded_message,
|
352
|
+
error: circuit_result[:circuit_breaker][:error],
|
353
|
+
retry_count: 0,
|
354
|
+
transport: 'circuit_breaker'
|
355
|
+
)
|
356
|
+
|
315
357
|
# TODO: Integrate with structured logging when implemented
|
316
|
-
# TODO: Send to dead letter queue when implemented
|
317
358
|
# TODO: Emit metrics/events for monitoring
|
318
|
-
|
319
|
-
#
|
359
|
+
|
360
|
+
# Record the failure in simple stats
|
320
361
|
SS.add(message_header.message_class, message_processor, 'circuit_breaker_fallback')
|
321
362
|
end
|
322
363
|
|
data/lib/smart_message/header.rb
CHANGED
@@ -57,5 +57,10 @@ module SmartMessage
|
|
57
57
|
property :reply_to,
|
58
58
|
required: false,
|
59
59
|
description: "Optional unique identifier of the entity that should receive replies to this message. Defaults to 'from' entity if not specified"
|
60
|
+
|
61
|
+
# Serialization tracking for wrapper architecture
|
62
|
+
property :serializer,
|
63
|
+
required: false,
|
64
|
+
description: "Class name of the serializer used to encode the payload (e.g., 'SmartMessage::Serializer::Json'). Used by DLQ and cross-serializer gateway patterns"
|
60
65
|
end
|
61
66
|
end
|
@@ -4,5 +4,25 @@
|
|
4
4
|
|
5
5
|
module SmartMessage::Logger
|
6
6
|
class Base
|
7
|
+
# Standard logging methods that subclasses should implement
|
8
|
+
def debug(message = nil, &block)
|
9
|
+
raise NotImplementedError, "Subclass must implement #debug"
|
10
|
+
end
|
11
|
+
|
12
|
+
def info(message = nil, &block)
|
13
|
+
raise NotImplementedError, "Subclass must implement #info"
|
14
|
+
end
|
15
|
+
|
16
|
+
def warn(message = nil, &block)
|
17
|
+
raise NotImplementedError, "Subclass must implement #warn"
|
18
|
+
end
|
19
|
+
|
20
|
+
def error(message = nil, &block)
|
21
|
+
raise NotImplementedError, "Subclass must implement #error"
|
22
|
+
end
|
23
|
+
|
24
|
+
def fatal(message = nil, &block)
|
25
|
+
raise NotImplementedError, "Subclass must implement #fatal"
|
26
|
+
end
|
7
27
|
end
|
8
|
-
end # module SmartMessage::Logger
|
28
|
+
end # module SmartMessage::Logger
|