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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.irbrc +24 -0
  4. data/CHANGELOG.md +96 -0
  5. data/Gemfile.lock +6 -1
  6. data/README.md +289 -15
  7. data/docs/README.md +3 -1
  8. data/docs/addressing.md +119 -13
  9. data/docs/architecture.md +68 -0
  10. data/docs/dead_letter_queue.md +673 -0
  11. data/docs/dispatcher.md +87 -0
  12. data/docs/examples.md +59 -1
  13. data/docs/getting-started.md +8 -1
  14. data/docs/logging.md +382 -326
  15. data/docs/message_filtering.md +451 -0
  16. data/examples/01_point_to_point_orders.rb +54 -53
  17. data/examples/02_publish_subscribe_events.rb +14 -10
  18. data/examples/03_many_to_many_chat.rb +16 -8
  19. data/examples/04_redis_smart_home_iot.rb +20 -10
  20. data/examples/05_proc_handlers.rb +12 -11
  21. data/examples/06_custom_logger_example.rb +95 -100
  22. data/examples/07_error_handling_scenarios.rb +4 -2
  23. data/examples/08_entity_addressing_basic.rb +18 -6
  24. data/examples/08_entity_addressing_with_filtering.rb +27 -9
  25. data/examples/09_dead_letter_queue_demo.rb +559 -0
  26. data/examples/09_regex_filtering_microservices.rb +407 -0
  27. data/examples/10_header_block_configuration.rb +263 -0
  28. data/examples/11_global_configuration_example.rb +219 -0
  29. data/examples/README.md +102 -0
  30. data/examples/dead_letters.jsonl +12 -0
  31. data/examples/performance_metrics/benchmark_results_ractor_20250818_205603.json +135 -0
  32. data/examples/performance_metrics/benchmark_results_ractor_20250818_205831.json +135 -0
  33. data/examples/performance_metrics/benchmark_results_test_20250818_204942.json +130 -0
  34. data/examples/performance_metrics/benchmark_results_threadpool_20250818_204942.json +130 -0
  35. data/examples/performance_metrics/benchmark_results_threadpool_20250818_204959.json +130 -0
  36. data/examples/performance_metrics/benchmark_results_threadpool_20250818_205044.json +130 -0
  37. data/examples/performance_metrics/benchmark_results_threadpool_20250818_205109.json +130 -0
  38. data/examples/performance_metrics/benchmark_results_threadpool_20250818_205252.json +130 -0
  39. data/examples/performance_metrics/benchmark_results_unknown_20250819_172852.json +130 -0
  40. data/examples/performance_metrics/compare_benchmarks.rb +519 -0
  41. data/examples/performance_metrics/dead_letters.jsonl +3100 -0
  42. data/examples/performance_metrics/performance_benchmark.rb +344 -0
  43. data/examples/show_logger.rb +367 -0
  44. data/examples/show_me.rb +145 -0
  45. data/examples/temp.txt +94 -0
  46. data/examples/tmux_chat/bot_agent.rb +4 -2
  47. data/examples/tmux_chat/human_agent.rb +4 -2
  48. data/examples/tmux_chat/room_monitor.rb +4 -2
  49. data/examples/tmux_chat/shared_chat_system.rb +6 -3
  50. data/lib/smart_message/addressing.rb +259 -0
  51. data/lib/smart_message/base.rb +121 -599
  52. data/lib/smart_message/circuit_breaker.rb +2 -1
  53. data/lib/smart_message/configuration.rb +199 -0
  54. data/lib/smart_message/dead_letter_queue.rb +27 -10
  55. data/lib/smart_message/dispatcher.rb +90 -49
  56. data/lib/smart_message/header.rb +5 -0
  57. data/lib/smart_message/logger/base.rb +21 -1
  58. data/lib/smart_message/logger/default.rb +88 -138
  59. data/lib/smart_message/logger/lumberjack.rb +324 -0
  60. data/lib/smart_message/logger/null.rb +81 -0
  61. data/lib/smart_message/logger.rb +17 -9
  62. data/lib/smart_message/messaging.rb +100 -0
  63. data/lib/smart_message/plugins.rb +132 -0
  64. data/lib/smart_message/serializer/base.rb +25 -8
  65. data/lib/smart_message/serializer/json.rb +5 -4
  66. data/lib/smart_message/subscription.rb +193 -0
  67. data/lib/smart_message/transport/base.rb +72 -41
  68. data/lib/smart_message/transport/memory_transport.rb +7 -5
  69. data/lib/smart_message/transport/redis_transport.rb +15 -45
  70. data/lib/smart_message/transport/stdout_transport.rb +18 -8
  71. data/lib/smart_message/transport.rb +1 -34
  72. data/lib/smart_message/utilities.rb +142 -0
  73. data/lib/smart_message/version.rb +1 -1
  74. data/lib/smart_message/versioning.rb +85 -0
  75. data/lib/smart_message/wrapper.rb.bak +132 -0
  76. data/lib/smart_message.rb +74 -28
  77. data/smart_message.gemspec +3 -0
  78. metadata +76 -3
  79. data/lib/smart_message/serializer.rb +0 -10
  80. 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
- puts "Warning: Failed to store message in DLQ: #{dlq_error.message}" if $DEBUG
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 message_header [SmartMessage::Header] The message header
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(message_header, message_payload, error_info = {})
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', # Track serialization format
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
- puts "Warning: Corrupted DLQ entry skipped: #{e.message}" if $DEBUG
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
- message_class = entry.dig(:header, :message_class) || 'Unknown'
213
- stats[:by_class][message_class] += 1
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
- puts "Warning: Unknown payload format '#{format}', attempting JSON" if $DEBUG
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
- puts "Error: Failed to deserialize payload as #{format}: #{e.message}" if $DEBUG
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
- puts "Warning: Failed to restore some header fields: #{e.message}" if $DEBUG
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(Array.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] += [subscription]
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(Array.new)
133
+ @subscribers = Hash.new { |h, k| h[k] = [] }
121
134
  end
122
135
 
123
136
 
124
- # message_header is of class SmartMessage::Header
125
- # message_payload is a string buffer that is a serialized
126
- # SmartMessage
127
- def route(message_header, message_payload)
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
- return nil if @subscribers[message_klass].empty?
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, message_header, message_payload)
161
+ SmartMessage::Base.call_proc_handler(message_processor, decoded_message)
147
162
  else
148
- # Original method call logic
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(message_header, message_payload)
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, message_header, message_payload, message_processor)
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].include?(message_header.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].include?(message_header.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 message_header [SmartMessage::Header] The message header
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, message_header, message_payload, message_processor)
308
- # Log circuit breaker activation
309
- if $DEBUG
310
- puts "Circuit breaker activated for processor: #{message_processor}"
311
- puts "Error: #{circuit_result[:circuit_breaker][:error]}"
312
- puts "Message: #{message_header.message_class} from #{message_header.from}"
313
- end
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
- # For now, record the failure in simple stats
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
 
@@ -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