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.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +3 -0
  3. data/.gitignore +8 -0
  4. data/.travis.yml +7 -0
  5. data/CHANGELOG.md +100 -0
  6. data/COMMITS.md +196 -0
  7. data/Gemfile +6 -0
  8. data/Gemfile.lock +71 -0
  9. data/README.md +303 -0
  10. data/Rakefile +10 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/docs/README.md +52 -0
  14. data/docs/architecture.md +370 -0
  15. data/docs/dispatcher.md +593 -0
  16. data/docs/examples.md +808 -0
  17. data/docs/getting-started.md +235 -0
  18. data/docs/ideas_to_think_about.md +329 -0
  19. data/docs/serializers.md +575 -0
  20. data/docs/transports.md +501 -0
  21. data/docs/troubleshooting.md +582 -0
  22. data/examples/01_point_to_point_orders.rb +200 -0
  23. data/examples/02_publish_subscribe_events.rb +364 -0
  24. data/examples/03_many_to_many_chat.rb +608 -0
  25. data/examples/README.md +335 -0
  26. data/examples/tmux_chat/README.md +283 -0
  27. data/examples/tmux_chat/bot_agent.rb +272 -0
  28. data/examples/tmux_chat/human_agent.rb +197 -0
  29. data/examples/tmux_chat/room_monitor.rb +158 -0
  30. data/examples/tmux_chat/shared_chat_system.rb +295 -0
  31. data/examples/tmux_chat/start_chat_demo.sh +190 -0
  32. data/examples/tmux_chat/stop_chat_demo.sh +22 -0
  33. data/lib/simple_stats.rb +57 -0
  34. data/lib/smart_message/base.rb +284 -0
  35. data/lib/smart_message/dispatcher/.keep +0 -0
  36. data/lib/smart_message/dispatcher.rb +146 -0
  37. data/lib/smart_message/errors.rb +29 -0
  38. data/lib/smart_message/header.rb +20 -0
  39. data/lib/smart_message/logger/base.rb +8 -0
  40. data/lib/smart_message/logger.rb +7 -0
  41. data/lib/smart_message/serializer/base.rb +23 -0
  42. data/lib/smart_message/serializer/json.rb +22 -0
  43. data/lib/smart_message/serializer.rb +10 -0
  44. data/lib/smart_message/transport/base.rb +85 -0
  45. data/lib/smart_message/transport/memory_transport.rb +69 -0
  46. data/lib/smart_message/transport/registry.rb +59 -0
  47. data/lib/smart_message/transport/stdout_transport.rb +62 -0
  48. data/lib/smart_message/transport.rb +41 -0
  49. data/lib/smart_message/version.rb +7 -0
  50. data/lib/smart_message/wrapper.rb +43 -0
  51. data/lib/smart_message.rb +54 -0
  52. data/smart_message.gemspec +53 -0
  53. metadata +252 -0
@@ -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