smart_message 0.0.5 → 0.0.7
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 +122 -0
- data/Gemfile.lock +9 -1
- data/README.md +58 -4
- data/docs/README.md +1 -0
- data/docs/addressing.md +364 -0
- data/docs/getting-started.md +38 -0
- data/examples/01_point_to_point_orders.rb +4 -2
- data/examples/02_publish_subscribe_events.rb +2 -1
- data/examples/03_many_to_many_chat.rb +10 -5
- data/examples/04_redis_smart_home_iot.rb +32 -16
- data/examples/05_proc_handlers.rb +3 -2
- data/examples/06_custom_logger_example.rb +13 -7
- data/examples/07_error_handling_scenarios.rb +26 -15
- data/examples/08_entity_addressing_basic.rb +373 -0
- data/examples/08_entity_addressing_with_filtering.rb +430 -0
- data/examples/README.md +68 -0
- data/examples/tmux_chat/bot_agent.rb +2 -1
- data/examples/tmux_chat/shared_chat_system.rb +4 -2
- data/lib/smart_message/base.rb +204 -12
- data/lib/smart_message/circuit_breaker.rb +227 -0
- data/lib/smart_message/dispatcher.rb +189 -20
- data/lib/smart_message/header.rb +14 -0
- data/lib/smart_message/serializer/base.rb +77 -4
- data/lib/smart_message/serializer/json.rb +2 -2
- data/lib/smart_message/transport/base.rb +141 -4
- data/lib/smart_message/transport/memory_transport.rb +1 -1
- data/lib/smart_message/transport/redis_transport.rb +45 -15
- data/lib/smart_message/transport/stdout_transport.rb +1 -1
- data/lib/smart_message/version.rb +1 -1
- data/lib/smart_message.rb +1 -0
- data/smart_message.gemspec +1 -0
- metadata +19 -1
@@ -3,26 +3,25 @@
|
|
3
3
|
# frozen_string_literal: true
|
4
4
|
|
5
5
|
require 'concurrent'
|
6
|
+
require_relative 'circuit_breaker'
|
6
7
|
|
7
8
|
module SmartMessage
|
8
9
|
|
9
10
|
# The disoatcher routes incoming messages to all of the methods that
|
10
11
|
# have been subscribed to the message.
|
11
12
|
class Dispatcher
|
13
|
+
include BreakerMachines::DSL
|
12
14
|
|
13
15
|
# TODO: setup forwardable for some @router_pool methods
|
14
16
|
|
15
|
-
def initialize
|
17
|
+
def initialize(circuit_breaker_options = {})
|
16
18
|
@subscribers = Hash.new(Array.new)
|
17
19
|
@router_pool = Concurrent::CachedThreadPool.new
|
20
|
+
|
21
|
+
# Configure circuit breakers
|
22
|
+
configure_circuit_breakers(circuit_breaker_options)
|
18
23
|
at_exit do
|
19
|
-
|
20
|
-
@router_pool.shutdown
|
21
|
-
while @router_pool.shuttingdown?
|
22
|
-
print '.'
|
23
|
-
sleep 1
|
24
|
-
end
|
25
|
-
puts " done."
|
24
|
+
shutdown_pool
|
26
25
|
end
|
27
26
|
end
|
28
27
|
|
@@ -83,17 +82,30 @@ module SmartMessage
|
|
83
82
|
end
|
84
83
|
|
85
84
|
|
86
|
-
def add(message_class, process_method_as_string)
|
85
|
+
def add(message_class, process_method_as_string, filter_options = {})
|
87
86
|
klass = String(message_class)
|
88
|
-
|
89
|
-
|
87
|
+
|
88
|
+
# Create subscription entry with filter options
|
89
|
+
subscription = {
|
90
|
+
process_method: process_method_as_string,
|
91
|
+
filters: filter_options
|
92
|
+
}
|
93
|
+
|
94
|
+
# Check if this exact subscription already exists
|
95
|
+
existing_subscription = @subscribers[klass].find do |sub|
|
96
|
+
sub[:process_method] == process_method_as_string && sub[:filters] == filter_options
|
97
|
+
end
|
98
|
+
|
99
|
+
unless existing_subscription
|
100
|
+
@subscribers[klass] += [subscription]
|
90
101
|
end
|
91
102
|
end
|
92
103
|
|
93
104
|
|
94
105
|
# drop a processer from a subscribed message
|
95
106
|
def drop(message_class, process_method_as_string)
|
96
|
-
|
107
|
+
klass = String(message_class)
|
108
|
+
@subscribers[klass].reject! { |sub| sub[:process_method] == process_method_as_string }
|
97
109
|
end
|
98
110
|
|
99
111
|
|
@@ -115,10 +127,19 @@ module SmartMessage
|
|
115
127
|
def route(message_header, message_payload)
|
116
128
|
message_klass = message_header.message_class
|
117
129
|
return nil if @subscribers[message_klass].empty?
|
118
|
-
|
130
|
+
|
131
|
+
@subscribers[message_klass].each do |subscription|
|
132
|
+
# Extract subscription details
|
133
|
+
message_processor = subscription[:process_method]
|
134
|
+
filters = subscription[:filters]
|
135
|
+
|
136
|
+
# Check if message matches filters
|
137
|
+
next unless message_matches_filters?(message_header, filters)
|
138
|
+
|
119
139
|
SS.add(message_klass, message_processor, 'routed' )
|
120
140
|
@router_pool.post do
|
121
|
-
|
141
|
+
# Use circuit breaker to protect message processing
|
142
|
+
circuit_result = circuit(:message_processor).wrap do
|
122
143
|
# Check if this is a proc handler or a regular method call
|
123
144
|
if proc_handler?(message_processor)
|
124
145
|
# Call the proc handler via SmartMessage::Base
|
@@ -132,17 +153,90 @@ module SmartMessage
|
|
132
153
|
.method(class_method)
|
133
154
|
.call(message_header, message_payload)
|
134
155
|
end
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
156
|
+
end
|
157
|
+
|
158
|
+
# Handle circuit breaker fallback responses
|
159
|
+
if circuit_result.is_a?(Hash) && circuit_result[:circuit_breaker]
|
160
|
+
handle_circuit_breaker_fallback(circuit_result, message_header, message_payload, message_processor)
|
140
161
|
end
|
141
162
|
end
|
142
163
|
end
|
143
164
|
end
|
144
165
|
|
145
|
-
|
166
|
+
# Get circuit breaker statistics
|
167
|
+
# @return [Hash] Circuit breaker statistics
|
168
|
+
def circuit_breaker_stats
|
169
|
+
stats = {}
|
170
|
+
|
171
|
+
begin
|
172
|
+
if respond_to?(:circuit)
|
173
|
+
breaker = circuit(:message_processor)
|
174
|
+
if breaker
|
175
|
+
stats[:message_processor] = {
|
176
|
+
status: breaker.status,
|
177
|
+
closed: breaker.closed?,
|
178
|
+
open: breaker.open?,
|
179
|
+
half_open: breaker.half_open?,
|
180
|
+
last_error: breaker.last_error,
|
181
|
+
opened_at: breaker.opened_at,
|
182
|
+
stats: breaker.stats
|
183
|
+
}
|
184
|
+
end
|
185
|
+
end
|
186
|
+
rescue => e
|
187
|
+
stats[:error] = "Failed to get circuit breaker stats: #{e.message}"
|
188
|
+
end
|
189
|
+
|
190
|
+
stats
|
191
|
+
end
|
192
|
+
|
193
|
+
# Reset circuit breakers
|
194
|
+
# @param circuit_name [Symbol] Optional specific circuit to reset
|
195
|
+
def reset_circuit_breakers!(circuit_name = nil)
|
196
|
+
if circuit_name
|
197
|
+
circuit(circuit_name)&.reset!
|
198
|
+
else
|
199
|
+
# Reset all known circuits
|
200
|
+
circuit(:message_processor)&.reset!
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# Shutdown the router pool with timeout and fallback
|
205
|
+
def shutdown_pool
|
206
|
+
@router_pool.shutdown
|
207
|
+
|
208
|
+
# Wait for graceful shutdown, force kill if timeout
|
209
|
+
unless @router_pool.wait_for_termination(3)
|
210
|
+
@router_pool.kill
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# Check if a message matches the subscription filters
|
215
|
+
# @param message_header [SmartMessage::Header] The message header
|
216
|
+
# @param filters [Hash] The filter criteria
|
217
|
+
# @return [Boolean] True if the message matches all filters
|
218
|
+
def message_matches_filters?(message_header, filters)
|
219
|
+
# If no filters specified, accept all messages (backward compatibility)
|
220
|
+
return true if filters.nil? || filters.empty? || filters.values.all?(&:nil?)
|
221
|
+
|
222
|
+
# Check from filter
|
223
|
+
if filters[:from]
|
224
|
+
from_match = filters[:from].include?(message_header.from)
|
225
|
+
return false unless from_match
|
226
|
+
end
|
227
|
+
|
228
|
+
# Check to/broadcast filters (OR logic between them)
|
229
|
+
if filters[:broadcast] || filters[:to]
|
230
|
+
broadcast_match = filters[:broadcast] && message_header.to.nil?
|
231
|
+
to_match = filters[:to] && filters[:to].include?(message_header.to)
|
232
|
+
|
233
|
+
# If either broadcast or to filter is specified, at least one must match
|
234
|
+
combined_match = (broadcast_match || to_match)
|
235
|
+
return false unless combined_match
|
236
|
+
end
|
237
|
+
|
238
|
+
true
|
239
|
+
end
|
146
240
|
|
147
241
|
# Check if a message processor is a proc handler
|
148
242
|
# @param message_processor [String] The message processor identifier
|
@@ -151,6 +245,81 @@ module SmartMessage
|
|
151
245
|
SmartMessage::Base.proc_handler?(message_processor)
|
152
246
|
end
|
153
247
|
|
248
|
+
# Configure circuit breakers for the dispatcher
|
249
|
+
# @param options [Hash] Circuit breaker configuration options
|
250
|
+
def configure_circuit_breakers(options = {})
|
251
|
+
# Ensure CircuitBreaker module is available
|
252
|
+
return unless defined?(SmartMessage::CircuitBreaker::DEFAULT_CONFIGS)
|
253
|
+
|
254
|
+
# Configure message processor circuit breaker
|
255
|
+
default_config = SmartMessage::CircuitBreaker::DEFAULT_CONFIGS[:message_processor]
|
256
|
+
return unless default_config
|
257
|
+
|
258
|
+
processor_config = default_config.merge(options[:message_processor] || {})
|
259
|
+
|
260
|
+
# Define the circuit using the class-level DSL
|
261
|
+
self.class.circuit :message_processor do
|
262
|
+
threshold failures: processor_config[:threshold][:failures],
|
263
|
+
within: processor_config[:threshold][:within].seconds
|
264
|
+
reset_after processor_config[:reset_after].seconds
|
265
|
+
|
266
|
+
# Configure storage backend
|
267
|
+
case processor_config[:storage]
|
268
|
+
when :redis
|
269
|
+
# Use Redis storage if configured and available
|
270
|
+
if defined?(SmartMessage::Transport::RedisTransport)
|
271
|
+
begin
|
272
|
+
redis_transport = SmartMessage::Transport::RedisTransport.new
|
273
|
+
storage BreakerMachines::Storage::Redis.new(redis: redis_transport.redis_pub)
|
274
|
+
rescue
|
275
|
+
# Fall back to memory storage if Redis not available
|
276
|
+
storage BreakerMachines::Storage::Memory.new
|
277
|
+
end
|
278
|
+
else
|
279
|
+
storage BreakerMachines::Storage::Memory.new
|
280
|
+
end
|
281
|
+
else
|
282
|
+
storage BreakerMachines::Storage::Memory.new
|
283
|
+
end
|
284
|
+
|
285
|
+
# Default fallback for message processing failures
|
286
|
+
fallback do |exception|
|
287
|
+
{
|
288
|
+
circuit_breaker: {
|
289
|
+
circuit: :message_processor,
|
290
|
+
state: 'open',
|
291
|
+
error: exception.message,
|
292
|
+
error_class: exception.class.name,
|
293
|
+
timestamp: Time.now.iso8601,
|
294
|
+
fallback_triggered: true
|
295
|
+
}
|
296
|
+
}
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
end
|
301
|
+
|
302
|
+
# Handle circuit breaker fallback responses
|
303
|
+
# @param circuit_result [Hash] The circuit breaker fallback result
|
304
|
+
# @param message_header [SmartMessage::Header] The message header
|
305
|
+
# @param message_payload [String] The message payload
|
306
|
+
# @param message_processor [String] The processor that failed
|
307
|
+
def handle_circuit_breaker_fallback(circuit_result, message_header, message_payload, message_processor)
|
308
|
+
# Log circuit breaker activation
|
309
|
+
if $DEBUG
|
310
|
+
puts "Circuit breaker activated for processor: #{message_processor}"
|
311
|
+
puts "Error: #{circuit_result[:circuit_breaker][:error]}"
|
312
|
+
puts "Message: #{message_header.message_class} from #{message_header.from}"
|
313
|
+
end
|
314
|
+
|
315
|
+
# TODO: Integrate with structured logging when implemented
|
316
|
+
# TODO: Send to dead letter queue when implemented
|
317
|
+
# TODO: Emit metrics/events for monitoring
|
318
|
+
|
319
|
+
# For now, record the failure in simple stats
|
320
|
+
SS.add(message_header.message_class, message_processor, 'circuit_breaker_fallback')
|
321
|
+
end
|
322
|
+
|
154
323
|
|
155
324
|
#######################################################
|
156
325
|
## Class methods
|
data/lib/smart_message/header.rb
CHANGED
@@ -43,5 +43,19 @@ module SmartMessage
|
|
43
43
|
description: "Schema version of the message format, used for schema evolution and compatibility checking",
|
44
44
|
validate: ->(v) { v.is_a?(Integer) && v > 0 },
|
45
45
|
validation_message: "Header version must be a positive integer"
|
46
|
+
|
47
|
+
# Message addressing properties for entity-to-entity communication
|
48
|
+
property :from,
|
49
|
+
required: true,
|
50
|
+
message: "From entity ID is required for message routing and replies",
|
51
|
+
description: "Unique identifier of the entity sending this message, used for routing responses and audit trails"
|
52
|
+
|
53
|
+
property :to,
|
54
|
+
required: false,
|
55
|
+
description: "Optional unique identifier of the intended recipient entity. When nil, message is broadcast to all subscribers"
|
56
|
+
|
57
|
+
property :reply_to,
|
58
|
+
required: false,
|
59
|
+
description: "Optional unique identifier of the entity that should receive replies to this message. Defaults to 'from' entity if not specified"
|
46
60
|
end
|
47
61
|
end
|
@@ -2,22 +2,95 @@
|
|
2
2
|
# encoding: utf-8
|
3
3
|
# frozen_string_literal: true
|
4
4
|
|
5
|
+
require_relative '../circuit_breaker'
|
6
|
+
|
5
7
|
module SmartMessage::Serializer
|
6
8
|
# the standard super class
|
7
9
|
class Base
|
10
|
+
include BreakerMachines::DSL
|
11
|
+
|
8
12
|
# provide basic configuration
|
9
13
|
def initialize
|
10
|
-
|
14
|
+
configure_serializer_circuit_breakers
|
11
15
|
end
|
12
16
|
|
13
17
|
def encode(message_instance)
|
14
|
-
|
15
|
-
|
18
|
+
circuit(:serializer).wrap do
|
19
|
+
do_encode(message_instance)
|
20
|
+
end
|
21
|
+
rescue => e
|
22
|
+
# Handle circuit breaker fallback
|
23
|
+
if e.is_a?(Hash) && e[:circuit_breaker]
|
24
|
+
handle_serializer_fallback(e, :encode, message_instance)
|
25
|
+
else
|
26
|
+
raise
|
27
|
+
end
|
16
28
|
end
|
17
29
|
|
18
30
|
def decode(payload)
|
19
|
-
|
31
|
+
circuit(:serializer).wrap do
|
32
|
+
do_decode(payload)
|
33
|
+
end
|
34
|
+
rescue => e
|
35
|
+
# Handle circuit breaker fallback
|
36
|
+
if e.is_a?(Hash) && e[:circuit_breaker]
|
37
|
+
handle_serializer_fallback(e, :decode, payload)
|
38
|
+
else
|
39
|
+
raise
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Template methods for actual serialization (implement in subclasses)
|
44
|
+
def do_encode(message_instance)
|
45
|
+
raise ::SmartMessage::Errors::NotImplemented
|
46
|
+
end
|
47
|
+
|
48
|
+
def do_decode(payload)
|
20
49
|
raise ::SmartMessage::Errors::NotImplemented
|
21
50
|
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# Configure circuit breaker for serializer operations
|
55
|
+
def configure_serializer_circuit_breakers
|
56
|
+
serializer_config = SmartMessage::CircuitBreaker::DEFAULT_CONFIGS[:serializer]
|
57
|
+
|
58
|
+
self.class.circuit :serializer do
|
59
|
+
threshold failures: serializer_config[:threshold][:failures],
|
60
|
+
within: serializer_config[:threshold][:within].seconds
|
61
|
+
reset_after serializer_config[:reset_after].seconds
|
62
|
+
|
63
|
+
storage BreakerMachines::Storage::Memory.new
|
64
|
+
|
65
|
+
# Fallback for serializer failures
|
66
|
+
fallback do |exception|
|
67
|
+
{
|
68
|
+
circuit_breaker: {
|
69
|
+
circuit: :serializer,
|
70
|
+
serializer_type: self.class.name,
|
71
|
+
state: 'open',
|
72
|
+
error: exception.message,
|
73
|
+
error_class: exception.class.name,
|
74
|
+
timestamp: Time.now.iso8601,
|
75
|
+
fallback_triggered: true
|
76
|
+
}
|
77
|
+
}
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Handle serializer circuit breaker fallback
|
83
|
+
def handle_serializer_fallback(fallback_result, operation, data)
|
84
|
+
if $DEBUG
|
85
|
+
puts "Serializer circuit breaker activated: #{self.class.name}"
|
86
|
+
puts "Operation: #{operation}"
|
87
|
+
puts "Error: #{fallback_result[:circuit_breaker][:error]}"
|
88
|
+
end
|
89
|
+
|
90
|
+
# TODO: Integrate with structured logging when implemented
|
91
|
+
|
92
|
+
# Return the fallback result
|
93
|
+
fallback_result
|
94
|
+
end
|
22
95
|
end # class Base
|
23
96
|
end # module SmartMessage::Serializer
|
@@ -6,13 +6,13 @@ require 'json' # STDLIB
|
|
6
6
|
|
7
7
|
module SmartMessage::Serializer
|
8
8
|
class JSON < Base
|
9
|
-
def
|
9
|
+
def do_encode(message_instance)
|
10
10
|
# TODO: is this the right place to insert an automated-invisible
|
11
11
|
# message header?
|
12
12
|
message_instance.to_json
|
13
13
|
end
|
14
14
|
|
15
|
-
def
|
15
|
+
def do_decode(payload)
|
16
16
|
# TODO: so how do I know to which message class this payload
|
17
17
|
# belongs? The class needs to be in some kind of message
|
18
18
|
# header.
|
@@ -2,17 +2,22 @@
|
|
2
2
|
# encoding: utf-8
|
3
3
|
# frozen_string_literal: true
|
4
4
|
|
5
|
+
require_relative '../circuit_breaker'
|
6
|
+
|
5
7
|
module SmartMessage
|
6
8
|
module Transport
|
7
9
|
# Base class for all transport implementations
|
8
10
|
# This defines the standard interface that all transports must implement
|
9
11
|
class Base
|
12
|
+
include BreakerMachines::DSL
|
13
|
+
|
10
14
|
attr_reader :options, :dispatcher
|
11
15
|
|
12
16
|
def initialize(**options)
|
13
17
|
@options = default_options.merge(options)
|
14
18
|
@dispatcher = options[:dispatcher] || SmartMessage::Dispatcher.new
|
15
19
|
configure
|
20
|
+
configure_transport_circuit_breakers
|
16
21
|
end
|
17
22
|
|
18
23
|
# Transport-specific configuration
|
@@ -25,18 +30,34 @@ module SmartMessage
|
|
25
30
|
{}
|
26
31
|
end
|
27
32
|
|
28
|
-
# Publish a message
|
33
|
+
# Publish a message with circuit breaker protection
|
29
34
|
# @param message_header [SmartMessage::Header] Message routing information
|
30
35
|
# @param message_payload [String] Serialized message content
|
31
36
|
def publish(message_header, message_payload)
|
32
|
-
|
37
|
+
circuit(:transport_publish).wrap do
|
38
|
+
do_publish(message_header, message_payload)
|
39
|
+
end
|
40
|
+
rescue => e
|
41
|
+
# Re-raise if it's not a circuit breaker fallback
|
42
|
+
raise unless e.is_a?(Hash) && e[:circuit_breaker]
|
43
|
+
|
44
|
+
# Handle circuit breaker fallback
|
45
|
+
handle_publish_fallback(e, message_header, message_payload)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Template method for actual publishing (implement in subclasses)
|
49
|
+
# @param message_header [SmartMessage::Header] Message routing information
|
50
|
+
# @param message_payload [String] Serialized message content
|
51
|
+
def do_publish(message_header, message_payload)
|
52
|
+
raise NotImplementedError, 'Transport must implement #do_publish'
|
33
53
|
end
|
34
54
|
|
35
55
|
# Subscribe to a message class
|
36
56
|
# @param message_class [String] The message class name
|
37
57
|
# @param process_method [String] The processing method identifier
|
38
|
-
|
39
|
-
|
58
|
+
# @param filter_options [Hash] Optional filtering criteria
|
59
|
+
def subscribe(message_class, process_method, filter_options = {})
|
60
|
+
@dispatcher.add(message_class, process_method, filter_options)
|
40
61
|
end
|
41
62
|
|
42
63
|
# Unsubscribe from a specific message class and process method
|
@@ -72,6 +93,47 @@ module SmartMessage
|
|
72
93
|
# Override in subclasses if cleanup is needed
|
73
94
|
end
|
74
95
|
|
96
|
+
# Get transport circuit breaker statistics
|
97
|
+
# @return [Hash] Circuit breaker statistics
|
98
|
+
def transport_circuit_stats
|
99
|
+
stats = {}
|
100
|
+
|
101
|
+
[:transport_publish, :transport_subscribe].each do |circuit_name|
|
102
|
+
begin
|
103
|
+
if respond_to?(:circuit)
|
104
|
+
breaker = circuit(circuit_name)
|
105
|
+
if breaker
|
106
|
+
stats[circuit_name] = {
|
107
|
+
status: breaker.status,
|
108
|
+
closed: breaker.closed?,
|
109
|
+
open: breaker.open?,
|
110
|
+
half_open: breaker.half_open?,
|
111
|
+
last_error: breaker.last_error,
|
112
|
+
opened_at: breaker.opened_at,
|
113
|
+
stats: breaker.stats
|
114
|
+
}
|
115
|
+
end
|
116
|
+
end
|
117
|
+
rescue => e
|
118
|
+
stats[circuit_name] = { error: "Failed to get stats: #{e.message}" }
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
stats
|
123
|
+
end
|
124
|
+
|
125
|
+
# Reset transport circuit breakers
|
126
|
+
# @param circuit_name [Symbol] Optional specific circuit to reset
|
127
|
+
def reset_transport_circuits!(circuit_name = nil)
|
128
|
+
if circuit_name
|
129
|
+
circuit(circuit_name)&.reset!
|
130
|
+
else
|
131
|
+
# Reset all transport circuits
|
132
|
+
circuit(:transport_publish)&.reset!
|
133
|
+
circuit(:transport_subscribe)&.reset!
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
75
137
|
# Receive and route a message (called by transport implementations)
|
76
138
|
# @param message_header [SmartMessage::Header] Message routing information
|
77
139
|
# @param message_payload [String] Serialized message content
|
@@ -80,6 +142,81 @@ module SmartMessage
|
|
80
142
|
def receive(message_header, message_payload)
|
81
143
|
@dispatcher.route(message_header, message_payload)
|
82
144
|
end
|
145
|
+
|
146
|
+
# Configure circuit breakers for transport operations
|
147
|
+
def configure_transport_circuit_breakers
|
148
|
+
# Configure publish circuit breaker
|
149
|
+
publish_config = SmartMessage::CircuitBreaker::DEFAULT_CONFIGS[:transport_publish]
|
150
|
+
|
151
|
+
self.class.circuit :transport_publish do
|
152
|
+
threshold failures: publish_config[:threshold][:failures],
|
153
|
+
within: publish_config[:threshold][:within].seconds
|
154
|
+
reset_after publish_config[:reset_after].seconds
|
155
|
+
|
156
|
+
# Use memory storage by default for transport circuits
|
157
|
+
storage BreakerMachines::Storage::Memory.new
|
158
|
+
|
159
|
+
# Fallback for publish failures
|
160
|
+
fallback do |exception|
|
161
|
+
{
|
162
|
+
circuit_breaker: {
|
163
|
+
circuit: :transport_publish,
|
164
|
+
transport_type: self.class.name,
|
165
|
+
state: 'open',
|
166
|
+
error: exception.message,
|
167
|
+
error_class: exception.class.name,
|
168
|
+
timestamp: Time.now.iso8601,
|
169
|
+
fallback_triggered: true
|
170
|
+
}
|
171
|
+
}
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# Configure subscribe circuit breaker
|
176
|
+
subscribe_config = SmartMessage::CircuitBreaker::DEFAULT_CONFIGS[:transport_subscribe]
|
177
|
+
|
178
|
+
self.class.circuit :transport_subscribe do
|
179
|
+
threshold failures: subscribe_config[:threshold][:failures],
|
180
|
+
within: subscribe_config[:threshold][:within].seconds
|
181
|
+
reset_after subscribe_config[:reset_after].seconds
|
182
|
+
|
183
|
+
storage BreakerMachines::Storage::Memory.new
|
184
|
+
|
185
|
+
# Fallback for subscribe failures
|
186
|
+
fallback do |exception|
|
187
|
+
{
|
188
|
+
circuit_breaker: {
|
189
|
+
circuit: :transport_subscribe,
|
190
|
+
transport_type: self.class.name,
|
191
|
+
state: 'open',
|
192
|
+
error: exception.message,
|
193
|
+
error_class: exception.class.name,
|
194
|
+
timestamp: Time.now.iso8601,
|
195
|
+
fallback_triggered: true
|
196
|
+
}
|
197
|
+
}
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# Handle publish circuit breaker fallback
|
203
|
+
# @param fallback_result [Hash] The circuit breaker fallback result
|
204
|
+
# @param message_header [SmartMessage::Header] The message header
|
205
|
+
# @param message_payload [String] The message payload
|
206
|
+
def handle_publish_fallback(fallback_result, message_header, message_payload)
|
207
|
+
# Log the circuit breaker activation
|
208
|
+
if $DEBUG
|
209
|
+
puts "Transport publish circuit breaker activated: #{self.class.name}"
|
210
|
+
puts "Error: #{fallback_result[:circuit_breaker][:error]}"
|
211
|
+
puts "Message: #{message_header.message_class}"
|
212
|
+
end
|
213
|
+
|
214
|
+
# TODO: Integrate with structured logging when implemented
|
215
|
+
# TODO: Queue for retry or send to dead letter queue
|
216
|
+
|
217
|
+
# Return the fallback result to indicate failure
|
218
|
+
fallback_result
|
219
|
+
end
|
83
220
|
end
|
84
221
|
end
|
85
222
|
end
|
@@ -22,7 +22,7 @@ module SmartMessage
|
|
22
22
|
end
|
23
23
|
|
24
24
|
# Publish message to memory queue
|
25
|
-
def
|
25
|
+
def do_publish(message_header, message_payload)
|
26
26
|
@message_mutex.synchronize do
|
27
27
|
# Prevent memory overflow
|
28
28
|
@messages.shift if @messages.size >= @options[:max_messages]
|
@@ -5,6 +5,7 @@
|
|
5
5
|
require 'redis'
|
6
6
|
require 'securerandom'
|
7
7
|
require 'set'
|
8
|
+
require 'json'
|
8
9
|
|
9
10
|
module SmartMessage
|
10
11
|
module Transport
|
@@ -35,19 +36,26 @@ module SmartMessage
|
|
35
36
|
end
|
36
37
|
|
37
38
|
# Publish message to Redis channel using message class name
|
38
|
-
def
|
39
|
+
def do_publish(message_header, message_payload)
|
39
40
|
channel = message_header.message_class
|
40
41
|
|
42
|
+
# Combine header and payload for Redis transport
|
43
|
+
# This ensures header information (from, to, reply_to, etc.) is preserved
|
44
|
+
redis_message = {
|
45
|
+
header: message_header.to_hash,
|
46
|
+
payload: message_payload
|
47
|
+
}.to_json
|
48
|
+
|
41
49
|
begin
|
42
|
-
@redis_pub.publish(channel,
|
50
|
+
@redis_pub.publish(channel, redis_message)
|
43
51
|
rescue Redis::ConnectionError
|
44
|
-
retry_with_reconnect('publish') { @redis_pub.publish(channel,
|
52
|
+
retry_with_reconnect('publish') { @redis_pub.publish(channel, redis_message) }
|
45
53
|
end
|
46
54
|
end
|
47
55
|
|
48
56
|
# Subscribe to a message class (Redis channel)
|
49
|
-
def subscribe(message_class, process_method)
|
50
|
-
super(message_class, process_method)
|
57
|
+
def subscribe(message_class, process_method, filter_options = {})
|
58
|
+
super(message_class, process_method, filter_options)
|
51
59
|
|
52
60
|
@mutex.synchronize do
|
53
61
|
@subscribed_channels.add(message_class)
|
@@ -135,16 +143,38 @@ module SmartMessage
|
|
135
143
|
|
136
144
|
begin
|
137
145
|
@redis_sub.subscribe(*@subscribed_channels) do |on|
|
138
|
-
on.message do |channel,
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
146
|
+
on.message do |channel, redis_message|
|
147
|
+
begin
|
148
|
+
# Parse the Redis message to extract header and payload
|
149
|
+
parsed_message = JSON.parse(redis_message)
|
150
|
+
|
151
|
+
if parsed_message.is_a?(Hash) && parsed_message.has_key?('header') && parsed_message.has_key?('payload')
|
152
|
+
# Reconstruct the original header from the parsed data
|
153
|
+
header_data = parsed_message['header']
|
154
|
+
message_header = SmartMessage::Header.new(header_data)
|
155
|
+
message_payload = parsed_message['payload']
|
156
|
+
else
|
157
|
+
# Fallback for messages that don't have header/payload structure (legacy support)
|
158
|
+
message_header = SmartMessage::Header.new(
|
159
|
+
message_class: channel,
|
160
|
+
uuid: SecureRandom.uuid,
|
161
|
+
published_at: Time.now,
|
162
|
+
publisher_pid: 'redis_subscriber'
|
163
|
+
)
|
164
|
+
message_payload = redis_message
|
165
|
+
end
|
166
|
+
|
167
|
+
receive(message_header, message_payload)
|
168
|
+
rescue JSON::ParserError
|
169
|
+
# Handle malformed JSON - fallback to legacy behavior
|
170
|
+
message_header = SmartMessage::Header.new(
|
171
|
+
message_class: channel,
|
172
|
+
uuid: SecureRandom.uuid,
|
173
|
+
published_at: Time.now,
|
174
|
+
publisher_pid: 'redis_subscriber'
|
175
|
+
)
|
176
|
+
receive(message_header, redis_message)
|
177
|
+
end
|
148
178
|
end
|
149
179
|
|
150
180
|
on.subscribe do |channel, subscriptions|
|