smart_message 0.0.1
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 +7 -0
- data/.envrc +3 -0
- data/.gitignore +8 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +100 -0
- data/COMMITS.md +196 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +71 -0
- data/README.md +303 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/docs/README.md +52 -0
- data/docs/architecture.md +370 -0
- data/docs/dispatcher.md +593 -0
- data/docs/examples.md +808 -0
- data/docs/getting-started.md +235 -0
- data/docs/ideas_to_think_about.md +329 -0
- data/docs/serializers.md +575 -0
- data/docs/transports.md +501 -0
- data/docs/troubleshooting.md +582 -0
- data/examples/01_point_to_point_orders.rb +200 -0
- data/examples/02_publish_subscribe_events.rb +364 -0
- data/examples/03_many_to_many_chat.rb +608 -0
- data/examples/README.md +335 -0
- data/examples/tmux_chat/README.md +283 -0
- data/examples/tmux_chat/bot_agent.rb +272 -0
- data/examples/tmux_chat/human_agent.rb +197 -0
- data/examples/tmux_chat/room_monitor.rb +158 -0
- data/examples/tmux_chat/shared_chat_system.rb +295 -0
- data/examples/tmux_chat/start_chat_demo.sh +190 -0
- data/examples/tmux_chat/stop_chat_demo.sh +22 -0
- data/lib/simple_stats.rb +57 -0
- data/lib/smart_message/base.rb +284 -0
- data/lib/smart_message/dispatcher/.keep +0 -0
- data/lib/smart_message/dispatcher.rb +146 -0
- data/lib/smart_message/errors.rb +29 -0
- data/lib/smart_message/header.rb +20 -0
- data/lib/smart_message/logger/base.rb +8 -0
- data/lib/smart_message/logger.rb +7 -0
- data/lib/smart_message/serializer/base.rb +23 -0
- data/lib/smart_message/serializer/json.rb +22 -0
- data/lib/smart_message/serializer.rb +10 -0
- data/lib/smart_message/transport/base.rb +85 -0
- data/lib/smart_message/transport/memory_transport.rb +69 -0
- data/lib/smart_message/transport/registry.rb +59 -0
- data/lib/smart_message/transport/stdout_transport.rb +62 -0
- data/lib/smart_message/transport.rb +41 -0
- data/lib/smart_message/version.rb +7 -0
- data/lib/smart_message/wrapper.rb +43 -0
- data/lib/smart_message.rb +54 -0
- data/smart_message.gemspec +53 -0
- metadata +252 -0
data/docs/dispatcher.md
ADDED
@@ -0,0 +1,593 @@
|
|
1
|
+
# Dispatcher & Message Routing
|
2
|
+
|
3
|
+
The dispatcher is the heart of SmartMessage's message routing system. It manages subscriptions, routes incoming messages to appropriate handlers, and coordinates concurrent processing using thread pools.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
The dispatcher handles:
|
8
|
+
- **Subscription Management**: Tracking which classes want to receive which messages
|
9
|
+
- **Message Routing**: Directing incoming messages to registered handlers
|
10
|
+
- **Concurrent Processing**: Using thread pools for parallel message processing
|
11
|
+
- **Statistics Collection**: Tracking message processing metrics
|
12
|
+
- **Error Isolation**: Preventing individual message failures from affecting the system
|
13
|
+
|
14
|
+
## Core Components
|
15
|
+
|
16
|
+
### SmartMessage::Dispatcher
|
17
|
+
|
18
|
+
Located at `lib/smart_message/dispatcher.rb:11-147`, the dispatcher is the central routing engine.
|
19
|
+
|
20
|
+
**Key Features:**
|
21
|
+
- Thread-safe subscription management
|
22
|
+
- Concurrent message processing via `Concurrent::CachedThreadPool`
|
23
|
+
- Automatic thread pool lifecycle management
|
24
|
+
- Built-in statistics collection
|
25
|
+
- Graceful shutdown handling
|
26
|
+
|
27
|
+
## Subscription Management
|
28
|
+
|
29
|
+
### Adding Subscriptions
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
# Basic subscription - uses default process method
|
33
|
+
MyMessage.subscribe
|
34
|
+
# Registers "MyMessage.process" as the handler
|
35
|
+
|
36
|
+
# Custom process method
|
37
|
+
MyMessage.subscribe("MyMessage.custom_handler")
|
38
|
+
# Registers "MyMessage.custom_handler" as the handler
|
39
|
+
|
40
|
+
# Multiple handlers for the same message
|
41
|
+
MyMessage.subscribe("MyMessage.handler_one")
|
42
|
+
MyMessage.subscribe("MyMessage.handler_two")
|
43
|
+
# Both handlers will receive the message
|
44
|
+
```
|
45
|
+
|
46
|
+
### Removing Subscriptions
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
# Remove specific handler
|
50
|
+
MyMessage.unsubscribe("MyMessage.custom_handler")
|
51
|
+
|
52
|
+
# Remove ALL handlers for a message class
|
53
|
+
MyMessage.unsubscribe!
|
54
|
+
|
55
|
+
# Remove all subscriptions (useful for testing)
|
56
|
+
dispatcher = SmartMessage::Dispatcher.new
|
57
|
+
dispatcher.drop_all!
|
58
|
+
```
|
59
|
+
|
60
|
+
### Viewing Subscriptions
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
dispatcher = SmartMessage::Dispatcher.new
|
64
|
+
|
65
|
+
# View all subscriptions
|
66
|
+
puts dispatcher.subscribers
|
67
|
+
# => {"MyMessage" => ["MyMessage.process", "MyMessage.audit"]}
|
68
|
+
|
69
|
+
# Check specific message subscriptions
|
70
|
+
puts dispatcher.subscribers["MyMessage"]
|
71
|
+
# => ["MyMessage.process", "MyMessage.audit"]
|
72
|
+
```
|
73
|
+
|
74
|
+
## Message Routing Process
|
75
|
+
|
76
|
+
### 1. Message Reception
|
77
|
+
|
78
|
+
When a transport receives a message, it calls the dispatcher:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
# Transport receives message and routes it
|
82
|
+
transport.receive(message_header, message_payload)
|
83
|
+
# This internally calls:
|
84
|
+
dispatcher.route(message_header, message_payload)
|
85
|
+
```
|
86
|
+
|
87
|
+
### 2. Subscription Lookup
|
88
|
+
|
89
|
+
The dispatcher finds all registered handlers:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
def route(message_header, message_payload)
|
93
|
+
message_klass = message_header.message_class
|
94
|
+
return nil if @subscribers[message_klass].empty?
|
95
|
+
|
96
|
+
@subscribers[message_klass].each do |message_processor|
|
97
|
+
# Process each handler
|
98
|
+
end
|
99
|
+
end
|
100
|
+
```
|
101
|
+
|
102
|
+
### 3. Concurrent Processing
|
103
|
+
|
104
|
+
Each handler is processed in its own thread:
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
@subscribers[message_klass].each do |message_processor|
|
108
|
+
SS.add(message_klass, message_processor, 'routed')
|
109
|
+
|
110
|
+
@router_pool.post do
|
111
|
+
# This runs in a separate thread
|
112
|
+
parts = message_processor.split('.')
|
113
|
+
target_klass = parts[0] # "MyMessage"
|
114
|
+
class_method = parts[1] # "process"
|
115
|
+
|
116
|
+
begin
|
117
|
+
result = target_klass.constantize
|
118
|
+
.method(class_method)
|
119
|
+
.call(message_header, message_payload)
|
120
|
+
rescue Exception => e
|
121
|
+
# Error handling - doesn't crash the dispatcher
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
## Thread Pool Management
|
128
|
+
|
129
|
+
### Thread Pool Configuration
|
130
|
+
|
131
|
+
The dispatcher uses `Concurrent::CachedThreadPool` which automatically manages thread creation and destruction:
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
def initialize
|
135
|
+
@router_pool = Concurrent::CachedThreadPool.new
|
136
|
+
|
137
|
+
# Automatic cleanup on exit
|
138
|
+
at_exit do
|
139
|
+
shutdown_thread_pool
|
140
|
+
end
|
141
|
+
end
|
142
|
+
```
|
143
|
+
|
144
|
+
### Monitoring Thread Pool Status
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
dispatcher = SmartMessage::Dispatcher.new
|
148
|
+
|
149
|
+
# Get comprehensive status
|
150
|
+
status = dispatcher.status
|
151
|
+
puts "Running: #{status[:running]}"
|
152
|
+
puts "Queue length: #{status[:queue_length]}"
|
153
|
+
puts "Scheduled tasks: #{status[:scheduled_task_count]}"
|
154
|
+
puts "Completed tasks: #{status[:completed_task_count]}"
|
155
|
+
puts "Current threads: #{status[:length]}"
|
156
|
+
|
157
|
+
# Individual status methods
|
158
|
+
puts dispatcher.running? # Is the pool active?
|
159
|
+
puts dispatcher.queue_length # How many tasks are waiting?
|
160
|
+
puts dispatcher.scheduled_task_count # Total tasks scheduled
|
161
|
+
puts dispatcher.completed_task_count # Total tasks completed
|
162
|
+
puts dispatcher.current_length # Current number of threads
|
163
|
+
```
|
164
|
+
|
165
|
+
### Thread Pool Lifecycle
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
# Automatic shutdown handling
|
169
|
+
at_exit do
|
170
|
+
print "Shutting down the dispatcher's thread pool..."
|
171
|
+
@router_pool.shutdown
|
172
|
+
|
173
|
+
while @router_pool.shuttingdown?
|
174
|
+
print '.'
|
175
|
+
sleep 1
|
176
|
+
end
|
177
|
+
|
178
|
+
puts " done."
|
179
|
+
end
|
180
|
+
```
|
181
|
+
|
182
|
+
## Message Processing Patterns
|
183
|
+
|
184
|
+
### Standard Processing
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
class OrderMessage < SmartMessage::Base
|
188
|
+
property :order_id
|
189
|
+
property :customer_id
|
190
|
+
property :items
|
191
|
+
|
192
|
+
# Standard process method
|
193
|
+
def self.process(message_header, message_payload)
|
194
|
+
# 1. Decode the message
|
195
|
+
data = JSON.parse(message_payload)
|
196
|
+
order = new(data)
|
197
|
+
|
198
|
+
# 2. Execute business logic
|
199
|
+
fulfill_order(order)
|
200
|
+
|
201
|
+
# 3. Optional: publish follow-up messages
|
202
|
+
ShippingMessage.new(
|
203
|
+
order_id: order.order_id,
|
204
|
+
address: get_shipping_address(order.customer_id)
|
205
|
+
).publish
|
206
|
+
end
|
207
|
+
|
208
|
+
private
|
209
|
+
|
210
|
+
def self.fulfill_order(order)
|
211
|
+
# Business logic here
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# Subscribe to receive messages
|
216
|
+
OrderMessage.subscribe
|
217
|
+
```
|
218
|
+
|
219
|
+
### Multiple Handlers
|
220
|
+
|
221
|
+
```ruby
|
222
|
+
class PaymentMessage < SmartMessage::Base
|
223
|
+
property :payment_id
|
224
|
+
property :amount
|
225
|
+
property :customer_id
|
226
|
+
|
227
|
+
# Primary payment processing
|
228
|
+
def self.process(message_header, message_payload)
|
229
|
+
data = JSON.parse(message_payload)
|
230
|
+
payment = new(data)
|
231
|
+
|
232
|
+
process_payment(payment)
|
233
|
+
end
|
234
|
+
|
235
|
+
# Audit logging handler
|
236
|
+
def self.audit(message_header, message_payload)
|
237
|
+
data = JSON.parse(message_payload)
|
238
|
+
payment = new(data)
|
239
|
+
|
240
|
+
log_payment_attempt(payment)
|
241
|
+
end
|
242
|
+
|
243
|
+
# Fraud detection handler
|
244
|
+
def self.fraud_check(message_header, message_payload)
|
245
|
+
data = JSON.parse(message_payload)
|
246
|
+
payment = new(data)
|
247
|
+
|
248
|
+
if suspicious_payment?(payment)
|
249
|
+
flag_for_review(payment)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
# Register all handlers
|
255
|
+
PaymentMessage.subscribe("PaymentMessage.process")
|
256
|
+
PaymentMessage.subscribe("PaymentMessage.audit")
|
257
|
+
PaymentMessage.subscribe("PaymentMessage.fraud_check")
|
258
|
+
```
|
259
|
+
|
260
|
+
### Error Handling in Processors
|
261
|
+
|
262
|
+
```ruby
|
263
|
+
class RobustMessage < SmartMessage::Base
|
264
|
+
property :data
|
265
|
+
|
266
|
+
def self.process(message_header, message_payload)
|
267
|
+
begin
|
268
|
+
data = JSON.parse(message_payload)
|
269
|
+
message = new(data)
|
270
|
+
|
271
|
+
# Main processing logic
|
272
|
+
process_business_logic(message)
|
273
|
+
|
274
|
+
rescue JSON::ParserError => e
|
275
|
+
# Handle malformed messages
|
276
|
+
log_error("Invalid message format", message_header, e)
|
277
|
+
|
278
|
+
rescue BusinessLogicError => e
|
279
|
+
# Handle business logic failures
|
280
|
+
log_error("Business logic failed", message_header, e)
|
281
|
+
|
282
|
+
# Optionally republish to error queue
|
283
|
+
ErrorMessage.new(
|
284
|
+
original_message: message_payload,
|
285
|
+
error: e.message,
|
286
|
+
retry_count: get_retry_count(message_header)
|
287
|
+
).publish
|
288
|
+
|
289
|
+
rescue => e
|
290
|
+
# Handle unexpected errors
|
291
|
+
log_error("Unexpected error", message_header, e)
|
292
|
+
raise # Re-raise to trigger dispatcher error handling
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
private
|
297
|
+
|
298
|
+
def self.log_error(type, header, error)
|
299
|
+
puts "#{type}: #{error.message}"
|
300
|
+
puts "Message class: #{header.message_class}"
|
301
|
+
puts "Message UUID: #{header.uuid}"
|
302
|
+
end
|
303
|
+
end
|
304
|
+
```
|
305
|
+
|
306
|
+
## Advanced Routing Patterns
|
307
|
+
|
308
|
+
### Conditional Processing
|
309
|
+
|
310
|
+
```ruby
|
311
|
+
class ConditionalMessage < SmartMessage::Base
|
312
|
+
property :environment
|
313
|
+
property :data
|
314
|
+
|
315
|
+
def self.process(message_header, message_payload)
|
316
|
+
data = JSON.parse(message_payload)
|
317
|
+
message = new(data)
|
318
|
+
|
319
|
+
# Route based on message content
|
320
|
+
case message.environment
|
321
|
+
when 'production'
|
322
|
+
production_handler(message)
|
323
|
+
when 'staging'
|
324
|
+
staging_handler(message)
|
325
|
+
when 'development'
|
326
|
+
development_handler(message)
|
327
|
+
else
|
328
|
+
default_handler(message)
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
332
|
+
```
|
333
|
+
|
334
|
+
### Message Transformation and Republishing
|
335
|
+
|
336
|
+
```ruby
|
337
|
+
class TransformMessage < SmartMessage::Base
|
338
|
+
property :raw_data
|
339
|
+
property :format
|
340
|
+
|
341
|
+
def self.process(message_header, message_payload)
|
342
|
+
data = JSON.parse(message_payload)
|
343
|
+
message = new(data)
|
344
|
+
|
345
|
+
# Transform the message
|
346
|
+
case message.format
|
347
|
+
when 'csv'
|
348
|
+
transformed = transform_csv(message.raw_data)
|
349
|
+
when 'xml'
|
350
|
+
transformed = transform_xml(message.raw_data)
|
351
|
+
else
|
352
|
+
transformed = message.raw_data
|
353
|
+
end
|
354
|
+
|
355
|
+
# Republish as a different message type
|
356
|
+
ProcessedMessage.new(
|
357
|
+
original_id: message_header.uuid,
|
358
|
+
processed_data: transformed,
|
359
|
+
processed_at: Time.now
|
360
|
+
).publish
|
361
|
+
end
|
362
|
+
end
|
363
|
+
```
|
364
|
+
|
365
|
+
### Fan-out Processing
|
366
|
+
|
367
|
+
```ruby
|
368
|
+
class EventMessage < SmartMessage::Base
|
369
|
+
property :event_type
|
370
|
+
property :user_id
|
371
|
+
property :data
|
372
|
+
|
373
|
+
def self.process(message_header, message_payload)
|
374
|
+
data = JSON.parse(message_payload)
|
375
|
+
event = new(data)
|
376
|
+
|
377
|
+
# Fan out to multiple specialized handlers
|
378
|
+
case event.event_type
|
379
|
+
when 'user_signup'
|
380
|
+
WelcomeEmailMessage.new(user_id: event.user_id).publish
|
381
|
+
AnalyticsMessage.new(event: 'signup', user_id: event.user_id).publish
|
382
|
+
AuditMessage.new(action: 'user_created', user_id: event.user_id).publish
|
383
|
+
|
384
|
+
when 'purchase'
|
385
|
+
InventoryMessage.new(items: event.data['items']).publish
|
386
|
+
ReceiptMessage.new(user_id: event.user_id, total: event.data['total']).publish
|
387
|
+
LoyaltyMessage.new(user_id: event.user_id, points: calculate_points(event.data)).publish
|
388
|
+
end
|
389
|
+
end
|
390
|
+
end
|
391
|
+
```
|
392
|
+
|
393
|
+
## Statistics and Monitoring
|
394
|
+
|
395
|
+
### Built-in Statistics
|
396
|
+
|
397
|
+
The dispatcher automatically collects statistics via the `SimpleStats` (`SS`) system:
|
398
|
+
|
399
|
+
```ruby
|
400
|
+
# Statistics are automatically collected for:
|
401
|
+
# - Message publishing: SS.add(message_class, 'publish')
|
402
|
+
# - Message routing: SS.add(message_class, process_method, 'routed')
|
403
|
+
|
404
|
+
# View all statistics
|
405
|
+
puts SS.stat
|
406
|
+
|
407
|
+
# Get specific statistics
|
408
|
+
publish_count = SS.get("MyMessage", "publish")
|
409
|
+
process_count = SS.get("MyMessage", "MyMessage.process", "routed")
|
410
|
+
|
411
|
+
# Reset statistics
|
412
|
+
SS.reset # Clear all
|
413
|
+
SS.reset("MyMessage", "publish") # Clear specific stat
|
414
|
+
```
|
415
|
+
|
416
|
+
### Custom Monitoring
|
417
|
+
|
418
|
+
```ruby
|
419
|
+
class MonitoredMessage < SmartMessage::Base
|
420
|
+
property :data
|
421
|
+
|
422
|
+
def self.process(message_header, message_payload)
|
423
|
+
start_time = Time.now
|
424
|
+
|
425
|
+
begin
|
426
|
+
# Process the message
|
427
|
+
data = JSON.parse(message_payload)
|
428
|
+
message = new(data)
|
429
|
+
|
430
|
+
process_business_logic(message)
|
431
|
+
|
432
|
+
# Record success metrics
|
433
|
+
record_processing_time(Time.now - start_time)
|
434
|
+
increment_success_counter
|
435
|
+
|
436
|
+
rescue => e
|
437
|
+
# Record failure metrics
|
438
|
+
record_error(e)
|
439
|
+
increment_failure_counter
|
440
|
+
raise
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
private
|
445
|
+
|
446
|
+
def self.record_processing_time(duration)
|
447
|
+
SS.add("MonitoredMessage", "processing_time", how_many: duration)
|
448
|
+
end
|
449
|
+
|
450
|
+
def self.increment_success_counter
|
451
|
+
SS.add("MonitoredMessage", "success")
|
452
|
+
end
|
453
|
+
|
454
|
+
def self.increment_failure_counter
|
455
|
+
SS.add("MonitoredMessage", "failure")
|
456
|
+
end
|
457
|
+
end
|
458
|
+
```
|
459
|
+
|
460
|
+
## Performance Considerations
|
461
|
+
|
462
|
+
### Thread Pool Sizing
|
463
|
+
|
464
|
+
The `CachedThreadPool` automatically manages thread creation, but you can influence behavior:
|
465
|
+
|
466
|
+
```ruby
|
467
|
+
# For high-throughput scenarios, consider a custom thread pool
|
468
|
+
class CustomDispatcher < SmartMessage::Dispatcher
|
469
|
+
def initialize(min_threads: 5, max_threads: 50)
|
470
|
+
@router_pool = Concurrent::ThreadPoolExecutor.new(
|
471
|
+
min_threads: min_threads,
|
472
|
+
max_threads: max_threads,
|
473
|
+
max_queue: 1000,
|
474
|
+
fallback_policy: :caller_runs
|
475
|
+
)
|
476
|
+
|
477
|
+
# Rest of initialization
|
478
|
+
end
|
479
|
+
end
|
480
|
+
```
|
481
|
+
|
482
|
+
### Processing Optimization
|
483
|
+
|
484
|
+
```ruby
|
485
|
+
class OptimizedMessage < SmartMessage::Base
|
486
|
+
property :data
|
487
|
+
|
488
|
+
def self.process(message_header, message_payload)
|
489
|
+
# Parse once, use multiple times
|
490
|
+
data = JSON.parse(message_payload)
|
491
|
+
message = new(data)
|
492
|
+
|
493
|
+
# Batch operations when possible
|
494
|
+
batch_operations(message)
|
495
|
+
|
496
|
+
# Use connection pooling for database operations
|
497
|
+
connection_pool.with do |conn|
|
498
|
+
save_to_database(message, conn)
|
499
|
+
end
|
500
|
+
end
|
501
|
+
end
|
502
|
+
```
|
503
|
+
|
504
|
+
## Testing Dispatcher Behavior
|
505
|
+
|
506
|
+
### Dispatcher Testing
|
507
|
+
|
508
|
+
```ruby
|
509
|
+
RSpec.describe SmartMessage::Dispatcher do
|
510
|
+
let(:dispatcher) { SmartMessage::Dispatcher.new }
|
511
|
+
|
512
|
+
before do
|
513
|
+
dispatcher.drop_all! # Clear subscriptions
|
514
|
+
end
|
515
|
+
|
516
|
+
describe "subscription management" do
|
517
|
+
it "adds subscriptions" do
|
518
|
+
dispatcher.add("TestMessage", "TestMessage.process")
|
519
|
+
|
520
|
+
expect(dispatcher.subscribers["TestMessage"]).to include("TestMessage.process")
|
521
|
+
end
|
522
|
+
|
523
|
+
it "removes subscriptions" do
|
524
|
+
dispatcher.add("TestMessage", "TestMessage.process")
|
525
|
+
dispatcher.drop("TestMessage", "TestMessage.process")
|
526
|
+
|
527
|
+
expect(dispatcher.subscribers["TestMessage"]).not_to include("TestMessage.process")
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
describe "message routing" do
|
532
|
+
let(:header) { double("header", message_class: "TestMessage") }
|
533
|
+
let(:payload) { '{"data": "test"}' }
|
534
|
+
|
535
|
+
before do
|
536
|
+
# Mock the message class
|
537
|
+
stub_const("TestMessage", Class.new do
|
538
|
+
def self.process(header, payload)
|
539
|
+
@processed_messages ||= []
|
540
|
+
@processed_messages << [header, payload]
|
541
|
+
end
|
542
|
+
|
543
|
+
def self.processed_messages
|
544
|
+
@processed_messages || []
|
545
|
+
end
|
546
|
+
end)
|
547
|
+
end
|
548
|
+
|
549
|
+
it "routes messages to subscribers" do
|
550
|
+
dispatcher.add("TestMessage", "TestMessage.process")
|
551
|
+
dispatcher.route(header, payload)
|
552
|
+
|
553
|
+
# Wait for async processing
|
554
|
+
sleep 0.1
|
555
|
+
|
556
|
+
expect(TestMessage.processed_messages).to have(1).message
|
557
|
+
end
|
558
|
+
end
|
559
|
+
end
|
560
|
+
```
|
561
|
+
|
562
|
+
### Message Processing Testing
|
563
|
+
|
564
|
+
```ruby
|
565
|
+
RSpec.describe "Message Processing" do
|
566
|
+
let(:transport) { SmartMessage::Transport.create(:memory, auto_process: true) }
|
567
|
+
|
568
|
+
before do
|
569
|
+
TestMessage.config do
|
570
|
+
transport transport
|
571
|
+
serializer SmartMessage::Serializer::JSON.new
|
572
|
+
end
|
573
|
+
|
574
|
+
TestMessage.subscribe
|
575
|
+
end
|
576
|
+
|
577
|
+
it "processes published messages" do
|
578
|
+
expect(TestMessage).to receive(:process).once
|
579
|
+
|
580
|
+
TestMessage.new(data: "test").publish
|
581
|
+
|
582
|
+
# Wait for async processing
|
583
|
+
sleep 0.1
|
584
|
+
end
|
585
|
+
end
|
586
|
+
```
|
587
|
+
|
588
|
+
## Next Steps
|
589
|
+
|
590
|
+
- [Thread Safety](thread-safety.md) - Understanding concurrent processing
|
591
|
+
- [Statistics & Monitoring](monitoring.md) - Detailed monitoring guide
|
592
|
+
- [Custom Transports](custom-transports.md) - How transports interact with the dispatcher
|
593
|
+
- [Troubleshooting](troubleshooting.md) - Common dispatcher issues
|