smart_message 0.0.9 → 0.0.12
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/.github/workflows/deploy-github-pages.yml +38 -0
- data/.gitignore +5 -0
- data/CHANGELOG.md +53 -0
- data/Gemfile.lock +35 -4
- data/README.md +265 -69
- data/Rakefile +29 -4
- data/docs/assets/images/ddq_architecture.svg +130 -0
- data/docs/assets/images/dlq_architecture.svg +115 -0
- data/docs/assets/images/enhanced-dual-publishing.svg +136 -0
- data/docs/assets/images/enhanced-fluent-api.svg +149 -0
- data/docs/assets/images/enhanced-microservices-routing.svg +115 -0
- data/docs/assets/images/enhanced-pattern-matching.svg +107 -0
- data/docs/assets/images/fluent-api-demo.svg +59 -0
- data/docs/assets/images/performance-comparison.svg +161 -0
- data/docs/assets/images/redis-basic-architecture.svg +53 -0
- data/docs/assets/images/redis-enhanced-architecture.svg +88 -0
- data/docs/assets/images/redis-queue-architecture.svg +101 -0
- data/docs/assets/images/smart_message.jpg +0 -0
- data/docs/assets/images/smart_message_walking.jpg +0 -0
- data/docs/assets/images/smartmessage_architecture_overview.svg +173 -0
- data/docs/assets/images/transport-comparison-matrix.svg +171 -0
- data/docs/assets/javascripts/mathjax.js +17 -0
- data/docs/assets/stylesheets/extra.css +51 -0
- data/docs/{addressing.md → core-concepts/addressing.md} +5 -7
- data/docs/{architecture.md → core-concepts/architecture.md} +129 -119
- data/docs/{dispatcher.md → core-concepts/dispatcher.md} +21 -21
- data/docs/{message_filtering.md → core-concepts/message-filtering.md} +2 -3
- data/docs/{message_processing.md → core-concepts/message-processing.md} +17 -17
- data/docs/{troubleshooting.md → development/troubleshooting.md} +7 -7
- data/docs/{examples.md → getting-started/examples.md} +115 -89
- data/docs/{getting-started.md → getting-started/quick-start.md} +47 -18
- data/docs/guides/redis-queue-getting-started.md +697 -0
- data/docs/guides/redis-queue-patterns.md +889 -0
- data/docs/guides/redis-queue-production.md +1091 -0
- data/docs/index.md +64 -0
- data/docs/{dead_letter_queue.md → reference/dead-letter-queue.md} +2 -3
- data/docs/{logging.md → reference/logging.md} +1 -1
- data/docs/reference/message-deduplication.md +489 -0
- data/docs/{proc_handlers_summary.md → reference/proc-handlers.md} +7 -6
- data/docs/{serializers.md → reference/serializers.md} +3 -5
- data/docs/{transports.md → reference/transports.md} +133 -11
- data/docs/transports/memory-transport.md +374 -0
- data/docs/transports/redis-enhanced-transport.md +524 -0
- data/docs/transports/redis-queue-transport.md +1304 -0
- data/docs/transports/redis-transport-comparison.md +496 -0
- data/docs/transports/redis-transport.md +509 -0
- data/examples/README.md +98 -5
- data/examples/city_scenario/911_emergency_call_flow.svg +99 -0
- data/examples/city_scenario/README.md +515 -0
- data/examples/city_scenario/ai_visitor_intelligence_flow.svg +108 -0
- data/examples/city_scenario/citizen.rb +195 -0
- data/examples/city_scenario/city_diagram.svg +125 -0
- data/examples/city_scenario/common/health_monitor.rb +80 -0
- data/examples/city_scenario/common/logger.rb +30 -0
- data/examples/city_scenario/emergency_dispatch_center.rb +270 -0
- data/examples/city_scenario/fire_department.rb +446 -0
- data/examples/city_scenario/fire_emergency_flow.svg +95 -0
- data/examples/city_scenario/health_department.rb +100 -0
- data/examples/city_scenario/health_monitoring_system.svg +130 -0
- data/examples/city_scenario/house.rb +244 -0
- data/examples/city_scenario/local_bank.rb +217 -0
- data/examples/city_scenario/messages/emergency_911_message.rb +81 -0
- data/examples/city_scenario/messages/emergency_resolved_message.rb +43 -0
- data/examples/city_scenario/messages/fire_dispatch_message.rb +43 -0
- data/examples/city_scenario/messages/fire_emergency_message.rb +45 -0
- data/examples/city_scenario/messages/health_check_message.rb +22 -0
- data/examples/city_scenario/messages/health_status_message.rb +35 -0
- data/examples/city_scenario/messages/police_dispatch_message.rb +46 -0
- data/examples/city_scenario/messages/silent_alarm_message.rb +38 -0
- data/examples/city_scenario/police_department.rb +316 -0
- data/examples/city_scenario/redis_monitor.rb +129 -0
- data/examples/city_scenario/redis_stats.rb +743 -0
- data/examples/city_scenario/room_for_improvement.md +240 -0
- data/examples/city_scenario/security_emergency_flow.svg +95 -0
- data/examples/city_scenario/service_internal_architecture.svg +154 -0
- data/examples/city_scenario/smart_message_ai_agent.rb +364 -0
- data/examples/city_scenario/start_demo.sh +236 -0
- data/examples/city_scenario/stop_demo.sh +106 -0
- data/examples/city_scenario/visitor.rb +631 -0
- data/examples/memory/01_message_deduplication_demo.rb +209 -0
- data/examples/{09_dead_letter_queue_demo.rb → memory/02_dead_letter_queue_demo.rb} +13 -40
- data/examples/{01_point_to_point_orders.rb → memory/03_point_to_point_orders.rb} +1 -1
- data/examples/{02_publish_subscribe_events.rb → memory/04_publish_subscribe_events.rb} +2 -2
- data/examples/{03_many_to_many_chat.rb → memory/05_many_to_many_chat.rb} +4 -4
- data/examples/{show_me.rb → memory/06_pretty_print_demo.rb} +1 -1
- data/examples/{05_proc_handlers.rb → memory/07_proc_handlers_demo.rb} +2 -2
- data/examples/{06_custom_logger_example.rb → memory/08_custom_logger_demo.rb} +17 -14
- data/examples/{07_error_handling_scenarios.rb → memory/09_error_handling_demo.rb} +4 -4
- data/examples/{08_entity_addressing_basic.rb → memory/10_entity_addressing_basic.rb} +8 -8
- data/examples/{08_entity_addressing_with_filtering.rb → memory/11_entity_addressing_with_filtering.rb} +6 -6
- data/examples/{09_regex_filtering_microservices.rb → memory/12_regex_filtering_microservices.rb} +2 -2
- data/examples/{10_header_block_configuration.rb → memory/13_header_block_configuration.rb} +6 -6
- data/examples/{11_global_configuration_example.rb → memory/14_global_configuration_demo.rb} +19 -8
- data/examples/{show_logger.rb → memory/15_logger_demo.rb} +1 -1
- data/examples/memory/README.md +163 -0
- data/examples/memory/memory_transport_architecture.svg +90 -0
- data/examples/memory/point_to_point_pattern.svg +94 -0
- data/examples/memory/publish_subscribe_pattern.svg +125 -0
- data/examples/{04_redis_smart_home_iot.rb → redis/01_smart_home_iot_demo.rb} +5 -5
- data/examples/redis/README.md +230 -0
- data/examples/redis/alert_system_flow.svg +127 -0
- data/examples/redis/dashboard_status_flow.svg +107 -0
- data/examples/redis/device_command_flow.svg +113 -0
- data/examples/redis/redis_transport_architecture.svg +115 -0
- data/examples/{smart_home_iot_dataflow.md → redis/smart_home_iot_dataflow.md} +4 -116
- data/examples/redis/smart_home_system_architecture.svg +133 -0
- data/examples/redis_enhanced/README.md +319 -0
- data/examples/redis_enhanced/enhanced_01_basic_patterns.rb +233 -0
- data/examples/redis_enhanced/enhanced_02_fluent_api.rb +331 -0
- data/examples/redis_enhanced/enhanced_03_dual_publishing.rb +281 -0
- data/examples/redis_enhanced/enhanced_04_advanced_routing.rb +419 -0
- data/examples/redis_queue/01_basic_messaging.rb +221 -0
- data/examples/redis_queue/01_comprehensive_examples.rb +508 -0
- data/examples/redis_queue/02_pattern_routing.rb +405 -0
- data/examples/redis_queue/03_fluent_api.rb +422 -0
- data/examples/redis_queue/04_load_balancing.rb +486 -0
- data/examples/redis_queue/05_microservices.rb +735 -0
- data/examples/redis_queue/06_emergency_alerts.rb +777 -0
- data/examples/redis_queue/07_queue_management.rb +587 -0
- data/examples/redis_queue/README.md +366 -0
- data/examples/redis_queue/enhanced_01_basic_patterns.rb +233 -0
- data/examples/redis_queue/enhanced_02_fluent_api.rb +331 -0
- data/examples/redis_queue/enhanced_03_dual_publishing.rb +281 -0
- data/examples/redis_queue/enhanced_04_advanced_routing.rb +419 -0
- data/examples/redis_queue/redis_queue_architecture.svg +148 -0
- data/ideas/README.md +41 -0
- data/ideas/agents.md +1001 -0
- data/ideas/database_transport.md +980 -0
- data/ideas/improvement.md +359 -0
- data/ideas/meshage.md +1788 -0
- data/ideas/message_discovery.md +178 -0
- data/ideas/message_schema.md +1381 -0
- data/lib/smart_message/.idea/.gitignore +8 -0
- data/lib/smart_message/.idea/markdown.xml +6 -0
- data/lib/smart_message/.idea/misc.xml +4 -0
- data/lib/smart_message/.idea/modules.xml +8 -0
- data/lib/smart_message/.idea/smart_message.iml +16 -0
- data/lib/smart_message/.idea/vcs.xml +6 -0
- data/lib/smart_message/addressing.rb +15 -0
- data/lib/smart_message/base.rb +2 -2
- data/lib/smart_message/configuration.rb +1 -1
- 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/logger.rb +15 -4
- data/lib/smart_message/plugins.rb +5 -2
- data/lib/smart_message/serializer.rb +14 -0
- data/lib/smart_message/subscription.rb +10 -7
- data/lib/smart_message/transport/redis_enhanced_transport.rb +399 -0
- data/lib/smart_message/transport/redis_queue_transport.rb +555 -0
- data/lib/smart_message/transport/registry.rb +1 -0
- data/lib/smart_message/transport.rb +34 -1
- data/lib/smart_message/version.rb +1 -1
- data/lib/smart_message.rb +5 -52
- data/mkdocs.yml +184 -0
- data/p2p_plan.md +326 -0
- data/p2p_roadmap.md +287 -0
- data/smart_message.gemspec +2 -0
- data/smart_message.svg +51 -0
- metadata +175 -42
- data/docs/README.md +0 -57
- data/examples/dead_letters.jsonl +0 -12
- data/examples/temp.txt +0 -94
- data/examples/tmux_chat/README.md +0 -283
- data/examples/tmux_chat/bot_agent.rb +0 -278
- data/examples/tmux_chat/human_agent.rb +0 -199
- data/examples/tmux_chat/room_monitor.rb +0 -160
- data/examples/tmux_chat/shared_chat_system.rb +0 -328
- data/examples/tmux_chat/start_chat_demo.sh +0 -190
- data/examples/tmux_chat/stop_chat_demo.sh +0 -22
- /data/docs/{properties.md → core-concepts/properties.md} +0 -0
- /data/docs/{ideas_to_think_about.md → development/ideas.md} +0 -0
data/README.md
CHANGED
@@ -1,13 +1,29 @@
|
|
1
|
+
<table border="0">
|
2
|
+
<tr>
|
3
|
+
<td width="30%" valign="top">
|
4
|
+
|
5
|
+
<img src="docs/assets/images/smart_message.jpg" alt="SmartMessage Logo" />
|
6
|
+
<br/>
|
7
|
+
See <a href="https://madbomber.github.io/smart_message/">Documentation Websit</a>
|
8
|
+
</td>
|
9
|
+
<td width="70%" valign="top">
|
10
|
+
|
1
11
|
# SmartMessage
|
2
12
|
|
3
|
-
|
4
|
-
|
13
|
+
Can Walk, Talk, and Think at the Same Time
|
14
|
+
|
15
|
+
**SmartMessage** is a powerful Ruby framework that transforms ordinary messages into intelligent, self-aware entities capable of routing themselves, validating their contents, and executing business logic. By abstracting away the complexities of transport mechanisms (Redis, RabbitMQ, Kafka) and serialization formats (JSON, MessagePack), SmartMessage lets you focus on what matters: your business logic.
|
16
|
+
|
17
|
+
Think of SmartMessage as ActiveRecord for messaging - just as ActiveRecord frees you from database-specific SQL, SmartMessage liberates your messages from transport-specific implementations. Each message knows how to validate itself, where it came from, where it's going, and what to do when it arrives. With built-in support for filtering, versioning, deduplication, and concurrent processing, SmartMessage provides enterprise-grade messaging capabilities with the simplicity Ruby developers love.
|
5
18
|
|
6
|
-
|
19
|
+
</td>
|
20
|
+
</tr>
|
21
|
+
</table>
|
7
22
|
|
8
23
|
## Features
|
9
24
|
|
10
25
|
- **Transport Abstraction**: Plugin architecture supporting multiple message transports (Redis, RabbitMQ, Kafka, etc.)
|
26
|
+
- **🌟 Redis Queue Transport**: Advanced transport with RabbitMQ-style routing patterns, persistent FIFO queues, load balancing, and 10x faster performance than traditional message brokers. Built on Ruby's Async framework for fiber-based concurrency supporting thousands of concurrent subscriptions - [see full documentation](docs/transports/redis-queue.md)
|
11
27
|
- **Serialization Flexibility**: Pluggable serialization formats (JSON, MessagePack, etc.)
|
12
28
|
- **Entity-to-Entity Addressing**: Built-in FROM/TO/REPLY_TO addressing for point-to-point and broadcast messaging patterns
|
13
29
|
- **Advanced Message Filtering**: Filter subscriptions using exact strings, regular expressions, or mixed arrays for precise message routing
|
@@ -16,9 +32,10 @@ SmartMessage is a message abstraction framework that decouples business logic fr
|
|
16
32
|
- **Message Documentation**: Built-in documentation support for message classes and properties with automatic defaults
|
17
33
|
- **Flexible Message Handlers**: Multiple subscription patterns - default methods, custom methods, blocks, procs, and lambdas
|
18
34
|
- **Dual-Level Configuration**: Class and instance-level plugin overrides for gateway patterns
|
19
|
-
- **Concurrent Processing**: Thread-safe message routing using `Concurrent::CachedThreadPool`
|
35
|
+
- **Concurrent Processing**: Thread-safe message routing using `Concurrent::CachedThreadPool` with Async/Fiber-based Redis Queue Transport for massive scalability
|
20
36
|
- **Advanced Logging System**: Comprehensive logging with colorized console output, JSON structured logging, and file rolling
|
21
37
|
- **Built-in Statistics**: Message processing metrics and monitoring
|
38
|
+
- **Message Deduplication**: Handler-scoped deduplication queues (DDQ) with memory or Redis storage for preventing duplicate message processing
|
22
39
|
- **Development Tools**: STDOUT and in-memory transports for testing
|
23
40
|
- **Production Ready**: Redis transport with automatic reconnection and error handling
|
24
41
|
- **Dead Letter Queue**: File-based DLQ with JSON Lines format for failed message capture and replay
|
@@ -34,78 +51,106 @@ gem 'smart_message'
|
|
34
51
|
|
35
52
|
And then execute:
|
36
53
|
|
37
|
-
|
54
|
+
bundle install
|
38
55
|
|
39
56
|
Or install it yourself as:
|
40
57
|
|
41
|
-
|
58
|
+
gem install smart_message
|
59
|
+
|
60
|
+
### Redis Transport Setup
|
61
|
+
|
62
|
+
To use the built-in Redis transport, you'll need to have Redis server installed:
|
63
|
+
|
64
|
+
**macOS:**
|
65
|
+
```bash
|
66
|
+
brew install redis
|
67
|
+
brew services start redis # To start Redis as a service
|
68
|
+
```
|
69
|
+
|
70
|
+
**Ubuntu/Debian:**
|
71
|
+
```bash
|
72
|
+
sudo apt-get update
|
73
|
+
sudo apt-get install redis-server
|
74
|
+
```
|
75
|
+
|
76
|
+
**CentOS/RHEL/Fedora:**
|
77
|
+
```bash
|
78
|
+
sudo yum install redis
|
79
|
+
sudo systemctl start redis
|
80
|
+
```
|
42
81
|
|
43
82
|
## Quick Start
|
44
83
|
|
45
84
|
### 1. Define a Message Class
|
46
85
|
|
47
86
|
```ruby
|
87
|
+
require 'smart_message'
|
88
|
+
|
48
89
|
class OrderMessage < SmartMessage::Base
|
49
90
|
# Declare schema version for compatibility tracking
|
50
91
|
version 2
|
51
|
-
|
92
|
+
|
52
93
|
# Add a description for the message class
|
53
94
|
description "Represents customer order data for processing and fulfillment"
|
54
|
-
|
95
|
+
|
55
96
|
# Configure entity addressing (Method 1: Direct methods)
|
56
97
|
from 'order-service'
|
57
98
|
to 'fulfillment-service' # Point-to-point message
|
58
99
|
reply_to 'order-service' # Responses come back here
|
59
|
-
|
100
|
+
|
60
101
|
# Alternative Method 2: Using header block
|
61
102
|
# header do
|
62
103
|
# from 'order-service'
|
63
104
|
# to 'fulfillment-service'
|
64
105
|
# reply_to 'order-service'
|
65
106
|
# end
|
66
|
-
|
107
|
+
|
67
108
|
# Required properties with validation
|
68
|
-
property :order_id,
|
109
|
+
property :order_id,
|
69
110
|
required: true,
|
70
111
|
message: "Order ID is required",
|
71
112
|
validate: ->(v) { v.is_a?(String) && v.length > 0 },
|
72
113
|
validation_message: "Order ID must be a non-empty string",
|
73
114
|
description: "Unique order identifier"
|
74
|
-
|
75
|
-
property :customer_id,
|
115
|
+
|
116
|
+
property :customer_id,
|
76
117
|
required: true,
|
77
118
|
message: "Customer ID is required",
|
78
119
|
description: "Customer's unique ID"
|
79
|
-
|
80
|
-
property :amount,
|
120
|
+
|
121
|
+
property :amount,
|
81
122
|
required: true,
|
82
123
|
message: "Amount is required",
|
83
124
|
validate: ->(v) { v.is_a?(Numeric) && v > 0 },
|
84
125
|
validation_message: "Amount must be a positive number",
|
85
126
|
description: "Total order amount in dollars"
|
86
|
-
|
87
|
-
property :items,
|
127
|
+
|
128
|
+
property :items,
|
88
129
|
default: [],
|
89
130
|
description: "Array of ordered items"
|
90
131
|
|
91
132
|
# Configure transport and serializer at class level
|
92
133
|
config do
|
134
|
+
# Option 1: Simple STDOUT for development
|
93
135
|
transport SmartMessage::Transport.create(:stdout, loopback: true)
|
136
|
+
|
137
|
+
# Option 2: Redis Queue for production (10x faster than RabbitMQ!)
|
138
|
+
# transport SmartMessage::Transport.create(:redis_queue,
|
139
|
+
# url: 'redis://localhost:6379',
|
140
|
+
# queue_prefix: 'myapp'
|
141
|
+
# )
|
142
|
+
|
94
143
|
serializer SmartMessage::Serializer::JSON.new
|
95
144
|
end
|
96
145
|
|
97
146
|
# Business logic for processing received messages
|
98
|
-
def self.process(
|
99
|
-
#
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
# Process the order
|
104
|
-
puts "Processing order #{order.order_id} for customer #{order.customer_id}"
|
105
|
-
puts "Amount: $#{order.amount}"
|
106
|
-
|
147
|
+
def self.process(message_instance)
|
148
|
+
# Message instance is already decoded and validated
|
149
|
+
puts "Processing order #{message_instance.order_id} for customer #{message_instance.customer_id}"
|
150
|
+
puts "Amount: $#{message_instance.amount}"
|
151
|
+
|
107
152
|
# Your business logic here
|
108
|
-
process_order(
|
153
|
+
process_order(message_instance)
|
109
154
|
end
|
110
155
|
|
111
156
|
private
|
@@ -122,7 +167,7 @@ end
|
|
122
167
|
# Create and publish a message (automatically validated before publishing)
|
123
168
|
order = OrderMessage.new(
|
124
169
|
order_id: "ORD-123",
|
125
|
-
customer_id: "CUST-456",
|
170
|
+
customer_id: "CUST-456",
|
126
171
|
amount: 99.99,
|
127
172
|
items: ["Widget A", "Widget B"]
|
128
173
|
)
|
@@ -151,20 +196,23 @@ OrderMessage.subscribe
|
|
151
196
|
OrderMessage.subscribe("PaymentService.process_order")
|
152
197
|
|
153
198
|
# 3. Block handler (NEW!)
|
154
|
-
OrderMessage.subscribe do |
|
199
|
+
OrderMessage.subscribe do |wrapper|
|
200
|
+
header, payload = wrapper.split
|
155
201
|
order_data = JSON.parse(payload)
|
156
202
|
puts "Quick processing: Order #{order_data['order_id']}"
|
157
203
|
end
|
158
204
|
|
159
205
|
# 4. Proc handler (NEW!)
|
160
|
-
order_processor = proc do |
|
206
|
+
order_processor = proc do |wrapper|
|
207
|
+
header, payload = wrapper.split
|
161
208
|
order_data = JSON.parse(payload)
|
162
209
|
EmailService.send_confirmation(order_data['customer_id'])
|
163
210
|
end
|
164
211
|
OrderMessage.subscribe(order_processor)
|
165
212
|
|
166
213
|
# 5. Lambda handler (NEW!)
|
167
|
-
audit_handler = lambda do |
|
214
|
+
audit_handler = lambda do |wrapper|
|
215
|
+
header, payload = wrapper.split
|
168
216
|
AuditLog.record("Order processed at #{header.published_at}")
|
169
217
|
end
|
170
218
|
OrderMessage.subscribe(audit_handler)
|
@@ -192,7 +240,7 @@ OrderMessage.subscribe(to: /^(dev|staging)-.*/)
|
|
192
240
|
|
193
241
|
# Combined filtering
|
194
242
|
OrderMessage.subscribe(
|
195
|
-
from: /^admin-.*/,
|
243
|
+
from: /^admin-.*/,
|
196
244
|
to: ['order-service', /^fulfillment-.*/]
|
197
245
|
)
|
198
246
|
|
@@ -201,7 +249,103 @@ DevService.subscribe(to: /^(dev|staging)-.*/)
|
|
201
249
|
ProdService.subscribe(to: /^prod-.*/)
|
202
250
|
```
|
203
251
|
|
204
|
-
### 5.
|
252
|
+
### 5. Message Deduplication
|
253
|
+
|
254
|
+
SmartMessage provides handler-scoped message deduplication to prevent duplicate processing of messages with the same UUID. Each handler gets its own Deduplication Queue (DDQ) that tracks recently processed message UUIDs.
|
255
|
+
|
256
|
+
#### Basic Deduplication Setup
|
257
|
+
|
258
|
+
```ruby
|
259
|
+
class OrderMessage < SmartMessage::Base
|
260
|
+
version 1
|
261
|
+
property :order_id, required: true
|
262
|
+
property :amount, required: true
|
263
|
+
|
264
|
+
from "order-service"
|
265
|
+
|
266
|
+
# Configure deduplication
|
267
|
+
ddq_size 100 # Track last 100 message UUIDs
|
268
|
+
ddq_storage :memory # Use memory storage (or :redis for distributed)
|
269
|
+
enable_deduplication! # Enable deduplication for this message class
|
270
|
+
|
271
|
+
def self.process(message_instance)
|
272
|
+
puts "Processing order: #{message_instance.order_id}"
|
273
|
+
# Business logic here
|
274
|
+
end
|
275
|
+
end
|
276
|
+
```
|
277
|
+
|
278
|
+
#### Handler-Scoped Isolation
|
279
|
+
|
280
|
+
Each handler gets its own DDQ scope, preventing cross-contamination between different subscribers:
|
281
|
+
|
282
|
+
```ruby
|
283
|
+
# Each handler gets separate deduplication tracking
|
284
|
+
OrderMessage.subscribe('PaymentService.process') # DDQ: "OrderMessage:PaymentService.process"
|
285
|
+
OrderMessage.subscribe('FulfillmentService.handle') # DDQ: "OrderMessage:FulfillmentService.handle"
|
286
|
+
OrderMessage.subscribe('AuditService.log_order') # DDQ: "OrderMessage:AuditService.log_order"
|
287
|
+
|
288
|
+
# Same handler across message classes = separate DDQs
|
289
|
+
PaymentMessage.subscribe('PaymentService.process') # DDQ: "PaymentMessage:PaymentService.process"
|
290
|
+
InvoiceMessage.subscribe('PaymentService.process') # DDQ: "InvoiceMessage:PaymentService.process"
|
291
|
+
```
|
292
|
+
|
293
|
+
#### Storage Options
|
294
|
+
|
295
|
+
```ruby
|
296
|
+
# Memory-based DDQ (single process)
|
297
|
+
class LocalMessage < SmartMessage::Base
|
298
|
+
ddq_size 50
|
299
|
+
ddq_storage :memory
|
300
|
+
enable_deduplication!
|
301
|
+
end
|
302
|
+
|
303
|
+
# Redis-based DDQ (distributed/multi-process)
|
304
|
+
class DistributedMessage < SmartMessage::Base
|
305
|
+
ddq_size 1000
|
306
|
+
ddq_storage :redis, redis_url: 'redis://localhost:6379', redis_db: 1
|
307
|
+
enable_deduplication!
|
308
|
+
end
|
309
|
+
```
|
310
|
+
|
311
|
+
#### DDQ Statistics and Management
|
312
|
+
|
313
|
+
```ruby
|
314
|
+
# Check deduplication configuration
|
315
|
+
config = OrderMessage.ddq_config
|
316
|
+
puts "Enabled: #{config[:enabled]}"
|
317
|
+
puts "Size: #{config[:size]}"
|
318
|
+
puts "Storage: #{config[:storage]}"
|
319
|
+
|
320
|
+
# Get DDQ statistics
|
321
|
+
stats = OrderMessage.ddq_stats
|
322
|
+
puts "Current count: #{stats[:current_count]}"
|
323
|
+
puts "Utilization: #{stats[:utilization]}%"
|
324
|
+
|
325
|
+
# Clear DDQ if needed
|
326
|
+
OrderMessage.clear_ddq!
|
327
|
+
|
328
|
+
# Check if specific UUID is duplicate
|
329
|
+
OrderMessage.duplicate_uuid?("some-uuid-123")
|
330
|
+
```
|
331
|
+
|
332
|
+
#### How Deduplication Works
|
333
|
+
|
334
|
+
1. **Message Receipt**: When a message arrives, the dispatcher checks the handler's DDQ for the message UUID
|
335
|
+
2. **Duplicate Detection**: If UUID exists in DDQ, the message is ignored (logged but not processed)
|
336
|
+
3. **Processing**: If UUID is new, the message is processed by the handler
|
337
|
+
4. **UUID Storage**: After successful processing, the UUID is added to the handler's DDQ
|
338
|
+
5. **Circular Buffer**: When DDQ reaches capacity, oldest UUIDs are evicted to make room for new ones
|
339
|
+
|
340
|
+
#### Benefits
|
341
|
+
|
342
|
+
- **Handler Isolation**: Each handler maintains independent deduplication state
|
343
|
+
- **Cross-Process Support**: Redis DDQ enables deduplication across multiple processes
|
344
|
+
- **Memory Efficient**: Circular buffer with configurable size limits memory usage
|
345
|
+
- **High Performance**: O(1) UUID lookup using hybrid array + set data structure
|
346
|
+
- **Automatic Integration**: Seamlessly works with existing subscription patterns
|
347
|
+
|
348
|
+
### 6. Entity Addressing
|
205
349
|
|
206
350
|
SmartMessage supports entity-to-entity addressing with FROM/TO/REPLY_TO fields for advanced message routing. You can configure addressing using three different approaches:
|
207
351
|
|
@@ -212,7 +356,7 @@ class PaymentMessage < SmartMessage::Base
|
|
212
356
|
from 'payment-service' # Required: sender identity
|
213
357
|
to 'bank-gateway' # Optional: specific recipient
|
214
358
|
reply_to 'payment-service' # Optional: where responses go
|
215
|
-
|
359
|
+
|
216
360
|
property :amount, required: true
|
217
361
|
property :account_id, required: true
|
218
362
|
end
|
@@ -222,14 +366,14 @@ end
|
|
222
366
|
```ruby
|
223
367
|
class PaymentMessage < SmartMessage::Base
|
224
368
|
version 1
|
225
|
-
|
369
|
+
|
226
370
|
# Configure all addressing in a single block
|
227
371
|
header do
|
228
372
|
from 'payment-service'
|
229
373
|
to 'bank-gateway'
|
230
374
|
reply_to 'payment-service'
|
231
375
|
end
|
232
|
-
|
376
|
+
|
233
377
|
property :amount, required: true
|
234
378
|
property :account_id, required: true
|
235
379
|
end
|
@@ -267,13 +411,13 @@ payment.publish
|
|
267
411
|
```ruby
|
268
412
|
class SystemAnnouncementMessage < SmartMessage::Base
|
269
413
|
version 1
|
270
|
-
|
414
|
+
|
271
415
|
# Using header block for broadcast configuration
|
272
416
|
header do
|
273
417
|
from 'admin-service' # Required: sender identity
|
274
418
|
# No 'to' field = broadcast to all subscribers
|
275
419
|
end
|
276
|
-
|
420
|
+
|
277
421
|
property :message, required: true
|
278
422
|
property :priority, default: 'normal'
|
279
423
|
end
|
@@ -282,7 +426,7 @@ end
|
|
282
426
|
#### Messaging Patterns Supported
|
283
427
|
|
284
428
|
- **Point-to-Point**: Set `to` field for direct entity targeting
|
285
|
-
- **Broadcast**: Omit `to` field (nil) for message broadcast to all subscribers
|
429
|
+
- **Broadcast**: Omit `to` field (nil) for message broadcast to all subscribers
|
286
430
|
- **Request-Reply**: Use `reply_to` field to specify response routing
|
287
431
|
- **Gateway Patterns**: Override addressing at instance level for message forwarding
|
288
432
|
|
@@ -336,9 +480,9 @@ SmartMessage.configure do |config|
|
|
336
480
|
end
|
337
481
|
|
338
482
|
logger = SmartMessage.configuration.default_logger
|
339
|
-
logger.info("User action",
|
340
|
-
user_id: 12345,
|
341
|
-
action: "login",
|
483
|
+
logger.info("User action",
|
484
|
+
user_id: 12345,
|
485
|
+
action: "login",
|
342
486
|
ip_address: "192.168.1.1")
|
343
487
|
# Output: {"timestamp":"2025-01-15T10:30:45.123Z","level":"INFO","message":"User action","user_id":12345,"action":"login","ip_address":"192.168.1.1","source":"app.rb:42:in `authenticate`"}
|
344
488
|
```
|
@@ -352,7 +496,7 @@ SmartMessage.configure do |config|
|
|
352
496
|
roll_by_size: true,
|
353
497
|
max_file_size: 10 * 1024 * 1024, # 10 MB
|
354
498
|
keep_files: 5, # Keep 5 old files
|
355
|
-
|
499
|
+
|
356
500
|
# Date-based rolling (alternative to size-based)
|
357
501
|
roll_by_date: false, # Set to true for date-based
|
358
502
|
date_pattern: '%Y-%m-%d' # Daily rolling pattern
|
@@ -368,11 +512,11 @@ SmartMessage classes automatically use the configured logger:
|
|
368
512
|
class OrderMessage < SmartMessage::Base
|
369
513
|
property :order_id, required: true
|
370
514
|
property :amount, required: true
|
371
|
-
|
515
|
+
|
372
516
|
def process
|
373
517
|
# Logger is automatically available
|
374
|
-
logger.info("Processing order",
|
375
|
-
order_id: order_id,
|
518
|
+
logger.info("Processing order",
|
519
|
+
order_id: order_id,
|
376
520
|
amount: amount,
|
377
521
|
header: _sm_header.to_h,
|
378
522
|
payload: _sm_payload)
|
@@ -412,9 +556,10 @@ Pluggable message encoding/decoding:
|
|
412
556
|
#### Dispatcher
|
413
557
|
Concurrent message routing engine that:
|
414
558
|
- Uses thread pools for async processing
|
415
|
-
- Routes messages to subscribed handlers
|
416
|
-
- Provides processing statistics
|
559
|
+
- Routes messages to subscribed handlers with handler-scoped deduplication
|
560
|
+
- Provides processing statistics and DDQ management
|
417
561
|
- Handles graceful shutdown
|
562
|
+
- Maintains separate DDQ instances per handler for isolated deduplication tracking
|
418
563
|
|
419
564
|
### Plugin Architecture
|
420
565
|
|
@@ -441,6 +586,57 @@ This enables gateway patterns where messages can be received from one transport/
|
|
441
586
|
|
442
587
|
## Transport Implementations
|
443
588
|
|
589
|
+
### Redis Queue Transport (Featured) 🌟
|
590
|
+
|
591
|
+
The Redis Queue Transport provides enterprise-grade message routing with exceptional performance:
|
592
|
+
|
593
|
+
```ruby
|
594
|
+
# Configure with RabbitMQ-style routing
|
595
|
+
transport = SmartMessage::Transport.create(:redis_queue,
|
596
|
+
url: 'redis://localhost:6379',
|
597
|
+
queue_prefix: 'myapp',
|
598
|
+
consumer_group: 'workers'
|
599
|
+
)
|
600
|
+
|
601
|
+
# Pattern-based subscriptions (RabbitMQ compatible)
|
602
|
+
transport.subscribe_pattern("#.*.payment_service") # All messages TO payment_service
|
603
|
+
transport.subscribe_pattern("#.api_gateway.*") # All messages FROM api_gateway
|
604
|
+
transport.subscribe_pattern("order.#.*.*") # All order messages
|
605
|
+
|
606
|
+
# Fluent API for complex routing
|
607
|
+
transport.where
|
608
|
+
.from('web_app')
|
609
|
+
.to('analytics')
|
610
|
+
.consumer_group('analytics_workers')
|
611
|
+
.subscribe
|
612
|
+
|
613
|
+
# Configure message class
|
614
|
+
class OrderMessage < SmartMessage::Base
|
615
|
+
transport :redis_queue
|
616
|
+
|
617
|
+
property :order_id, required: true
|
618
|
+
property :amount, required: true
|
619
|
+
end
|
620
|
+
|
621
|
+
# Publish with enhanced routing
|
622
|
+
OrderMessage.new(
|
623
|
+
order_id: 'ORD-001',
|
624
|
+
amount: 99.99,
|
625
|
+
_sm_header: { from: 'api_gateway', to: 'payment_service' }
|
626
|
+
).publish
|
627
|
+
```
|
628
|
+
|
629
|
+
**Key Features:**
|
630
|
+
- 10x faster than RabbitMQ (0.5ms vs 5ms latency)
|
631
|
+
- Pattern routing with `#` and `*` wildcards
|
632
|
+
- Persistent FIFO queues using Redis Lists
|
633
|
+
- Load balancing via consumer groups
|
634
|
+
- Enhanced routing keys: `namespace.type.from.to`
|
635
|
+
- Queue monitoring and management
|
636
|
+
- Production-ready with circuit breakers and dead letter queues
|
637
|
+
|
638
|
+
📚 **Full Documentation:** [Redis Queue Transport Guide](docs/transports/redis-queue.md) | [Getting Started](docs/guides/redis-queue-getting-started.md) | [Examples](examples/redis_queue/)
|
639
|
+
|
444
640
|
### STDOUT Transport (Development)
|
445
641
|
|
446
642
|
```ruby
|
@@ -473,7 +669,7 @@ transport.process_all # Process all pending messages
|
|
473
669
|
|
474
670
|
```ruby
|
475
671
|
# Basic Redis configuration
|
476
|
-
transport = SmartMessage::Transport.create(:redis,
|
672
|
+
transport = SmartMessage::Transport.create(:redis,
|
477
673
|
url: 'redis://localhost:6379',
|
478
674
|
db: 0
|
479
675
|
)
|
@@ -508,7 +704,7 @@ The Redis transport uses the message class name as the Redis channel name, enabl
|
|
508
704
|
```ruby
|
509
705
|
class WebhookTransport < SmartMessage::Transport::Base
|
510
706
|
def default_options
|
511
|
-
{
|
707
|
+
{
|
512
708
|
webhook_url: "https://api.example.com/webhooks",
|
513
709
|
timeout: 30,
|
514
710
|
retries: 3
|
@@ -523,19 +719,19 @@ class WebhookTransport < SmartMessage::Transport::Base
|
|
523
719
|
def publish(message_header, message_payload)
|
524
720
|
http = Net::HTTP.new(@uri.host, @uri.port)
|
525
721
|
http.use_ssl = @uri.scheme == 'https'
|
526
|
-
|
722
|
+
|
527
723
|
request = Net::HTTP::Post.new(@uri)
|
528
724
|
request['Content-Type'] = 'application/json'
|
529
725
|
request['X-Message-Class'] = message_header.message_class
|
530
726
|
request.body = message_payload
|
531
|
-
|
727
|
+
|
532
728
|
response = http.request(request)
|
533
729
|
raise "Webhook failed: #{response.code}" unless response.code.to_i < 400
|
534
730
|
end
|
535
731
|
|
536
732
|
def subscribe(message_class, process_method)
|
537
733
|
super
|
538
|
-
# For webhooks, subscription would typically be configured
|
734
|
+
# For webhooks, subscription would typically be configured
|
539
735
|
# externally on the webhook provider's side
|
540
736
|
end
|
541
737
|
end
|
@@ -545,7 +741,7 @@ SmartMessage::Transport.register(:webhook, WebhookTransport)
|
|
545
741
|
|
546
742
|
# Use the transport
|
547
743
|
MyMessage.config do
|
548
|
-
transport SmartMessage::Transport.create(:webhook,
|
744
|
+
transport SmartMessage::Transport.create(:webhook,
|
549
745
|
webhook_url: "https://api.myservice.com/messages"
|
550
746
|
)
|
551
747
|
end
|
@@ -576,7 +772,7 @@ Declare your message schema version using the `version` class method:
|
|
576
772
|
```ruby
|
577
773
|
class OrderMessage < SmartMessage::Base
|
578
774
|
version 2 # Schema version 2
|
579
|
-
|
775
|
+
|
580
776
|
property :order_id, required: true
|
581
777
|
property :customer_email # Added in version 2
|
582
778
|
end
|
@@ -589,22 +785,22 @@ Properties support multiple validation types with custom error messages:
|
|
589
785
|
```ruby
|
590
786
|
class UserMessage < SmartMessage::Base
|
591
787
|
version 1
|
592
|
-
|
788
|
+
|
593
789
|
# Required field validation (Hashie built-in)
|
594
|
-
property :user_id,
|
790
|
+
property :user_id,
|
595
791
|
required: true,
|
596
792
|
message: "User ID is required and cannot be blank"
|
597
|
-
|
793
|
+
|
598
794
|
# Custom validation with lambda
|
599
795
|
property :age,
|
600
796
|
validate: ->(v) { v.is_a?(Integer) && v.between?(1, 120) },
|
601
797
|
validation_message: "Age must be an integer between 1 and 120"
|
602
|
-
|
798
|
+
|
603
799
|
# Email validation with regex
|
604
800
|
property :email,
|
605
801
|
validate: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i,
|
606
802
|
validation_message: "Must be a valid email address"
|
607
|
-
|
803
|
+
|
608
804
|
# Inclusion validation with array
|
609
805
|
property :status,
|
610
806
|
validate: ['active', 'inactive', 'pending'],
|
@@ -682,20 +878,20 @@ Use the `description` DSL method to document what your message class represents:
|
|
682
878
|
```ruby
|
683
879
|
class OrderMessage < SmartMessage::Base
|
684
880
|
description "Represents customer order data for processing and fulfillment"
|
685
|
-
|
881
|
+
|
686
882
|
property :order_id, required: true
|
687
883
|
property :amount, required: true
|
688
884
|
end
|
689
885
|
|
690
|
-
class UserMessage < SmartMessage::Base
|
886
|
+
class UserMessage < SmartMessage::Base
|
691
887
|
description "Handles user management operations including registration and updates"
|
692
|
-
|
888
|
+
|
693
889
|
property :user_id, required: true
|
694
890
|
property :email, required: true
|
695
891
|
end
|
696
892
|
|
697
893
|
# Access descriptions
|
698
|
-
puts OrderMessage.description
|
894
|
+
puts OrderMessage.description
|
699
895
|
# => "Represents customer order data for processing and fulfillment"
|
700
896
|
|
701
897
|
puts UserMessage.description
|
@@ -716,7 +912,7 @@ class MyMessage < SmartMessage::Base
|
|
716
912
|
property :data
|
717
913
|
end
|
718
914
|
|
719
|
-
puts MyMessage.description
|
915
|
+
puts MyMessage.description
|
720
916
|
# => "MyMessage is a SmartMessage"
|
721
917
|
```
|
722
918
|
|
@@ -727,10 +923,10 @@ Combine class descriptions with property descriptions for comprehensive document
|
|
727
923
|
```ruby
|
728
924
|
class FullyDocumented < SmartMessage::Base
|
729
925
|
description "A fully documented message class for demonstration purposes"
|
730
|
-
|
731
|
-
property :id,
|
926
|
+
|
927
|
+
property :id,
|
732
928
|
description: "Unique identifier for the record"
|
733
|
-
property :name,
|
929
|
+
property :name,
|
734
930
|
description: "Display name for the entity"
|
735
931
|
property :status,
|
736
932
|
description: "Current processing status",
|
@@ -945,4 +1141,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/MadBom
|
|
945
1141
|
|
946
1142
|
## License
|
947
1143
|
|
948
|
-
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
1144
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
CHANGED
@@ -1,10 +1,35 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
2
|
require "rake/testtask"
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
# Main test task - runs all tests with proper environment setup
|
5
|
+
task :test do
|
6
|
+
ENV['SM_LOGGER_TEST'] = 'true'
|
7
|
+
|
8
|
+
puts "Running SmartMessage test suite..."
|
9
|
+
puts "=" * 40
|
10
|
+
|
11
|
+
# Check for Redis availability for informational purposes
|
12
|
+
begin
|
13
|
+
require 'redis'
|
14
|
+
redis = Redis.new(url: 'redis://localhost:6379')
|
15
|
+
redis.ping
|
16
|
+
puts "✅ Redis available - all tests including Redis Queue Transport will run"
|
17
|
+
redis.quit
|
18
|
+
rescue => e
|
19
|
+
puts "⚠️ Redis not available: #{e.message}"
|
20
|
+
puts " Redis Queue Transport tests will be skipped automatically"
|
21
|
+
end
|
22
|
+
|
23
|
+
puts ""
|
24
|
+
|
25
|
+
# Run the actual tests
|
26
|
+
Rake::TestTask.new(:run_tests) do |t|
|
27
|
+
t.libs << "test"
|
28
|
+
t.libs << "lib"
|
29
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
30
|
+
end
|
31
|
+
|
32
|
+
Rake::Task[:run_tests].invoke
|
8
33
|
end
|
9
34
|
|
10
35
|
task :default => :test
|