smart_message 0.0.7 → 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 +143 -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 +23 -6
  53. data/lib/smart_message/configuration.rb +199 -0
  54. data/lib/smart_message/dead_letter_queue.rb +361 -0
  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 +84 -53
  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 -27
  77. data/smart_message.gemspec +3 -0
  78. metadata +77 -3
  79. data/lib/smart_message/serializer.rb +0 -10
  80. data/lib/smart_message/wrapper.rb +0 -43
@@ -159,23 +159,40 @@ module SmartMessage
159
159
 
160
160
  # Configure fallback handlers for different scenarios
161
161
  module Fallbacks
162
- # Dead letter queue fallback
163
- def self.dead_letter_queue(dlq_transport = nil)
162
+ # Dead letter queue fallback - stores failed messages to file-based DLQ
163
+ def self.dead_letter_queue(dlq_instance = nil)
164
164
  proc do |exception, *args|
165
165
  # Extract message details from args if available
166
166
  message_header = args[0] if args[0].is_a?(SmartMessage::Header)
167
167
  message_payload = args[1] if args.length > 1
168
168
 
169
- # Log to dead letter queue if transport provided
170
- if dlq_transport && message_header && message_payload
171
- dlq_transport.publish(message_header, message_payload)
169
+ # Use provided DLQ instance or default
170
+ dlq = dlq_instance || SmartMessage::DeadLetterQueue.default
171
+
172
+ # Store failed message in dead letter queue
173
+ sent_to_dlq = false
174
+ if message_header && message_payload
175
+ begin
176
+ dlq.enqueue(message_header, message_payload,
177
+ error: exception.message,
178
+ retry_count: 0,
179
+ serializer: 'json', # Default to JSON, could be enhanced to detect actual serializer
180
+ stack_trace: exception.backtrace&.join("\n")
181
+ )
182
+ sent_to_dlq = true
183
+ rescue => dlq_error
184
+ # DLQ storage failed - log but don't raise
185
+ # Note: Logger might not be available in circuit breaker context
186
+ warn "Warning: Failed to store message in DLQ: #{dlq_error.message}"
187
+ end
172
188
  end
173
189
 
174
190
  {
175
191
  circuit_breaker: {
192
+ circuit: :transport_publish, # Default circuit name, overridden by specific configurations
176
193
  state: 'open',
177
194
  error: exception.message,
178
- sent_to_dlq: !!(dlq_transport && message_header && message_payload),
195
+ sent_to_dlq: sent_to_dlq,
179
196
  timestamp: Time.now.iso8601
180
197
  }
181
198
  }
@@ -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
@@ -0,0 +1,361 @@
1
+ # lib/smart_message/dead_letter_queue.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ require 'json'
6
+ require 'fileutils'
7
+
8
+ module SmartMessage
9
+ # File-based Dead Letter Queue implementation using JSON Lines format
10
+ # Provides FIFO queue operations with replay capabilities for failed messages
11
+ class DeadLetterQueue
12
+ attr_reader :file_path
13
+
14
+ # Default singleton instance
15
+ @@default_instance = nil
16
+
17
+ def self.default
18
+ @@default_instance ||= new
19
+ end
20
+
21
+ def self.configure_default(file_path)
22
+ @@default_instance = new(file_path)
23
+ end
24
+
25
+ def initialize(file_path = 'dead_letters.jsonl')
26
+ @file_path = File.expand_path(file_path)
27
+ @mutex = Mutex.new
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
40
+ end
41
+
42
+ public
43
+
44
+ # Core FIFO queue operations
45
+
46
+ # Add a failed message to the dead letter queue
47
+ # @param message [SmartMessage::Base] The message instance
48
+ # @param error_info [Hash] Error details including :error, :retry_count, :transport, etc.
49
+ def enqueue(message, error_info = {})
50
+ message_header = message._sm_header
51
+ message_payload = message.encode
52
+
53
+ entry = {
54
+ timestamp: Time.now.iso8601,
55
+ header: message_header.to_hash,
56
+ payload: message_payload,
57
+ payload_format: error_info[:serializer] || 'json',
58
+ error: error_info[:error] || 'Unknown error',
59
+ retry_count: error_info[:retry_count] || 0,
60
+ transport: error_info[:transport],
61
+ stack_trace: error_info[:stack_trace]
62
+ }
63
+
64
+ @mutex.synchronize do
65
+ File.open(@file_path, 'a') do |file|
66
+ file.puts entry.to_json
67
+ file.fsync # Ensure immediate write to disk
68
+ end
69
+ end
70
+
71
+ entry
72
+ end
73
+
74
+ # Remove and return the oldest message from the queue
75
+ # @return [Hash, nil] The oldest DLQ entry or nil if queue is empty
76
+ def dequeue
77
+ @mutex.synchronize do
78
+ return nil unless File.exist?(@file_path)
79
+
80
+ lines = File.readlines(@file_path)
81
+ return nil if lines.empty?
82
+
83
+ # Get first line (oldest)
84
+ oldest_line = lines.shift
85
+ oldest_entry = JSON.parse(oldest_line.strip, symbolize_names: true)
86
+
87
+ # Rewrite file without the first line
88
+ File.open(@file_path, 'w') do |file|
89
+ lines.each { |line| file.write(line) }
90
+ end
91
+
92
+ oldest_entry
93
+ end
94
+ rescue JSON::ParserError => e
95
+ logger.warn { "[SmartMessage] Warning: Corrupted DLQ entry skipped: #{e.message}" }
96
+ nil
97
+ end
98
+
99
+ # Look at the oldest message without removing it
100
+ # @return [Hash, nil] The oldest DLQ entry or nil if queue is empty
101
+ def peek
102
+ return nil unless File.exist?(@file_path)
103
+
104
+ File.open(@file_path, 'r') do |file|
105
+ first_line = file.readline
106
+ return nil if first_line.nil? || first_line.strip.empty?
107
+ JSON.parse(first_line.strip, symbolize_names: true)
108
+ end
109
+ rescue EOFError, JSON::ParserError
110
+ nil
111
+ end
112
+
113
+ # Get the number of messages in the queue
114
+ # @return [Integer] Number of messages in the DLQ
115
+ def size
116
+ return 0 unless File.exist?(@file_path)
117
+ File.readlines(@file_path).size
118
+ end
119
+
120
+ # Clear all messages from the queue
121
+ def clear
122
+ @mutex.synchronize do
123
+ File.delete(@file_path) if File.exist?(@file_path)
124
+ end
125
+ end
126
+
127
+ # Replay capabilities
128
+
129
+ # Replay all messages in the queue
130
+ # @param transport [SmartMessage::Transport::Base] Optional transport override
131
+ # @return [Hash] Results summary
132
+ def replay_all(transport = nil)
133
+ results = { success: 0, failed: 0, errors: [] }
134
+
135
+ while (entry = dequeue)
136
+ result = replay_entry(entry, transport)
137
+ if result[:success]
138
+ results[:success] += 1
139
+ else
140
+ results[:failed] += 1
141
+ results[:errors] << result[:error]
142
+ # Re-enqueue failed replay attempts
143
+ header = SmartMessage::Header.new(entry[:header])
144
+ enqueue(header, entry[:payload],
145
+ error: "Replay failed: #{result[:error]}",
146
+ retry_count: (entry[:retry_count] || 0) + 1)
147
+ end
148
+ end
149
+
150
+ results
151
+ end
152
+
153
+ # Replay the oldest message
154
+ # @param transport [SmartMessage::Transport::Base] Optional transport override
155
+ # @return [Hash] Result of replay attempt
156
+ def replay_one(transport = nil)
157
+ entry = dequeue
158
+ return { success: false, error: 'Queue is empty' } unless entry
159
+
160
+ replay_entry(entry, transport)
161
+ end
162
+
163
+ # Replay a batch of messages
164
+ # @param count [Integer] Number of messages to replay
165
+ # @param transport [SmartMessage::Transport::Base] Optional transport override
166
+ # @return [Hash] Results summary
167
+ def replay_batch(count = 10, transport = nil)
168
+ results = { success: 0, failed: 0, errors: [] }
169
+
170
+ count.times do
171
+ break if size == 0
172
+
173
+ result = replay_one(transport)
174
+ if result[:success]
175
+ results[:success] += 1
176
+ else
177
+ results[:failed] += 1
178
+ results[:errors] << result[:error]
179
+ end
180
+ end
181
+
182
+ results
183
+ end
184
+
185
+ # Administrative utilities
186
+
187
+ # Inspect messages in the queue without removing them
188
+ # @param limit [Integer] Maximum number of messages to show
189
+ # @return [Array<Hash>] Array of DLQ entries
190
+ def inspect_messages(limit: 10)
191
+ count = 0
192
+ read_entries_with_filter do |entry|
193
+ return [] if count >= limit
194
+ count += 1
195
+ entry
196
+ end
197
+ end
198
+
199
+ # Filter messages by message class
200
+ # @param message_class [String] The message class name to filter by
201
+ # @return [Array<Hash>] Filtered DLQ entries
202
+ def filter_by_class(message_class)
203
+ read_entries_with_filter do |entry|
204
+ entry if entry.dig(:header, :message_class) == message_class
205
+ end
206
+ end
207
+
208
+ # Filter messages by error pattern
209
+ # @param pattern [Regexp, String] Pattern to match against error messages
210
+ # @return [Array<Hash>] Filtered DLQ entries
211
+ def filter_by_error_pattern(pattern)
212
+ pattern = Regexp.new(pattern) if pattern.is_a?(String)
213
+
214
+ read_entries_with_filter do |entry|
215
+ entry if pattern.match?(entry[:error].to_s)
216
+ end
217
+ end
218
+
219
+ # Get statistics about the dead letter queue
220
+ # @return [Hash] Statistics summary
221
+ def statistics
222
+ stats = { total: 0, by_class: Hash.new(0), by_error: Hash.new(0) }
223
+
224
+ read_entries_with_filter do |entry|
225
+ stats[:total] += 1
226
+
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
231
+
232
+ error = entry[:error] || 'Unknown error'
233
+ stats[:by_error][error] += 1
234
+
235
+ nil # Don't collect entries, just process for side effects
236
+ end
237
+
238
+ stats
239
+ end
240
+
241
+ # Export messages within a time range
242
+ # @param start_time [Time] Start of time range
243
+ # @param end_time [Time] End of time range
244
+ # @return [Array<Hash>] DLQ entries within the time range
245
+ def export_range(start_time, end_time)
246
+ read_entries_with_filter do |entry|
247
+ begin
248
+ timestamp = Time.parse(entry[:timestamp])
249
+ entry if timestamp >= start_time && timestamp <= end_time
250
+ rescue ArgumentError
251
+ # Skip entries with invalid timestamps
252
+ nil
253
+ end
254
+ end
255
+ end
256
+
257
+ private
258
+
259
+ # Replay a single DLQ entry by recreating the message instance
260
+ # @param entry [Hash] The DLQ entry to replay
261
+ # @param transport_override [SmartMessage::Transport::Base] Optional transport override
262
+ # @return [Hash] Result of replay attempt
263
+ def replay_entry(entry, transport_override = nil)
264
+ message_class_name = entry.dig(:header, :message_class)
265
+ return { success: false, error: 'Missing message class' } unless message_class_name
266
+
267
+ # Get the message class
268
+ message_class = message_class_name.constantize
269
+
270
+ # Deserialize the payload using the appropriate format
271
+ payload_data = deserialize_payload(entry[:payload], entry[:payload_format] || 'json')
272
+ return { success: false, error: 'Failed to deserialize payload' } unless payload_data
273
+
274
+ # Remove the header from payload data (it's stored separately in DLQ)
275
+ payload_data.delete(:_sm_header)
276
+
277
+ # Create new message instance with original data
278
+ message = message_class.new(**payload_data)
279
+
280
+ # Restore complete header information
281
+ restore_header_fields(message, entry[:header])
282
+
283
+ # Override transport if provided - this must be done before publishing
284
+ if transport_override
285
+ message.transport(transport_override)
286
+ end
287
+
288
+ # Attempt to publish the message
289
+ message.publish
290
+
291
+ { success: true, message: message }
292
+ rescue => e
293
+ { success: false, error: "#{e.class.name}: #{e.message}" }
294
+ end
295
+
296
+ # Deserialize payload based on format
297
+ # @param payload [String] The serialized payload
298
+ # @param format [String] The serialization format (json, etc.)
299
+ # @return [Hash, nil] Deserialized data or nil if failed
300
+ def deserialize_payload(payload, format)
301
+ case format.to_s.downcase
302
+ when 'json'
303
+ JSON.parse(payload, symbolize_names: true)
304
+ else
305
+ # For unknown formats, assume JSON as fallback but log warning
306
+ logger.warn { "[SmartMessage] Warning: Unknown payload format '#{format}', attempting JSON" }
307
+ JSON.parse(payload, symbolize_names: true)
308
+ end
309
+ rescue JSON::ParserError => e
310
+ logger.error { "[SmartMessage] Error in payload deserialization: #{e.class.name} - #{e.message}" }
311
+ nil
312
+ end
313
+
314
+ # Restore all header fields from DLQ entry
315
+ # @param message [SmartMessage::Base] The message instance
316
+ # @param header_data [Hash] The stored header data
317
+ def restore_header_fields(message, header_data)
318
+ return unless header_data
319
+
320
+ # Restore all available header fields
321
+ message._sm_header.uuid = header_data[:uuid] if header_data[:uuid]
322
+ message._sm_header.message_class = header_data[:message_class] if header_data[:message_class]
323
+ message._sm_header.published_at = Time.parse(header_data[:published_at]) if header_data[:published_at]
324
+ message._sm_header.publisher_pid = header_data[:publisher_pid] if header_data[:publisher_pid]
325
+ message._sm_header.version = header_data[:version] if header_data[:version]
326
+ message._sm_header.from = header_data[:from] if header_data[:from]
327
+ message._sm_header.to = header_data[:to] if header_data[:to]
328
+ message._sm_header.reply_to = header_data[:reply_to] if header_data[:reply_to]
329
+ rescue => e
330
+ logger.warn { "[SmartMessage] Warning: Failed to restore some header fields: #{e.message}" }
331
+ end
332
+
333
+ # Generic file reading iterator with error handling
334
+ # @param block [Proc] Block to execute for each valid entry
335
+ # @return [Array] Results from the block
336
+ def read_entries_with_filter(&block)
337
+ return [] unless File.exist?(@file_path)
338
+
339
+ results = []
340
+ File.open(@file_path, 'r') do |file|
341
+ file.each_line do |line|
342
+ begin
343
+ entry = JSON.parse(line.strip, symbolize_names: true)
344
+ result = block.call(entry)
345
+ results << result if result
346
+ rescue JSON::ParserError
347
+ # Skip corrupted lines
348
+ end
349
+ end
350
+ end
351
+
352
+ results
353
+ end
354
+
355
+ # Ensure the directory for the DLQ file exists
356
+ def ensure_directory_exists
357
+ directory = File.dirname(@file_path)
358
+ FileUtils.mkdir_p(directory) unless Dir.exist?(directory)
359
+ end
360
+ end
361
+ end