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.
@@ -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
- raise NotImplementedError, 'Transport must implement #publish'
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 publish(message_header, message_payload)
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 publish(message_header, message_payload)
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, message_payload)
50
+ @redis_pub.publish(channel, redis_message)
43
51
  rescue Redis::ConnectionError
44
- retry_with_reconnect('publish') { @redis_pub.publish(channel, message_payload) }
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, 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)
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|
@@ -28,7 +28,7 @@ module SmartMessage
28
28
  end
29
29
 
30
30
  # Publish message to STDOUT
31
- def publish(message_header, message_payload)
31
+ def do_publish(message_header, message_payload)
32
32
  @output.puts format_message(message_header, message_payload)
33
33
  @output.flush
34
34
 
@@ -3,5 +3,5 @@
3
3
  # frozen_string_literal: true
4
4
 
5
5
  module SmartMessage
6
- VERSION = '0.0.6'
6
+ VERSION = '0.0.8'
7
7
  end
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'
@@ -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.6
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