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.
@@ -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
- print "Shuttingdown down the dispatcher's @router_pool ..."
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
- unless @subscribers[klass].include? process_method_as_string
89
- @subscribers[klass] += [process_method_as_string]
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
- @subscribers[String(message_class)].delete process_method_as_string
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
- @subscribers[message_klass].each do |message_processor|
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
- begin
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
- rescue Exception => e
136
- # TODO: Add proper exception logging
137
- # Exception details: #{e.message}
138
- # Processor: #{message_processor}
139
- puts "Error processing message: #{e.message}" if $DEBUG
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
- private
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
@@ -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
- # TODO: write this
14
+ configure_serializer_circuit_breakers
11
15
  end
12
16
 
13
17
  def encode(message_instance)
14
- # TODO: Add proper logging here
15
- raise ::SmartMessage::Errors::NotImplemented
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
- # TODO: Add proper logging here
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 encode(message_instance)
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 decode(payload)
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
- 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
36
56
  # @param message_class [String] The message class name
37
57
  # @param process_method [String] The processing method identifier
38
- def subscribe(message_class, process_method)
39
- @dispatcher.add(message_class, process_method)
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 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,19 +36,26 @@ 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
 
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, 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|