smart_message 0.0.8 → 0.0.10

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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.irbrc +24 -0
  4. data/CHANGELOG.md +119 -0
  5. data/Gemfile.lock +6 -1
  6. data/README.md +389 -17
  7. data/docs/README.md +3 -1
  8. data/docs/addressing.md +119 -13
  9. data/docs/architecture.md +184 -46
  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_deduplication.md +488 -0
  16. data/docs/message_filtering.md +451 -0
  17. data/examples/01_point_to_point_orders.rb +54 -53
  18. data/examples/02_publish_subscribe_events.rb +14 -10
  19. data/examples/03_many_to_many_chat.rb +16 -8
  20. data/examples/04_redis_smart_home_iot.rb +20 -10
  21. data/examples/05_proc_handlers.rb +12 -11
  22. data/examples/06_custom_logger_example.rb +95 -100
  23. data/examples/07_error_handling_scenarios.rb +4 -2
  24. data/examples/08_entity_addressing_basic.rb +18 -6
  25. data/examples/08_entity_addressing_with_filtering.rb +27 -9
  26. data/examples/09_dead_letter_queue_demo.rb +559 -0
  27. data/examples/09_regex_filtering_microservices.rb +407 -0
  28. data/examples/10_header_block_configuration.rb +263 -0
  29. data/examples/10_message_deduplication.rb +209 -0
  30. data/examples/11_global_configuration_example.rb +219 -0
  31. data/examples/README.md +102 -0
  32. data/examples/dead_letters.jsonl +12 -0
  33. data/examples/performance_metrics/benchmark_results_ractor_20250818_205603.json +135 -0
  34. data/examples/performance_metrics/benchmark_results_ractor_20250818_205831.json +135 -0
  35. data/examples/performance_metrics/benchmark_results_test_20250818_204942.json +130 -0
  36. data/examples/performance_metrics/benchmark_results_threadpool_20250818_204942.json +130 -0
  37. data/examples/performance_metrics/benchmark_results_threadpool_20250818_204959.json +130 -0
  38. data/examples/performance_metrics/benchmark_results_threadpool_20250818_205044.json +130 -0
  39. data/examples/performance_metrics/benchmark_results_threadpool_20250818_205109.json +130 -0
  40. data/examples/performance_metrics/benchmark_results_threadpool_20250818_205252.json +130 -0
  41. data/examples/performance_metrics/benchmark_results_unknown_20250819_172852.json +130 -0
  42. data/examples/performance_metrics/compare_benchmarks.rb +519 -0
  43. data/examples/performance_metrics/dead_letters.jsonl +3100 -0
  44. data/examples/performance_metrics/performance_benchmark.rb +344 -0
  45. data/examples/show_logger.rb +367 -0
  46. data/examples/show_me.rb +145 -0
  47. data/examples/temp.txt +94 -0
  48. data/examples/tmux_chat/bot_agent.rb +4 -2
  49. data/examples/tmux_chat/human_agent.rb +4 -2
  50. data/examples/tmux_chat/room_monitor.rb +4 -2
  51. data/examples/tmux_chat/shared_chat_system.rb +6 -3
  52. data/lib/smart_message/addressing.rb +259 -0
  53. data/lib/smart_message/base.rb +123 -599
  54. data/lib/smart_message/circuit_breaker.rb +2 -1
  55. data/lib/smart_message/configuration.rb +199 -0
  56. data/lib/smart_message/ddq/base.rb +71 -0
  57. data/lib/smart_message/ddq/memory.rb +109 -0
  58. data/lib/smart_message/ddq/redis.rb +168 -0
  59. data/lib/smart_message/ddq.rb +31 -0
  60. data/lib/smart_message/dead_letter_queue.rb +27 -10
  61. data/lib/smart_message/deduplication.rb +174 -0
  62. data/lib/smart_message/dispatcher.rb +259 -61
  63. data/lib/smart_message/header.rb +5 -0
  64. data/lib/smart_message/logger/base.rb +21 -1
  65. data/lib/smart_message/logger/default.rb +88 -138
  66. data/lib/smart_message/logger/lumberjack.rb +324 -0
  67. data/lib/smart_message/logger/null.rb +81 -0
  68. data/lib/smart_message/logger.rb +17 -9
  69. data/lib/smart_message/messaging.rb +100 -0
  70. data/lib/smart_message/plugins.rb +132 -0
  71. data/lib/smart_message/serializer/base.rb +25 -8
  72. data/lib/smart_message/serializer/json.rb +5 -4
  73. data/lib/smart_message/subscription.rb +196 -0
  74. data/lib/smart_message/transport/base.rb +72 -41
  75. data/lib/smart_message/transport/memory_transport.rb +7 -5
  76. data/lib/smart_message/transport/redis_transport.rb +15 -45
  77. data/lib/smart_message/transport/stdout_transport.rb +18 -8
  78. data/lib/smart_message/transport.rb +1 -34
  79. data/lib/smart_message/utilities.rb +142 -0
  80. data/lib/smart_message/version.rb +1 -1
  81. data/lib/smart_message/versioning.rb +85 -0
  82. data/lib/smart_message/wrapper.rb.bak +132 -0
  83. data/lib/smart_message.rb +74 -28
  84. data/smart_message.gemspec +3 -0
  85. metadata +83 -3
  86. data/lib/smart_message/serializer.rb +0 -10
  87. data/lib/smart_message/wrapper.rb +0 -43
@@ -0,0 +1,174 @@
1
+ # lib/smart_message/deduplication.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ require_relative 'ddq'
6
+
7
+ module SmartMessage
8
+ # Deduplication functionality for message classes
9
+ #
10
+ # Provides class-level configuration and instance-level deduplication
11
+ # checking using a Deduplication Queue (DDQ).
12
+ module Deduplication
13
+ def self.included(base)
14
+ base.extend(ClassMethods)
15
+ base.instance_variable_set(:@ddq_size, DDQ::DEFAULT_SIZE)
16
+ base.instance_variable_set(:@ddq_storage, DDQ::DEFAULT_STORAGE)
17
+ base.instance_variable_set(:@ddq_options, {})
18
+ base.instance_variable_set(:@ddq_enabled, false)
19
+ base.instance_variable_set(:@ddq_instance, nil)
20
+ end
21
+
22
+ module ClassMethods
23
+ # Configure DDQ size for this message class
24
+ # @param size [Integer] Maximum number of UUIDs to track
25
+ def ddq_size(size)
26
+ unless size.is_a?(Integer) && size > 0
27
+ raise ArgumentError, "DDQ size must be a positive integer, got: #{size.inspect}"
28
+ end
29
+ @ddq_size = size
30
+ reset_ddq_instance! if ddq_enabled?
31
+ end
32
+
33
+ # Configure DDQ storage type for this message class
34
+ # @param storage [Symbol] Storage type (:memory or :redis)
35
+ # @param options [Hash] Additional options for the storage backend
36
+ def ddq_storage(storage, **options)
37
+ unless [:memory, :redis].include?(storage.to_sym)
38
+ raise ArgumentError, "DDQ storage must be :memory or :redis, got: #{storage.inspect}"
39
+ end
40
+ @ddq_storage = storage.to_sym
41
+ @ddq_options = options
42
+ reset_ddq_instance! if ddq_enabled?
43
+ end
44
+
45
+ # Enable deduplication for this message class
46
+ def enable_deduplication!
47
+ @ddq_enabled = true
48
+ get_ddq_instance # Initialize the DDQ
49
+ end
50
+
51
+ # Disable deduplication for this message class
52
+ def disable_deduplication!
53
+ @ddq_enabled = false
54
+ @ddq_instance = nil
55
+ end
56
+
57
+ # Check if deduplication is enabled
58
+ # @return [Boolean] true if DDQ is enabled
59
+ def ddq_enabled?
60
+ !!@ddq_enabled
61
+ end
62
+
63
+ # Get the current DDQ configuration
64
+ # @return [Hash] Current DDQ configuration
65
+ def ddq_config
66
+ {
67
+ enabled: ddq_enabled?,
68
+ size: @ddq_size,
69
+ storage: @ddq_storage,
70
+ options: @ddq_options
71
+ }
72
+ end
73
+
74
+ # Get DDQ statistics
75
+ # @return [Hash] DDQ statistics
76
+ def ddq_stats
77
+ return { enabled: false } unless ddq_enabled?
78
+
79
+ ddq = get_ddq_instance
80
+ if ddq
81
+ ddq.stats.merge(enabled: true)
82
+ else
83
+ { enabled: true, error: "DDQ instance not available" }
84
+ end
85
+ end
86
+
87
+ # Clear the DDQ
88
+ def clear_ddq!
89
+ return unless ddq_enabled?
90
+
91
+ ddq = get_ddq_instance
92
+ ddq&.clear
93
+ end
94
+
95
+ # Check if a UUID is a duplicate (for external use)
96
+ # @param uuid [String] The UUID to check
97
+ # @return [Boolean] true if UUID is a duplicate
98
+ def duplicate_uuid?(uuid)
99
+ return false unless ddq_enabled?
100
+
101
+ ddq = get_ddq_instance
102
+ ddq ? ddq.contains?(uuid) : false
103
+ end
104
+
105
+ # Get the DDQ instance (exposed for testing)
106
+ def get_ddq_instance
107
+ return nil unless ddq_enabled?
108
+
109
+ # Return cached instance if available and configuration hasn't changed
110
+ if @ddq_instance
111
+ return @ddq_instance
112
+ end
113
+
114
+ # Create new DDQ instance
115
+ size = @ddq_size
116
+ storage = @ddq_storage
117
+ options = @ddq_options
118
+
119
+ ddq = DDQ.create(storage, size, options)
120
+ @ddq_instance = ddq
121
+
122
+ SmartMessage::Logger.default.debug do
123
+ "[SmartMessage::Deduplication] Created DDQ for #{self.name}: " \
124
+ "storage=#{storage}, size=#{size}, options=#{options}"
125
+ end
126
+
127
+ ddq
128
+ rescue => e
129
+ SmartMessage::Logger.default.error do
130
+ "[SmartMessage::Deduplication] Failed to create DDQ for #{self.name}: #{e.message}"
131
+ end
132
+ nil
133
+ end
134
+
135
+ private
136
+
137
+ def reset_ddq_instance!
138
+ @ddq_instance = nil
139
+ end
140
+ end
141
+
142
+ # Instance methods for deduplication checking
143
+
144
+ # Check if this message is a duplicate based on its UUID
145
+ # @return [Boolean] true if this message UUID has been seen before
146
+ def duplicate?
147
+ return false unless self.class.ddq_enabled?
148
+ return false unless uuid
149
+
150
+ self.class.duplicate_uuid?(uuid)
151
+ end
152
+
153
+ # Mark this message as processed (add UUID to DDQ)
154
+ # @return [void]
155
+ def mark_as_processed!
156
+ return unless self.class.ddq_enabled?
157
+ return unless uuid
158
+
159
+ ddq = self.class.send(:get_ddq_instance)
160
+ if ddq
161
+ ddq.add(uuid)
162
+ SmartMessage::Logger.default.debug do
163
+ "[SmartMessage::Deduplication] Marked UUID as processed: #{uuid}"
164
+ end
165
+ end
166
+ end
167
+
168
+ # Get the message UUID
169
+ # @return [String, nil] The message UUID
170
+ def uuid
171
+ _sm_header&.uuid
172
+ end
173
+ end
174
+ end
@@ -15,15 +15,29 @@ 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
+ @subscriber_ddqs = {} # Hash to store DDQs per subscriber: "MessageClass:subscriber_id" => DDQ
19
20
  @router_pool = Concurrent::CachedThreadPool.new
20
-
21
+
21
22
  # Configure circuit breakers
22
23
  configure_circuit_breakers(circuit_breaker_options)
23
24
  at_exit do
24
25
  shutdown_pool
25
26
  end
27
+
28
+ logger.debug { "[SmartMessage::Dispatcher] Initialized with circuit breaker options: #{circuit_breaker_options}" }
29
+ rescue => e
30
+ logger.error { "[SmartMessage] Error in dispatcher initialization: #{e.class.name} - #{e.message}" }
31
+ raise
32
+ end
33
+
34
+ private
35
+
36
+ def logger
37
+ @logger ||= SmartMessage::Logger.default
26
38
  end
39
+
40
+ public
27
41
 
28
42
 
29
43
  def what_can_i_do?
@@ -85,57 +99,128 @@ module SmartMessage
85
99
  def add(message_class, process_method_as_string, filter_options = {})
86
100
  klass = String(message_class)
87
101
 
88
- # Create subscription entry with filter options
102
+ # Subscriber ID is derived from the process method (handler-only scoping)
103
+ subscriber_id = process_method_as_string
104
+
105
+ # Initialize DDQ for this handler if message class supports deduplication
106
+ begin
107
+ message_class_obj = klass.constantize
108
+ if message_class_obj.respond_to?(:ddq_enabled?) && message_class_obj.ddq_enabled?
109
+ initialize_ddq_for_subscriber(klass, subscriber_id, message_class_obj)
110
+ end
111
+ rescue NameError
112
+ # Message class doesn't exist as a constant (e.g., in tests with fake class names)
113
+ # Skip DDQ initialization - this is fine since the class wouldn't support deduplication anyway
114
+ end
115
+
116
+ # Create subscription entry with filter options and derived subscriber ID
89
117
  subscription = {
118
+ subscriber_id: subscriber_id,
90
119
  process_method: process_method_as_string,
91
120
  filters: filter_options
92
121
  }
93
-
122
+
94
123
  # Check if this exact subscription already exists
95
124
  existing_subscription = @subscribers[klass].find do |sub|
96
- sub[:process_method] == process_method_as_string && sub[:filters] == filter_options
125
+ sub[:subscriber_id] == subscriber_id &&
126
+ sub[:process_method] == process_method_as_string &&
127
+ sub[:filters] == filter_options
97
128
  end
98
-
129
+
99
130
  unless existing_subscription
100
- @subscribers[klass] += [subscription]
131
+ @subscribers[klass] << subscription
132
+ logger.debug { "[SmartMessage::Dispatcher] Added subscription: #{klass} for #{subscriber_id}" }
101
133
  end
102
134
  end
103
135
 
104
136
 
105
137
  # drop a processer from a subscribed message
106
- def drop(message_class, process_method_as_string)
138
+ def drop(message_class, process_method_as_string, filter_options = {})
107
139
  klass = String(message_class)
108
- @subscribers[klass].reject! { |sub| sub[:process_method] == process_method_as_string }
140
+
141
+ # Subscriber ID is derived from the process method
142
+ subscriber_id = process_method_as_string
143
+
144
+ # Drop the specific method (now uniquely identified by handler)
145
+ @subscribers[klass].reject! do |sub|
146
+ sub[:subscriber_id] == subscriber_id && sub[:process_method] == process_method_as_string
147
+ end
148
+
149
+ # Clean up DDQ if no more subscriptions for this handler
150
+ cleanup_ddq_if_empty(klass, subscriber_id)
109
151
  end
110
152
 
153
+ # drop all processors for a specific handler (subscriber)
154
+ def drop_subscriber(message_class, handler_method)
155
+ klass = String(message_class)
156
+ unless handler_method.is_a?(String) && !handler_method.empty?
157
+ raise ArgumentError, "handler_method must be a non-empty String, got: #{handler_method.inspect}"
158
+ end
159
+
160
+ # Handler method is the subscriber ID
161
+ subscriber_id = handler_method
162
+ @subscribers[klass].reject! { |sub| sub[:subscriber_id] == subscriber_id }
163
+ cleanup_ddq_if_empty(klass, subscriber_id)
164
+ end
111
165
 
112
- # drop all processer from a subscribed message
166
+ # drop all processors from a subscribed message
113
167
  def drop_all(message_class)
114
- @subscribers.delete String(message_class)
168
+ klass = String(message_class)
169
+
170
+ # Clean up all DDQs for this message class
171
+ if @subscribers[klass]
172
+ @subscribers[klass].each do |sub|
173
+ cleanup_ddq_if_empty(klass, sub[:subscriber_id], force: true)
174
+ end
175
+ end
176
+
177
+ @subscribers.delete klass
115
178
  end
116
179
 
117
-
118
180
  # complete reset all subscriptions
119
181
  def drop_all!
120
- @subscribers = Hash.new(Array.new)
182
+ # Clean up all DDQs
183
+ @subscriber_ddqs&.clear
184
+ @subscribers = Hash.new { |h, k| h[k] = [] }
121
185
  end
122
186
 
123
187
 
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)
188
+ # Route a decoded message to appropriate message processors
189
+ # @param decoded_message [SmartMessage::Base] The decoded message instance
190
+ def route(decoded_message)
191
+ message_header = decoded_message._sm_header
128
192
  message_klass = message_header.message_class
129
- return nil if @subscribers[message_klass].empty?
130
193
 
194
+ # Try to get the message class object for deduplication checks
195
+ message_class_obj = nil
196
+ begin
197
+ message_class_obj = message_klass.constantize
198
+ rescue NameError
199
+ # Message class doesn't exist as a constant - skip deduplication checks
200
+ end
201
+
202
+ logger.debug { "[SmartMessage::Dispatcher] Routing message #{message_klass} to #{@subscribers[message_klass]&.size || 0} subscribers" }
203
+ logger.debug { "[SmartMessage::Dispatcher] Available subscribers: #{@subscribers.keys}" }
204
+ return nil if @subscribers[message_klass].nil? || @subscribers[message_klass].empty?
205
+
131
206
  @subscribers[message_klass].each do |subscription|
132
207
  # Extract subscription details
208
+ subscriber_id = subscription[:subscriber_id]
133
209
  message_processor = subscription[:process_method]
134
210
  filters = subscription[:filters]
135
-
211
+
136
212
  # Check if message matches filters
137
213
  next unless message_matches_filters?(message_header, filters)
138
-
214
+
215
+ # Subscriber-specific deduplication check (only if class exists and supports it)
216
+ if message_class_obj&.respond_to?(:ddq_enabled?) && message_class_obj.ddq_enabled?
217
+ ddq = get_ddq_for_subscriber(message_klass, subscriber_id)
218
+ if ddq&.contains?(decoded_message.uuid)
219
+ logger.warn { "[SmartMessage::Dispatcher] Skipping duplicate for #{subscriber_id}: #{decoded_message.uuid} | Header: #{decoded_message._sm_header.to_h}" }
220
+ next # Skip this subscriber, continue to next
221
+ end
222
+ end
223
+
139
224
  SS.add(message_klass, message_processor, 'routed' )
140
225
  @router_pool.post do
141
226
  # Use circuit breaker to protect message processing
@@ -143,21 +228,30 @@ module SmartMessage
143
228
  # Check if this is a proc handler or a regular method call
144
229
  if proc_handler?(message_processor)
145
230
  # Call the proc handler via SmartMessage::Base
146
- SmartMessage::Base.call_proc_handler(message_processor, message_header, message_payload)
231
+ SmartMessage::Base.call_proc_handler(message_processor, decoded_message)
147
232
  else
148
- # Original method call logic
233
+ # Method call logic with decoded message
149
234
  parts = message_processor.split('.')
150
235
  target_klass = parts[0]
151
236
  class_method = parts[1]
152
237
  target_klass.constantize
153
238
  .method(class_method)
154
- .call(message_header, message_payload)
239
+ .call(decoded_message)
155
240
  end
156
241
  end
157
-
242
+
158
243
  # Handle circuit breaker fallback responses
159
244
  if circuit_result.is_a?(Hash) && circuit_result[:circuit_breaker]
160
- handle_circuit_breaker_fallback(circuit_result, message_header, message_payload, message_processor)
245
+ handle_circuit_breaker_fallback(circuit_result, decoded_message, message_processor)
246
+ else
247
+ # Mark message as processed in subscriber's DDQ after successful processing
248
+ if message_class_obj&.respond_to?(:ddq_enabled?) && message_class_obj.ddq_enabled?
249
+ ddq = get_ddq_for_subscriber(message_klass, subscriber_id)
250
+ if ddq && decoded_message.uuid
251
+ ddq.add(decoded_message.uuid)
252
+ logger.debug { "[SmartMessage::Dispatcher] Marked UUID as processed for #{subscriber_id}: #{decoded_message.uuid}" }
253
+ end
254
+ end
161
255
  end
162
256
  end
163
257
  end
@@ -167,7 +261,7 @@ module SmartMessage
167
261
  # @return [Hash] Circuit breaker statistics
168
262
  def circuit_breaker_stats
169
263
  stats = {}
170
-
264
+
171
265
  begin
172
266
  if respond_to?(:circuit)
173
267
  breaker = circuit(:message_processor)
@@ -186,7 +280,7 @@ module SmartMessage
186
280
  rescue => e
187
281
  stats[:error] = "Failed to get circuit breaker stats: #{e.message}"
188
282
  end
189
-
283
+
190
284
  stats
191
285
  end
192
286
 
@@ -204,7 +298,7 @@ module SmartMessage
204
298
  # Shutdown the router pool with timeout and fallback
205
299
  def shutdown_pool
206
300
  @router_pool.shutdown
207
-
301
+
208
302
  # Wait for graceful shutdown, force kill if timeout
209
303
  unless @router_pool.wait_for_termination(3)
210
304
  @router_pool.kill
@@ -216,26 +310,45 @@ module SmartMessage
216
310
  # @param filters [Hash] The filter criteria
217
311
  # @return [Boolean] True if the message matches all filters
218
312
  def message_matches_filters?(message_header, filters)
219
- # If no filters specified, accept all messages (backward compatibility)
220
- return true if filters.nil? || filters.empty? || filters.values.all?(&:nil?)
221
-
222
- # Check from filter
313
+ # Check from filter first (if specified)
223
314
  if filters[:from]
224
- from_match = filters[:from].include?(message_header.from)
315
+ from_match = filter_value_matches?(message_header.from, filters[:from])
225
316
  return false unless from_match
226
317
  end
227
-
318
+
228
319
  # Check to/broadcast filters (OR logic between them)
229
- if filters[:broadcast] || filters[:to]
320
+ # Only consider explicit filtering if the values are not nil
321
+ if filters[:to] || filters[:broadcast]
322
+ # Explicit filtering specified
230
323
  broadcast_match = filters[:broadcast] && message_header.to.nil?
231
- to_match = filters[:to] && filters[:to].include?(message_header.to)
324
+ to_match = filters[:to] && filter_value_matches?(message_header.to, filters[:to])
232
325
 
233
- # If either broadcast or to filter is specified, at least one must match
234
- combined_match = (broadcast_match || to_match)
235
- return false unless combined_match
326
+ # At least one must match
327
+ return (broadcast_match || to_match)
328
+ else
329
+ # No to/broadcast filtering - accept all messages (backward compatibility)
330
+ return true
331
+ end
332
+ end
333
+
334
+ # Check if a value matches any of the filter criteria
335
+ # Supports both exact string matching and regex pattern matching
336
+ # @param value [String, nil] The value to match against
337
+ # @param filter_array [Array] Array of strings and/or regexps to match against
338
+ # @return [Boolean] True if the value matches any filter in the array
339
+ def filter_value_matches?(value, filter_array)
340
+ return false if value.nil? || filter_array.nil?
341
+
342
+ filter_array.any? do |filter|
343
+ case filter
344
+ when String
345
+ filter == value
346
+ when Regexp
347
+ filter.match?(value)
348
+ else
349
+ false
350
+ end
236
351
  end
237
-
238
- true
239
352
  end
240
353
 
241
354
  # Check if a message processor is a proc handler
@@ -250,19 +363,19 @@ module SmartMessage
250
363
  def configure_circuit_breakers(options = {})
251
364
  # Ensure CircuitBreaker module is available
252
365
  return unless defined?(SmartMessage::CircuitBreaker::DEFAULT_CONFIGS)
253
-
366
+
254
367
  # Configure message processor circuit breaker
255
368
  default_config = SmartMessage::CircuitBreaker::DEFAULT_CONFIGS[:message_processor]
256
369
  return unless default_config
257
-
370
+
258
371
  processor_config = default_config.merge(options[:message_processor] || {})
259
-
372
+
260
373
  # Define the circuit using the class-level DSL
261
374
  self.class.circuit :message_processor do
262
- threshold failures: processor_config[:threshold][:failures],
375
+ threshold failures: processor_config[:threshold][:failures],
263
376
  within: processor_config[:threshold][:within].seconds
264
377
  reset_after processor_config[:reset_after].seconds
265
-
378
+
266
379
  # Configure storage backend
267
380
  case processor_config[:storage]
268
381
  when :redis
@@ -281,7 +394,7 @@ module SmartMessage
281
394
  else
282
395
  storage BreakerMachines::Storage::Memory.new
283
396
  end
284
-
397
+
285
398
  # Default fallback for message processing failures
286
399
  fallback do |exception|
287
400
  {
@@ -296,30 +409,115 @@ module SmartMessage
296
409
  }
297
410
  end
298
411
  end
299
-
412
+
300
413
  end
301
414
 
302
415
  # Handle circuit breaker fallback responses
303
416
  # @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
417
+ # @param decoded_message [SmartMessage::Base] The decoded message instance
306
418
  # @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
-
419
+ def handle_circuit_breaker_fallback(circuit_result, decoded_message, message_processor)
420
+ message_header = decoded_message._sm_header
421
+
422
+ # Always log circuit breaker activation for debugging
423
+ error_msg = circuit_result[:circuit_breaker][:error]
424
+ logger.error { "[SmartMessage::Dispatcher] Circuit breaker activated for processor: #{message_processor}" }
425
+ logger.error { "[SmartMessage::Dispatcher] Error: #{error_msg}" }
426
+ logger.error { "[SmartMessage::Dispatcher] Message: #{message_header.message_class} from #{message_header.from}" }
427
+
428
+ # Send to dead letter queue
429
+ SmartMessage::DeadLetterQueue.default.enqueue(decoded_message,
430
+ error: circuit_result[:circuit_breaker][:error],
431
+ retry_count: 0,
432
+ transport: 'circuit_breaker'
433
+ )
434
+
315
435
  # TODO: Integrate with structured logging when implemented
316
- # TODO: Send to dead letter queue when implemented
317
436
  # TODO: Emit metrics/events for monitoring
318
-
319
- # For now, record the failure in simple stats
437
+
438
+ # Record the failure in simple stats
320
439
  SS.add(message_header.message_class, message_processor, 'circuit_breaker_fallback')
321
440
  end
322
441
 
442
+ # Initialize DDQ for a subscriber if message class supports deduplication
443
+ # @param message_class [String] The message class name
444
+ # @param subscriber_id [String] The subscriber identifier
445
+ # @param message_class_obj [Class] The message class object
446
+ def initialize_ddq_for_subscriber(message_class, subscriber_id, message_class_obj)
447
+ ddq_key = "#{message_class}:#{subscriber_id}"
448
+ return if @subscriber_ddqs[ddq_key] # Already initialized
449
+
450
+ # Get DDQ configuration from message class
451
+ ddq_config = message_class_obj.ddq_config
452
+
453
+ begin
454
+ # Create DDQ instance using message class configuration
455
+ ddq = SmartMessage::DDQ.create(
456
+ ddq_config[:storage],
457
+ ddq_config[:size],
458
+ ddq_config[:options].merge(key_prefix: ddq_key)
459
+ )
460
+
461
+ @subscriber_ddqs[ddq_key] = ddq
462
+
463
+ logger.debug do
464
+ "[SmartMessage::Dispatcher] Initialized DDQ for #{subscriber_id}: " \
465
+ "storage=#{ddq_config[:storage]}, size=#{ddq_config[:size]}"
466
+ end
467
+ rescue => e
468
+ logger.error do
469
+ "[SmartMessage::Dispatcher] Failed to initialize DDQ for #{subscriber_id}: #{e.message}"
470
+ end
471
+ end
472
+ end
473
+
474
+ # Get DDQ instance for a specific subscriber
475
+ # @param message_class [String] The message class name
476
+ # @param subscriber_id [String] The subscriber identifier
477
+ # @return [SmartMessage::DDQ::Base, nil] The DDQ instance or nil
478
+ def get_ddq_for_subscriber(message_class, subscriber_id)
479
+ ddq_key = "#{message_class}:#{subscriber_id}"
480
+ @subscriber_ddqs[ddq_key]
481
+ end
482
+
483
+ # Clean up DDQ if no more subscriptions exist for a subscriber
484
+ # @param message_class [String] The message class name
485
+ # @param subscriber_id [String] The subscriber identifier
486
+ # @param force [Boolean] Force cleanup even if subscriptions exist
487
+ def cleanup_ddq_if_empty(message_class, subscriber_id, force: false)
488
+ ddq_key = "#{message_class}:#{subscriber_id}"
489
+
490
+ # Check if any subscriptions still exist for this subscriber
491
+ has_subscriptions = @subscribers[message_class]&.any? do |sub|
492
+ sub[:subscriber_id] == subscriber_id
493
+ end
494
+
495
+ if force || !has_subscriptions
496
+ ddq = @subscriber_ddqs.delete(ddq_key)
497
+ if ddq
498
+ logger.debug do
499
+ "[SmartMessage::Dispatcher] Cleaned up DDQ for #{subscriber_id}"
500
+ end
501
+ end
502
+ end
503
+ end
504
+
505
+ # Get DDQ statistics for all subscribers
506
+ # @return [Hash] DDQ statistics keyed by subscriber
507
+ def ddq_stats
508
+ stats = {}
509
+
510
+ @subscriber_ddqs.each do |ddq_key, ddq|
511
+ begin
512
+ stats[ddq_key] = ddq.stats
513
+ rescue => e
514
+ stats[ddq_key] = { error: e.message }
515
+ end
516
+ end
517
+
518
+ stats
519
+ end
520
+
323
521
 
324
522
  #######################################################
325
523
  ## Class methods
@@ -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