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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +120 -0
- data/Gemfile.lock +3 -3
- data/README.md +71 -25
- data/docs/index.md +2 -0
- data/docs/reference/transports.md +46 -21
- data/docs/transports/memory-transport.md +2 -1
- data/docs/transports/multi-transport.md +484 -0
- 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 -30
- data/examples/memory/02_dead_letter_queue_demo.rb +9 -9
- data/examples/memory/03_point_to_point_orders.rb +3 -3
- data/examples/memory/04_publish_subscribe_events.rb +15 -15
- data/examples/memory/05_many_to_many_chat.rb +19 -19
- data/examples/memory/06_stdout_publish_only.rb +145 -0
- data/examples/memory/07_proc_handlers_demo.rb +13 -13
- data/examples/memory/08_custom_logger_demo.rb +136 -136
- data/examples/memory/09_error_handling_demo.rb +7 -7
- data/examples/memory/10_entity_addressing_basic.rb +25 -25
- data/examples/memory/11_entity_addressing_with_filtering.rb +32 -32
- data/examples/memory/12_regex_filtering_microservices.rb +10 -10
- data/examples/memory/14_global_configuration_demo.rb +12 -12
- data/examples/memory/README.md +34 -17
- 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 -20
- 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 +5 -7
- 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 +36 -6
- data/lib/smart_message/plugins.rb +46 -4
- 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/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/partitioned_files.rb +46 -0
- data/lib/smart_message/transport/stdout_transport.rb +50 -36
- data/lib/smart_message/transport/stdout_transport.rb.backup +88 -0
- data/lib/smart_message/version.rb +1 -1
- metadata +24 -10
- 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/wrapper.rb.bak +0 -132
- /data/examples/memory/{06_pretty_print_demo.rb → 16_pretty_print_demo.rb} +0 -0
@@ -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
|
@@ -0,0 +1,200 @@
|
|
1
|
+
# lib/smart_message/transport/file_operations.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
module SmartMessage
|
6
|
+
module Transport
|
7
|
+
# Module for shared file operations, including buffering, rotation, and basic I/O.
|
8
|
+
module FileOperations
|
9
|
+
def configure_file_output
|
10
|
+
ensure_directory_exists if @options[:create_directories]
|
11
|
+
@file_handle = open_file_handle
|
12
|
+
@write_buffer = []
|
13
|
+
@last_flush = Time.now
|
14
|
+
setup_rotation_timer if rotation_enabled?
|
15
|
+
@file_mutex = Mutex.new # For thread-safety
|
16
|
+
end
|
17
|
+
|
18
|
+
def write_to_file(serialized_message)
|
19
|
+
content = prepare_file_content(serialized_message)
|
20
|
+
|
21
|
+
@file_mutex.synchronize do
|
22
|
+
if buffered_mode?
|
23
|
+
buffer_write(content)
|
24
|
+
else
|
25
|
+
direct_write(content)
|
26
|
+
end
|
27
|
+
|
28
|
+
rotate_file_if_needed
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def flush_buffer
|
33
|
+
return if @write_buffer.empty?
|
34
|
+
|
35
|
+
# Only synchronize if we're not already holding the lock
|
36
|
+
if @file_mutex.owned?
|
37
|
+
@file_handle.write(@write_buffer.join)
|
38
|
+
@file_handle.flush
|
39
|
+
@write_buffer.clear
|
40
|
+
@last_flush = Time.now
|
41
|
+
else
|
42
|
+
@file_mutex.synchronize do
|
43
|
+
@file_handle.write(@write_buffer.join)
|
44
|
+
@file_handle.flush
|
45
|
+
@write_buffer.clear
|
46
|
+
@last_flush = Time.now
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def close_file_handle
|
52
|
+
flush_buffer if buffered_mode?
|
53
|
+
if @file_mutex
|
54
|
+
@file_mutex.synchronize do
|
55
|
+
@file_handle&.close
|
56
|
+
@file_handle = nil
|
57
|
+
end
|
58
|
+
else
|
59
|
+
@file_handle&.close
|
60
|
+
@file_handle = nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def prepare_file_content(serialized_message)
|
67
|
+
case @options[:file_format]
|
68
|
+
when :lines
|
69
|
+
"#{serialized_message}\n"
|
70
|
+
when :raw
|
71
|
+
serialized_message
|
72
|
+
else
|
73
|
+
"#{serialized_message}\n" # default to lines
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def open_file_handle
|
78
|
+
File.open(current_file_path, file_mode, encoding: @options[:encoding])
|
79
|
+
end
|
80
|
+
|
81
|
+
def ensure_directory_exists
|
82
|
+
return unless @options[:create_directories]
|
83
|
+
|
84
|
+
dir = File.dirname(current_file_path)
|
85
|
+
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
|
86
|
+
end
|
87
|
+
|
88
|
+
def current_file_path
|
89
|
+
if rotation_enabled? && time_based_rotation?
|
90
|
+
timestamped_file_path
|
91
|
+
else
|
92
|
+
@options[:file_path]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def file_mode
|
97
|
+
@options[:file_mode] || 'a' # append by default
|
98
|
+
end
|
99
|
+
|
100
|
+
def buffered_mode?
|
101
|
+
@options[:buffer_size] && @options[:buffer_size] > 0
|
102
|
+
end
|
103
|
+
|
104
|
+
def buffer_write(content)
|
105
|
+
@write_buffer << content
|
106
|
+
|
107
|
+
if buffer_full? || flush_interval_exceeded?
|
108
|
+
flush_buffer
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def direct_write(content)
|
113
|
+
@file_handle.write(content)
|
114
|
+
@file_handle.flush if @options[:auto_flush]
|
115
|
+
end
|
116
|
+
|
117
|
+
def buffer_full?
|
118
|
+
@write_buffer.join.bytesize >= @options[:buffer_size]
|
119
|
+
end
|
120
|
+
|
121
|
+
def flush_interval_exceeded?
|
122
|
+
@options[:flush_interval] &&
|
123
|
+
(Time.now - @last_flush) >= @options[:flush_interval]
|
124
|
+
end
|
125
|
+
|
126
|
+
def rotation_enabled?
|
127
|
+
@options[:rotate_size] || @options[:rotate_time]
|
128
|
+
end
|
129
|
+
|
130
|
+
def time_based_rotation?
|
131
|
+
@options[:rotate_time]
|
132
|
+
end
|
133
|
+
|
134
|
+
def should_rotate?
|
135
|
+
size_rotation_needed? || time_rotation_needed?
|
136
|
+
end
|
137
|
+
|
138
|
+
def size_rotation_needed?
|
139
|
+
@options[:rotate_size] &&
|
140
|
+
File.exist?(current_file_path) && File.size(current_file_path) >= @options[:rotate_size]
|
141
|
+
end
|
142
|
+
|
143
|
+
def time_rotation_needed?
|
144
|
+
return false unless @options[:rotate_time]
|
145
|
+
|
146
|
+
case @options[:rotate_time]
|
147
|
+
when :hourly
|
148
|
+
Time.now.min == 0 && Time.now.sec == 0
|
149
|
+
when :daily
|
150
|
+
Time.now.hour == 0 && Time.now.min == 0 && Time.now.sec == 0
|
151
|
+
else
|
152
|
+
false
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def rotate_file_if_needed
|
157
|
+
return unless should_rotate?
|
158
|
+
|
159
|
+
close_current_file
|
160
|
+
archive_current_file
|
161
|
+
@file_handle = open_file_handle
|
162
|
+
end
|
163
|
+
|
164
|
+
def close_current_file
|
165
|
+
flush_buffer if buffered_mode?
|
166
|
+
@file_handle&.close
|
167
|
+
@file_handle = nil
|
168
|
+
end
|
169
|
+
|
170
|
+
def archive_current_file
|
171
|
+
return unless File.exist?(current_file_path)
|
172
|
+
|
173
|
+
timestamp = Time.now.strftime(@options[:timestamp_format] || '%Y%m%d_%H%M%S')
|
174
|
+
base = File.basename(@options[:file_path], '.*')
|
175
|
+
ext = File.extname(@options[:file_path])
|
176
|
+
dir = File.dirname(@options[:file_path])
|
177
|
+
archive_path = File.join(dir, "#{base}_#{timestamp}#{ext}")
|
178
|
+
|
179
|
+
FileUtils.mv(current_file_path, archive_path)
|
180
|
+
|
181
|
+
# Maintain rotation count
|
182
|
+
if @options[:rotate_count]
|
183
|
+
files = Dir.glob(File.join(dir, "#{base}_*#{ext}")).sort
|
184
|
+
while files.size > @options[:rotate_count]
|
185
|
+
File.delete(files.shift)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def timestamped_file_path
|
191
|
+
base = File.basename(@options[:file_path], '.*')
|
192
|
+
ext = File.extname(@options[:file_path])
|
193
|
+
dir = File.dirname(@options[:file_path])
|
194
|
+
timestamp = Time.now.strftime(@options[:timestamp_format] || '%Y%m%d_%H%M%S')
|
195
|
+
|
196
|
+
File.join(dir, "#{base}_#{timestamp}#{ext}")
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# lib/smart_message/transport/file_transport.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
require_relative 'file_operations'
|
6
|
+
require_relative 'file_watching'
|
7
|
+
require_relative 'partitioned_files'
|
8
|
+
require_relative 'async_publish_queue'
|
9
|
+
require_relative 'fifo_operations'
|
10
|
+
|
11
|
+
module SmartMessage
|
12
|
+
module Transport
|
13
|
+
class FileTransport < Base
|
14
|
+
include FileOperations
|
15
|
+
include FileWatching
|
16
|
+
include PartitionedFiles
|
17
|
+
include AsyncPublishQueue
|
18
|
+
include FifoOperations
|
19
|
+
|
20
|
+
# @param path [String, IO, Pathname] file path or IO-like object
|
21
|
+
# @param mode [String] file open mode ("a" for append, etc.)
|
22
|
+
# @param encoding [String, nil] file encoding
|
23
|
+
def initialize(options = {})
|
24
|
+
@current_message_class = nil
|
25
|
+
super(**options)
|
26
|
+
end
|
27
|
+
|
28
|
+
def default_options
|
29
|
+
{
|
30
|
+
file_path: 'messages.log',
|
31
|
+
file_mode: 'a',
|
32
|
+
encoding: nil,
|
33
|
+
file_format: :lines,
|
34
|
+
buffer_size: 0,
|
35
|
+
flush_interval: nil,
|
36
|
+
auto_flush: true,
|
37
|
+
rotate_size: nil,
|
38
|
+
rotate_time: nil,
|
39
|
+
rotate_count: 5,
|
40
|
+
timestamp_format: '%Y%m%d_%H%M%S',
|
41
|
+
create_directories: true,
|
42
|
+
async: false,
|
43
|
+
max_queue: nil,
|
44
|
+
drop_when_full: false,
|
45
|
+
queue_overflow_strategy: :block,
|
46
|
+
max_retries: 3,
|
47
|
+
max_retry_delay: 30,
|
48
|
+
worker_timeout: 5,
|
49
|
+
shutdown_timeout: 10,
|
50
|
+
queue_warning_threshold: 0.8,
|
51
|
+
enable_queue_monitoring: true,
|
52
|
+
drain_queue_on_shutdown: true,
|
53
|
+
send_dropped_to_dlq: false,
|
54
|
+
read_from_end: true,
|
55
|
+
poll_interval: 1.0,
|
56
|
+
file_type: :regular,
|
57
|
+
create_fifo: false,
|
58
|
+
fifo_mode: :blocking,
|
59
|
+
fifo_permissions: 0644,
|
60
|
+
fallback_transport: nil,
|
61
|
+
enable_subscriptions: false,
|
62
|
+
subscription_mode: :polling,
|
63
|
+
filename_selector: nil,
|
64
|
+
directory: nil,
|
65
|
+
subscription_file_path: nil
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def configure
|
70
|
+
# Call parent configuration first
|
71
|
+
super if defined?(super)
|
72
|
+
|
73
|
+
# Then configure our file-specific features
|
74
|
+
if @options[:async]
|
75
|
+
configure_async_publishing
|
76
|
+
elsif @options[:file_type] == :fifo
|
77
|
+
configure_fifo
|
78
|
+
else
|
79
|
+
configure_file_output
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def publish(payload)
|
84
|
+
do_publish(nil, payload)
|
85
|
+
end
|
86
|
+
|
87
|
+
def do_publish(message_class, serialized_message)
|
88
|
+
@current_message_class = message_class
|
89
|
+
if @options[:async]
|
90
|
+
async_publish(message_class, serialized_message)
|
91
|
+
else
|
92
|
+
if @options[:filename_selector] || @options[:directory]
|
93
|
+
header = { message_class_name: message_class.to_s }
|
94
|
+
path = determine_file_path(serialized_message, header)
|
95
|
+
@file_handle = get_or_open_partition_handle(path)
|
96
|
+
write_to_file(serialized_message)
|
97
|
+
elsif @options[:file_type] == :fifo
|
98
|
+
write_to_fifo(serialized_message)
|
99
|
+
else
|
100
|
+
write_to_file(serialized_message)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def subscribe(message_class, process_method, filter_options = {})
|
106
|
+
unless @options[:enable_subscriptions]
|
107
|
+
logger.warn { "[FileTransport] Subscriptions disabled - set enable_subscriptions: true" }
|
108
|
+
return
|
109
|
+
end
|
110
|
+
|
111
|
+
if @options[:file_type] == :fifo
|
112
|
+
start_fifo_reader(message_class, process_method, filter_options)
|
113
|
+
else
|
114
|
+
start_file_polling(message_class, process_method, filter_options)
|
115
|
+
end
|
116
|
+
|
117
|
+
super(message_class, process_method, filter_options)
|
118
|
+
end
|
119
|
+
|
120
|
+
def connected?
|
121
|
+
case @options[:file_type]
|
122
|
+
when :fifo
|
123
|
+
subscription_active? || fifo_active?
|
124
|
+
else
|
125
|
+
(@file_handle && !@file_handle.closed?) || subscription_active?
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def disconnect
|
130
|
+
stop_file_subscriptions
|
131
|
+
stop_fifo_operations if @options[:file_type] == :fifo
|
132
|
+
stop_async_publishing if @options[:async]
|
133
|
+
close_partition_handles if @options[:filename_selector] || @options[:directory]
|
134
|
+
close_file_handle
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def subscription_active?
|
140
|
+
@polling_thread&.alive? || fifo_active?
|
141
|
+
end
|
142
|
+
|
143
|
+
def stop_file_subscriptions
|
144
|
+
@polling_thread&.kill
|
145
|
+
@polling_thread&.join(5)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# lib/smart_message/transport/file_watching.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
module SmartMessage
|
6
|
+
module Transport
|
7
|
+
# Module for file watching and subscription support (reading/tailing).
|
8
|
+
module FileWatching
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def logger
|
13
|
+
# Ensure we have a proper logger, not just an IO object
|
14
|
+
if @logger && @logger.respond_to?(:error) && @logger.respond_to?(:info) && @logger.respond_to?(:warn)
|
15
|
+
return @logger
|
16
|
+
end
|
17
|
+
@logger = SmartMessage::Logger.default
|
18
|
+
end
|
19
|
+
|
20
|
+
public
|
21
|
+
def start_file_polling(message_class, process_method, filter_options)
|
22
|
+
poll_interval = filter_options[:poll_interval] || @options[:poll_interval] || 1.0
|
23
|
+
file_path = subscription_file_path(message_class, filter_options)
|
24
|
+
|
25
|
+
@polling_thread = Thread.new do
|
26
|
+
Thread.current.name = "FileTransport-Poller"
|
27
|
+
last_position = @options[:read_from_end] ? (File.exist?(file_path) ? File.size(file_path) : 0) : 0
|
28
|
+
|
29
|
+
loop do
|
30
|
+
sleep poll_interval
|
31
|
+
next unless File.exist?(file_path)
|
32
|
+
|
33
|
+
current_size = File.size(file_path)
|
34
|
+
if current_size > last_position
|
35
|
+
process_new_file_content(file_path, last_position, current_size, message_class)
|
36
|
+
last_position = current_size
|
37
|
+
end
|
38
|
+
end
|
39
|
+
rescue => e
|
40
|
+
logger.error { "[FileTransport] Polling thread error: #{e.message}" }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def process_new_file_content(file_path, start_pos, end_pos, message_class)
|
45
|
+
File.open(file_path, 'r', encoding: @options[:encoding]) do |file|
|
46
|
+
file.seek(start_pos)
|
47
|
+
content = file.read(end_pos - start_pos)
|
48
|
+
|
49
|
+
content.each_line do |line|
|
50
|
+
next if line.strip.empty?
|
51
|
+
|
52
|
+
begin
|
53
|
+
receive(message_class, line.strip)
|
54
|
+
rescue => e
|
55
|
+
begin
|
56
|
+
logger.error { "[FileTransport] Error processing message: #{e.message}" }
|
57
|
+
rescue
|
58
|
+
# Fallback if logger is not available
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def subscription_file_path(message_class, filter_options)
|
66
|
+
filter_options[:file_path] ||
|
67
|
+
@options[:subscription_file_path] ||
|
68
|
+
File.join(File.dirname(@options[:file_path]), "#{message_class.to_s.downcase}.jsonl")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|