smart_message 0.0.13 → 0.0.16

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +120 -0
  4. data/Gemfile.lock +3 -3
  5. data/README.md +71 -25
  6. data/docs/index.md +2 -0
  7. data/docs/reference/transports.md +46 -21
  8. data/docs/transports/memory-transport.md +2 -1
  9. data/docs/transports/multi-transport.md +484 -0
  10. data/examples/file/00_run_all_file_demos.rb +260 -0
  11. data/examples/file/01_basic_file_transport_demo.rb +237 -0
  12. data/examples/file/02_fifo_transport_demo.rb +289 -0
  13. data/examples/file/03_file_watching_demo.rb +332 -0
  14. data/examples/file/04_multi_transport_file_demo.rb +432 -0
  15. data/examples/file/README.md +257 -0
  16. data/examples/memory/00_run_all_demos.rb +317 -0
  17. data/examples/memory/01_message_deduplication_demo.rb +18 -30
  18. data/examples/memory/02_dead_letter_queue_demo.rb +9 -9
  19. data/examples/memory/03_point_to_point_orders.rb +3 -3
  20. data/examples/memory/04_publish_subscribe_events.rb +15 -15
  21. data/examples/memory/05_many_to_many_chat.rb +19 -19
  22. data/examples/memory/06_stdout_publish_only.rb +145 -0
  23. data/examples/memory/07_proc_handlers_demo.rb +13 -13
  24. data/examples/memory/08_custom_logger_demo.rb +136 -136
  25. data/examples/memory/09_error_handling_demo.rb +7 -7
  26. data/examples/memory/10_entity_addressing_basic.rb +25 -25
  27. data/examples/memory/11_entity_addressing_with_filtering.rb +32 -32
  28. data/examples/memory/12_regex_filtering_microservices.rb +10 -10
  29. data/examples/memory/14_global_configuration_demo.rb +12 -12
  30. data/examples/memory/README.md +34 -17
  31. data/examples/memory/log/demo_app.log.1 +100 -0
  32. data/examples/memory/log/demo_app.log.2 +100 -0
  33. data/examples/multi_transport_example.rb +114 -0
  34. data/examples/redis/01_smart_home_iot_demo.rb +20 -20
  35. data/examples/utilities/box_it.rb +12 -0
  36. data/examples/utilities/doing.rb +19 -0
  37. data/examples/utilities/temp.md +28 -0
  38. data/lib/smart_message/base.rb +5 -7
  39. data/lib/smart_message/errors.rb +3 -0
  40. data/lib/smart_message/header.rb +1 -1
  41. data/lib/smart_message/logger/default.rb +1 -1
  42. data/lib/smart_message/messaging.rb +36 -6
  43. data/lib/smart_message/plugins.rb +46 -4
  44. data/lib/smart_message/serializer/base.rb +1 -1
  45. data/lib/smart_message/serializer.rb +3 -2
  46. data/lib/smart_message/subscription.rb +18 -20
  47. data/lib/smart_message/transport/async_publish_queue.rb +284 -0
  48. data/lib/smart_message/transport/fifo_operations.rb +264 -0
  49. data/lib/smart_message/transport/file_operations.rb +200 -0
  50. data/lib/smart_message/transport/file_transport.rb +149 -0
  51. data/lib/smart_message/transport/file_watching.rb +72 -0
  52. data/lib/smart_message/transport/partitioned_files.rb +46 -0
  53. data/lib/smart_message/transport/stdout_transport.rb +50 -36
  54. data/lib/smart_message/transport/stdout_transport.rb.backup +88 -0
  55. data/lib/smart_message/version.rb +1 -1
  56. metadata +24 -10
  57. data/ideas/README.md +0 -41
  58. data/ideas/agents.md +0 -1001
  59. data/ideas/database_transport.md +0 -980
  60. data/ideas/improvement.md +0 -359
  61. data/ideas/meshage.md +0 -1788
  62. data/ideas/message_discovery.md +0 -178
  63. data/ideas/message_schema.md +0 -1381
  64. data/lib/smart_message/wrapper.rb.bak +0 -132
  65. /data/examples/memory/{06_pretty_print_demo.rb → 16_pretty_print_demo.rb} +0 -0
@@ -30,9 +30,14 @@ module SmartMessage
30
30
  def transport(klass_or_instance = nil)
31
31
  if klass_or_instance.nil?
32
32
  # Return instance transport, class transport, or global configuration
33
- @transport || self.class.class_variable_get(:@@transport) || SmartMessage::Transport.default
33
+ # For backward compatibility, return first transport if array, otherwise single transport
34
+ transport_value = @transport || self.class.class_variable_get(:@@transport) || SmartMessage::Transport.default
35
+ transport_value.is_a?(Array) ? transport_value.first : transport_value
34
36
  else
35
- @transport = klass_or_instance
37
+ # Normalize to array for internal consistent handling
38
+ @transport = Array(klass_or_instance)
39
+ # Return the original value for backward compatibility with method chaining
40
+ klass_or_instance
36
41
  end
37
42
  end
38
43
 
@@ -44,6 +49,22 @@ module SmartMessage
44
49
  end
45
50
  def reset_transport; @transport = nil; end
46
51
 
52
+ # Utility methods for working with transport collections
53
+ def transports
54
+ # Get the raw transport value (which is internally stored as array)
55
+ raw_transport = @transport || self.class.class_variable_get(:@@transport) || SmartMessage::Transport.default
56
+ # Always return as array for consistent handling
57
+ raw_transport.is_a?(Array) ? raw_transport : Array(raw_transport)
58
+ end
59
+
60
+ def single_transport?
61
+ transports.length == 1
62
+ end
63
+
64
+ def multiple_transports?
65
+ transports.length > 1
66
+ end
67
+
47
68
  module ClassMethods
48
69
  #########################################################
49
70
  ## class-level configuration
@@ -58,9 +79,14 @@ module SmartMessage
58
79
  def transport(klass_or_instance = nil)
59
80
  if klass_or_instance.nil?
60
81
  # Return class-level transport or fall back to global configuration
61
- class_variable_get(:@@transport) || SmartMessage::Transport.default
82
+ # For backward compatibility, return first transport if array, otherwise single transport
83
+ transport_value = class_variable_get(:@@transport) || SmartMessage::Transport.default
84
+ transport_value.is_a?(Array) ? transport_value.first : transport_value
62
85
  else
63
- class_variable_set(:@@transport, klass_or_instance)
86
+ # Normalize to array for internal consistent handling
87
+ class_variable_set(:@@transport, Array(klass_or_instance))
88
+ # Return the original value for backward compatibility with method chaining
89
+ klass_or_instance
64
90
  end
65
91
  end
66
92
 
@@ -71,6 +97,22 @@ module SmartMessage
71
97
  end
72
98
  def reset_transport; class_variable_set(:@@transport, nil); end
73
99
 
100
+ # Utility methods for working with transport collections
101
+ def transports
102
+ # Get the raw transport value (which is internally stored as array)
103
+ raw_transport = class_variable_get(:@@transport) || SmartMessage::Transport.default
104
+ # Always return as array for consistent handling
105
+ raw_transport.is_a?(Array) ? raw_transport : Array(raw_transport)
106
+ end
107
+
108
+ def single_transport?
109
+ transports.length == 1
110
+ end
111
+
112
+ def multiple_transports?
113
+ transports.length > 1
114
+ end
115
+
74
116
  #########################################################
75
117
  ## class-level logger configuration
76
118
 
@@ -58,7 +58,7 @@ module SmartMessage::Serializer
58
58
 
59
59
  # Template methods for actual serialization (implement in subclasses)
60
60
  def do_encode(message_instance)
61
- # Default implementation: serialize only the payload portion for wrapper architecture
61
+ # Default implementation: serialize only the payload portion for message architecture
62
62
  # Subclasses can override this for specific serialization formats
63
63
  message_hash = message_instance.to_h
64
64
  payload_portion = message_hash[:_sm_payload]
@@ -6,8 +6,9 @@ module SmartMessage
6
6
  module Serializer
7
7
  class << self
8
8
  def default
9
- # Check global configuration first, then fall back to framework default
10
- SmartMessage.configuration.default_serializer
9
+ # Return the framework's default serializer class
10
+ # Note: Serialization is handled by transports, not messages
11
+ SmartMessage::Serializer::Json
11
12
  end
12
13
  end
13
14
  end
@@ -32,12 +32,11 @@ module SmartMessage
32
32
 
33
33
  # Call a registered proc handler
34
34
  # @param handler_id [String] The handler identifier
35
- # @param wrapper [SmartMessage::Wrapper::Base] The message wrapper
36
- def call_proc_handler(handler_id, wrapper)
35
+ def call_proc_handler(handler_id, message)
37
36
  handler_proc = class_variable_get(:@@proc_handlers)[handler_id]
38
37
  return unless handler_proc
39
38
 
40
- handler_proc.call(wrapper)
39
+ handler_proc.call(message)
41
40
  end
42
41
 
43
42
  # Remove a proc handler from the registry
@@ -61,8 +60,8 @@ module SmartMessage
61
60
  # an exception.
62
61
  #
63
62
  # @param process_method [String, Proc, nil] The processing method:
64
- # - String: Method name like "MyService.handle_message"
65
- # - Proc: A proc/lambda that accepts (message_header, message_payload)
63
+ # - String: Method name like "MyService.handle_message"
64
+ # - Proc: A proc/lambda that accepts (message)
66
65
  # - nil: Uses default "MessageClass.process" method
67
66
  # @param broadcast [Boolean, nil] Filter for broadcast messages (to: nil)
68
67
  # @param to [String, Array, nil] Filter for messages directed to specific entities
@@ -70,26 +69,25 @@ module SmartMessage
70
69
  # @param block [Proc] Alternative way to pass a processing block
71
70
  # @return [String] The identifier used for this subscription
72
71
  #
73
- # @example Using default handler
72
+ # @example Using default handler
74
73
  # MyMessage.subscribe
75
74
  #
76
75
  # @example Using custom method name with filtering
77
76
  # MyMessage.subscribe("MyService.handle_message", from: ['order-service'])
78
77
  #
79
78
  # @example Using a block with broadcast filtering
80
- # MyMessage.subscribe(broadcast: true) do |header, payload|
81
- # data = JSON.parse(payload)
82
- # puts "Received broadcast: #{data}"
79
+ # MyMessage.subscribe(broadcast: true) do |message|
80
+ # puts "Received broadcast: #{message.content}"
83
81
  # end
84
82
  #
85
83
  # @example Entity-specific filtering (receives only messages from payment service)
86
84
  # MyMessage.subscribe("OrderService.process", from: ['payment'])
87
85
  #
88
- # @example Explicit to filter
86
+ # @example Explicit to filter
89
87
  # MyMessage.subscribe("AdminService.handle", to: 'admin', broadcast: false)
90
88
  def subscribe(process_method = nil, broadcast: nil, to: nil, from: nil, &block)
91
89
  message_class = whoami
92
-
90
+
93
91
  # Handle different parameter types
94
92
  if block_given?
95
93
  # Block was passed - use it as the handler
@@ -107,11 +105,11 @@ module SmartMessage
107
105
 
108
106
  # Subscriber identity is derived from the process method (handler)
109
107
  # This ensures each handler gets its own DDQ scope per message class
110
-
108
+
111
109
  # Normalize string filters to arrays
112
110
  to_filter = normalize_filter_value(to)
113
111
  from_filter = normalize_filter_value(from)
114
-
112
+
115
113
  # Create filter options (no explicit subscriber identity needed)
116
114
  filter_options = {
117
115
  broadcast: broadcast,
@@ -121,16 +119,16 @@ module SmartMessage
121
119
 
122
120
  # Add proper logging
123
121
  logger = SmartMessage::Logger.default
124
-
122
+
125
123
  begin
126
124
  raise Errors::TransportNotConfigured if transport_missing?
127
125
  transport.subscribe(message_class, process_method, filter_options)
128
-
126
+
129
127
  # Log successful subscription
130
128
  handler_desc = block_given? || process_method.respond_to?(:call) ? " with block/proc handler" : ""
131
129
  logger.info { "[SmartMessage] Subscribed: #{self.name}#{handler_desc}" }
132
130
  logger.debug { "[SmartMessage::Subscription] Subscribed #{message_class} with filters: #{filter_options}" }
133
-
131
+
134
132
  process_method
135
133
  rescue => e
136
134
  logger.error { "[SmartMessage] Error in message subscription: #{e.class.name} - #{e.message}" }
@@ -148,16 +146,16 @@ module SmartMessage
148
146
  process_method = message_class + '.process' if process_method.nil?
149
147
  # Add proper logging
150
148
  logger = SmartMessage::Logger.default
151
-
149
+
152
150
  begin
153
151
  if transport_configured?
154
152
  transport.unsubscribe(message_class, process_method)
155
-
153
+
156
154
  # If this was a proc handler, clean it up from the registry
157
155
  if proc_handler?(process_method)
158
156
  unregister_proc_handler(process_method)
159
157
  end
160
-
158
+
161
159
  # Log successful unsubscription
162
160
  logger.info { "[SmartMessage] Unsubscribed: #{self.name}" }
163
161
  logger.debug { "[SmartMessage::Subscription] Unsubscribed #{message_class} from #{process_method}" }
@@ -193,4 +191,4 @@ module SmartMessage
193
191
  end
194
192
  end
195
193
  end
196
- end
194
+ end
@@ -0,0 +1,284 @@
1
+ # lib/smart_message/transport/async_publish_queue.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ require 'timeout'
6
+
7
+ module SmartMessage
8
+ module Transport
9
+ # Module for asynchronous publishing queue.
10
+ module AsyncPublishQueue
11
+
12
+ private
13
+
14
+ def logger
15
+ # Ensure we have a proper logger, not just an IO object
16
+ if @logger && @logger.respond_to?(:error) && @logger.respond_to?(:info) && @logger.respond_to?(:warn)
17
+ return @logger
18
+ end
19
+ @logger = SmartMessage::Logger.default
20
+ end
21
+
22
+ public
23
+ def configure_async_publishing
24
+ return unless @options[:async]
25
+
26
+ # Ensure file configuration is set up first
27
+ configure_file_output if respond_to?(:configure_file_output)
28
+
29
+ @publish_queue = @options[:max_queue] ? SizedQueue.new(@options[:max_queue]) : Queue.new
30
+ @queue_stats = { queued: 0, processed: 0, failed: 0, dropped: 0, blocked_count: 0, max_queue_size_reached: 0 }
31
+ @last_warning_time = nil
32
+
33
+ start_publish_worker
34
+ start_queue_monitoring if @options[:enable_queue_monitoring]
35
+ end
36
+
37
+ def async_publish(message_class, serialized_message)
38
+ check_queue_warning
39
+
40
+ if queue_full?
41
+ handle_queue_overflow(message_class, serialized_message)
42
+ return false
43
+ end
44
+
45
+ @publish_queue << {
46
+ message_class: message_class,
47
+ serialized_message: serialized_message,
48
+ timestamp: Time.now,
49
+ retry_count: 0
50
+ }
51
+ @queue_stats[:queued] += 1
52
+ true
53
+ rescue => e
54
+ begin
55
+ logger.error { "[FileTransport] Error queuing message: #{e.message}" }
56
+ rescue
57
+ # Fallback if logger is not available
58
+ end
59
+ false
60
+ end
61
+
62
+ def queue_full?
63
+ @publish_queue.is_a?(SizedQueue) && @publish_queue.size >= @options[:max_queue]
64
+ end
65
+
66
+ def queue_usage_percentage
67
+ return 0 unless @publish_queue.is_a?(SizedQueue)
68
+ (@publish_queue.size.to_f / @options[:max_queue] * 100).round(1)
69
+ end
70
+
71
+ def publish_stats
72
+ return {} unless @options[:async]
73
+ @queue_stats.merge(
74
+ current_size: @publish_queue.size,
75
+ worker_alive: @publish_worker_thread&.alive? || false
76
+ )
77
+ end
78
+
79
+ private
80
+
81
+ def start_publish_worker
82
+ @publish_worker_thread = Thread.new do
83
+ Thread.current.name = "FileTransport-Publisher"
84
+
85
+ loop do
86
+ begin
87
+ message_data = Timeout.timeout(@options[:worker_timeout] || 5) { @publish_queue.pop }
88
+ process_queued_message(message_data)
89
+ rescue Timeout::Error
90
+ next
91
+ rescue => e
92
+ begin
93
+ logger.error { "[FileTransport] Publish worker error: #{e.message}" }
94
+ rescue
95
+ # Fallback if logger is not available
96
+ end
97
+ sleep 1
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ def process_queued_message(message_data)
104
+ success = do_sync_publish(message_data[:message_class], message_data[:serialized_message])
105
+ if success
106
+ @queue_stats[:processed] += 1
107
+ else
108
+ handle_publish_failure(message_data)
109
+ end
110
+ end
111
+
112
+ def do_sync_publish(message_class, serialized_message)
113
+ begin
114
+ if @options[:file_type] == :fifo
115
+ write_to_fifo(serialized_message)
116
+ else
117
+ write_to_file(serialized_message)
118
+ end
119
+ true
120
+ rescue => e
121
+ begin
122
+ logger.error { "[FileTransport] Sync write error: #{e.message}" }
123
+ rescue
124
+ # Fallback if logger is not available
125
+ end
126
+ false
127
+ end
128
+ end
129
+
130
+ def handle_publish_failure(message_data)
131
+ retry_count = message_data[:retry_count] + 1
132
+ max_retries = @options[:max_retries] || 3
133
+
134
+ if retry_count <= max_retries
135
+ sleep_time = [2 ** retry_count, @options[:max_retry_delay] || 30].min
136
+ Thread.new do
137
+ sleep sleep_time
138
+ message_data[:retry_count] = retry_count
139
+ @publish_queue << message_data
140
+ end
141
+ logger.warn { "[FileTransport] Retrying message publish (attempt #{retry_count}/#{max_retries})" }
142
+ else
143
+ @queue_stats[:failed] += 1
144
+ send_to_dead_letter_queue(message_data) if defined?(SmartMessage::DeadLetterQueue)
145
+ end
146
+ end
147
+
148
+ def handle_queue_overflow(message_class, serialized_message)
149
+ case @options[:queue_overflow_strategy]
150
+ when :drop_newest
151
+ @queue_stats[:dropped] += 1
152
+ logger.warn { "[FileTransport] Dropping newest message - queue full (#{@publish_queue.size}/#{@options[:max_queue]})" }
153
+ when :drop_oldest
154
+ begin
155
+ dropped_message = @publish_queue.pop(true)
156
+ @publish_queue << {
157
+ message_class: message_class,
158
+ serialized_message: serialized_message,
159
+ timestamp: Time.now,
160
+ retry_count: 0
161
+ }
162
+ @queue_stats[:dropped] += 1
163
+ logger.warn { "[FileTransport] Dropped oldest message to make room for new message" }
164
+ send_to_dead_letter_queue(dropped_message) if @options[:send_dropped_to_dlq]
165
+ rescue ThreadError
166
+ @queue_stats[:dropped] += 1
167
+ logger.error { "[FileTransport] Queue overflow but queue appears empty - dropping message" }
168
+ end
169
+ when :block
170
+ @queue_stats[:blocked_count] += 1
171
+ start_time = Time.now
172
+ logger.warn { "[FileTransport] Queue full (#{@publish_queue.size}/#{@options[:max_queue]}) - blocking until space available" }
173
+
174
+ @publish_queue << {
175
+ message_class: message_class,
176
+ serialized_message: serialized_message,
177
+ timestamp: Time.now,
178
+ retry_count: 0
179
+ }
180
+ logger.info { "[FileTransport] Queue space available after #{(Time.now - start_time).round(2)}s - message queued" }
181
+ @queue_stats[:queued] += 1
182
+ return true
183
+ else
184
+ @queue_stats[:dropped] += 1
185
+ logger.error { "[FileTransport] Unknown overflow strategy '#{@options[:queue_overflow_strategy]}', dropping message" }
186
+ end
187
+ false
188
+ end
189
+
190
+ def send_to_dead_letter_queue(message_data)
191
+ SmartMessage::DeadLetterQueue.default.enqueue(
192
+ message_data[:message_class],
193
+ message_data[:serialized_message],
194
+ error: "Failed async publish after #{@options[:max_retries]} retries",
195
+ transport: self.class.name,
196
+ retry_count: message_data[:retry_count]
197
+ )
198
+ rescue => e
199
+ begin
200
+ logger.error { "[FileTransport] Failed to send to DLQ: #{e.message}" }
201
+ rescue
202
+ # Fallback if logger is not available
203
+ end
204
+ end
205
+
206
+ def check_queue_warning
207
+ return unless @options[:queue_warning_threshold]
208
+ return if @options[:max_queue] == 0
209
+
210
+ usage = queue_usage_percentage
211
+ threshold = @options[:queue_warning_threshold] * 100
212
+
213
+ if usage >= threshold
214
+ now = Time.now
215
+ if @last_warning_time.nil? || (now - @last_warning_time) > 60
216
+ logger.warn { "[FileTransport] Publish queue is #{usage}% full (#{@publish_queue.size}/#{@options[:max_queue]})" }
217
+ @last_warning_time = now
218
+ end
219
+ end
220
+ end
221
+
222
+ def start_queue_monitoring
223
+ @queue_monitoring_thread = Thread.new do
224
+ Thread.current.name = "FileTransport-QueueMonitor"
225
+
226
+ loop do
227
+ sleep 30
228
+
229
+ begin
230
+ stats = publish_stats
231
+ usage = queue_usage_percentage
232
+
233
+ logger.info do
234
+ "[FileTransport] Queue stats: #{stats[:current_size]} messages " \
235
+ "(#{usage}% full), processed: #{stats[:processed]}, " \
236
+ "failed: #{stats[:failed]}, worker: #{stats[:worker_alive] ? 'alive' : 'dead'}"
237
+ end
238
+ rescue => e
239
+ logger.error { "[FileTransport] Queue monitoring error: #{e.message}" }
240
+ end
241
+ end
242
+ end
243
+ end
244
+
245
+ def stop_async_publishing
246
+ return unless @publish_worker_thread
247
+
248
+ @publish_worker_thread.kill
249
+ @publish_worker_thread.join(@options[:shutdown_timeout] || 10)
250
+
251
+ if @options[:drain_queue_on_shutdown]
252
+ drain_publish_queue
253
+ end
254
+
255
+ @publish_worker_thread = nil
256
+ @queue_monitoring_thread&.kill
257
+ @queue_monitoring_thread&.join(5)
258
+ end
259
+
260
+ def drain_publish_queue
261
+ begin
262
+ logger.info { "[FileTransport] Draining #{@publish_queue.size} queued messages" }
263
+ rescue
264
+ # Fallback if logger is not available
265
+ end
266
+
267
+ until @publish_queue.empty?
268
+ begin
269
+ message_data = @publish_queue.pop(true)
270
+ process_queued_message(message_data)
271
+ rescue ThreadError
272
+ break
273
+ end
274
+ end
275
+
276
+ begin
277
+ logger.info { "[FileTransport] Queue drained" }
278
+ rescue
279
+ # Fallback if logger is not available
280
+ end
281
+ end
282
+ end
283
+ end
284
+ end