smart_message 0.0.12 → 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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +155 -1
- data/Gemfile.lock +6 -6
- data/README.md +71 -25
- data/docs/core-concepts/architecture.md +5 -10
- data/docs/getting-started/examples.md +0 -12
- data/docs/getting-started/quick-start.md +4 -9
- data/docs/index.md +6 -4
- data/docs/reference/serializers.md +160 -488
- data/docs/reference/transports.md +47 -146
- data/docs/transports/memory-transport.md +2 -1
- data/docs/transports/multi-transport.md +484 -0
- data/docs/transports/redis-transport-comparison.md +215 -350
- data/docs/transports/redis-transport.md +3 -22
- data/examples/README.md +6 -9
- data/examples/city_scenario/README.md +1 -1
- data/examples/city_scenario/messages/emergency_911_message.rb +0 -1
- data/examples/city_scenario/messages/emergency_resolved_message.rb +0 -1
- data/examples/city_scenario/messages/fire_dispatch_message.rb +0 -1
- data/examples/city_scenario/messages/fire_emergency_message.rb +0 -1
- data/examples/city_scenario/messages/health_check_message.rb +0 -1
- data/examples/city_scenario/messages/health_status_message.rb +0 -1
- data/examples/city_scenario/messages/police_dispatch_message.rb +0 -1
- data/examples/city_scenario/messages/silent_alarm_message.rb +0 -1
- data/examples/file/00_run_all_file_demos.rb +260 -0
- data/examples/file/01_basic_file_transport_demo.rb +237 -0
- data/examples/file/02_fifo_transport_demo.rb +289 -0
- data/examples/file/03_file_watching_demo.rb +332 -0
- data/examples/file/04_multi_transport_file_demo.rb +432 -0
- data/examples/file/README.md +257 -0
- data/examples/memory/00_run_all_demos.rb +317 -0
- data/examples/memory/01_message_deduplication_demo.rb +18 -32
- data/examples/memory/02_dead_letter_queue_demo.rb +9 -12
- data/examples/memory/03_point_to_point_orders.rb +3 -5
- data/examples/memory/04_publish_subscribe_events.rb +15 -16
- data/examples/memory/05_many_to_many_chat.rb +19 -22
- data/examples/memory/06_stdout_publish_only.rb +145 -0
- data/examples/memory/07_proc_handlers_demo.rb +13 -14
- data/examples/memory/08_custom_logger_demo.rb +136 -140
- data/examples/memory/09_error_handling_demo.rb +7 -10
- data/examples/memory/10_entity_addressing_basic.rb +25 -31
- data/examples/memory/11_entity_addressing_with_filtering.rb +32 -36
- data/examples/memory/12_regex_filtering_microservices.rb +10 -11
- data/examples/memory/13_header_block_configuration.rb +0 -5
- data/examples/memory/14_global_configuration_demo.rb +12 -14
- data/examples/memory/15_logger_demo.rb +0 -1
- data/examples/memory/README.md +37 -20
- data/examples/memory/log/demo_app.log.1 +100 -0
- data/examples/memory/log/demo_app.log.2 +100 -0
- data/examples/multi_transport_example.rb +114 -0
- data/examples/redis/01_smart_home_iot_demo.rb +20 -24
- data/examples/redis/README.md +0 -2
- data/examples/utilities/box_it.rb +12 -0
- data/examples/utilities/doing.rb +19 -0
- data/examples/utilities/temp.md +28 -0
- data/lib/smart_message/base.rb +24 -17
- data/lib/smart_message/configuration.rb +2 -23
- data/lib/smart_message/dead_letter_queue.rb +1 -1
- data/lib/smart_message/errors.rb +3 -0
- data/lib/smart_message/header.rb +1 -1
- data/lib/smart_message/logger/default.rb +1 -1
- data/lib/smart_message/messaging.rb +37 -66
- data/lib/smart_message/plugins.rb +42 -41
- data/lib/smart_message/serializer/base.rb +1 -1
- data/lib/smart_message/serializer.rb +3 -2
- data/lib/smart_message/subscription.rb +18 -20
- data/lib/smart_message/transport/async_publish_queue.rb +284 -0
- data/lib/smart_message/transport/base.rb +42 -8
- data/lib/smart_message/transport/fifo_operations.rb +264 -0
- data/lib/smart_message/transport/file_operations.rb +200 -0
- data/lib/smart_message/transport/file_transport.rb +149 -0
- data/lib/smart_message/transport/file_watching.rb +72 -0
- data/lib/smart_message/transport/memory_transport.rb +23 -4
- data/lib/smart_message/transport/partitioned_files.rb +46 -0
- data/lib/smart_message/transport/redis_transport.rb +11 -0
- data/lib/smart_message/transport/registry.rb +0 -1
- data/lib/smart_message/transport/stdout_transport.rb +73 -41
- data/lib/smart_message/transport/stdout_transport.rb.backup +88 -0
- data/lib/smart_message/transport.rb +0 -1
- data/lib/smart_message/version.rb +1 -1
- metadata +25 -37
- data/docs/guides/redis-queue-getting-started.md +0 -697
- data/docs/guides/redis-queue-patterns.md +0 -889
- data/docs/guides/redis-queue-production.md +0 -1091
- data/docs/transports/redis-enhanced-transport.md +0 -524
- data/docs/transports/redis-queue-transport.md +0 -1304
- data/examples/redis_enhanced/README.md +0 -319
- data/examples/redis_enhanced/enhanced_01_basic_patterns.rb +0 -233
- data/examples/redis_enhanced/enhanced_02_fluent_api.rb +0 -331
- data/examples/redis_enhanced/enhanced_03_dual_publishing.rb +0 -281
- data/examples/redis_enhanced/enhanced_04_advanced_routing.rb +0 -419
- data/examples/redis_queue/01_basic_messaging.rb +0 -221
- data/examples/redis_queue/01_comprehensive_examples.rb +0 -508
- data/examples/redis_queue/02_pattern_routing.rb +0 -405
- data/examples/redis_queue/03_fluent_api.rb +0 -422
- data/examples/redis_queue/04_load_balancing.rb +0 -486
- data/examples/redis_queue/05_microservices.rb +0 -735
- data/examples/redis_queue/06_emergency_alerts.rb +0 -777
- data/examples/redis_queue/07_queue_management.rb +0 -587
- data/examples/redis_queue/README.md +0 -366
- data/examples/redis_queue/enhanced_01_basic_patterns.rb +0 -233
- data/examples/redis_queue/enhanced_02_fluent_api.rb +0 -331
- data/examples/redis_queue/enhanced_03_dual_publishing.rb +0 -281
- data/examples/redis_queue/enhanced_04_advanced_routing.rb +0 -419
- data/examples/redis_queue/redis_queue_architecture.svg +0 -148
- data/ideas/README.md +0 -41
- data/ideas/agents.md +0 -1001
- data/ideas/database_transport.md +0 -980
- data/ideas/improvement.md +0 -359
- data/ideas/meshage.md +0 -1788
- data/ideas/message_discovery.md +0 -178
- data/ideas/message_schema.md +0 -1381
- data/lib/smart_message/transport/redis_enhanced_transport.rb +0 -399
- data/lib/smart_message/transport/redis_queue_transport.rb +0 -555
- data/lib/smart_message/wrapper.rb.bak +0 -132
- /data/examples/memory/{06_pretty_print_demo.rb → 16_pretty_print_demo.rb} +0 -0
@@ -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
|
@@ -11,15 +11,17 @@ module SmartMessage
|
|
11
11
|
class Base
|
12
12
|
include BreakerMachines::DSL
|
13
13
|
|
14
|
-
attr_reader :options, :dispatcher
|
14
|
+
attr_reader :options, :dispatcher, :serializer
|
15
15
|
|
16
16
|
def initialize(**options)
|
17
17
|
@options = default_options.merge(options)
|
18
18
|
@dispatcher = options[:dispatcher] || SmartMessage::Dispatcher.new
|
19
|
+
@serializer = options[:serializer] || default_serializer
|
19
20
|
configure
|
20
21
|
configure_transport_circuit_breakers
|
21
22
|
|
22
23
|
logger.debug { "[SmartMessage::Transport::#{self.class.name.split('::').last}] Initialized with options: #{@options}" }
|
24
|
+
logger.debug { "[SmartMessage::Transport::#{self.class.name.split('::').last}] Using serializer: #{@serializer.class.name}" }
|
23
25
|
rescue => e
|
24
26
|
logger&.error { "[SmartMessage] Error in transport initialization: #{e.class.name} - #{e.message}" }
|
25
27
|
raise
|
@@ -43,10 +45,20 @@ module SmartMessage
|
|
43
45
|
{}
|
44
46
|
end
|
45
47
|
|
48
|
+
# Default serializer for this transport (override in subclasses)
|
49
|
+
def default_serializer
|
50
|
+
SmartMessage::Serializer::Json.new
|
51
|
+
end
|
52
|
+
|
46
53
|
# Publish a message with circuit breaker protection
|
47
|
-
# @param
|
48
|
-
|
49
|
-
|
54
|
+
# @param message [SmartMessage::Base] The message instance to publish
|
55
|
+
def publish(message)
|
56
|
+
# Extract routing info from message header
|
57
|
+
message_class = message._sm_header.message_class
|
58
|
+
|
59
|
+
# Serialize the entire message (flat structure with _sm_header)
|
60
|
+
serialized_message = encode_message(message)
|
61
|
+
|
50
62
|
circuit(:transport_publish).wrap do
|
51
63
|
do_publish(message_class, serialized_message)
|
52
64
|
end
|
@@ -58,7 +70,7 @@ module SmartMessage
|
|
58
70
|
raise unless e.is_a?(Hash) && e[:circuit_breaker]
|
59
71
|
|
60
72
|
# Handle circuit breaker fallback
|
61
|
-
handle_publish_fallback(e, message_class, serialized_message)
|
73
|
+
handle_publish_fallback(e, message._sm_header.message_class, serialized_message)
|
62
74
|
end
|
63
75
|
|
64
76
|
# Template method for actual publishing (implement in subclasses)
|
@@ -150,13 +162,32 @@ module SmartMessage
|
|
150
162
|
end
|
151
163
|
end
|
152
164
|
|
165
|
+
# Encode a message using the transport's serializer
|
166
|
+
# @param message [SmartMessage::Base] The message to encode
|
167
|
+
# @return [String] The serialized message
|
168
|
+
def encode_message(message)
|
169
|
+
# Update header with serializer info
|
170
|
+
message._sm_header.serializer = @serializer.class.to_s
|
171
|
+
|
172
|
+
# Serialize the entire message as a flat structure
|
173
|
+
@serializer.encode(message.to_hash)
|
174
|
+
end
|
175
|
+
|
176
|
+
# Decode a message using the transport's serializer
|
177
|
+
# @param serialized_message [String] The serialized message
|
178
|
+
# @return [Hash] The decoded message data
|
179
|
+
def decode_message(serialized_message)
|
180
|
+
@serializer.decode(serialized_message)
|
181
|
+
end
|
182
|
+
|
153
183
|
# Receive and route a message (called by transport implementations)
|
154
184
|
# @param message_class [String] The message class name
|
155
185
|
# @param serialized_message [String] The serialized message content
|
156
186
|
protected
|
157
187
|
|
158
188
|
def receive(message_class, serialized_message)
|
159
|
-
# Decode the message using
|
189
|
+
# Decode the message using transport's serializer
|
190
|
+
decoded_data = decode_message(serialized_message)
|
160
191
|
|
161
192
|
# Add defensive check for message_class type
|
162
193
|
unless message_class.respond_to?(:constantize)
|
@@ -165,10 +196,13 @@ module SmartMessage
|
|
165
196
|
raise ArgumentError, "message_class must be a String, got #{message_class.class.name}"
|
166
197
|
end
|
167
198
|
|
199
|
+
# Reconstruct the message instance from flat structure
|
168
200
|
message_class_obj = message_class.constantize
|
169
|
-
|
201
|
+
# Convert string keys to symbols for keyword arguments
|
202
|
+
symbol_data = decoded_data.transform_keys(&:to_sym)
|
203
|
+
message = message_class_obj.new(**symbol_data)
|
170
204
|
|
171
|
-
@dispatcher.route(
|
205
|
+
@dispatcher.route(message)
|
172
206
|
rescue => e
|
173
207
|
logger.error { "[SmartMessage] Error in transport receive: #{e.class.name} - #{e.message}" }
|
174
208
|
logger.error { "[SmartMessage] message_class: #{message_class.inspect} (#{message_class.class.name})" }
|
@@ -0,0 +1,264 @@
|
|
1
|
+
# lib/smart_message/transport/fifo_operations.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
require 'rbconfig'
|
6
|
+
|
7
|
+
module SmartMessage
|
8
|
+
module Transport
|
9
|
+
# Module for FIFO operations, with cross-platform considerations.
|
10
|
+
module FifoOperations
|
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_fifo
|
24
|
+
unless platform_supports_fifo?
|
25
|
+
begin
|
26
|
+
logger.warn { "[FileTransport] FIFO not supported, falling back to regular file" }
|
27
|
+
rescue
|
28
|
+
# Fallback if logger is not available
|
29
|
+
end
|
30
|
+
@options[:file_type] = :regular
|
31
|
+
return configure_file_output
|
32
|
+
end
|
33
|
+
|
34
|
+
create_named_pipe if @options[:create_fifo]
|
35
|
+
end
|
36
|
+
|
37
|
+
def create_named_pipe
|
38
|
+
case RbConfig::CONFIG['host_os']
|
39
|
+
when /mswin|mingw|cygwin/
|
40
|
+
create_windows_named_pipe
|
41
|
+
else
|
42
|
+
File.mkfifo(@options[:file_path], @options[:fifo_permissions] || 0644)
|
43
|
+
begin
|
44
|
+
logger.info { "[FileTransport] Created FIFO: #{@options[:file_path]}" }
|
45
|
+
rescue
|
46
|
+
# Fallback if logger is not available
|
47
|
+
end
|
48
|
+
end
|
49
|
+
rescue NotImplementedError
|
50
|
+
begin
|
51
|
+
logger.error { "[FileTransport] Named pipes not supported on this platform" }
|
52
|
+
rescue
|
53
|
+
# Fallback if logger is not available
|
54
|
+
end
|
55
|
+
raise
|
56
|
+
rescue => e
|
57
|
+
begin
|
58
|
+
logger.error { "[FileTransport] Failed to create FIFO: #{e.message}" }
|
59
|
+
rescue
|
60
|
+
# Fallback if logger is not available
|
61
|
+
end
|
62
|
+
raise
|
63
|
+
end
|
64
|
+
|
65
|
+
def create_windows_named_pipe
|
66
|
+
require 'win32/pipe'
|
67
|
+
pipe_name = "\\\\.\\pipe\\#{File.basename(@options[:file_path])}"
|
68
|
+
@windows_pipe_server = Win32::Pipe::Server.new(pipe_name)
|
69
|
+
begin
|
70
|
+
logger.info { "[FileTransport] Created Windows named pipe: #{pipe_name}" }
|
71
|
+
rescue
|
72
|
+
# Fallback if logger is not available
|
73
|
+
end
|
74
|
+
rescue LoadError
|
75
|
+
raise "Windows named pipes require win32-pipe gem: gem install win32-pipe"
|
76
|
+
rescue => e
|
77
|
+
begin
|
78
|
+
logger.error { "[FileTransport] Failed to create Windows named pipe: #{e.message}" }
|
79
|
+
rescue
|
80
|
+
# Fallback if logger is not available
|
81
|
+
end
|
82
|
+
raise
|
83
|
+
end
|
84
|
+
|
85
|
+
def platform_supports_fifo?
|
86
|
+
case RbConfig::CONFIG['host_os']
|
87
|
+
when /mswin|mingw|cygwin/
|
88
|
+
defined?(Win32::Pipe)
|
89
|
+
else
|
90
|
+
true
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def write_to_fifo(serialized_message)
|
95
|
+
handle = open_fifo_for_writing
|
96
|
+
unless handle
|
97
|
+
handle_fifo_write_failure(serialized_message)
|
98
|
+
return false
|
99
|
+
end
|
100
|
+
|
101
|
+
content = prepare_file_content(serialized_message)
|
102
|
+
handle.write(content)
|
103
|
+
handle.flush
|
104
|
+
true
|
105
|
+
rescue Errno::EPIPE
|
106
|
+
begin
|
107
|
+
logger.warn { "[FileTransport] FIFO reader disconnected" }
|
108
|
+
rescue
|
109
|
+
# Fallback if logger is not available
|
110
|
+
end
|
111
|
+
handle_fifo_write_failure(serialized_message)
|
112
|
+
false
|
113
|
+
rescue => e
|
114
|
+
begin
|
115
|
+
logger.error { "[FileTransport] FIFO write error: #{e.message}" }
|
116
|
+
rescue
|
117
|
+
# Fallback if logger is not available
|
118
|
+
end
|
119
|
+
handle_fifo_write_failure(serialized_message)
|
120
|
+
false
|
121
|
+
ensure
|
122
|
+
handle&.close
|
123
|
+
end
|
124
|
+
|
125
|
+
def open_fifo_for_writing
|
126
|
+
mode = @options[:fifo_mode] == :non_blocking ? File::WRONLY | File::NONBLOCK : 'w'
|
127
|
+
File.open(@options[:file_path], mode)
|
128
|
+
rescue Errno::ENXIO, Errno::ENOENT
|
129
|
+
nil
|
130
|
+
end
|
131
|
+
|
132
|
+
def handle_fifo_write_failure(serialized_message)
|
133
|
+
if @options[:fallback_transport]
|
134
|
+
begin
|
135
|
+
@options[:fallback_transport].do_publish(@current_message_class, serialized_message)
|
136
|
+
begin
|
137
|
+
logger.info { "[FileTransport] Message sent to fallback transport" }
|
138
|
+
rescue
|
139
|
+
# Fallback if logger is not available
|
140
|
+
end
|
141
|
+
rescue => e
|
142
|
+
begin
|
143
|
+
logger.error { "[FileTransport] Fallback transport failed: #{e.message}" }
|
144
|
+
rescue
|
145
|
+
# Fallback if logger is not available
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def start_fifo_reader(message_class, process_method, filter_options)
|
152
|
+
case @options[:subscription_mode]
|
153
|
+
when :fifo_blocking
|
154
|
+
start_blocking_fifo_reader(message_class, process_method)
|
155
|
+
when :fifo_select
|
156
|
+
start_select_fifo_reader(message_class, process_method)
|
157
|
+
when :fifo_polling
|
158
|
+
start_polling_fifo_reader(message_class, process_method)
|
159
|
+
else
|
160
|
+
begin
|
161
|
+
logger.warn { "[FileTransport] Invalid FIFO subscription mode: #{@options[:subscription_mode]}" }
|
162
|
+
rescue
|
163
|
+
# Fallback if logger is not available
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def start_blocking_fifo_reader(message_class, process_method)
|
169
|
+
@fifo_reader_thread = Thread.new do
|
170
|
+
Thread.current.name = "FileTransport-FifoReader"
|
171
|
+
loop do
|
172
|
+
begin
|
173
|
+
File.open(@options[:file_path], 'r') do |fifo|
|
174
|
+
while line = fifo.gets
|
175
|
+
next if line.strip.empty?
|
176
|
+
receive(message_class, line.strip)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
rescue => e
|
180
|
+
begin
|
181
|
+
logger.error { "[FileTransport] FIFO reader error: #{e.message}" }
|
182
|
+
rescue
|
183
|
+
# Fallback if logger is not available
|
184
|
+
end
|
185
|
+
sleep 1
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def start_select_fifo_reader(message_class, process_method)
|
192
|
+
@fifo_select_thread = Thread.new do
|
193
|
+
Thread.current.name = "FileTransport-FifoSelect"
|
194
|
+
fifo = File.open(@options[:file_path], File::RDONLY | File::NONBLOCK)
|
195
|
+
|
196
|
+
loop do
|
197
|
+
ready = IO.select([fifo], nil, nil, 1.0)
|
198
|
+
if ready
|
199
|
+
begin
|
200
|
+
while line = fifo.gets
|
201
|
+
next if line.strip.empty?
|
202
|
+
receive(message_class, line.strip)
|
203
|
+
end
|
204
|
+
rescue IO::WaitReadable
|
205
|
+
next
|
206
|
+
rescue => e
|
207
|
+
begin
|
208
|
+
logger.error { "[FileTransport] FIFO select error: #{e.message}" }
|
209
|
+
rescue
|
210
|
+
# Fallback if logger is not available
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
rescue => e
|
216
|
+
begin
|
217
|
+
logger.error { "[FileTransport] FIFO select thread error: #{e.message}" }
|
218
|
+
rescue
|
219
|
+
# Fallback if logger is not available
|
220
|
+
end
|
221
|
+
ensure
|
222
|
+
fifo&.close
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def start_polling_fifo_reader(message_class, process_method)
|
227
|
+
@fifo_reader_thread = Thread.new do
|
228
|
+
Thread.current.name = "FileTransport-FifoPoller"
|
229
|
+
loop do
|
230
|
+
begin
|
231
|
+
File.open(@options[:file_path], File::RDONLY | File::NONBLOCK) do |fifo|
|
232
|
+
while line = fifo.gets
|
233
|
+
next if line.strip.empty?
|
234
|
+
receive(message_class, line.strip)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
rescue Errno::EAGAIN
|
238
|
+
sleep(@options[:poll_interval] || 1.0)
|
239
|
+
rescue => e
|
240
|
+
begin
|
241
|
+
logger.error { "[FileTransport] FIFO polling error: #{e.message}" }
|
242
|
+
rescue
|
243
|
+
# Fallback if logger is not available
|
244
|
+
end
|
245
|
+
sleep 1
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def stop_fifo_operations
|
252
|
+
@fifo_reader_thread&.kill
|
253
|
+
@fifo_reader_thread&.join(2)
|
254
|
+
@fifo_select_thread&.kill
|
255
|
+
@fifo_select_thread&.join(2)
|
256
|
+
@fifo_handle&.close
|
257
|
+
end
|
258
|
+
|
259
|
+
def fifo_active?
|
260
|
+
@fifo_reader_thread&.alive? || @fifo_select_thread&.alive?
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|