smart_message 0.0.9 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/Gemfile.lock +1 -1
- data/README.md +101 -3
- data/docs/architecture.md +139 -69
- data/docs/message_deduplication.md +488 -0
- data/examples/10_message_deduplication.rb +209 -0
- data/lib/smart_message/base.rb +2 -0
- data/lib/smart_message/ddq/base.rb +71 -0
- data/lib/smart_message/ddq/memory.rb +109 -0
- data/lib/smart_message/ddq/redis.rb +168 -0
- data/lib/smart_message/ddq.rb +31 -0
- data/lib/smart_message/deduplication.rb +174 -0
- data/lib/smart_message/dispatcher.rb +175 -18
- data/lib/smart_message/subscription.rb +10 -7
- data/lib/smart_message/version.rb +1 -1
- metadata +8 -1
@@ -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.
|
@@ -0,0 +1,209 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# examples/10_message_deduplication.rb
|
3
|
+
|
4
|
+
require_relative '../lib/smart_message'
|
5
|
+
|
6
|
+
# Example demonstrating message deduplication with DDQ (Deduplication Queue)
|
7
|
+
|
8
|
+
# Message class with deduplication enabled
|
9
|
+
class OrderMessage < SmartMessage::Base
|
10
|
+
version 1
|
11
|
+
property :order_id, required: true
|
12
|
+
property :amount, required: true
|
13
|
+
|
14
|
+
from "order-service"
|
15
|
+
|
16
|
+
# Configure deduplication
|
17
|
+
ddq_size 100 # Keep track of last 100 message UUIDs
|
18
|
+
ddq_storage :memory # Use memory storage (could be :redis)
|
19
|
+
enable_deduplication! # Enable deduplication for this message class
|
20
|
+
|
21
|
+
def self.process(message)
|
22
|
+
puts "✅ Processing order: #{message.order_id} for $#{message.amount}"
|
23
|
+
@@processed_orders ||= []
|
24
|
+
@@processed_orders << message.order_id
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.processed_orders
|
28
|
+
@@processed_orders || []
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.clear_processed
|
32
|
+
@@processed_orders = []
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Regular message class without deduplication
|
37
|
+
class NotificationMessage < SmartMessage::Base
|
38
|
+
version 1
|
39
|
+
property :message, required: true
|
40
|
+
property :recipient, required: true
|
41
|
+
|
42
|
+
from "notification-service"
|
43
|
+
|
44
|
+
def self.process(message)
|
45
|
+
puts "📧 Sending: '#{message.message}' to #{message.recipient}"
|
46
|
+
@@sent_notifications ||= []
|
47
|
+
@@sent_notifications << { message: message.message, recipient: message.recipient }
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.sent_notifications
|
51
|
+
@@sent_notifications || []
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.clear_processed
|
55
|
+
@@sent_notifications = []
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def demonstrate_deduplication
|
60
|
+
puts "=== SmartMessage Deduplication Demo ==="
|
61
|
+
puts
|
62
|
+
|
63
|
+
# Setup transport and subscriptions
|
64
|
+
transport = SmartMessage::Transport::MemoryTransport.new
|
65
|
+
|
66
|
+
OrderMessage.transport(transport)
|
67
|
+
OrderMessage.serializer(SmartMessage::Serializer::Json.new)
|
68
|
+
OrderMessage.subscribe('OrderMessage.process')
|
69
|
+
|
70
|
+
NotificationMessage.transport(transport)
|
71
|
+
NotificationMessage.serializer(SmartMessage::Serializer::Json.new)
|
72
|
+
NotificationMessage.subscribe('NotificationMessage.process')
|
73
|
+
|
74
|
+
# Clear any previous state
|
75
|
+
OrderMessage.clear_processed
|
76
|
+
NotificationMessage.clear_processed
|
77
|
+
OrderMessage.clear_ddq!
|
78
|
+
|
79
|
+
puts "📊 DDQ Configuration:"
|
80
|
+
config = OrderMessage.ddq_config
|
81
|
+
puts " - Enabled: #{config[:enabled]}"
|
82
|
+
puts " - Size: #{config[:size]}"
|
83
|
+
puts " - Storage: #{config[:storage]}"
|
84
|
+
puts
|
85
|
+
|
86
|
+
# Create a specific UUID for testing duplicates
|
87
|
+
uuid = SecureRandom.uuid
|
88
|
+
puts "🔍 Testing with UUID: #{uuid}"
|
89
|
+
puts
|
90
|
+
|
91
|
+
# Test 1: OrderMessage with deduplication
|
92
|
+
puts "--- Test 1: OrderMessage (with deduplication) ---"
|
93
|
+
|
94
|
+
# Create header with specific UUID
|
95
|
+
header = SmartMessage::Header.new(
|
96
|
+
uuid: uuid,
|
97
|
+
message_class: "OrderMessage",
|
98
|
+
published_at: Time.now,
|
99
|
+
publisher_pid: Process.pid,
|
100
|
+
version: 1,
|
101
|
+
from: "order-service"
|
102
|
+
)
|
103
|
+
|
104
|
+
# First message
|
105
|
+
order1 = OrderMessage.new(
|
106
|
+
_sm_header: header,
|
107
|
+
_sm_payload: { order_id: "ORD-001", amount: 99.99 }
|
108
|
+
)
|
109
|
+
|
110
|
+
puts "Publishing first order message..."
|
111
|
+
order1.publish
|
112
|
+
sleep 0.1 # Allow processing
|
113
|
+
|
114
|
+
# Second message with SAME UUID (should be deduplicated)
|
115
|
+
order2 = OrderMessage.new(
|
116
|
+
_sm_header: header,
|
117
|
+
_sm_payload: { order_id: "ORD-002", amount: 149.99 }
|
118
|
+
)
|
119
|
+
|
120
|
+
puts "Publishing duplicate order message (same UUID)..."
|
121
|
+
order2.publish
|
122
|
+
sleep 0.1 # Allow processing
|
123
|
+
|
124
|
+
puts "📈 Results:"
|
125
|
+
puts " - Processed orders: #{OrderMessage.processed_orders.length}"
|
126
|
+
puts " - Orders: #{OrderMessage.processed_orders}"
|
127
|
+
puts
|
128
|
+
|
129
|
+
# Test 2: NotificationMessage without deduplication
|
130
|
+
puts "--- Test 2: NotificationMessage (no deduplication) ---"
|
131
|
+
|
132
|
+
# Create header with same UUID
|
133
|
+
notification_header = SmartMessage::Header.new(
|
134
|
+
uuid: uuid, # Same UUID as orders!
|
135
|
+
message_class: "NotificationMessage",
|
136
|
+
published_at: Time.now,
|
137
|
+
publisher_pid: Process.pid,
|
138
|
+
version: 1,
|
139
|
+
from: "notification-service"
|
140
|
+
)
|
141
|
+
|
142
|
+
# First notification
|
143
|
+
notif1 = NotificationMessage.new(
|
144
|
+
_sm_header: notification_header,
|
145
|
+
_sm_payload: { message: "Order confirmed", recipient: "customer@example.com" }
|
146
|
+
)
|
147
|
+
|
148
|
+
puts "Publishing first notification..."
|
149
|
+
notif1.publish
|
150
|
+
sleep 0.1
|
151
|
+
|
152
|
+
# Second notification with same UUID (should NOT be deduplicated)
|
153
|
+
notif2 = NotificationMessage.new(
|
154
|
+
_sm_header: notification_header,
|
155
|
+
_sm_payload: { message: "Order shipped", recipient: "customer@example.com" }
|
156
|
+
)
|
157
|
+
|
158
|
+
puts "Publishing duplicate notification (same UUID)..."
|
159
|
+
notif2.publish
|
160
|
+
sleep 0.1
|
161
|
+
|
162
|
+
puts "📈 Results:"
|
163
|
+
puts " - Sent notifications: #{NotificationMessage.sent_notifications.length}"
|
164
|
+
puts " - Notifications: #{NotificationMessage.sent_notifications}"
|
165
|
+
puts
|
166
|
+
|
167
|
+
# Show DDQ statistics
|
168
|
+
puts "📊 DDQ Statistics:"
|
169
|
+
stats = OrderMessage.ddq_stats
|
170
|
+
if stats[:enabled]
|
171
|
+
puts " - Current count: #{stats[:current_count]}"
|
172
|
+
puts " - Utilization: #{stats[:utilization]}%"
|
173
|
+
puts " - Storage type: #{stats[:storage_type]}"
|
174
|
+
else
|
175
|
+
puts " - DDQ not enabled"
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def demonstrate_memory_efficiency
|
180
|
+
puts
|
181
|
+
puts "=== Memory Usage Demonstration ==="
|
182
|
+
puts
|
183
|
+
|
184
|
+
# Show memory usage for different DDQ sizes
|
185
|
+
test_sizes = [10, 100, 1000]
|
186
|
+
|
187
|
+
test_sizes.each do |size|
|
188
|
+
memory_usage = size * 48 # Approximate bytes per UUID
|
189
|
+
puts "DDQ size #{size}: ~#{memory_usage} bytes (~#{(memory_usage / 1024.0).round(1)} KB)"
|
190
|
+
end
|
191
|
+
|
192
|
+
puts
|
193
|
+
puts "💡 Memory is very reasonable - even 1000 entries uses less than 50KB!"
|
194
|
+
end
|
195
|
+
|
196
|
+
if __FILE__ == $0
|
197
|
+
demonstrate_deduplication
|
198
|
+
demonstrate_memory_efficiency
|
199
|
+
|
200
|
+
puts
|
201
|
+
puts "✨ Key Benefits:"
|
202
|
+
puts " - O(1) duplicate detection"
|
203
|
+
puts " - Configurable queue size"
|
204
|
+
puts " - Memory or Redis storage"
|
205
|
+
puts " - Per-message-class configuration"
|
206
|
+
puts " - Automatic integration with dispatcher"
|
207
|
+
puts
|
208
|
+
puts "🚀 Ready for production multi-transport scenarios!"
|
209
|
+
end
|
data/lib/smart_message/base.rb
CHANGED
@@ -12,6 +12,7 @@ require_relative './subscription.rb'
|
|
12
12
|
require_relative './versioning.rb'
|
13
13
|
require_relative './messaging.rb'
|
14
14
|
require_relative './utilities.rb'
|
15
|
+
require_relative './deduplication.rb'
|
15
16
|
|
16
17
|
module SmartMessage
|
17
18
|
# The foundation class for the smart message
|
@@ -26,6 +27,7 @@ module SmartMessage
|
|
26
27
|
include SmartMessage::Versioning
|
27
28
|
include SmartMessage::Messaging
|
28
29
|
include SmartMessage::Utilities
|
30
|
+
include SmartMessage::Deduplication
|
29
31
|
|
30
32
|
include Hashie::Extensions::Coercion
|
31
33
|
include Hashie::Extensions::DeepMerge
|