smart_message 0.0.2 → 0.0.3
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/CHANGELOG.md +31 -1
- data/Gemfile.lock +6 -1
- data/README.md +92 -14
- data/docs/architecture.md +41 -8
- data/docs/dispatcher.md +52 -16
- data/docs/getting-started.md +64 -2
- data/docs/message_processing.md +423 -0
- data/docs/proc_handlers_summary.md +247 -0
- data/docs/transports.md +202 -8
- data/examples/04_redis_smart_home_iot.rb +649 -0
- data/examples/05_proc_handlers.rb +181 -0
- data/examples/README.md +118 -3
- data/examples/smart_home_iot_dataflow.md +257 -0
- data/lib/smart_message/base.rb +94 -4
- data/lib/smart_message/dispatcher.rb +22 -6
- data/lib/smart_message/transport/redis_transport.rb +190 -0
- data/lib/smart_message/transport/registry.rb +1 -0
- data/lib/smart_message/transport.rb +1 -0
- data/lib/smart_message/version.rb +1 -1
- data/smart_message.gemspec +1 -0
- metadata +21 -1
data/lib/smart_message/base.rb
CHANGED
@@ -16,6 +16,9 @@ module SmartMessage
|
|
16
16
|
@@transport = nil
|
17
17
|
@@serializer = nil
|
18
18
|
@@logger = nil
|
19
|
+
|
20
|
+
# Registry for proc-based message handlers
|
21
|
+
@@proc_handlers = {}
|
19
22
|
|
20
23
|
include Hashie::Extensions::Dash::PropertyTranslation
|
21
24
|
|
@@ -184,6 +187,44 @@ module SmartMessage
|
|
184
187
|
end
|
185
188
|
|
186
189
|
|
190
|
+
#########################################################
|
191
|
+
## proc handler management
|
192
|
+
|
193
|
+
# Register a proc handler and return a unique identifier for it
|
194
|
+
# @param message_class [String] The message class name
|
195
|
+
# @param handler_proc [Proc] The proc to register
|
196
|
+
# @return [String] Unique identifier for this handler
|
197
|
+
def register_proc_handler(message_class, handler_proc)
|
198
|
+
handler_id = "#{message_class}.proc_#{SecureRandom.hex(8)}"
|
199
|
+
@@proc_handlers[handler_id] = handler_proc
|
200
|
+
handler_id
|
201
|
+
end
|
202
|
+
|
203
|
+
# Call a registered proc handler
|
204
|
+
# @param handler_id [String] The handler identifier
|
205
|
+
# @param message_header [SmartMessage::Header] The message header
|
206
|
+
# @param message_payload [String] The message payload
|
207
|
+
def call_proc_handler(handler_id, message_header, message_payload)
|
208
|
+
handler_proc = @@proc_handlers[handler_id]
|
209
|
+
return unless handler_proc
|
210
|
+
|
211
|
+
handler_proc.call(message_header, message_payload)
|
212
|
+
end
|
213
|
+
|
214
|
+
# Remove a proc handler from the registry
|
215
|
+
# @param handler_id [String] The handler identifier to remove
|
216
|
+
def unregister_proc_handler(handler_id)
|
217
|
+
@@proc_handlers.delete(handler_id)
|
218
|
+
end
|
219
|
+
|
220
|
+
# Check if a handler ID refers to a proc handler
|
221
|
+
# @param handler_id [String] The handler identifier
|
222
|
+
# @return [Boolean] True if this is a proc handler
|
223
|
+
def proc_handler?(handler_id)
|
224
|
+
@@proc_handlers.key?(handler_id)
|
225
|
+
end
|
226
|
+
|
227
|
+
|
187
228
|
#########################################################
|
188
229
|
## class-level transport configuration
|
189
230
|
|
@@ -226,25 +267,74 @@ module SmartMessage
|
|
226
267
|
# Add this message class to the transport's catalog of
|
227
268
|
# subscribed messages. If the transport is missing, raise
|
228
269
|
# an exception.
|
229
|
-
|
230
|
-
|
231
|
-
|
270
|
+
#
|
271
|
+
# @param process_method [String, Proc, nil] The processing method:
|
272
|
+
# - String: Method name like "MyService.handle_message"
|
273
|
+
# - Proc: A proc/lambda that accepts (message_header, message_payload)
|
274
|
+
# - nil: Uses default "MessageClass.process" method
|
275
|
+
# @param block [Proc] Alternative way to pass a processing block
|
276
|
+
# @return [String] The identifier used for this subscription
|
277
|
+
#
|
278
|
+
# @example Using default handler
|
279
|
+
# MyMessage.subscribe
|
280
|
+
#
|
281
|
+
# @example Using custom method name
|
282
|
+
# MyMessage.subscribe("MyService.handle_message")
|
283
|
+
#
|
284
|
+
# @example Using a block
|
285
|
+
# MyMessage.subscribe do |header, payload|
|
286
|
+
# data = JSON.parse(payload)
|
287
|
+
# puts "Received: #{data}"
|
288
|
+
# end
|
289
|
+
#
|
290
|
+
# @example Using a proc
|
291
|
+
# handler = proc { |header, payload| puts "Processing..." }
|
292
|
+
# MyMessage.subscribe(handler)
|
293
|
+
def subscribe(process_method = nil, &block)
|
294
|
+
message_class = whoami
|
295
|
+
|
296
|
+
# Handle different parameter types
|
297
|
+
if block_given?
|
298
|
+
# Block was passed - use it as the handler
|
299
|
+
handler_proc = block
|
300
|
+
process_method = register_proc_handler(message_class, handler_proc)
|
301
|
+
elsif process_method.respond_to?(:call)
|
302
|
+
# Proc/lambda was passed as first parameter
|
303
|
+
handler_proc = process_method
|
304
|
+
process_method = register_proc_handler(message_class, handler_proc)
|
305
|
+
elsif process_method.nil?
|
306
|
+
# Use default handler
|
307
|
+
process_method = message_class + '.process'
|
308
|
+
end
|
309
|
+
# If process_method is a String, use it as-is
|
232
310
|
|
233
311
|
# TODO: Add proper logging here
|
234
312
|
|
235
313
|
raise Errors::TransportNotConfigured if transport_missing?
|
236
314
|
transport.subscribe(message_class, process_method)
|
315
|
+
|
316
|
+
process_method
|
237
317
|
end
|
238
318
|
|
239
319
|
|
240
320
|
# Remove this process_method for this message class from the
|
241
321
|
# subscribers list.
|
322
|
+
# @param process_method [String, nil] The processing method identifier to remove
|
323
|
+
# - String: Method name like "MyService.handle_message" or proc handler ID
|
324
|
+
# - nil: Uses default "MessageClass.process" method
|
242
325
|
def unsubscribe(process_method = nil)
|
243
326
|
message_class = whoami
|
244
327
|
process_method = message_class + '.process' if process_method.nil?
|
245
328
|
# TODO: Add proper logging here
|
246
329
|
|
247
|
-
|
330
|
+
if transport_configured?
|
331
|
+
transport.unsubscribe(message_class, process_method)
|
332
|
+
|
333
|
+
# If this was a proc handler, clean it up from the registry
|
334
|
+
if proc_handler?(process_method)
|
335
|
+
unregister_proc_handler(process_method)
|
336
|
+
end
|
337
|
+
end
|
248
338
|
end
|
249
339
|
|
250
340
|
|
@@ -118,13 +118,20 @@ module SmartMessage
|
|
118
118
|
@subscribers[message_klass].each do |message_processor|
|
119
119
|
SS.add(message_klass, message_processor, 'routed' )
|
120
120
|
@router_pool.post do
|
121
|
-
parts = message_processor.split('.')
|
122
|
-
target_klass = parts[0]
|
123
|
-
class_method = parts[1]
|
124
121
|
begin
|
125
|
-
|
126
|
-
|
127
|
-
|
122
|
+
# Check if this is a proc handler or a regular method call
|
123
|
+
if proc_handler?(message_processor)
|
124
|
+
# Call the proc handler via SmartMessage::Base
|
125
|
+
SmartMessage::Base.call_proc_handler(message_processor, message_header, message_payload)
|
126
|
+
else
|
127
|
+
# Original method call logic
|
128
|
+
parts = message_processor.split('.')
|
129
|
+
target_klass = parts[0]
|
130
|
+
class_method = parts[1]
|
131
|
+
target_klass.constantize
|
132
|
+
.method(class_method)
|
133
|
+
.call(message_header, message_payload)
|
134
|
+
end
|
128
135
|
rescue Exception => e
|
129
136
|
# TODO: Add proper exception logging
|
130
137
|
# Exception details: #{e.message}
|
@@ -135,6 +142,15 @@ module SmartMessage
|
|
135
142
|
end
|
136
143
|
end
|
137
144
|
|
145
|
+
private
|
146
|
+
|
147
|
+
# Check if a message processor is a proc handler
|
148
|
+
# @param message_processor [String] The message processor identifier
|
149
|
+
# @return [Boolean] True if this is a proc handler
|
150
|
+
def proc_handler?(message_processor)
|
151
|
+
SmartMessage::Base.proc_handler?(message_processor)
|
152
|
+
end
|
153
|
+
|
138
154
|
|
139
155
|
#######################################################
|
140
156
|
## Class methods
|
@@ -0,0 +1,190 @@
|
|
1
|
+
# lib/smart_message/transport/redis_transport.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
require 'redis'
|
6
|
+
require 'securerandom'
|
7
|
+
require 'set'
|
8
|
+
|
9
|
+
module SmartMessage
|
10
|
+
module Transport
|
11
|
+
# Redis pub/sub transport for SmartMessage
|
12
|
+
# Uses message class name as the Redis channel name
|
13
|
+
class RedisTransport < Base
|
14
|
+
attr_reader :redis_pub, :redis_sub, :subscriber_thread
|
15
|
+
|
16
|
+
def default_options
|
17
|
+
{
|
18
|
+
url: 'redis://localhost:6379',
|
19
|
+
db: 0,
|
20
|
+
auto_subscribe: true,
|
21
|
+
reconnect_attempts: 5,
|
22
|
+
reconnect_delay: 1
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def configure
|
27
|
+
@redis_pub = Redis.new(url: @options[:url], db: @options[:db])
|
28
|
+
@redis_sub = Redis.new(url: @options[:url], db: @options[:db])
|
29
|
+
@subscribed_channels = Set.new
|
30
|
+
@subscriber_thread = nil
|
31
|
+
@running = false
|
32
|
+
@mutex = Mutex.new
|
33
|
+
|
34
|
+
start_subscriber if @options[:auto_subscribe]
|
35
|
+
end
|
36
|
+
|
37
|
+
# Publish message to Redis channel using message class name
|
38
|
+
def publish(message_header, message_payload)
|
39
|
+
channel = message_header.message_class
|
40
|
+
|
41
|
+
begin
|
42
|
+
@redis_pub.publish(channel, message_payload)
|
43
|
+
rescue Redis::ConnectionError
|
44
|
+
retry_with_reconnect('publish') { @redis_pub.publish(channel, message_payload) }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Subscribe to a message class (Redis channel)
|
49
|
+
def subscribe(message_class, process_method)
|
50
|
+
super(message_class, process_method)
|
51
|
+
|
52
|
+
@mutex.synchronize do
|
53
|
+
@subscribed_channels.add(message_class)
|
54
|
+
restart_subscriber if @running
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Unsubscribe from a specific message class and process method
|
59
|
+
def unsubscribe(message_class, process_method)
|
60
|
+
super(message_class, process_method)
|
61
|
+
|
62
|
+
@mutex.synchronize do
|
63
|
+
# If no more subscribers for this message class, unsubscribe from channel
|
64
|
+
if @dispatcher.subscribers[message_class].nil? || @dispatcher.subscribers[message_class].empty?
|
65
|
+
@subscribed_channels.delete(message_class)
|
66
|
+
restart_subscriber if @running
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Unsubscribe from all process methods for a message class
|
72
|
+
def unsubscribe!(message_class)
|
73
|
+
super(message_class)
|
74
|
+
|
75
|
+
@mutex.synchronize do
|
76
|
+
@subscribed_channels.delete(message_class)
|
77
|
+
restart_subscriber if @running
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def connected?
|
82
|
+
begin
|
83
|
+
@redis_pub.ping == 'PONG' && @redis_sub.ping == 'PONG'
|
84
|
+
rescue Redis::ConnectionError
|
85
|
+
false
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def connect
|
90
|
+
@redis_pub.ping
|
91
|
+
@redis_sub.ping
|
92
|
+
start_subscriber unless @running
|
93
|
+
end
|
94
|
+
|
95
|
+
def disconnect
|
96
|
+
stop_subscriber
|
97
|
+
@redis_pub.quit if @redis_pub
|
98
|
+
@redis_sub.quit if @redis_sub
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def start_subscriber
|
104
|
+
return if @running
|
105
|
+
|
106
|
+
@running = true
|
107
|
+
@subscriber_thread = Thread.new do
|
108
|
+
begin
|
109
|
+
subscribe_to_channels
|
110
|
+
rescue => e
|
111
|
+
# Log error but don't crash the thread
|
112
|
+
puts "Redis subscriber error: #{e.message}" if @options[:debug]
|
113
|
+
retry_subscriber
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def stop_subscriber
|
119
|
+
@running = false
|
120
|
+
|
121
|
+
if @subscriber_thread
|
122
|
+
@subscriber_thread.kill
|
123
|
+
@subscriber_thread.join(5) # Wait up to 5 seconds
|
124
|
+
@subscriber_thread = nil
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def restart_subscriber
|
129
|
+
stop_subscriber
|
130
|
+
start_subscriber if @subscribed_channels.any?
|
131
|
+
end
|
132
|
+
|
133
|
+
def subscribe_to_channels
|
134
|
+
return unless @subscribed_channels.any?
|
135
|
+
|
136
|
+
begin
|
137
|
+
@redis_sub.subscribe(*@subscribed_channels) do |on|
|
138
|
+
on.message do |channel, message_payload|
|
139
|
+
# Create a header with the channel as message_class
|
140
|
+
message_header = SmartMessage::Header.new(
|
141
|
+
message_class: channel,
|
142
|
+
uuid: SecureRandom.uuid,
|
143
|
+
published_at: Time.now,
|
144
|
+
publisher_pid: 'redis_subscriber'
|
145
|
+
)
|
146
|
+
|
147
|
+
receive(message_header, message_payload)
|
148
|
+
end
|
149
|
+
|
150
|
+
on.subscribe do |channel, subscriptions|
|
151
|
+
puts "Subscribed to Redis channel: #{channel} (#{subscriptions} total)" if @options[:debug]
|
152
|
+
end
|
153
|
+
|
154
|
+
on.unsubscribe do |channel, subscriptions|
|
155
|
+
puts "Unsubscribed from Redis channel: #{channel} (#{subscriptions} total)" if @options[:debug]
|
156
|
+
end
|
157
|
+
end
|
158
|
+
rescue => e
|
159
|
+
# Silently handle connection errors during subscription
|
160
|
+
puts "Redis subscription error: #{e.class.name}" if @options[:debug]
|
161
|
+
retry_subscriber if @running
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def retry_subscriber
|
166
|
+
return unless @running
|
167
|
+
|
168
|
+
sleep(@options[:reconnect_delay])
|
169
|
+
subscribe_to_channels if @running
|
170
|
+
end
|
171
|
+
|
172
|
+
def retry_with_reconnect(operation)
|
173
|
+
attempts = 0
|
174
|
+
begin
|
175
|
+
yield
|
176
|
+
rescue Redis::ConnectionError => e
|
177
|
+
attempts += 1
|
178
|
+
if attempts <= @options[:reconnect_attempts]
|
179
|
+
sleep(@options[:reconnect_delay])
|
180
|
+
# Reconnect
|
181
|
+
@redis_pub = Redis.new(url: @options[:url], db: @options[:db])
|
182
|
+
retry
|
183
|
+
else
|
184
|
+
raise e
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
@@ -6,6 +6,7 @@ require_relative 'transport/base'
|
|
6
6
|
require_relative 'transport/registry'
|
7
7
|
require_relative 'transport/stdout_transport'
|
8
8
|
require_relative 'transport/memory_transport'
|
9
|
+
require_relative 'transport/redis_transport'
|
9
10
|
|
10
11
|
module SmartMessage
|
11
12
|
# Transport layer abstraction for SmartMessage
|
data/smart_message.gemspec
CHANGED
@@ -39,6 +39,7 @@ Gem::Specification.new do |spec|
|
|
39
39
|
spec.add_dependency 'hashie'
|
40
40
|
spec.add_dependency 'activesupport'
|
41
41
|
spec.add_dependency 'concurrent-ruby'
|
42
|
+
spec.add_dependency 'redis'
|
42
43
|
|
43
44
|
spec.add_development_dependency 'bundler'
|
44
45
|
spec.add_development_dependency 'rake'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: smart_message
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dewayne VanHoozer
|
@@ -51,6 +51,20 @@ dependencies:
|
|
51
51
|
- - ">="
|
52
52
|
- !ruby/object:Gem::Version
|
53
53
|
version: '0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: redis
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
type: :runtime
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
54
68
|
- !ruby/object:Gem::Dependency
|
55
69
|
name: bundler
|
56
70
|
requirement: !ruby/object:Gem::Requirement
|
@@ -190,6 +204,8 @@ files:
|
|
190
204
|
- docs/examples.md
|
191
205
|
- docs/getting-started.md
|
192
206
|
- docs/ideas_to_think_about.md
|
207
|
+
- docs/message_processing.md
|
208
|
+
- docs/proc_handlers_summary.md
|
193
209
|
- docs/properties.md
|
194
210
|
- docs/serializers.md
|
195
211
|
- docs/transports.md
|
@@ -197,7 +213,10 @@ files:
|
|
197
213
|
- examples/01_point_to_point_orders.rb
|
198
214
|
- examples/02_publish_subscribe_events.rb
|
199
215
|
- examples/03_many_to_many_chat.rb
|
216
|
+
- examples/04_redis_smart_home_iot.rb
|
217
|
+
- examples/05_proc_handlers.rb
|
200
218
|
- examples/README.md
|
219
|
+
- examples/smart_home_iot_dataflow.md
|
201
220
|
- examples/tmux_chat/README.md
|
202
221
|
- examples/tmux_chat/bot_agent.rb
|
203
222
|
- examples/tmux_chat/human_agent.rb
|
@@ -221,6 +240,7 @@ files:
|
|
221
240
|
- lib/smart_message/transport.rb
|
222
241
|
- lib/smart_message/transport/base.rb
|
223
242
|
- lib/smart_message/transport/memory_transport.rb
|
243
|
+
- lib/smart_message/transport/redis_transport.rb
|
224
244
|
- lib/smart_message/transport/registry.rb
|
225
245
|
- lib/smart_message/transport/stdout_transport.rb
|
226
246
|
- lib/smart_message/version.rb
|