smart_message 0.0.6 ā 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 +55 -0
- data/Gemfile.lock +9 -1
- 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 +14 -7
- data/examples/08_entity_addressing_with_filtering.rb +24 -12
- 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 +6 -3
- data/lib/smart_message/circuit_breaker.rb +227 -0
- data/lib/smart_message/dispatcher.rb +136 -15
- 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 +138 -2
- data/lib/smart_message/transport/memory_transport.rb +1 -1
- data/lib/smart_message/transport/redis_transport.rb +43 -13
- 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 +16 -1
@@ -87,7 +87,8 @@ puts "\nš¤ Publishing test messages..."
|
|
87
87
|
# Broadcast message - should only be received by broadcast handler
|
88
88
|
broadcast_msg = ServiceMessage.new(
|
89
89
|
message_type: 'system_announcement',
|
90
|
-
data: 'System maintenance at 2 AM'
|
90
|
+
data: 'System maintenance at 2 AM',
|
91
|
+
from: 'sender-service'
|
91
92
|
)
|
92
93
|
broadcast_msg.to(nil) # Explicitly set as broadcast
|
93
94
|
puts "\n1. Publishing broadcast message (no 'to' field)..."
|
@@ -97,7 +98,8 @@ sleep(0.2) # Allow time for handlers to process
|
|
97
98
|
# Directed message to 'my-service' - should only be received by directed handler
|
98
99
|
directed_msg = ServiceMessage.new(
|
99
100
|
message_type: 'service_update',
|
100
|
-
data: 'Update your configuration'
|
101
|
+
data: 'Update your configuration',
|
102
|
+
from: 'sender-service'
|
101
103
|
)
|
102
104
|
directed_msg.to('my-service')
|
103
105
|
puts "\n2. Publishing message to 'my-service'..."
|
@@ -107,7 +109,8 @@ sleep(0.2) # Allow time for handlers to process
|
|
107
109
|
# Directed message to different service - should NOT be received
|
108
110
|
other_msg = ServiceMessage.new(
|
109
111
|
message_type: 'other_update',
|
110
|
-
data: 'This is for another service'
|
112
|
+
data: 'This is for another service',
|
113
|
+
from: 'sender-service'
|
111
114
|
)
|
112
115
|
other_msg.to('other-service')
|
113
116
|
puts "\n3. Publishing message to 'other-service' (should not be received)..."
|
@@ -117,7 +120,8 @@ sleep(0.2) # Allow time to confirm no handlers process this
|
|
117
120
|
# Message from admin - should only be received by admin handler
|
118
121
|
admin_msg = ServiceMessage.new(
|
119
122
|
message_type: 'admin_command',
|
120
|
-
data: 'Restart all services'
|
123
|
+
data: 'Restart all services',
|
124
|
+
from: 'admin-service'
|
121
125
|
)
|
122
126
|
admin_msg.from('admin-service')
|
123
127
|
admin_msg.to('my-service')
|
@@ -191,7 +195,8 @@ puts "\nš¤ Publishing alert messages..."
|
|
191
195
|
broadcast_alert = AlertMessage.new(
|
192
196
|
severity: 'warning',
|
193
197
|
alert_text: 'CPU usage high across cluster',
|
194
|
-
source_system: 'cluster-monitor'
|
198
|
+
source_system: 'cluster-monitor',
|
199
|
+
from: 'monitoring-system-1'
|
195
200
|
)
|
196
201
|
broadcast_alert.from('monitoring-system-1')
|
197
202
|
broadcast_alert.to(nil) # Broadcast
|
@@ -203,7 +208,8 @@ sleep(0.2) # Allow time for handlers to process
|
|
203
208
|
directed_alert = AlertMessage.new(
|
204
209
|
severity: 'critical',
|
205
210
|
alert_text: 'Database connection lost',
|
206
|
-
source_system: 'db-monitor'
|
211
|
+
source_system: 'db-monitor',
|
212
|
+
from: 'monitoring-system-2'
|
207
213
|
)
|
208
214
|
directed_alert.from('monitoring-system-2')
|
209
215
|
directed_alert.to('alert-service')
|
@@ -215,7 +221,8 @@ sleep(0.2) # Allow time for handlers to process
|
|
215
221
|
other_alert = AlertMessage.new(
|
216
222
|
severity: 'info',
|
217
223
|
alert_text: 'Backup completed successfully',
|
218
|
-
source_system: 'backup-system'
|
224
|
+
source_system: 'backup-system',
|
225
|
+
from: 'monitoring-system-1'
|
219
226
|
)
|
220
227
|
other_alert.from('monitoring-system-1')
|
221
228
|
other_alert.to('backup-service')
|
@@ -285,7 +292,8 @@ normal_order = OrderMessage.new(
|
|
285
292
|
order_id: "ORD-001",
|
286
293
|
priority: 'normal',
|
287
294
|
items: ["Widget A", "Widget B"],
|
288
|
-
total_amount: 99.99
|
295
|
+
total_amount: 99.99,
|
296
|
+
from: 'order-service'
|
289
297
|
)
|
290
298
|
puts "\n1. Publishing normal order to fulfillment..."
|
291
299
|
normal_order.publish
|
@@ -296,7 +304,8 @@ high_priority_order = OrderMessage.new(
|
|
296
304
|
order_id: "ORD-002",
|
297
305
|
priority: 'high',
|
298
306
|
items: ["Premium Widget", "Express Gadget"],
|
299
|
-
total_amount: 999.99
|
307
|
+
total_amount: 999.99,
|
308
|
+
from: 'order-service'
|
300
309
|
)
|
301
310
|
puts "\n2. Publishing high-priority order..."
|
302
311
|
high_priority_order.publish
|
@@ -307,7 +316,8 @@ misrouted_order = OrderMessage.new(
|
|
307
316
|
order_id: "ORD-003",
|
308
317
|
priority: 'normal',
|
309
318
|
items: ["Test Item"],
|
310
|
-
total_amount: 50.00
|
319
|
+
total_amount: 50.00,
|
320
|
+
from: 'order-service'
|
311
321
|
)
|
312
322
|
misrouted_order.to('wrong-service')
|
313
323
|
puts "\n3. Publishing order to 'wrong-service' (should not be received)..."
|
@@ -371,7 +381,8 @@ puts "\nš¤ Publishing service requests..."
|
|
371
381
|
api_request = ServiceRequest.new(
|
372
382
|
request_id: SecureRandom.uuid,
|
373
383
|
request_type: 'user_lookup',
|
374
|
-
data: { user_id: 'USER-123' }
|
384
|
+
data: { user_id: 'USER-123' },
|
385
|
+
from: 'web-frontend'
|
375
386
|
)
|
376
387
|
api_request.from('web-frontend')
|
377
388
|
api_request.to('api-service')
|
@@ -384,7 +395,8 @@ sleep(0.2) # Allow time for handlers to process
|
|
384
395
|
data_request = ServiceRequest.new(
|
385
396
|
request_id: SecureRandom.uuid,
|
386
397
|
request_type: 'query',
|
387
|
-
data: { table: 'orders', limit: 100 }
|
398
|
+
data: { table: 'orders', limit: 100 },
|
399
|
+
from: 'analytics-service'
|
388
400
|
)
|
389
401
|
data_request.from('analytics-service')
|
390
402
|
data_request.to('data-service')
|
@@ -110,7 +110,8 @@ class BotChatAgent < BaseAgent
|
|
110
110
|
user_name: chat_data['sender_name'],
|
111
111
|
command: command,
|
112
112
|
parameters: parameters,
|
113
|
-
timestamp: Time.now.iso8601
|
113
|
+
timestamp: Time.now.iso8601,
|
114
|
+
from: chat_data['sender_id']
|
114
115
|
)
|
115
116
|
|
116
117
|
bot_command.publish
|
@@ -244,7 +244,8 @@ class BaseAgent
|
|
244
244
|
message_type: message_type,
|
245
245
|
timestamp: Time.now.iso8601,
|
246
246
|
mentions: extract_mentions(content),
|
247
|
-
metadata: { agent_type: @agent_type }
|
247
|
+
metadata: { agent_type: @agent_type },
|
248
|
+
from: @agent_id
|
248
249
|
)
|
249
250
|
|
250
251
|
log_display("š¬ [#{room_id}] #{@name}: #{content}")
|
@@ -291,7 +292,8 @@ class BaseAgent
|
|
291
292
|
notification_type: notification_type,
|
292
293
|
content: content,
|
293
294
|
timestamp: Time.now.iso8601,
|
294
|
-
metadata: { triggered_by: @agent_id }
|
295
|
+
metadata: { triggered_by: @agent_id },
|
296
|
+
from: @agent_id
|
295
297
|
)
|
296
298
|
|
297
299
|
notification.publish
|
data/lib/smart_message/base.rb
CHANGED
@@ -73,10 +73,13 @@ module SmartMessage
|
|
73
73
|
@serializer = nil
|
74
74
|
@logger = nil
|
75
75
|
|
76
|
+
# Extract addressing information from props before creating header
|
77
|
+
addressing_props = props.extract!(:from, :to, :reply_to)
|
78
|
+
|
76
79
|
# instance-level over ride of class addressing
|
77
|
-
@from =
|
78
|
-
@to =
|
79
|
-
@reply_to =
|
80
|
+
@from = addressing_props[:from]
|
81
|
+
@to = addressing_props[:to]
|
82
|
+
@reply_to = addressing_props[:reply_to]
|
80
83
|
|
81
84
|
# Create header with version validation specific to this message class
|
82
85
|
header = SmartMessage::Header.new(
|
@@ -0,0 +1,227 @@
|
|
1
|
+
# lib/smart_message/circuit_breaker.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
require 'breaker_machines'
|
6
|
+
|
7
|
+
module SmartMessage
|
8
|
+
# Circuit breaker configuration and management for SmartMessage
|
9
|
+
# Provides production-grade reliability patterns using BreakerMachines gem
|
10
|
+
module CircuitBreaker
|
11
|
+
extend self
|
12
|
+
|
13
|
+
# Default circuit breaker configurations
|
14
|
+
DEFAULT_CONFIGS = {
|
15
|
+
message_processor: {
|
16
|
+
threshold: { failures: 3, within: 60 }, # 3 failures within 1 minute
|
17
|
+
reset_after: 30, # Reset after 30 seconds
|
18
|
+
storage: :memory # Use memory storage by default
|
19
|
+
},
|
20
|
+
transport_publish: {
|
21
|
+
threshold: { failures: 5, within: 30 }, # 5 failures within 30 seconds
|
22
|
+
reset_after: 15, # Reset after 15 seconds
|
23
|
+
storage: :memory
|
24
|
+
},
|
25
|
+
transport_subscribe: {
|
26
|
+
threshold: { failures: 3, within: 60 }, # 3 failures within 1 minute
|
27
|
+
reset_after: 45, # Reset after 45 seconds
|
28
|
+
storage: :memory
|
29
|
+
},
|
30
|
+
serializer: {
|
31
|
+
threshold: { failures: 5, within: 30 }, # 5 failures within 30 seconds
|
32
|
+
reset_after: 10, # Reset after 10 seconds
|
33
|
+
storage: :memory
|
34
|
+
},
|
35
|
+
dispatcher_shutdown: {
|
36
|
+
threshold: { failures: 2, within: 10 }, # 2 failures within 10 seconds
|
37
|
+
reset_after: 5, # Reset after 5 seconds
|
38
|
+
storage: :memory
|
39
|
+
}
|
40
|
+
}.freeze
|
41
|
+
|
42
|
+
# Configure circuit breakers for a class
|
43
|
+
# @param target_class [Class] The class to add circuit breakers to
|
44
|
+
# @param options [Hash] Configuration options
|
45
|
+
def configure_for(target_class, options = {})
|
46
|
+
target_class.include BreakerMachines::DSL
|
47
|
+
|
48
|
+
# Configure each circuit breaker type
|
49
|
+
DEFAULT_CONFIGS.each do |circuit_name, config|
|
50
|
+
final_config = config.merge(options[circuit_name] || {})
|
51
|
+
|
52
|
+
target_class.circuit circuit_name do
|
53
|
+
threshold failures: final_config[:threshold][:failures],
|
54
|
+
within: final_config[:threshold][:within].seconds
|
55
|
+
reset_after final_config[:reset_after].seconds
|
56
|
+
|
57
|
+
# Configure storage backend
|
58
|
+
case final_config[:storage]
|
59
|
+
when :redis
|
60
|
+
# Use Redis storage if configured
|
61
|
+
storage BreakerMachines::Storage::Redis.new(
|
62
|
+
redis: SmartMessage::Transport::RedisTransport.new.redis_pub
|
63
|
+
)
|
64
|
+
else
|
65
|
+
# Default to memory storage
|
66
|
+
storage BreakerMachines::Storage::Memory.new
|
67
|
+
end
|
68
|
+
|
69
|
+
# Default fallback that logs the failure
|
70
|
+
fallback do |exception|
|
71
|
+
{
|
72
|
+
circuit_breaker: {
|
73
|
+
circuit: circuit_name,
|
74
|
+
state: 'open',
|
75
|
+
error: exception.message,
|
76
|
+
timestamp: Time.now.iso8601,
|
77
|
+
fallback_triggered: true
|
78
|
+
}
|
79
|
+
}
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Create a specialized circuit breaker for entity-specific processing
|
86
|
+
# @param target_class [Class] The class to add the circuit to
|
87
|
+
# @param entity_id [String] The entity identifier
|
88
|
+
# @param options [Hash] Configuration options
|
89
|
+
def configure_entity_circuit(target_class, entity_id, options = {})
|
90
|
+
circuit_name = "entity_#{entity_id}".to_sym
|
91
|
+
config = DEFAULT_CONFIGS[:message_processor].merge(options)
|
92
|
+
|
93
|
+
target_class.circuit circuit_name do
|
94
|
+
threshold failures: config[:threshold][:failures],
|
95
|
+
within: config[:threshold][:within].seconds
|
96
|
+
reset_after config[:reset_after].seconds
|
97
|
+
|
98
|
+
# Configure storage
|
99
|
+
case config[:storage]
|
100
|
+
when :redis
|
101
|
+
storage BreakerMachines::Storage::Redis.new(
|
102
|
+
redis: SmartMessage::Transport::RedisTransport.new.redis_pub
|
103
|
+
)
|
104
|
+
else
|
105
|
+
storage BreakerMachines::Storage::Memory.new
|
106
|
+
end
|
107
|
+
|
108
|
+
# Entity-specific fallback
|
109
|
+
fallback do |exception|
|
110
|
+
{
|
111
|
+
circuit_breaker: {
|
112
|
+
circuit: circuit_name,
|
113
|
+
entity_id: entity_id,
|
114
|
+
state: 'open',
|
115
|
+
error: exception.message,
|
116
|
+
timestamp: Time.now.iso8601,
|
117
|
+
fallback_triggered: true
|
118
|
+
}
|
119
|
+
}
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
circuit_name
|
124
|
+
end
|
125
|
+
|
126
|
+
# Get circuit breaker statistics
|
127
|
+
# @param circuit_instance [Object] Instance with circuit breakers
|
128
|
+
# @param circuit_name [Symbol] Name of the circuit
|
129
|
+
def stats(circuit_instance, circuit_name)
|
130
|
+
breaker = circuit_instance.circuit(circuit_name)
|
131
|
+
return nil unless breaker
|
132
|
+
|
133
|
+
{
|
134
|
+
name: circuit_name,
|
135
|
+
state: breaker.state,
|
136
|
+
failure_count: breaker.failure_count,
|
137
|
+
last_failure_time: breaker.last_failure_time,
|
138
|
+
next_attempt_time: breaker.next_attempt_time
|
139
|
+
}
|
140
|
+
end
|
141
|
+
|
142
|
+
# Check if circuit breaker is available (closed or half-open)
|
143
|
+
# @param circuit_instance [Object] Instance with circuit breakers
|
144
|
+
# @param circuit_name [Symbol] Name of the circuit
|
145
|
+
def available?(circuit_instance, circuit_name)
|
146
|
+
breaker = circuit_instance.circuit(circuit_name)
|
147
|
+
return true unless breaker # No circuit breaker means always available
|
148
|
+
|
149
|
+
breaker.state != :open
|
150
|
+
end
|
151
|
+
|
152
|
+
# Manually reset a circuit breaker
|
153
|
+
# @param circuit_instance [Object] Instance with circuit breakers
|
154
|
+
# @param circuit_name [Symbol] Name of the circuit
|
155
|
+
def reset!(circuit_instance, circuit_name)
|
156
|
+
breaker = circuit_instance.circuit(circuit_name)
|
157
|
+
breaker&.reset!
|
158
|
+
end
|
159
|
+
|
160
|
+
# Configure fallback handlers for different scenarios
|
161
|
+
module Fallbacks
|
162
|
+
# Dead letter queue fallback
|
163
|
+
def self.dead_letter_queue(dlq_transport = nil)
|
164
|
+
proc do |exception, *args|
|
165
|
+
# Extract message details from args if available
|
166
|
+
message_header = args[0] if args[0].is_a?(SmartMessage::Header)
|
167
|
+
message_payload = args[1] if args.length > 1
|
168
|
+
|
169
|
+
# Log to dead letter queue if transport provided
|
170
|
+
if dlq_transport && message_header && message_payload
|
171
|
+
dlq_transport.publish(message_header, message_payload)
|
172
|
+
end
|
173
|
+
|
174
|
+
{
|
175
|
+
circuit_breaker: {
|
176
|
+
state: 'open',
|
177
|
+
error: exception.message,
|
178
|
+
sent_to_dlq: !!(dlq_transport && message_header && message_payload),
|
179
|
+
timestamp: Time.now.iso8601
|
180
|
+
}
|
181
|
+
}
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Retry with exponential backoff fallback
|
186
|
+
def self.retry_with_backoff(max_retries: 3, base_delay: 1)
|
187
|
+
proc do |exception, *args|
|
188
|
+
retry_count = Thread.current[:circuit_retry_count] ||= 0
|
189
|
+
|
190
|
+
if retry_count < max_retries
|
191
|
+
Thread.current[:circuit_retry_count] += 1
|
192
|
+
delay = base_delay * (2 ** retry_count)
|
193
|
+
sleep(delay)
|
194
|
+
|
195
|
+
# Re-raise to trigger retry
|
196
|
+
raise exception
|
197
|
+
else
|
198
|
+
Thread.current[:circuit_retry_count] = nil
|
199
|
+
|
200
|
+
{
|
201
|
+
circuit_breaker: {
|
202
|
+
state: 'open',
|
203
|
+
error: exception.message,
|
204
|
+
max_retries_exceeded: true,
|
205
|
+
timestamp: Time.now.iso8601
|
206
|
+
}
|
207
|
+
}
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# Graceful degradation fallback
|
213
|
+
def self.graceful_degradation(degraded_response)
|
214
|
+
proc do |exception|
|
215
|
+
{
|
216
|
+
circuit_breaker: {
|
217
|
+
state: 'open',
|
218
|
+
error: exception.message,
|
219
|
+
degraded_response: degraded_response,
|
220
|
+
timestamp: Time.now.iso8601
|
221
|
+
}
|
222
|
+
}
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
@@ -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
|
|
@@ -139,7 +138,8 @@ module SmartMessage
|
|
139
138
|
|
140
139
|
SS.add(message_klass, message_processor, 'routed' )
|
141
140
|
@router_pool.post do
|
142
|
-
|
141
|
+
# Use circuit breaker to protect message processing
|
142
|
+
circuit_result = circuit(:message_processor).wrap do
|
143
143
|
# Check if this is a proc handler or a regular method call
|
144
144
|
if proc_handler?(message_processor)
|
145
145
|
# Call the proc handler via SmartMessage::Base
|
@@ -153,17 +153,63 @@ module SmartMessage
|
|
153
153
|
.method(class_method)
|
154
154
|
.call(message_header, message_payload)
|
155
155
|
end
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
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)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
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
|
+
}
|
161
184
|
end
|
162
185
|
end
|
186
|
+
rescue => e
|
187
|
+
stats[:error] = "Failed to get circuit breaker stats: #{e.message}"
|
163
188
|
end
|
189
|
+
|
190
|
+
stats
|
164
191
|
end
|
165
192
|
|
166
|
-
|
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
|
167
213
|
|
168
214
|
# Check if a message matches the subscription filters
|
169
215
|
# @param message_header [SmartMessage::Header] The message header
|
@@ -199,6 +245,81 @@ module SmartMessage
|
|
199
245
|
SmartMessage::Base.proc_handler?(message_processor)
|
200
246
|
end
|
201
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
|
+
|
202
323
|
|
203
324
|
#######################################################
|
204
325
|
## Class methods
|
@@ -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
|