smart_message 0.0.8 → 0.0.10

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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.irbrc +24 -0
  4. data/CHANGELOG.md +119 -0
  5. data/Gemfile.lock +6 -1
  6. data/README.md +389 -17
  7. data/docs/README.md +3 -1
  8. data/docs/addressing.md +119 -13
  9. data/docs/architecture.md +184 -46
  10. data/docs/dead_letter_queue.md +673 -0
  11. data/docs/dispatcher.md +87 -0
  12. data/docs/examples.md +59 -1
  13. data/docs/getting-started.md +8 -1
  14. data/docs/logging.md +382 -326
  15. data/docs/message_deduplication.md +488 -0
  16. data/docs/message_filtering.md +451 -0
  17. data/examples/01_point_to_point_orders.rb +54 -53
  18. data/examples/02_publish_subscribe_events.rb +14 -10
  19. data/examples/03_many_to_many_chat.rb +16 -8
  20. data/examples/04_redis_smart_home_iot.rb +20 -10
  21. data/examples/05_proc_handlers.rb +12 -11
  22. data/examples/06_custom_logger_example.rb +95 -100
  23. data/examples/07_error_handling_scenarios.rb +4 -2
  24. data/examples/08_entity_addressing_basic.rb +18 -6
  25. data/examples/08_entity_addressing_with_filtering.rb +27 -9
  26. data/examples/09_dead_letter_queue_demo.rb +559 -0
  27. data/examples/09_regex_filtering_microservices.rb +407 -0
  28. data/examples/10_header_block_configuration.rb +263 -0
  29. data/examples/10_message_deduplication.rb +209 -0
  30. data/examples/11_global_configuration_example.rb +219 -0
  31. data/examples/README.md +102 -0
  32. data/examples/dead_letters.jsonl +12 -0
  33. data/examples/performance_metrics/benchmark_results_ractor_20250818_205603.json +135 -0
  34. data/examples/performance_metrics/benchmark_results_ractor_20250818_205831.json +135 -0
  35. data/examples/performance_metrics/benchmark_results_test_20250818_204942.json +130 -0
  36. data/examples/performance_metrics/benchmark_results_threadpool_20250818_204942.json +130 -0
  37. data/examples/performance_metrics/benchmark_results_threadpool_20250818_204959.json +130 -0
  38. data/examples/performance_metrics/benchmark_results_threadpool_20250818_205044.json +130 -0
  39. data/examples/performance_metrics/benchmark_results_threadpool_20250818_205109.json +130 -0
  40. data/examples/performance_metrics/benchmark_results_threadpool_20250818_205252.json +130 -0
  41. data/examples/performance_metrics/benchmark_results_unknown_20250819_172852.json +130 -0
  42. data/examples/performance_metrics/compare_benchmarks.rb +519 -0
  43. data/examples/performance_metrics/dead_letters.jsonl +3100 -0
  44. data/examples/performance_metrics/performance_benchmark.rb +344 -0
  45. data/examples/show_logger.rb +367 -0
  46. data/examples/show_me.rb +145 -0
  47. data/examples/temp.txt +94 -0
  48. data/examples/tmux_chat/bot_agent.rb +4 -2
  49. data/examples/tmux_chat/human_agent.rb +4 -2
  50. data/examples/tmux_chat/room_monitor.rb +4 -2
  51. data/examples/tmux_chat/shared_chat_system.rb +6 -3
  52. data/lib/smart_message/addressing.rb +259 -0
  53. data/lib/smart_message/base.rb +123 -599
  54. data/lib/smart_message/circuit_breaker.rb +2 -1
  55. data/lib/smart_message/configuration.rb +199 -0
  56. data/lib/smart_message/ddq/base.rb +71 -0
  57. data/lib/smart_message/ddq/memory.rb +109 -0
  58. data/lib/smart_message/ddq/redis.rb +168 -0
  59. data/lib/smart_message/ddq.rb +31 -0
  60. data/lib/smart_message/dead_letter_queue.rb +27 -10
  61. data/lib/smart_message/deduplication.rb +174 -0
  62. data/lib/smart_message/dispatcher.rb +259 -61
  63. data/lib/smart_message/header.rb +5 -0
  64. data/lib/smart_message/logger/base.rb +21 -1
  65. data/lib/smart_message/logger/default.rb +88 -138
  66. data/lib/smart_message/logger/lumberjack.rb +324 -0
  67. data/lib/smart_message/logger/null.rb +81 -0
  68. data/lib/smart_message/logger.rb +17 -9
  69. data/lib/smart_message/messaging.rb +100 -0
  70. data/lib/smart_message/plugins.rb +132 -0
  71. data/lib/smart_message/serializer/base.rb +25 -8
  72. data/lib/smart_message/serializer/json.rb +5 -4
  73. data/lib/smart_message/subscription.rb +196 -0
  74. data/lib/smart_message/transport/base.rb +72 -41
  75. data/lib/smart_message/transport/memory_transport.rb +7 -5
  76. data/lib/smart_message/transport/redis_transport.rb +15 -45
  77. data/lib/smart_message/transport/stdout_transport.rb +18 -8
  78. data/lib/smart_message/transport.rb +1 -34
  79. data/lib/smart_message/utilities.rb +142 -0
  80. data/lib/smart_message/version.rb +1 -1
  81. data/lib/smart_message/versioning.rb +85 -0
  82. data/lib/smart_message/wrapper.rb.bak +132 -0
  83. data/lib/smart_message.rb +74 -28
  84. data/smart_message.gemspec +3 -0
  85. metadata +83 -3
  86. data/lib/smart_message/serializer.rb +0 -10
  87. data/lib/smart_message/wrapper.rb +0 -43
@@ -0,0 +1,488 @@
1
+ # Message Deduplication
2
+
3
+ SmartMessage provides a comprehensive message deduplication system using Deduplication Queues (DDQ) to prevent duplicate processing of messages with the same UUID. The system is designed with handler-scoped isolation, ensuring that different message handlers maintain independent deduplication state.
4
+
5
+ ## Overview
6
+
7
+ Message deduplication in SmartMessage works by:
8
+
9
+ 1. **Handler-Scoped Tracking**: Each message handler (subscription) gets its own DDQ instance
10
+ 2. **UUID-Based Detection**: Message UUIDs are tracked in circular buffers for O(1) lookup performance
11
+ 3. **Configurable Storage**: Support for both memory-based and Redis-based storage backends
12
+ 4. **Automatic Integration**: Seamlessly integrates with the existing dispatcher and subscription system
13
+
14
+ ## Architecture
15
+
16
+ ### Handler-Only Scoping
17
+
18
+ The key innovation in SmartMessage's deduplication system is **handler-only scoping**. DDQ keys are automatically derived from the combination of message class and handler method:
19
+
20
+ ```
21
+ DDQ Key Format: "MessageClass:HandlerMethod"
22
+ ```
23
+
24
+ Examples:
25
+ - `"OrderMessage:PaymentService.process"`
26
+ - `"OrderMessage:FulfillmentService.handle"`
27
+ - `"InvoiceMessage:PaymentService.process"`
28
+
29
+ This design provides:
30
+ - **Natural Isolation**: Each handler has its own deduplication context
31
+ - **Cross-Process Support**: Same handler across different processes gets isolated DDQs
32
+ - **No Parameter Pollution**: No need for explicit subscriber identification in the API
33
+
34
+ ### DDQ Data Structure
35
+
36
+ Each DDQ uses a hybrid data structure for optimal performance:
37
+
38
+ ```ruby
39
+ # Hybrid Array + Set Design
40
+ @circular_array = Array.new(size) # Maintains insertion order for eviction
41
+ @lookup_set = Set.new # Provides O(1) UUID lookup
42
+ @index = 0 # Current insertion position
43
+ ```
44
+
45
+ Benefits:
46
+ - **O(1) Lookup**: Set provides constant-time duplicate detection
47
+ - **O(1) Insertion**: Array provides constant-time insertion and eviction
48
+ - **Memory Bounded**: Circular buffer automatically evicts oldest entries
49
+ - **Thread Safe**: Mutex protection for concurrent access
50
+
51
+ ## Configuration
52
+
53
+ ### Basic Setup
54
+
55
+ Enable deduplication for a message class:
56
+
57
+ ```ruby
58
+ class OrderMessage < SmartMessage::Base
59
+ version 1
60
+ property :order_id, required: true
61
+ property :amount, required: true
62
+
63
+ # Configure deduplication
64
+ ddq_size 100 # Track last 100 UUIDs (default: 100)
65
+ ddq_storage :memory # Storage backend (default: :memory)
66
+ enable_deduplication! # Enable DDQ for this message class
67
+
68
+ def self.process(message)
69
+ puts "Processing order: #{message.order_id}"
70
+ end
71
+ end
72
+ ```
73
+
74
+ ### Storage Backends
75
+
76
+ #### Memory Storage
77
+
78
+ Best for single-process applications:
79
+
80
+ ```ruby
81
+ class LocalMessage < SmartMessage::Base
82
+ ddq_size 50
83
+ ddq_storage :memory
84
+ enable_deduplication!
85
+ end
86
+ ```
87
+
88
+ Memory Usage (approximate):
89
+ - 10 UUIDs: ~480 bytes
90
+ - 100 UUIDs: ~4.8 KB
91
+ - 1000 UUIDs: ~48 KB
92
+
93
+ #### Redis Storage
94
+
95
+ Best for distributed/multi-process applications:
96
+
97
+ ```ruby
98
+ class DistributedMessage < SmartMessage::Base
99
+ ddq_size 1000
100
+ ddq_storage :redis,
101
+ redis_url: 'redis://localhost:6379',
102
+ redis_db: 1,
103
+ key_prefix: 'ddq'
104
+ enable_deduplication!
105
+ end
106
+ ```
107
+
108
+ Redis DDQ features:
109
+ - **Distributed State**: Shared across multiple processes
110
+ - **Persistence**: Survives process restarts
111
+ - **TTL Support**: Automatic expiration of old entries
112
+ - **Atomic Operations**: Transaction safety for concurrent access
113
+
114
+ ### Configuration Options
115
+
116
+ | Option | Type | Default | Description |
117
+ |--------|------|---------|-------------|
118
+ | `ddq_size` | Integer | 100 | Maximum UUIDs to track in circular buffer |
119
+ | `ddq_storage` | Symbol | `:memory` | Storage backend (`:memory` or `:redis`) |
120
+ | `redis_url` | String | `'redis://localhost:6379'` | Redis connection URL |
121
+ | `redis_db` | Integer | 0 | Redis database number |
122
+ | `key_prefix` | String | `'ddq'` | Prefix for Redis keys |
123
+ | `ttl` | Integer | 3600 | TTL for Redis entries (seconds) |
124
+
125
+ ## Usage Examples
126
+
127
+ ### Multiple Handlers per Message Class
128
+
129
+ ```ruby
130
+ class OrderMessage < SmartMessage::Base
131
+ ddq_size 200
132
+ ddq_storage :memory
133
+ enable_deduplication!
134
+ end
135
+
136
+ # Each gets separate DDQ tracking
137
+ OrderMessage.subscribe('PaymentService.process') # DDQ: "OrderMessage:PaymentService.process"
138
+ OrderMessage.subscribe('FulfillmentService.handle') # DDQ: "OrderMessage:FulfillmentService.handle"
139
+ OrderMessage.subscribe('AuditService.log_order') # DDQ: "OrderMessage:AuditService.log_order"
140
+
141
+ # Same UUID can be processed by each handler independently
142
+ order = OrderMessage.new(order_id: "12345", amount: 99.99)
143
+ order.publish # All three handlers will process this message
144
+ ```
145
+
146
+ ### Cross-Message-Class Handlers
147
+
148
+ ```ruby
149
+ class PaymentService
150
+ def self.process(message)
151
+ puts "PaymentService processing: #{message.class.name}"
152
+ end
153
+ end
154
+
155
+ # Same handler, different message classes = separate DDQs
156
+ OrderMessage.subscribe('PaymentService.process') # DDQ: "OrderMessage:PaymentService.process"
157
+ InvoiceMessage.subscribe('PaymentService.process') # DDQ: "InvoiceMessage:PaymentService.process"
158
+ RefundMessage.subscribe('PaymentService.process') # DDQ: "RefundMessage:PaymentService.process"
159
+ ```
160
+
161
+ ### Distributed Processing
162
+
163
+ ```ruby
164
+ # Process A (payment-service-1)
165
+ class OrderMessage < SmartMessage::Base
166
+ ddq_storage :redis, redis_url: 'redis://shared-redis:6379'
167
+ enable_deduplication!
168
+ end
169
+
170
+ OrderMessage.subscribe('PaymentService.process')
171
+
172
+ # Process B (payment-service-2)
173
+ # Same configuration, same handler = shared DDQ in Redis
174
+ OrderMessage.subscribe('PaymentService.process')
175
+
176
+ # Only one process will handle each unique UUID
177
+ ```
178
+
179
+ ## API Reference
180
+
181
+ ### Class Methods
182
+
183
+ #### `ddq_size(size)`
184
+ Configure the maximum number of UUIDs to track:
185
+ ```ruby
186
+ OrderMessage.ddq_size(500) # Track last 500 UUIDs
187
+ ```
188
+
189
+ #### `ddq_storage(storage, **options)`
190
+ Configure the storage backend:
191
+ ```ruby
192
+ OrderMessage.ddq_storage(:memory)
193
+ OrderMessage.ddq_storage(:redis, redis_url: 'redis://localhost:6379', redis_db: 2)
194
+ ```
195
+
196
+ #### `enable_deduplication!`
197
+ Enable deduplication for the message class:
198
+ ```ruby
199
+ OrderMessage.enable_deduplication!
200
+ ```
201
+
202
+ #### `disable_deduplication!`
203
+ Disable deduplication for the message class:
204
+ ```ruby
205
+ OrderMessage.disable_deduplication!
206
+ ```
207
+
208
+ #### `ddq_enabled?`
209
+ Check if deduplication is enabled:
210
+ ```ruby
211
+ puts OrderMessage.ddq_enabled? # => true/false
212
+ ```
213
+
214
+ #### `ddq_config`
215
+ Get current DDQ configuration:
216
+ ```ruby
217
+ config = OrderMessage.ddq_config
218
+ # => {enabled: true, size: 100, storage: :memory, options: {}}
219
+ ```
220
+
221
+ #### `ddq_stats`
222
+ Get DDQ statistics for all handlers:
223
+ ```ruby
224
+ stats = OrderMessage.ddq_stats
225
+ # => {enabled: true, current_count: 45, utilization: 45.0, ...}
226
+ ```
227
+
228
+ #### `clear_ddq!`
229
+ Clear all DDQ instances for the message class:
230
+ ```ruby
231
+ OrderMessage.clear_ddq!
232
+ ```
233
+
234
+ #### `duplicate_uuid?(uuid)`
235
+ Check if a UUID is tracked as duplicate:
236
+ ```ruby
237
+ is_dup = OrderMessage.duplicate_uuid?("some-uuid-123") # => true/false
238
+ ```
239
+
240
+ ### Instance Methods
241
+
242
+ #### `duplicate?`
243
+ Check if this message instance is a duplicate:
244
+ ```ruby
245
+ message = OrderMessage.new(order_id: "123", amount: 99.99)
246
+ puts message.duplicate? # => true/false
247
+ ```
248
+
249
+ #### `mark_as_processed!`
250
+ Manually mark this message as processed:
251
+ ```ruby
252
+ message.mark_as_processed! # Adds UUID to DDQ
253
+ ```
254
+
255
+ ## Integration with Dispatcher
256
+
257
+ The deduplication system integrates seamlessly with SmartMessage's dispatcher:
258
+
259
+ ### Message Flow with DDQ
260
+
261
+ 1. **Message Receipt**: Dispatcher receives decoded message
262
+ 2. **Handler Iteration**: For each subscribed handler:
263
+ - **DDQ Check**: Check handler's DDQ for message UUID
264
+ - **Skip Duplicates**: If UUID found, log and skip to next handler
265
+ - **Process New**: If UUID not found, route to handler
266
+ - **Mark Processed**: After successful processing, add UUID to handler's DDQ
267
+
268
+ ### Logging
269
+
270
+ The dispatcher provides detailed logging for deduplication events:
271
+
272
+ ```
273
+ [INFO] [SmartMessage::Dispatcher] Skipping duplicate for PaymentService.process: uuid-123
274
+ [DEBUG] [SmartMessage::Dispatcher] Marked UUID as processed for FulfillmentService.handle: uuid-456
275
+ ```
276
+
277
+ ### Statistics Integration
278
+
279
+ DDQ statistics are integrated with SmartMessage's built-in statistics system:
280
+
281
+ ```ruby
282
+ # Access via dispatcher
283
+ dispatcher = SmartMessage::Dispatcher.new
284
+ ddq_stats = dispatcher.ddq_stats
285
+
286
+ # Example output:
287
+ # {
288
+ # "OrderMessage:PaymentService.process" => {
289
+ # size: 100, current_count: 23, utilization: 23.0,
290
+ # storage_type: :memory, implementation: "SmartMessage::DDQ::Memory"
291
+ # },
292
+ # "OrderMessage:FulfillmentService.handle" => { ... }
293
+ # }
294
+ ```
295
+
296
+ ## Performance Characteristics
297
+
298
+ ### Memory DDQ Performance
299
+
300
+ - **Lookup Time**: O(1) - Set provides constant-time contains check
301
+ - **Insertion Time**: O(1) - Array provides constant-time insertion
302
+ - **Memory Usage**: ~48 bytes per UUID (including Set and Array overhead)
303
+ - **Thread Safety**: Mutex-protected for concurrent access
304
+
305
+ ### Redis DDQ Performance
306
+
307
+ - **Lookup Time**: O(1) - Redis SET provides constant-time membership test
308
+ - **Insertion Time**: O(1) - Redis LPUSH + LTRIM for circular behavior
309
+ - **Network Overhead**: 1-2 Redis commands per duplicate check
310
+ - **Persistence**: Automatic persistence and cross-process sharing
311
+
312
+ ### Benchmarks
313
+
314
+ Memory DDQ (1000 entries):
315
+ - **Memory Usage**: ~57 KB
316
+ - **Lookup Performance**: 0.001ms average
317
+ - **Insertion Performance**: 0.002ms average
318
+
319
+ Redis DDQ (1000 entries):
320
+ - **Memory Usage**: Stored in Redis
321
+ - **Lookup Performance**: 0.5-2ms average (network dependent)
322
+ - **Insertion Performance**: 1-3ms average (network dependent)
323
+
324
+ ## Best Practices
325
+
326
+ ### 1. Choose Appropriate DDQ Size
327
+
328
+ Size DDQ based on your message volume and acceptable duplicate window:
329
+
330
+ ```ruby
331
+ # High-volume service: larger DDQ
332
+ class HighVolumeMessage < SmartMessage::Base
333
+ ddq_size 10000 # Track last 10k messages
334
+ ddq_storage :redis
335
+ enable_deduplication!
336
+ end
337
+
338
+ # Low-volume service: smaller DDQ
339
+ class LowVolumeMessage < SmartMessage::Base
340
+ ddq_size 50 # Track last 50 messages
341
+ ddq_storage :memory
342
+ enable_deduplication!
343
+ end
344
+ ```
345
+
346
+ ### 2. Use Redis for Distributed Systems
347
+
348
+ For multi-process deployments, always use Redis storage:
349
+
350
+ ```ruby
351
+ class DistributedMessage < SmartMessage::Base
352
+ ddq_storage :redis,
353
+ redis_url: ENV.fetch('REDIS_URL', 'redis://localhost:6379'),
354
+ redis_db: ENV.fetch('DDQ_REDIS_DB', 1).to_i
355
+ enable_deduplication!
356
+ end
357
+ ```
358
+
359
+ ### 3. Monitor DDQ Statistics
360
+
361
+ Regularly monitor DDQ utilization:
362
+
363
+ ```ruby
364
+ # In monitoring/health check code
365
+ stats = OrderMessage.ddq_stats
366
+ if stats[:utilization] > 90
367
+ logger.warn "DDQ utilization high: #{stats[:utilization]}%"
368
+ end
369
+ ```
370
+
371
+ ### 4. Handle DDQ Errors Gracefully
372
+
373
+ The system is designed to fail-open (process messages when DDQ fails):
374
+
375
+ ```ruby
376
+ # DDQ failures are logged but don't prevent message processing
377
+ # Monitor logs for DDQ-related errors:
378
+ # [ERROR] [SmartMessage::DDQ] Failed to check duplicate: Redis connection error
379
+ ```
380
+
381
+ ## Troubleshooting
382
+
383
+ ### Common Issues
384
+
385
+ #### 1. Messages Not Being Deduplicated
386
+
387
+ **Symptoms**: Same UUID processed multiple times by same handler
388
+ **Causes**:
389
+ - Deduplication not enabled: `enable_deduplication!` missing
390
+ - Different handlers: Each handler has separate DDQ
391
+ - DDQ size too small: Old UUIDs evicted too quickly
392
+
393
+ **Solutions**:
394
+ ```ruby
395
+ # Verify deduplication is enabled
396
+ puts OrderMessage.ddq_enabled? # Should be true
397
+
398
+ # Check DDQ configuration
399
+ puts OrderMessage.ddq_config
400
+
401
+ # Increase DDQ size if needed
402
+ OrderMessage.ddq_size(1000)
403
+ ```
404
+
405
+ #### 2. Redis Connection Errors
406
+
407
+ **Symptoms**: DDQ errors in logs, messages still processing
408
+ **Causes**: Redis connectivity issues
409
+
410
+ **Solutions**:
411
+ ```ruby
412
+ # Verify Redis connection
413
+ redis_config = OrderMessage.ddq_config[:options]
414
+ puts "Redis URL: #{redis_config[:redis_url]}"
415
+
416
+ # Test Redis connectivity
417
+ require 'redis'
418
+ redis = Redis.new(url: redis_config[:redis_url])
419
+ puts redis.ping # Should return "PONG"
420
+ ```
421
+
422
+ #### 3. High Memory Usage
423
+
424
+ **Symptoms**: Increasing memory usage in memory DDQ
425
+ **Causes**: DDQ size too large for available memory
426
+
427
+ **Solutions**:
428
+ ```ruby
429
+ # Check memory usage
430
+ stats = OrderMessage.ddq_stats
431
+ puts "Memory usage: #{stats[:current_count] * 48} bytes"
432
+
433
+ # Reduce DDQ size
434
+ OrderMessage.ddq_size(100) # Smaller size
435
+
436
+ # Or switch to Redis
437
+ OrderMessage.ddq_storage(:redis)
438
+ ```
439
+
440
+ ### Debugging DDQ Issues
441
+
442
+ ```ruby
443
+ # Enable debug logging
444
+ SmartMessage.configure do |config|
445
+ config.log_level = :debug
446
+ end
447
+
448
+ # Check specific UUID
449
+ uuid = "test-uuid-123"
450
+ puts "Is duplicate: #{OrderMessage.duplicate_uuid?(uuid)}"
451
+
452
+ # Clear DDQ for testing
453
+ OrderMessage.clear_ddq!
454
+
455
+ # Monitor DDQ stats
456
+ stats = OrderMessage.ddq_stats
457
+ puts "Current count: #{stats[:current_count]}"
458
+ puts "Utilization: #{stats[:utilization]}%"
459
+ ```
460
+
461
+ ## Migration Guide
462
+
463
+ ### From Class-Level to Handler-Level DDQ
464
+
465
+ If upgrading from a previous version with class-level deduplication:
466
+
467
+ **Before (hypothetical)**:
468
+ ```ruby
469
+ # All handlers shared one DDQ per message class
470
+ OrderMessage.subscribe('PaymentService.process')
471
+ OrderMessage.subscribe('FulfillmentService.handle')
472
+ # Both shared the same DDQ
473
+ ```
474
+
475
+ **After (current)**:
476
+ ```ruby
477
+ # Each handler gets its own DDQ automatically
478
+ OrderMessage.subscribe('PaymentService.process') # DDQ: "OrderMessage:PaymentService.process"
479
+ OrderMessage.subscribe('FulfillmentService.handle') # DDQ: "OrderMessage:FulfillmentService.handle"
480
+ # Separate DDQs with isolated tracking
481
+ ```
482
+
483
+ **Benefits of Migration**:
484
+ - **Better Isolation**: Handler failures don't affect other handlers' deduplication
485
+ - **Flexible Filtering**: Different handlers can have different subscription filters
486
+ - **Cross-Process Safety**: Handlers with same name across processes get separate DDQs
487
+
488
+ The migration is automatic - no code changes required. The new system provides better isolation and reliability.