smart_message 0.0.6 → 0.0.8
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 +102 -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 +243 -0
- data/lib/smart_message/dead_letter_queue.rb +344 -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 +2 -0
- data/smart_message.gemspec +1 -0
- metadata +17 -1
@@ -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,11 +30,26 @@ 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
|
@@ -73,6 +93,47 @@ module SmartMessage
|
|
73
93
|
# Override in subclasses if cleanup is needed
|
74
94
|
end
|
75
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
|
+
|
76
137
|
# Receive and route a message (called by transport implementations)
|
77
138
|
# @param message_header [SmartMessage::Header] Message routing information
|
78
139
|
# @param message_payload [String] Serialized message content
|
@@ -81,6 +142,81 @@ module SmartMessage
|
|
81
142
|
def receive(message_header, message_payload)
|
82
143
|
@dispatcher.route(message_header, message_payload)
|
83
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 - use DLQ fallback
|
160
|
+
fallback SmartMessage::CircuitBreaker::Fallbacks.dead_letter_queue
|
161
|
+
end
|
162
|
+
|
163
|
+
# Configure subscribe circuit breaker
|
164
|
+
subscribe_config = SmartMessage::CircuitBreaker::DEFAULT_CONFIGS[:transport_subscribe]
|
165
|
+
|
166
|
+
self.class.circuit :transport_subscribe do
|
167
|
+
threshold failures: subscribe_config[:threshold][:failures],
|
168
|
+
within: subscribe_config[:threshold][:within].seconds
|
169
|
+
reset_after subscribe_config[:reset_after].seconds
|
170
|
+
|
171
|
+
storage BreakerMachines::Storage::Memory.new
|
172
|
+
|
173
|
+
# Fallback for subscribe failures - log and return error info
|
174
|
+
fallback do |exception|
|
175
|
+
{
|
176
|
+
circuit_breaker: {
|
177
|
+
circuit: :transport_subscribe,
|
178
|
+
transport_type: self.class.name,
|
179
|
+
state: 'open',
|
180
|
+
error: exception.message,
|
181
|
+
error_class: exception.class.name,
|
182
|
+
timestamp: Time.now.iso8601,
|
183
|
+
fallback_triggered: true
|
184
|
+
}
|
185
|
+
}
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Handle publish circuit breaker fallback
|
191
|
+
# @param fallback_result [Hash] The circuit breaker fallback result
|
192
|
+
# @param message_header [SmartMessage::Header] The message header
|
193
|
+
# @param message_payload [String] The message payload
|
194
|
+
def handle_publish_fallback(fallback_result, message_header, message_payload)
|
195
|
+
# Log the circuit breaker activation
|
196
|
+
if $DEBUG
|
197
|
+
puts "Transport publish circuit breaker activated: #{self.class.name}"
|
198
|
+
puts "Error: #{fallback_result[:circuit_breaker][:error]}"
|
199
|
+
puts "Message: #{message_header.message_class}"
|
200
|
+
puts "Sent to DLQ: #{fallback_result[:circuit_breaker][:sent_to_dlq]}"
|
201
|
+
end
|
202
|
+
|
203
|
+
# If message wasn't sent to DLQ by circuit breaker, send it now
|
204
|
+
unless fallback_result.dig(:circuit_breaker, :sent_to_dlq)
|
205
|
+
begin
|
206
|
+
SmartMessage::DeadLetterQueue.default.enqueue(
|
207
|
+
message_header,
|
208
|
+
message_payload,
|
209
|
+
error: fallback_result.dig(:circuit_breaker, :error) || 'Circuit breaker activated',
|
210
|
+
transport: self.class.name
|
211
|
+
)
|
212
|
+
rescue => dlq_error
|
213
|
+
puts "Warning: Failed to store message in DLQ: #{dlq_error.message}" if $DEBUG
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Return the fallback result to indicate failure
|
218
|
+
fallback_result
|
219
|
+
end
|
84
220
|
end
|
85
221
|
end
|
86
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,13 +36,20 @@ 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
|
|
@@ -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|
|
data/lib/smart_message.rb
CHANGED
@@ -21,6 +21,8 @@ require_relative './simple_stats'
|
|
21
21
|
|
22
22
|
require_relative './smart_message/version'
|
23
23
|
require_relative './smart_message/errors'
|
24
|
+
require_relative './smart_message/circuit_breaker'
|
25
|
+
require_relative './smart_message/dead_letter_queue'
|
24
26
|
|
25
27
|
require_relative './smart_message/dispatcher.rb'
|
26
28
|
require_relative './smart_message/transport.rb'
|
data/smart_message.gemspec
CHANGED
@@ -40,6 +40,7 @@ Gem::Specification.new do |spec|
|
|
40
40
|
spec.add_dependency 'activesupport'
|
41
41
|
spec.add_dependency 'concurrent-ruby'
|
42
42
|
spec.add_dependency 'redis'
|
43
|
+
spec.add_dependency 'breaker_machines'
|
43
44
|
|
44
45
|
spec.add_development_dependency 'bundler'
|
45
46
|
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.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dewayne VanHoozer
|
@@ -65,6 +65,20 @@ dependencies:
|
|
65
65
|
- - ">="
|
66
66
|
- !ruby/object:Gem::Version
|
67
67
|
version: '0'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: breaker_machines
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
type: :runtime
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
68
82
|
- !ruby/object:Gem::Dependency
|
69
83
|
name: bundler
|
70
84
|
requirement: !ruby/object:Gem::Requirement
|
@@ -234,6 +248,8 @@ files:
|
|
234
248
|
- lib/simple_stats.rb
|
235
249
|
- lib/smart_message.rb
|
236
250
|
- lib/smart_message/base.rb
|
251
|
+
- lib/smart_message/circuit_breaker.rb
|
252
|
+
- lib/smart_message/dead_letter_queue.rb
|
237
253
|
- lib/smart_message/dispatcher.rb
|
238
254
|
- lib/smart_message/dispatcher/.keep
|
239
255
|
- lib/smart_message/errors.rb
|