smart_message 0.0.7 → 0.0.9
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/.gitignore +1 -0
- data/.irbrc +24 -0
- data/CHANGELOG.md +143 -0
- data/Gemfile.lock +6 -1
- data/README.md +289 -15
- data/docs/README.md +3 -1
- data/docs/addressing.md +119 -13
- data/docs/architecture.md +68 -0
- data/docs/dead_letter_queue.md +673 -0
- data/docs/dispatcher.md +87 -0
- data/docs/examples.md +59 -1
- data/docs/getting-started.md +8 -1
- data/docs/logging.md +382 -326
- data/docs/message_filtering.md +451 -0
- data/examples/01_point_to_point_orders.rb +54 -53
- data/examples/02_publish_subscribe_events.rb +14 -10
- data/examples/03_many_to_many_chat.rb +16 -8
- data/examples/04_redis_smart_home_iot.rb +20 -10
- data/examples/05_proc_handlers.rb +12 -11
- data/examples/06_custom_logger_example.rb +95 -100
- data/examples/07_error_handling_scenarios.rb +4 -2
- data/examples/08_entity_addressing_basic.rb +18 -6
- data/examples/08_entity_addressing_with_filtering.rb +27 -9
- data/examples/09_dead_letter_queue_demo.rb +559 -0
- data/examples/09_regex_filtering_microservices.rb +407 -0
- data/examples/10_header_block_configuration.rb +263 -0
- data/examples/11_global_configuration_example.rb +219 -0
- data/examples/README.md +102 -0
- data/examples/dead_letters.jsonl +12 -0
- data/examples/performance_metrics/benchmark_results_ractor_20250818_205603.json +135 -0
- data/examples/performance_metrics/benchmark_results_ractor_20250818_205831.json +135 -0
- data/examples/performance_metrics/benchmark_results_test_20250818_204942.json +130 -0
- data/examples/performance_metrics/benchmark_results_threadpool_20250818_204942.json +130 -0
- data/examples/performance_metrics/benchmark_results_threadpool_20250818_204959.json +130 -0
- data/examples/performance_metrics/benchmark_results_threadpool_20250818_205044.json +130 -0
- data/examples/performance_metrics/benchmark_results_threadpool_20250818_205109.json +130 -0
- data/examples/performance_metrics/benchmark_results_threadpool_20250818_205252.json +130 -0
- data/examples/performance_metrics/benchmark_results_unknown_20250819_172852.json +130 -0
- data/examples/performance_metrics/compare_benchmarks.rb +519 -0
- data/examples/performance_metrics/dead_letters.jsonl +3100 -0
- data/examples/performance_metrics/performance_benchmark.rb +344 -0
- data/examples/show_logger.rb +367 -0
- data/examples/show_me.rb +145 -0
- data/examples/temp.txt +94 -0
- data/examples/tmux_chat/bot_agent.rb +4 -2
- data/examples/tmux_chat/human_agent.rb +4 -2
- data/examples/tmux_chat/room_monitor.rb +4 -2
- data/examples/tmux_chat/shared_chat_system.rb +6 -3
- data/lib/smart_message/addressing.rb +259 -0
- data/lib/smart_message/base.rb +121 -599
- data/lib/smart_message/circuit_breaker.rb +23 -6
- data/lib/smart_message/configuration.rb +199 -0
- data/lib/smart_message/dead_letter_queue.rb +361 -0
- data/lib/smart_message/dispatcher.rb +90 -49
- data/lib/smart_message/header.rb +5 -0
- data/lib/smart_message/logger/base.rb +21 -1
- data/lib/smart_message/logger/default.rb +88 -138
- data/lib/smart_message/logger/lumberjack.rb +324 -0
- data/lib/smart_message/logger/null.rb +81 -0
- data/lib/smart_message/logger.rb +17 -9
- data/lib/smart_message/messaging.rb +100 -0
- data/lib/smart_message/plugins.rb +132 -0
- data/lib/smart_message/serializer/base.rb +25 -8
- data/lib/smart_message/serializer/json.rb +5 -4
- data/lib/smart_message/subscription.rb +193 -0
- data/lib/smart_message/transport/base.rb +84 -53
- data/lib/smart_message/transport/memory_transport.rb +7 -5
- data/lib/smart_message/transport/redis_transport.rb +15 -45
- data/lib/smart_message/transport/stdout_transport.rb +18 -8
- data/lib/smart_message/transport.rb +1 -34
- data/lib/smart_message/utilities.rb +142 -0
- data/lib/smart_message/version.rb +1 -1
- data/lib/smart_message/versioning.rb +85 -0
- data/lib/smart_message/wrapper.rb.bak +132 -0
- data/lib/smart_message.rb +74 -27
- data/smart_message.gemspec +3 -0
- metadata +77 -3
- data/lib/smart_message/serializer.rb +0 -10
- data/lib/smart_message/wrapper.rb +0 -43
@@ -2,6 +2,7 @@
|
|
2
2
|
# encoding: utf-8
|
3
3
|
# frozen_string_literal: true
|
4
4
|
|
5
|
+
require 'json' # STDLIB
|
5
6
|
require_relative '../circuit_breaker'
|
6
7
|
|
7
8
|
module SmartMessage::Serializer
|
@@ -12,7 +13,20 @@ module SmartMessage::Serializer
|
|
12
13
|
# provide basic configuration
|
13
14
|
def initialize
|
14
15
|
configure_serializer_circuit_breakers
|
16
|
+
|
17
|
+
logger.debug { "[SmartMessage::Serializer::#{self.class.name.split('::').last}] Initialized" }
|
18
|
+
rescue => e
|
19
|
+
logger&.error { "[SmartMessage] Error in serializer initialization: #{e.class.name} - #{e.message}" }
|
20
|
+
raise
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def logger
|
26
|
+
@logger ||= SmartMessage::Logger.default
|
15
27
|
end
|
28
|
+
|
29
|
+
public
|
16
30
|
|
17
31
|
def encode(message_instance)
|
18
32
|
circuit(:serializer).wrap do
|
@@ -39,10 +53,16 @@ module SmartMessage::Serializer
|
|
39
53
|
raise
|
40
54
|
end
|
41
55
|
end
|
56
|
+
|
57
|
+
private
|
42
58
|
|
43
59
|
# Template methods for actual serialization (implement in subclasses)
|
44
60
|
def do_encode(message_instance)
|
45
|
-
|
61
|
+
# Default implementation: serialize only the payload portion for wrapper architecture
|
62
|
+
# Subclasses can override this for specific serialization formats
|
63
|
+
message_hash = message_instance.to_h
|
64
|
+
payload_portion = message_hash[:_sm_payload]
|
65
|
+
::JSON.generate(payload_portion)
|
46
66
|
end
|
47
67
|
|
48
68
|
def do_decode(payload)
|
@@ -81,13 +101,10 @@ module SmartMessage::Serializer
|
|
81
101
|
|
82
102
|
# Handle serializer circuit breaker fallback
|
83
103
|
def handle_serializer_fallback(fallback_result, operation, data)
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
end
|
89
|
-
|
90
|
-
# TODO: Integrate with structured logging when implemented
|
104
|
+
# Integrate with structured logging
|
105
|
+
logger.error { "[SmartMessage::Serializer] Circuit breaker activated: #{self.class.name}" }
|
106
|
+
logger.error { "[SmartMessage::Serializer] Operation: #{operation}" }
|
107
|
+
logger.error { "[SmartMessage::Serializer] Error: #{fallback_result[:circuit_breaker][:error]}" }
|
91
108
|
|
92
109
|
# Return the fallback result
|
93
110
|
fallback_result
|
@@ -5,11 +5,12 @@
|
|
5
5
|
require 'json' # STDLIB
|
6
6
|
|
7
7
|
module SmartMessage::Serializer
|
8
|
-
class
|
8
|
+
class Json < Base
|
9
9
|
def do_encode(message_instance)
|
10
|
-
#
|
11
|
-
#
|
12
|
-
message_instance.
|
10
|
+
# Single-tier serialization: serialize the complete message structure
|
11
|
+
# This includes both header and payload for full message reconstruction
|
12
|
+
message_hash = message_instance.to_h
|
13
|
+
::JSON.generate(message_hash)
|
13
14
|
end
|
14
15
|
|
15
16
|
def do_decode(payload)
|
@@ -0,0 +1,193 @@
|
|
1
|
+
# lib/smart_message/subscription.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
require 'securerandom' # STDLIB
|
6
|
+
|
7
|
+
module SmartMessage
|
8
|
+
# Subscription management module for SmartMessage::Base
|
9
|
+
# Handles subscribe/unsubscribe operations and proc handler management
|
10
|
+
module Subscription
|
11
|
+
def self.included(base)
|
12
|
+
base.extend(ClassMethods)
|
13
|
+
base.class_eval do
|
14
|
+
# Registry for proc-based message handlers
|
15
|
+
class_variable_set(:@@proc_handlers, {}) unless class_variable_defined?(:@@proc_handlers)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
#########################################################
|
21
|
+
## proc handler management
|
22
|
+
|
23
|
+
# Register a proc handler and return a unique identifier for it
|
24
|
+
# @param message_class [String] The message class name
|
25
|
+
# @param handler_proc [Proc] The proc to register
|
26
|
+
# @return [String] Unique identifier for this handler
|
27
|
+
def register_proc_handler(message_class, handler_proc)
|
28
|
+
handler_id = "#{message_class}.proc_#{SecureRandom.hex(8)}"
|
29
|
+
class_variable_get(:@@proc_handlers)[handler_id] = handler_proc
|
30
|
+
handler_id
|
31
|
+
end
|
32
|
+
|
33
|
+
# Call a registered proc handler
|
34
|
+
# @param handler_id [String] The handler identifier
|
35
|
+
# @param wrapper [SmartMessage::Wrapper::Base] The message wrapper
|
36
|
+
def call_proc_handler(handler_id, wrapper)
|
37
|
+
handler_proc = class_variable_get(:@@proc_handlers)[handler_id]
|
38
|
+
return unless handler_proc
|
39
|
+
|
40
|
+
handler_proc.call(wrapper)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Remove a proc handler from the registry
|
44
|
+
# @param handler_id [String] The handler identifier to remove
|
45
|
+
def unregister_proc_handler(handler_id)
|
46
|
+
class_variable_get(:@@proc_handlers).delete(handler_id)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Check if a handler ID refers to a proc handler
|
50
|
+
# @param handler_id [String] The handler identifier
|
51
|
+
# @return [Boolean] True if this is a proc handler
|
52
|
+
def proc_handler?(handler_id)
|
53
|
+
class_variable_get(:@@proc_handlers).key?(handler_id)
|
54
|
+
end
|
55
|
+
|
56
|
+
#########################################################
|
57
|
+
## class-level subscription management via the transport
|
58
|
+
|
59
|
+
# Add this message class to the transport's catalog of
|
60
|
+
# subscribed messages. If the transport is missing, raise
|
61
|
+
# an exception.
|
62
|
+
#
|
63
|
+
# @param process_method [String, Proc, nil] The processing method:
|
64
|
+
# - String: Method name like "MyService.handle_message"
|
65
|
+
# - Proc: A proc/lambda that accepts (message_header, message_payload)
|
66
|
+
# - nil: Uses default "MessageClass.process" method
|
67
|
+
# @param broadcast [Boolean, nil] Filter for broadcast messages (to: nil)
|
68
|
+
# @param to [String, Array, nil] Filter for messages directed to specific entities
|
69
|
+
# @param from [String, Array, nil] Filter for messages from specific entities
|
70
|
+
# @param block [Proc] Alternative way to pass a processing block
|
71
|
+
# @return [String] The identifier used for this subscription
|
72
|
+
#
|
73
|
+
# @example Using default handler (all messages)
|
74
|
+
# MyMessage.subscribe
|
75
|
+
#
|
76
|
+
# @example Using custom method name with filtering
|
77
|
+
# MyMessage.subscribe("MyService.handle_message", to: 'my-service')
|
78
|
+
#
|
79
|
+
# @example Using a block with broadcast filtering
|
80
|
+
# MyMessage.subscribe(broadcast: true) do |header, payload|
|
81
|
+
# data = JSON.parse(payload)
|
82
|
+
# puts "Received broadcast: #{data}"
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
# @example Entity-specific filtering
|
86
|
+
# MyMessage.subscribe(to: 'order-service', from: ['payment', 'user'])
|
87
|
+
#
|
88
|
+
# @example Broadcast + directed messages
|
89
|
+
# MyMessage.subscribe(to: 'my-service', broadcast: true)
|
90
|
+
def subscribe(process_method = nil, broadcast: nil, to: nil, from: nil, &block)
|
91
|
+
message_class = whoami
|
92
|
+
|
93
|
+
# Handle different parameter types
|
94
|
+
if block_given?
|
95
|
+
# Block was passed - use it as the handler
|
96
|
+
handler_proc = block
|
97
|
+
process_method = register_proc_handler(message_class, handler_proc)
|
98
|
+
elsif process_method.respond_to?(:call)
|
99
|
+
# Proc/lambda was passed as first parameter
|
100
|
+
handler_proc = process_method
|
101
|
+
process_method = register_proc_handler(message_class, handler_proc)
|
102
|
+
elsif process_method.nil?
|
103
|
+
# Use default handler
|
104
|
+
process_method = message_class + '.process'
|
105
|
+
end
|
106
|
+
# If process_method is a String, use it as-is
|
107
|
+
|
108
|
+
# Normalize string filters to arrays
|
109
|
+
to_filter = normalize_filter_value(to)
|
110
|
+
from_filter = normalize_filter_value(from)
|
111
|
+
|
112
|
+
# Create filter options
|
113
|
+
filter_options = {
|
114
|
+
broadcast: broadcast,
|
115
|
+
to: to_filter,
|
116
|
+
from: from_filter
|
117
|
+
}
|
118
|
+
|
119
|
+
# Add proper logging
|
120
|
+
logger = SmartMessage::Logger.default
|
121
|
+
|
122
|
+
begin
|
123
|
+
raise Errors::TransportNotConfigured if transport_missing?
|
124
|
+
transport.subscribe(message_class, process_method, filter_options)
|
125
|
+
|
126
|
+
# Log successful subscription
|
127
|
+
handler_desc = block_given? || process_method.respond_to?(:call) ? " with block/proc handler" : ""
|
128
|
+
logger.info { "[SmartMessage] Subscribed: #{self.name}#{handler_desc}" }
|
129
|
+
logger.debug { "[SmartMessage::Subscription] Subscribed #{message_class} with filters: #{filter_options}" }
|
130
|
+
|
131
|
+
process_method
|
132
|
+
rescue => e
|
133
|
+
logger.error { "[SmartMessage] Error in message subscription: #{e.class.name} - #{e.message}" }
|
134
|
+
raise
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Remove this process_method for this message class from the
|
139
|
+
# subscribers list.
|
140
|
+
# @param process_method [String, nil] The processing method identifier to remove
|
141
|
+
# - String: Method name like "MyService.handle_message" or proc handler ID
|
142
|
+
# - nil: Uses default "MessageClass.process" method
|
143
|
+
def unsubscribe(process_method = nil)
|
144
|
+
message_class = whoami
|
145
|
+
process_method = message_class + '.process' if process_method.nil?
|
146
|
+
# Add proper logging
|
147
|
+
logger = SmartMessage::Logger.default
|
148
|
+
|
149
|
+
begin
|
150
|
+
if transport_configured?
|
151
|
+
transport.unsubscribe(message_class, process_method)
|
152
|
+
|
153
|
+
# If this was a proc handler, clean it up from the registry
|
154
|
+
if proc_handler?(process_method)
|
155
|
+
unregister_proc_handler(process_method)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Log successful unsubscription
|
159
|
+
logger.info { "[SmartMessage] Unsubscribed: #{self.name}" }
|
160
|
+
logger.debug { "[SmartMessage::Subscription] Unsubscribed #{message_class} from #{process_method}" }
|
161
|
+
end
|
162
|
+
rescue => e
|
163
|
+
logger.error { "[SmartMessage] Error in message unsubscription: #{e.class.name} - #{e.message}" }
|
164
|
+
raise
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Remove this message class and all of its processing methods
|
169
|
+
# from the subscribers list.
|
170
|
+
def unsubscribe!
|
171
|
+
message_class = whoami
|
172
|
+
|
173
|
+
# TODO: Add proper logging here
|
174
|
+
|
175
|
+
transport.unsubscribe!(message_class) if transport_configured?
|
176
|
+
end
|
177
|
+
|
178
|
+
###################################################
|
179
|
+
## Business Logic resides in the #process method.
|
180
|
+
|
181
|
+
# When a transport receives a subscribed to message it
|
182
|
+
# creates an instance of the message and then calls
|
183
|
+
# the process method on that instance.
|
184
|
+
#
|
185
|
+
# It is expected that SmartMessage classes over ride
|
186
|
+
# the SmartMessage::Base#process method with appropriate
|
187
|
+
# business logic to handle the received message content.
|
188
|
+
def process(message_instance)
|
189
|
+
raise Errors::NotImplemented
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -10,7 +10,7 @@ module SmartMessage
|
|
10
10
|
# This defines the standard interface that all transports must implement
|
11
11
|
class Base
|
12
12
|
include BreakerMachines::DSL
|
13
|
-
|
13
|
+
|
14
14
|
attr_reader :options, :dispatcher
|
15
15
|
|
16
16
|
def initialize(**options)
|
@@ -18,7 +18,20 @@ module SmartMessage
|
|
18
18
|
@dispatcher = options[:dispatcher] || SmartMessage::Dispatcher.new
|
19
19
|
configure
|
20
20
|
configure_transport_circuit_breakers
|
21
|
+
|
22
|
+
logger.debug { "[SmartMessage::Transport::#{self.class.name.split('::').last}] Initialized with options: #{@options}" }
|
23
|
+
rescue => e
|
24
|
+
logger&.error { "[SmartMessage] Error in transport initialization: #{e.class.name} - #{e.message}" }
|
25
|
+
raise
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def logger
|
31
|
+
@logger ||= SmartMessage::Logger.default
|
21
32
|
end
|
33
|
+
|
34
|
+
public
|
22
35
|
|
23
36
|
# Transport-specific configuration
|
24
37
|
def configure
|
@@ -31,24 +44,27 @@ module SmartMessage
|
|
31
44
|
end
|
32
45
|
|
33
46
|
# Publish a message with circuit breaker protection
|
34
|
-
# @param
|
35
|
-
# @param
|
36
|
-
def publish(
|
47
|
+
# @param message_class [String] The message class name (used for channel routing)
|
48
|
+
# @param serialized_message [String] Complete serialized message content
|
49
|
+
def publish(message_class, serialized_message)
|
37
50
|
circuit(:transport_publish).wrap do
|
38
|
-
do_publish(
|
51
|
+
do_publish(message_class, serialized_message)
|
39
52
|
end
|
40
53
|
rescue => e
|
54
|
+
# Log the exception for debugging
|
55
|
+
logger.error { "[SmartMessage] Error in transport publish: #{e.class.name} - #{e.message}" }
|
56
|
+
|
41
57
|
# Re-raise if it's not a circuit breaker fallback
|
42
58
|
raise unless e.is_a?(Hash) && e[:circuit_breaker]
|
43
|
-
|
59
|
+
|
44
60
|
# Handle circuit breaker fallback
|
45
|
-
handle_publish_fallback(e,
|
61
|
+
handle_publish_fallback(e, message_class, serialized_message)
|
46
62
|
end
|
47
63
|
|
48
64
|
# Template method for actual publishing (implement in subclasses)
|
49
|
-
# @param
|
50
|
-
# @param
|
51
|
-
def do_publish(
|
65
|
+
# @param message_class [String] The message class name (used for channel routing)
|
66
|
+
# @param serialized_message [String] Complete serialized message content
|
67
|
+
def do_publish(message_class, serialized_message)
|
52
68
|
raise NotImplementedError, 'Transport must implement #do_publish'
|
53
69
|
end
|
54
70
|
|
@@ -61,7 +77,7 @@ module SmartMessage
|
|
61
77
|
end
|
62
78
|
|
63
79
|
# Unsubscribe from a specific message class and process method
|
64
|
-
# @param message_class [String] The message class name
|
80
|
+
# @param message_class [String] The message class name
|
65
81
|
# @param process_method [String] The processing method identifier
|
66
82
|
def unsubscribe(message_class, process_method)
|
67
83
|
@dispatcher.drop(message_class, process_method)
|
@@ -88,7 +104,7 @@ module SmartMessage
|
|
88
104
|
# Override in subclasses if connection setup is needed
|
89
105
|
end
|
90
106
|
|
91
|
-
# Disconnect from transport (if applicable)
|
107
|
+
# Disconnect from transport (if applicable)
|
92
108
|
def disconnect
|
93
109
|
# Override in subclasses if cleanup is needed
|
94
110
|
end
|
@@ -97,7 +113,7 @@ module SmartMessage
|
|
97
113
|
# @return [Hash] Circuit breaker statistics
|
98
114
|
def transport_circuit_stats
|
99
115
|
stats = {}
|
100
|
-
|
116
|
+
|
101
117
|
[:transport_publish, :transport_subscribe].each do |circuit_name|
|
102
118
|
begin
|
103
119
|
if respond_to?(:circuit)
|
@@ -118,7 +134,7 @@ module SmartMessage
|
|
118
134
|
stats[circuit_name] = { error: "Failed to get stats: #{e.message}" }
|
119
135
|
end
|
120
136
|
end
|
121
|
-
|
137
|
+
|
122
138
|
stats
|
123
139
|
end
|
124
140
|
|
@@ -135,54 +151,59 @@ module SmartMessage
|
|
135
151
|
end
|
136
152
|
|
137
153
|
# Receive and route a message (called by transport implementations)
|
138
|
-
# @param
|
139
|
-
# @param
|
154
|
+
# @param message_class [String] The message class name
|
155
|
+
# @param serialized_message [String] The serialized message content
|
140
156
|
protected
|
141
157
|
|
142
|
-
def receive(
|
143
|
-
|
158
|
+
def receive(message_class, serialized_message)
|
159
|
+
# Decode the message using the class's configured serializer
|
160
|
+
|
161
|
+
# Add defensive check for message_class type
|
162
|
+
unless message_class.respond_to?(:constantize)
|
163
|
+
logger.error { "[SmartMessage] Invalid message_class type: #{message_class.class.name} - #{message_class.inspect}" }
|
164
|
+
logger.error { "[SmartMessage] Expected String, got: #{message_class.class.name}" }
|
165
|
+
raise ArgumentError, "message_class must be a String, got #{message_class.class.name}"
|
166
|
+
end
|
167
|
+
|
168
|
+
message_class_obj = message_class.constantize
|
169
|
+
decoded_message = message_class_obj.decode(serialized_message)
|
170
|
+
|
171
|
+
@dispatcher.route(decoded_message)
|
172
|
+
rescue => e
|
173
|
+
logger.error { "[SmartMessage] Error in transport receive: #{e.class.name} - #{e.message}" }
|
174
|
+
logger.error { "[SmartMessage] message_class: #{message_class.inspect} (#{message_class.class.name})" }
|
175
|
+
logger.error { "[SmartMessage] serialized_message length: #{serialized_message&.length}" }
|
176
|
+
raise
|
144
177
|
end
|
145
178
|
|
146
179
|
# Configure circuit breakers for transport operations
|
147
180
|
def configure_transport_circuit_breakers
|
148
181
|
# Configure publish circuit breaker
|
149
182
|
publish_config = SmartMessage::CircuitBreaker::DEFAULT_CONFIGS[:transport_publish]
|
150
|
-
|
183
|
+
|
151
184
|
self.class.circuit :transport_publish do
|
152
|
-
threshold failures: publish_config[:threshold][:failures],
|
185
|
+
threshold failures: publish_config[:threshold][:failures],
|
153
186
|
within: publish_config[:threshold][:within].seconds
|
154
187
|
reset_after publish_config[:reset_after].seconds
|
155
|
-
|
188
|
+
|
156
189
|
# Use memory storage by default for transport circuits
|
157
190
|
storage BreakerMachines::Storage::Memory.new
|
158
|
-
|
159
|
-
# Fallback for publish failures
|
160
|
-
fallback
|
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
|
191
|
+
|
192
|
+
# Fallback for publish failures - use DLQ fallback
|
193
|
+
fallback SmartMessage::CircuitBreaker::Fallbacks.dead_letter_queue
|
173
194
|
end
|
174
195
|
|
175
196
|
# Configure subscribe circuit breaker
|
176
197
|
subscribe_config = SmartMessage::CircuitBreaker::DEFAULT_CONFIGS[:transport_subscribe]
|
177
|
-
|
198
|
+
|
178
199
|
self.class.circuit :transport_subscribe do
|
179
|
-
threshold failures: subscribe_config[:threshold][:failures],
|
200
|
+
threshold failures: subscribe_config[:threshold][:failures],
|
180
201
|
within: subscribe_config[:threshold][:within].seconds
|
181
202
|
reset_after subscribe_config[:reset_after].seconds
|
182
|
-
|
203
|
+
|
183
204
|
storage BreakerMachines::Storage::Memory.new
|
184
|
-
|
185
|
-
# Fallback for subscribe failures
|
205
|
+
|
206
|
+
# Fallback for subscribe failures - log and return error info
|
186
207
|
fallback do |exception|
|
187
208
|
{
|
188
209
|
circuit_breaker: {
|
@@ -201,22 +222,32 @@ module SmartMessage
|
|
201
222
|
|
202
223
|
# Handle publish circuit breaker fallback
|
203
224
|
# @param fallback_result [Hash] The circuit breaker fallback result
|
204
|
-
# @param
|
205
|
-
# @param
|
206
|
-
def handle_publish_fallback(fallback_result,
|
225
|
+
# @param message_class [String] The message class name
|
226
|
+
# @param serialized_message [String] The serialized message
|
227
|
+
def handle_publish_fallback(fallback_result, message_class, serialized_message)
|
207
228
|
# Log the circuit breaker activation
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
229
|
+
logger.error { "[SmartMessage::Transport] Circuit breaker activated: #{self.class.name}" }
|
230
|
+
logger.error { "[SmartMessage::Transport] Error: #{fallback_result[:circuit_breaker][:error]}" }
|
231
|
+
logger.error { "[SmartMessage::Transport] Message: #{message_class}" }
|
232
|
+
logger.info { "[SmartMessage::Transport] Sent to DLQ: #{fallback_result[:circuit_breaker][:sent_to_dlq]}" }
|
233
|
+
|
234
|
+
# If message wasn't sent to DLQ by circuit breaker, send it now
|
235
|
+
unless fallback_result.dig(:circuit_breaker, :sent_to_dlq)
|
236
|
+
begin
|
237
|
+
SmartMessage::DeadLetterQueue.default.enqueue(
|
238
|
+
message_class,
|
239
|
+
serialized_message,
|
240
|
+
error: fallback_result.dig(:circuit_breaker, :error) || 'Circuit breaker activated',
|
241
|
+
transport: self.class.name
|
242
|
+
)
|
243
|
+
rescue => dlq_error
|
244
|
+
logger.warn { "[SmartMessage] Warning: Failed to store message in DLQ: #{dlq_error.message}" }
|
245
|
+
end
|
212
246
|
end
|
213
|
-
|
214
|
-
# TODO: Integrate with structured logging when implemented
|
215
|
-
# TODO: Queue for retry or send to dead letter queue
|
216
|
-
|
247
|
+
|
217
248
|
# Return the fallback result to indicate failure
|
218
249
|
fallback_result
|
219
250
|
end
|
220
251
|
end
|
221
252
|
end
|
222
|
-
end
|
253
|
+
end
|
@@ -22,20 +22,22 @@ module SmartMessage
|
|
22
22
|
end
|
23
23
|
|
24
24
|
# Publish message to memory queue
|
25
|
-
def do_publish(
|
25
|
+
def do_publish(message_class, serialized_message)
|
26
26
|
@message_mutex.synchronize do
|
27
27
|
# Prevent memory overflow
|
28
28
|
@messages.shift if @messages.size >= @options[:max_messages]
|
29
29
|
|
30
30
|
@messages << {
|
31
|
-
|
32
|
-
|
31
|
+
message_class: message_class,
|
32
|
+
serialized_message: serialized_message,
|
33
33
|
published_at: Time.now
|
34
34
|
}
|
35
35
|
end
|
36
36
|
|
37
37
|
# Auto-process if enabled
|
38
|
-
|
38
|
+
if @options[:auto_process]
|
39
|
+
receive(message_class, serialized_message)
|
40
|
+
end
|
39
41
|
end
|
40
42
|
|
41
43
|
# Get all stored messages
|
@@ -57,7 +59,7 @@ module SmartMessage
|
|
57
59
|
def process_all
|
58
60
|
messages_to_process = @message_mutex.synchronize { @messages.dup }
|
59
61
|
messages_to_process.each do |msg|
|
60
|
-
receive(msg[:
|
62
|
+
receive(msg[:message_class], msg[:serialized_message])
|
61
63
|
end
|
62
64
|
end
|
63
65
|
|
@@ -36,20 +36,13 @@ module SmartMessage
|
|
36
36
|
end
|
37
37
|
|
38
38
|
# Publish message to Redis channel using message class name
|
39
|
-
def do_publish(
|
40
|
-
channel =
|
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
|
39
|
+
def do_publish(message_class, serialized_message)
|
40
|
+
channel = message_class
|
48
41
|
|
49
42
|
begin
|
50
|
-
@redis_pub.publish(channel,
|
43
|
+
@redis_pub.publish(channel, serialized_message)
|
51
44
|
rescue Redis::ConnectionError
|
52
|
-
retry_with_reconnect('publish') { @redis_pub.publish(channel,
|
45
|
+
retry_with_reconnect('publish') { @redis_pub.publish(channel, serialized_message) }
|
53
46
|
end
|
54
47
|
end
|
55
48
|
|
@@ -117,7 +110,7 @@ module SmartMessage
|
|
117
110
|
subscribe_to_channels
|
118
111
|
rescue => e
|
119
112
|
# Log error but don't crash the thread
|
120
|
-
|
113
|
+
logger.error { "[SmartMessage] Error in redis subscriber: #{e.class.name} - #{e.message}" }
|
121
114
|
retry_subscriber
|
122
115
|
end
|
123
116
|
end
|
@@ -143,51 +136,28 @@ module SmartMessage
|
|
143
136
|
|
144
137
|
begin
|
145
138
|
@redis_sub.subscribe(*@subscribed_channels) do |on|
|
146
|
-
on.message do |channel,
|
139
|
+
on.message do |channel, serialized_message|
|
147
140
|
begin
|
148
|
-
#
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
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)
|
141
|
+
# Channel name is the message class name
|
142
|
+
# Serialized message contains the complete message
|
143
|
+
receive(channel, serialized_message)
|
144
|
+
rescue => e
|
145
|
+
logger.error { "[SmartMessage] Error in redis message processing: #{e.class.name} - #{e.message}" }
|
146
|
+
# Continue processing other messages
|
177
147
|
end
|
178
148
|
end
|
179
149
|
|
180
150
|
on.subscribe do |channel, subscriptions|
|
181
|
-
|
151
|
+
logger.debug { "[SmartMessage::RedisTransport] Subscribed to Redis channel: #{channel} (#{subscriptions} total)" }
|
182
152
|
end
|
183
153
|
|
184
154
|
on.unsubscribe do |channel, subscriptions|
|
185
|
-
|
155
|
+
logger.debug { "[SmartMessage::RedisTransport] Unsubscribed from Redis channel: #{channel} (#{subscriptions} total)" }
|
186
156
|
end
|
187
157
|
end
|
188
158
|
rescue => e
|
189
159
|
# Silently handle connection errors during subscription
|
190
|
-
|
160
|
+
logger.error { "[SmartMessage] Error in redis subscription: #{e.class.name} - #{e.message}" }
|
191
161
|
retry_subscriber if @running
|
192
162
|
end
|
193
163
|
end
|