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.
@@ -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
@@ -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 = nil
78
- @to = nil
79
- @reply_to = nil
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,243 @@
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 - stores failed messages to file-based DLQ
163
+ def self.dead_letter_queue(dlq_instance = 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
+ # Use provided DLQ instance or default
170
+ dlq = dlq_instance || SmartMessage::DeadLetterQueue.default
171
+
172
+ # Store failed message in dead letter queue
173
+ sent_to_dlq = false
174
+ if message_header && message_payload
175
+ begin
176
+ dlq.enqueue(message_header, message_payload,
177
+ error: exception.message,
178
+ retry_count: 0,
179
+ serializer: 'json', # Default to JSON, could be enhanced to detect actual serializer
180
+ stack_trace: exception.backtrace&.join("\n")
181
+ )
182
+ sent_to_dlq = true
183
+ rescue => dlq_error
184
+ # DLQ storage failed - log but don't raise
185
+ puts "Warning: Failed to store message in DLQ: #{dlq_error.message}" if $DEBUG
186
+ end
187
+ end
188
+
189
+ {
190
+ circuit_breaker: {
191
+ circuit: :transport_publish, # Default circuit name, overridden by specific configurations
192
+ state: 'open',
193
+ error: exception.message,
194
+ sent_to_dlq: sent_to_dlq,
195
+ timestamp: Time.now.iso8601
196
+ }
197
+ }
198
+ end
199
+ end
200
+
201
+ # Retry with exponential backoff fallback
202
+ def self.retry_with_backoff(max_retries: 3, base_delay: 1)
203
+ proc do |exception, *args|
204
+ retry_count = Thread.current[:circuit_retry_count] ||= 0
205
+
206
+ if retry_count < max_retries
207
+ Thread.current[:circuit_retry_count] += 1
208
+ delay = base_delay * (2 ** retry_count)
209
+ sleep(delay)
210
+
211
+ # Re-raise to trigger retry
212
+ raise exception
213
+ else
214
+ Thread.current[:circuit_retry_count] = nil
215
+
216
+ {
217
+ circuit_breaker: {
218
+ state: 'open',
219
+ error: exception.message,
220
+ max_retries_exceeded: true,
221
+ timestamp: Time.now.iso8601
222
+ }
223
+ }
224
+ end
225
+ end
226
+ end
227
+
228
+ # Graceful degradation fallback
229
+ def self.graceful_degradation(degraded_response)
230
+ proc do |exception|
231
+ {
232
+ circuit_breaker: {
233
+ state: 'open',
234
+ error: exception.message,
235
+ degraded_response: degraded_response,
236
+ timestamp: Time.now.iso8601
237
+ }
238
+ }
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end